Merge remote-tracking branch 'origin/develop' into profile-avatar-switcher

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2022-01-25 00:37:12 +01:00
commit 3d2e4ab368
706 changed files with 19445 additions and 6078 deletions

View File

@ -67,8 +67,12 @@ module.exports = {
'consistent-return': 'error',
'dot-notation': 'error',
eqeqeq: 'error',
indent: ['warn', 2],
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',

9
.gitignore vendored
View File

@ -12,3 +12,12 @@ yarn-error.log*
/static-test/
/public/
/dist/
.idea
.DS_Store
# surge.sh
CNAME
AUTH
CORS
ROUTER

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

View File

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<link href="/manifest.json" rel="manifest">
<!--server-generated-meta-->
<link rel="icon" type="image/png" href="/favicon.png">
</head>

View File

@ -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": "<p>test quote of a quote<span class=\"quote-inline\"><br/>QT: <a class=\"status-url-link\" data-status-account-acct=\"alex\" data-status-id=\"107673570082615319\" href=\"https://fedibird.com/@alex/107673570082615319\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">fedibird.com/@alex/10767357008</span><span class=\"invisible\">2615319</span></a></span></p>",
"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": "<p></p>",
"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": "<p>test quote<span class=\"quote-inline\"><br/>QT: <a class=\"status-url-link\" data-status-account-acct=\"alex\" data-status-id=\"107673569214329435\" href=\"https://fedibird.com/@alex/107673569214329435\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">fedibird.com/@alex/10767356921</span><span class=\"invisible\">4329435</span></a></span></p>",
"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": "<p></p>",
"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
}
}

View File

@ -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": "<p>test quote<span class=\"quote-inline\"><br/>QT: <a class=\"status-url-link\" data-status-account-acct=\"alex\" data-status-id=\"107673569214329435\" href=\"https://fedibird.com/@alex/107673569214329435\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">fedibird.com/@alex/10767356921</span><span class=\"invisible\">4329435</span></a></span></p>",
"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": "<p></p>",
"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": "<p>test post</p>",
"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": "<p></p>",
"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
}
}

View File

@ -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",

View File

@ -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": "<a href=\"https://alexgleason.me\" rel=\"ugc\">https://alexgleason.me</a>"
},
{
"name": "Pleroma+Soapbox",
"value": "<a href=\"https://soapbox.pub\" rel=\"ugc\">https://soapbox.pub</a>"
},
{
"name": "Email",
"value": "alex@alexgleason.me"
},
{
"name": "Gender identity",
"value": "Soyboy"
},
{
"name": "Donate (PayPal)",
"value": "<a href=\"https://paypal.me/gleasonator\" rel=\"ugc\">https://paypal.me/gleasonator</a>"
},
{
"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.<br/><br/>I&#39;m vegan btw<br/><br/>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": "<p>Quote of quote post</p>",
"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": "<a href=\"https://alexgleason.me\" rel=\"ugc\">https://alexgleason.me</a>"
},
{
"name": "Pleroma+Soapbox",
"value": "<a href=\"https://soapbox.pub\" rel=\"ugc\">https://soapbox.pub</a>"
},
{
"name": "Email",
"value": "alex@alexgleason.me"
},
{
"name": "Gender identity",
"value": "Soyboy"
},
{
"name": "Donate (PayPal)",
"value": "<a href=\"https://paypal.me/gleasonator\" rel=\"ugc\">https://paypal.me/gleasonator</a>"
},
{
"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.<br/><br/>I&#39;m vegan btw<br/><br/>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": "<p>Quote post</p>",
"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"
}

View File

@ -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": "<a href=\"https://alexgleason.me\" rel=\"ugc\">https://alexgleason.me</a>"
},
{
"name": "Pleroma+Soapbox",
"value": "<a href=\"https://soapbox.pub\" rel=\"ugc\">https://soapbox.pub</a>"
},
{
"name": "Email",
"value": "alex@alexgleason.me"
},
{
"name": "Gender identity",
"value": "Soyboy"
},
{
"name": "Donate (PayPal)",
"value": "<a href=\"https://paypal.me/gleasonator\" rel=\"ugc\">https://paypal.me/gleasonator</a>"
},
{
"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.<br/><br/>I&#39;m vegan btw<br/><br/>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": "<p>Quote post</p>",
"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": "<a href=\"https://alexgleason.me\" rel=\"ugc\">https://alexgleason.me</a>"
},
{
"name": "Pleroma+Soapbox",
"value": "<a href=\"https://soapbox.pub\" rel=\"ugc\">https://soapbox.pub</a>"
},
{
"name": "Email",
"value": "alex@alexgleason.me"
},
{
"name": "Gender identity",
"value": "Soyboy"
},
{
"name": "Donate (PayPal)",
"value": "<a href=\"https://paypal.me/gleasonator\" rel=\"ugc\">https://paypal.me/gleasonator</a>"
},
{
"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.<br/><br/>I&#39;m vegan btw<br/><br/>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": "<p>Test post</p>",
"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"
}

View File

@ -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.<br/><br/>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": "<span class=\"h-card\"><a class=\"u-url mention\" data-user=\"9r7CIa1GGezUtzTFyq\" href=\"https://iddqd.social/users/NEETzsche\" rel=\"ugc\">@<span>NEETzsche</span></a></span> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"9qoN7ZXtt7bllprZtw\" href=\"https://gleasonator.com/users/alex\" rel=\"ugc\">@<span>alex</span></a></span> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"A3dBnkLBdLgHFdxV2G\" href=\"https://pleroma.skyshanty.xyz/users/Lumeinshin\" rel=\"ugc\">@<span>Lumeinshin</span></a></span> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"ACrsGCVvaMiNdATg7E\" href=\"https://social.silkky.cloud/users/sneeden\" rel=\"ugc\">@<span>sneeden</span></a></span> <br/>&gt;seething<br/>&#39;posting&#39;, 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"
}

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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';
@ -144,8 +154,16 @@ export function fetchAccountByUsername(username) {
const instance = state.get('instance');
const features = getFeatures(instance);
const me = state.get('me');
if (features.accountByUsername) {
if (!me && features.accountLookup) {
dispatch(accountLookup(username)).then(account => {
dispatch(fetchAccountSuccess(account));
}).catch(error => {
dispatch(fetchAccountFail(null, error));
dispatch(importErrorWhileFetchingAccountByUsername(username));
});
} else if (features.accountByUsername) {
api(getState).get(`/api/v1/accounts/${username}`).then(response => {
dispatch(fetchRelationships([response.data.id]));
dispatch(importFetchedAccount(response.data));
@ -948,6 +966,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 +1016,17 @@ 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;
});
};
}

View File

@ -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 });
});
};
}

View File

@ -1,10 +1,13 @@
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 api from '../api';
import { showAlertForError } from './alerts';
import { importFetchedAccount, importFetchedAccounts } from './importer';
import { ME_PATCH_SUCCESS } from './me';
import snackbar from './snackbar';
export const ALIASES_SUGGESTIONS_CHANGE = 'ALIASES_SUGGESTIONS_CHANGE';
export const ALIASES_SUGGESTIONS_READY = 'ALIASES_SUGGESTIONS_READY';

View File

@ -8,18 +8,22 @@
*/
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 api, { baseClient } from '../api';
import { importFetchedAccount } from './importer';
export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT';
export const AUTH_APP_CREATED = 'AUTH_APP_CREATED';
@ -31,6 +35,10 @@ export const VERIFY_CREDENTIALS_REQUEST = 'VERIFY_CREDENTIALS_REQUEST';
export const VERIFY_CREDENTIALS_SUCCESS = 'VERIFY_CREDENTIALS_SUCCESS';
export const VERIFY_CREDENTIALS_FAIL = 'VERIFY_CREDENTIALS_FAIL';
export const AUTH_ACCOUNT_REMEMBER_REQUEST = 'AUTH_ACCOUNT_REMEMBER_REQUEST';
export const AUTH_ACCOUNT_REMEMBER_SUCCESS = 'AUTH_ACCOUNT_REMEMBER_SUCCESS';
export const AUTH_ACCOUNT_REMEMBER_FAIL = 'AUTH_ACCOUNT_REMEMBER_FAIL';
export const messages = defineMessages({
loggedOut: { id: 'auth.logged_out', defaultMessage: 'Logged out.' },
invalidCredentials: { id: 'auth.invalid_credentials', defaultMessage: 'Wrong username or password' },
@ -135,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)));
};
}
@ -157,9 +166,31 @@ export function verifyCredentials(token, accountUrl) {
};
}
export function rememberAuthAccount(accountUrl) {
return (dispatch, getState) => {
dispatch({ type: AUTH_ACCOUNT_REMEMBER_REQUEST, accountUrl });
return KVStore.getItemOrError(`authAccount:${accountUrl}`).then(account => {
dispatch(importFetchedAccount(account));
dispatch({ type: AUTH_ACCOUNT_REMEMBER_SUCCESS, account, accountUrl });
if (account.id === getState().get('me')) dispatch(fetchMeSuccess(account));
return account;
}).catch(error => {
dispatch({ type: AUTH_ACCOUNT_REMEMBER_FAIL, error, accountUrl, skipAlert: true });
});
};
}
export function loadCredentials(token, accountUrl) {
return (dispatch, getState) => {
return dispatch(rememberAuthAccount(accountUrl)).finally(() => {
return dispatch(verifyCredentials(token, accountUrl));
});
};
}
export 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') {

View File

@ -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';

View File

@ -1,4 +1,5 @@
import api, { getLinks } from '../api';
import { importFetchedStatuses } from './importer';
export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST';
@ -9,15 +10,17 @@ export const BOOKMARKED_STATUSES_EXPAND_REQUEST = 'BOOKMARKED_STATUSES_EXPAND_RE
export const BOOKMARKED_STATUSES_EXPAND_SUCCESS = 'BOOKMARKED_STATUSES_EXPAND_SUCCESS';
export const BOOKMARKED_STATUSES_EXPAND_FAIL = 'BOOKMARKED_STATUSES_EXPAND_FAIL';
const noOp = () => new Promise(f => f());
export function fetchBookmarkedStatuses() {
return (dispatch, getState) => {
if (getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) {
return;
return dispatch(noOp);
}
dispatch(fetchBookmarkedStatusesRequest());
api(getState).get('/api/v1/bookmarks').then(response => {
return api(getState).get('/api/v1/bookmarks').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
@ -53,12 +56,12 @@ export function expandBookmarkedStatuses() {
const url = getState().getIn(['status_lists', 'bookmarks', 'next'], null);
if (url === null || getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) {
return;
return dispatch(noOp);
}
dispatch(expandBookmarkedStatusesRequest());
api(getState).get(url).then(response => {
return api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));

View File

@ -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;

View File

@ -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 { useEmoji } from './emojis';
import { importFetchedAccounts } from './importer';
import { uploadMedia, fetchMedia, updateMedia } from './media';
import { openModal, closeModal } from './modal';
import { getSettings } from './settings';
import { getFeatures } from 'soapbox/utils/features';
import { uploadMedia, fetchMedia, updateMedia } from './media';
import { isLoggedIn } from 'soapbox/utils/auth';
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,6 +73,9 @@ 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.' },
@ -93,10 +101,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 +121,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 +190,7 @@ export function handleComposeSubmit(dispatch, getState, data, status) {
dispatch(insertIntoTagHistory(data.tags || [], status));
dispatch(submitComposeSuccess({ ...data }));
dispatch(snackbar.show('post', messages.success));
dispatch(snackbar.success(messages.success));
}
const needsDescriptions = state => {
@ -183,6 +218,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 +236,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 +251,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 +259,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) {
@ -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,
});
};
}

View File

@ -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';

View File

@ -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,
});

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -6,21 +6,23 @@
* @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 { getFeatures } from 'soapbox/utils/features';
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.status === 401) {
if (error.response && error.response.status === 401) {
// Authenticated fetch is enabled.
// Continue with a limited featureset.
return ImmutableMap({ version: '0.0.0' });

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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);
@ -74,6 +69,15 @@ export function importFetchedStatus(status, idempotencyKey) {
dispatch(importFetchedStatus(status.reblog));
}
// Fedibird quotes
if (status.quote && status.quote.id) {
dispatch(importFetchedStatus(status.quote));
}
if (status.pleroma && status.pleroma.quote && status.pleroma.quote.id) {
dispatch(importFetchedStatus(status.pleroma.quote));
}
if (status.poll && 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) {
processStatus(status.reblog);
}
// Fedibird quotes
if (status.quote && status.quote.id) {
processStatus(status.quote);
}
if (status.pleroma && status.pleroma.quote && status.pleroma.quote.id) {
processStatus(status.pleroma.quote);
}
if (status.poll && status.poll.id) {
pushUnique(polls, normalizePoll(status.poll));
polls.push(normalizePoll(status.poll));
}
}

View File

@ -1,4 +1,5 @@
import escapeTextContentForBrowser from 'escape-html';
import emojify from '../../features/emoji/emoji';
import { unescapeHTML } from '../../utils/html';
@ -38,6 +39,11 @@ export function normalizeAccount(account) {
export function normalizeStatus(status, normalOldStatus, expandSpoilers) {
const normalStatus = { ...status };
// 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) {
@ -48,6 +54,18 @@ export function normalizeStatus(status, normalOldStatus, expandSpoilers) {
normalStatus.poll = status.poll.id;
}
if (status.pleroma && status.pleroma.quote && 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 && 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) {
@ -74,8 +92,9 @@ export function normalizePoll(poll) {
const emojiMap = makeEmojiMap(normalPoll);
normalPoll.options = poll.options.map(option => ({
normalPoll.options = poll.options.map((option, index) => ({
...option,
voted: poll.own_votes && poll.own_votes.includes(index),
title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap),
}));

View File

@ -1,62 +1,89 @@
import api from '../api';
import { get } from 'lodash';
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';
export const INSTANCE_FETCH_FAIL = 'INSTANCE_FETCH_FAIL';
export const INSTANCE_REMEMBER_REQUEST = 'INSTANCE_REMEMBER_REQUEST';
export const INSTANCE_REMEMBER_SUCCESS = 'INSTANCE_REMEMBER_SUCCESS';
export const INSTANCE_REMEMBER_FAIL = 'INSTANCE_REMEMBER_FAIL';
export const NODEINFO_FETCH_REQUEST = 'NODEINFO_FETCH_REQUEST';
export const NODEINFO_FETCH_SUCCESS = 'NODEINFO_FETCH_SUCCESS';
export const NODEINFO_FETCH_FAIL = 'NODEINFO_FETCH_FAIL';
const getMeUrl = state => {
const me = state.get('me');
return state.getIn(['accounts', me, 'url']);
};
// Figure out the appropriate instance to fetch depending on the state
export const getHost = state => {
const accountUrl = getMeUrl(state) || getAuthUserUrl(state);
try {
return new URL(accountUrl).host;
} catch {
return null;
}
};
export function rememberInstance(host) {
return (dispatch, getState) => {
dispatch({ type: INSTANCE_REMEMBER_REQUEST, host });
return KVStore.getItemOrError(`instance:${host}`).then(instance => {
dispatch({ type: INSTANCE_REMEMBER_SUCCESS, host, instance });
return instance;
}).catch(error => {
dispatch({ type: INSTANCE_REMEMBER_FAIL, host, error, skipAlert: true });
});
};
}
// We may need to fetch nodeinfo on Pleroma < 2.1
const needsNodeinfo = instance => {
const v = parseVersion(get(instance, 'version'));
return v.software === 'Pleroma' && !get(instance, ['pleroma', 'metadata']);
};
export function fetchInstance() {
return (dispatch, getState) => {
return api(getState).get('/api/v1/instance').then(response => {
dispatch(importInstance(response.data));
const v = parseVersion(get(response.data, 'version'));
if (v.software === 'Pleroma' && !get(response.data, ['pleroma', 'metadata'])) {
dispatch({ type: INSTANCE_FETCH_REQUEST });
return api(getState).get('/api/v1/instance').then(({ data: instance }) => {
dispatch({ type: INSTANCE_FETCH_SUCCESS, instance });
if (needsNodeinfo(instance)) {
dispatch(fetchNodeinfo()); // Pleroma < 2.1 backwards compatibility
}
}).catch(error => {
dispatch(instanceFail(error));
dispatch({ type: INSTANCE_FETCH_FAIL, error, skipAlert: true });
});
};
}
// Tries to remember the instance from browser storage before fetching it
export function loadInstance() {
return (dispatch, getState) => {
const host = getHost(getState());
return dispatch(rememberInstance(host)).finally(() => {
return dispatch(fetchInstance());
});
};
}
export function fetchNodeinfo() {
return (dispatch, getState) => {
api(getState).get('/nodeinfo/2.1.json').then(response => {
dispatch(importNodeinfo(response.data));
dispatch({ type: NODEINFO_FETCH_REQUEST });
api(getState).get('/nodeinfo/2.1.json').then(({ data: nodeinfo }) => {
dispatch({ type: NODEINFO_FETCH_SUCCESS, nodeinfo });
}).catch(error => {
dispatch(nodeinfoFail(error));
dispatch({ type: NODEINFO_FETCH_FAIL, error, skipAlert: true });
});
};
}
export function importInstance(instance) {
return {
type: INSTANCE_FETCH_SUCCESS,
instance,
};
}
export function instanceFail(error) {
return {
type: INSTANCE_FETCH_FAIL,
error,
skipAlert: true,
};
}
export function importNodeinfo(nodeinfo) {
return {
type: NODEINFO_FETCH_SUCCESS,
nodeinfo,
};
}
export function nodeinfoFail(error) {
return {
type: NODEINFO_FETCH_FAIL,
error,
skipAlert: true,
};
}

View File

@ -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,6 +51,10 @@ 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.' },
@ -77,7 +84,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 +163,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));
@ -477,3 +482,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,
};
}

View File

@ -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,
});

View File

@ -1,8 +1,10 @@
import api from '../api';
import { importFetchedAccount } from './importer';
import { verifyCredentials } 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';
@ -38,7 +40,7 @@ export function fetchMe() {
}
dispatch(fetchMeRequest());
return dispatch(verifyCredentials(token, accountUrl)).catch(error => {
return dispatch(loadCredentials(token, accountUrl)).catch(error => {
dispatch(fetchMeFail(error));
});
};
@ -66,7 +68,6 @@ export function fetchMeRequest() {
export function fetchMeSuccess(me) {
return (dispatch, getState) => {
dispatch(importFetchedAccount(me));
dispatch({
type: ME_FETCH_SUCCESS,
me,

View File

@ -1,6 +1,7 @@
import api from '../api';
import { getFeatures } from 'soapbox/utils/features';
import api from '../api';
const noOp = () => {};
export function fetchMedia(mediaId) {

View File

@ -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,
};
}

View File

@ -9,9 +9,10 @@ export function openModal(type, props) {
};
}
export function closeModal(type) {
export function closeModal(type, noPop) {
return {
type: MODAL_CLOSE,
modalType: type,
noPop,
};
}

View File

@ -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/modal';
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: () => {

View File

@ -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);

View File

@ -1,9 +1,11 @@
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 './modal';
import { isLoggedIn } from 'soapbox/utils/auth';
import { getNextLinkName } from 'soapbox/utils/quirks';
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';

View File

@ -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,16 +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';
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
@ -102,16 +106,20 @@ export function updateNotificationsQueue(notification, intlMessages, intlLocale,
// Desktop notifications
try {
if (typeof window.Notification !== 'undefined' && showAlert && !filtered) {
if (showAlert && !filtered) {
const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');
const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id });
notify.addEventListener('click', () => {
window.focus();
notify.close();
navigator.serviceWorker.ready.then(serviceWorkerRegistration => {
serviceWorkerRegistration.showNotification(title, {
body,
icon: notification.account.avatar,
tag: notification.id,
data: {
url: joinPublicPath('/notifications'),
},
});
}).catch(console.error);
}
} catch(e) {
console.warn(e);

View File

@ -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';

View File

@ -1,4 +1,5 @@
import api from '../api';
import { importFetchedPoll } from './importer';
export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST';

View File

@ -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';

View File

@ -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,

View File

@ -1,6 +1,9 @@
import api from '../../api';
import { decode as decodeBase64 } from '../../utils/base64';
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
@ -13,12 +16,6 @@ const urlBase64ToUint8Array = (base64String) => {
return decodeBase64(base64);
};
const getApplicationServerKey = getState => {
const key = getState().getIn(['auth', 'app', 'vapid_key']);
if (!key) console.error('Could not get vapid key. Push notifications will not work.');
return key;
};
const getRegistration = () => navigator.serviceWorker.ready;
const getPushSubscription = (registration) =>
@ -28,14 +25,16 @@ const getPushSubscription = (registration) =>
const subscribe = (registration, getState) =>
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey(getState)),
applicationServerKey: urlBase64ToUint8Array(getVapidKey(getState())),
});
const unsubscribe = ({ registration, subscription }) =>
subscription ? subscription.unsubscribe().then(() => registration) : registration;
const sendSubscriptionToBackend = (subscription, me) => {
const params = { subscription };
return (dispatch, getState) => {
const alerts = getState().getIn(['push_notifications', 'alerts']).toJS();
const params = { subscription, data: { alerts } };
if (me) {
const data = pushNotificationsSetting.get(me);
@ -44,7 +43,8 @@ const sendSubscriptionToBackend = (subscription, me) => {
}
}
return api().post('/api/web/push_subscriptions', params).then(response => response.data);
return dispatch(createPushSubsription(params));
};
};
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
@ -53,10 +53,16 @@ const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager'
export function register() {
return (dispatch, getState) => {
const me = getState().get('me');
const vapidKey = getVapidKey(getState());
dispatch(setBrowserSupport(supportsPushNotifications));
if (supportsPushNotifications) {
if (!getApplicationServerKey(getState)) {
if (!supportsPushNotifications) {
console.warn('Your browser does not support Web Push Notifications.');
return;
}
if (!vapidKey) {
console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.');
return;
}
@ -67,7 +73,7 @@ export function register() {
if (subscription !== null) {
// We have a subscription, check if it is still valid
const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString();
const subscriptionServerKey = urlBase64ToUint8Array(getApplicationServerKey(getState)).toString();
const subscriptionServerKey = urlBase64ToUint8Array(vapidKey).toString();
const serverEndpoint = getState().getIn(['push_notifications', 'subscription', 'endpoint']);
// If the VAPID public key did not change and the endpoint corresponds
@ -79,13 +85,13 @@ export function register() {
return unsubscribe({ registration, subscription }).then(registration => {
return subscribe(registration, getState);
}).then(
subscription => sendSubscriptionToBackend(subscription, me));
subscription => dispatch(sendSubscriptionToBackend(subscription, me)));
}
}
// No subscription, try to subscribe
return subscribe(registration, getState).then(
subscription => sendSubscriptionToBackend(subscription, me));
return subscribe(registration, getState)
.then(subscription => dispatch(sendSubscriptionToBackend(subscription, me)));
})
.then(subscription => {
// If we got a PushSubscription (and not a subscription object from the backend)
@ -98,14 +104,16 @@ 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') {
console.error('The VAPID public key seems to be invalid:', getApplicationServerKey(getState));
console.error('The VAPID public key seems to be invalid:', vapidKey);
}
// Clear alerts and hide UI settings
dispatch(clearSubscription());
if (me) {
pushNotificationsSetting.remove(me);
}
@ -115,23 +123,17 @@ export function register() {
.then(unsubscribe);
})
.catch(console.warn);
} else {
console.warn('Your browser does not support Web Push Notifications.');
}
};
}
export function saveSettings() {
return (_, getState) => {
return (dispatch, getState) => {
const state = getState().get('push_notifications');
const subscription = state.get('subscription');
const alerts = state.get('alerts');
const data = { alerts };
const me = getState().get('me');
api().put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
data,
}).then(() => {
return dispatch(updatePushSubscription({ data })).then(() => {
if (me) {
pushNotificationsSetting.set(me, data);
}

View File

@ -21,6 +21,7 @@ export function createPushSubsription(params) {
dispatch({ type: PUSH_SUBSCRIPTION_CREATE_REQUEST, params });
return api(getState).post('/api/v1/push/subscription', params).then(({ data: subscription }) => {
dispatch({ type: PUSH_SUBSCRIPTION_CREATE_SUCCESS, params, subscription });
return subscription;
}).catch(error => {
dispatch({ type: PUSH_SUBSCRIPTION_CREATE_FAIL, params, error });
});
@ -38,7 +39,7 @@ export function fetchPushSubsription() {
};
}
export function updatePushSubsription(params) {
export function updatePushSubscription(params) {
return (dispatch, getState) => {
dispatch({ type: PUSH_SUBSCRIPTION_UPDATE_REQUEST, params });
return api(getState).put('/api/v1/push/subscription', params).then(({ data: subscription }) => {

View File

@ -1,4 +1,5 @@
import api from '../api';
import { openModal, closeModal } from './modal';
export const REPORT_INIT = 'REPORT_INIT';

View File

@ -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 {
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,19 +96,23 @@ export function fetchSearchFail(error) {
};
}
export const setFilter = filterType => dispatch => {
export function setFilter(filterType) {
return (dispatch) => {
dispatch(submitSearch(filterType));
dispatch({
type: SEARCH_FILTER_SET,
path: ['search', 'filter'],
value: filterType,
});
};
};
}
export const expandSearch = type => (dispatch, getState) => {
const value = getState().getIn(['search', 'value']);
const offset = getState().getIn(['search', 'results', type]).size;
dispatch(expandSearchRequest());
dispatch(expandSearchRequest(type));
api(getState).get('/api/v2/search', {
params: {
@ -119,8 +136,9 @@ export const expandSearch = type => (dispatch, getState) => {
});
};
export const expandSearchRequest = () => ({
export const expandSearchRequest = (searchType) => ({
type: SEARCH_EXPAND_REQUEST,
searchType,
});
export const expandSearchSuccess = (results, searchTerm, searchType) => ({

View File

@ -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';

View File

@ -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,13 +34,15 @@ export const defaultSettings = ImmutableMap({
locale: navigator.language.split(/[-_]/)[0] || 'en',
showExplanationBox: true,
explanationBox: true,
otpEnabled: false,
autoloadTimelines: true,
autoloadMore: true,
systemFont: false,
dyslexicFont: false,
demetricator: false,
isDeveloper: false,
chats: ImmutableMap({
panes: ImmutableList(),
mainWindow: 'minimized',
@ -100,6 +106,7 @@ export const defaultSettings = ImmutableMap({
shows: ImmutableMap({
reblog: false,
reply: true,
direct: false,
}),
other: ImmutableMap({
onlyMedia: false,
@ -113,6 +120,7 @@ export const defaultSettings = ImmutableMap({
shows: ImmutableMap({
reblog: true,
reply: true,
direct: false,
}),
other: ImmutableMap({
onlyMedia: false,
@ -132,6 +140,7 @@ export const defaultSettings = ImmutableMap({
shows: ImmutableMap({
reblog: true,
pinned: true,
direct: false,
}),
}),
@ -159,6 +168,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({
@ -171,7 +192,8 @@ export function changeSetting(path, value) {
};
}
const debouncedSave = debounce((dispatch, getState) => {
export function saveSettingsImmediate() {
return (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
const state = getState();
@ -188,6 +210,11 @@ const debouncedSave = debounce((dispatch, getState) => {
}).catch(error => {
dispatch(showAlertForError(error));
});
};
}
const debouncedSave = debounce((dispatch, getState) => {
dispatch(saveSettingsImmediate());
}, 5000, { trailing: true });
export function saveSettings() {

View File

@ -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,
};
}

View File

@ -1,8 +1,12 @@
import { isLoggedIn } from 'soapbox/utils/auth';
import { getFeatures } from 'soapbox/utils/features';
import { shouldHaveCard } from 'soapbox/utils/status';
import api from '../api';
import { deleteFromTimelines } from './timelines';
import { importFetchedStatus, importFetchedStatuses } from './importer';
import { openModal } from './modal';
import { isLoggedIn } from 'soapbox/utils/auth';
import { deleteFromTimelines } from './timelines';
export const STATUS_CREATE_REQUEST = 'STATUS_CREATE_REQUEST';
export const STATUS_CREATE_SUCCESS = 'STATUS_CREATE_SUCCESS';
@ -33,13 +37,9 @@ export const STATUS_HIDE = 'STATUS_HIDE';
export const REDRAFT = 'REDRAFT';
export function fetchStatusRequest(id, skipLoading) {
return {
type: STATUS_FETCH_REQUEST,
id,
skipLoading,
};
}
const statusExists = (getState, statusId) => {
return getState().getIn(['statuses', statusId], null) !== null;
};
export function createStatus(params, idempotencyKey) {
return (dispatch, getState) => {
@ -48,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 && 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 });
@ -60,48 +83,32 @@ export function createStatus(params, idempotencyKey) {
export function fetchStatus(id) {
return (dispatch, getState) => {
const skipLoading = getState().getIn(['statuses', id], null) !== null;
const skipLoading = statusExists(getState, id);
dispatch(fetchContext(id));
dispatch({ type: STATUS_FETCH_REQUEST, id, skipLoading });
if (skipLoading) {
return;
}
dispatch(fetchStatusRequest(id, skipLoading));
api(getState).get(`/api/v1/statuses/${id}`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(fetchStatusSuccess(response.data, skipLoading));
return api(getState).get(`/api/v1/statuses/${id}`).then(({ data: status }) => {
dispatch(importFetchedStatus(status));
dispatch({ type: STATUS_FETCH_SUCCESS, status, skipLoading });
return status;
}).catch(error => {
dispatch(fetchStatusFail(id, error, skipLoading));
dispatch({ type: STATUS_FETCH_FAIL, id, error, skipLoading, skipAlert: true });
});
};
}
export function fetchStatusSuccess(status, skipLoading) {
return {
type: STATUS_FETCH_SUCCESS,
status,
skipLoading,
};
}
export function fetchStatusFail(id, error, skipLoading) {
return {
type: STATUS_FETCH_FAIL,
id,
error,
skipLoading,
skipAlert: true,
};
}
export function redraft(status, raw_text) {
return {
return (dispatch, getState) => {
const state = getState();
const instance = state.get('instance');
const { explicitAddressing } = getFeatures(instance);
dispatch({
type: REDRAFT,
status,
raw_text,
explicitAddressing,
});
};
}
@ -115,10 +122,10 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
}
dispatch(deleteStatusRequest(id));
dispatch({ type: STATUS_DELETE_REQUEST, id });
api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
dispatch(deleteStatusSuccess(id));
dispatch({ type: STATUS_DELETE_SUCCESS, id });
dispatch(deleteFromTimelines(id));
if (withRedraft) {
@ -126,73 +133,37 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
dispatch(openModal('COMPOSE'));
}
}).catch(error => {
dispatch(deleteStatusFail(id, error));
dispatch({ type: STATUS_DELETE_FAIL, id, error });
});
};
}
export function deleteStatusRequest(id) {
return {
type: STATUS_DELETE_REQUEST,
id: id,
};
}
export function deleteStatusSuccess(id) {
return {
type: STATUS_DELETE_SUCCESS,
id: id,
};
}
export function deleteStatusFail(id, error) {
return {
type: STATUS_DELETE_FAIL,
id: id,
error: error,
};
}
export function fetchContext(id) {
return (dispatch, getState) => {
dispatch(fetchContextRequest(id));
api(getState).get(`/api/v1/statuses/${id}/context`).then(response => {
dispatch(importFetchedStatuses(response.data.ancestors.concat(response.data.descendants)));
dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants));
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 });
return context;
}).catch(error => {
if (error.response && error.response.status === 404) {
dispatch(deleteFromTimelines(id));
}
dispatch(fetchContextFail(id, error));
dispatch({ type: CONTEXT_FETCH_FAIL, id, error, skipAlert: true });
});
};
}
export function fetchContextRequest(id) {
return {
type: CONTEXT_FETCH_REQUEST,
id,
};
}
export function fetchContextSuccess(id, ancestors, descendants) {
return {
type: CONTEXT_FETCH_SUCCESS,
id,
ancestors,
descendants,
};
}
export function fetchContextFail(id, error) {
return {
type: CONTEXT_FETCH_FAIL,
id,
error,
skipAlert: true,
export function fetchStatusWithContext(id) {
return (dispatch, getState) => {
return Promise.all([
dispatch(fetchContext(id)),
dispatch(fetchStatus(id)),
]);
};
}
@ -200,74 +171,28 @@ export function muteStatus(id) {
return (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
dispatch(muteStatusRequest(id));
dispatch({ type: STATUS_MUTE_REQUEST, id });
api(getState).post(`/api/v1/statuses/${id}/mute`).then(() => {
dispatch(muteStatusSuccess(id));
dispatch({ type: STATUS_MUTE_SUCCESS, id });
}).catch(error => {
dispatch(muteStatusFail(id, error));
dispatch({ type: STATUS_MUTE_FAIL, id, error });
});
};
}
export function muteStatusRequest(id) {
return {
type: STATUS_MUTE_REQUEST,
id,
};
}
export function muteStatusSuccess(id) {
return {
type: STATUS_MUTE_SUCCESS,
id,
};
}
export function muteStatusFail(id, error) {
return {
type: STATUS_MUTE_FAIL,
id,
error,
};
}
export function unmuteStatus(id) {
return (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
dispatch(unmuteStatusRequest(id));
dispatch({ type: STATUS_UNMUTE_REQUEST, id });
api(getState).post(`/api/v1/statuses/${id}/unmute`).then(() => {
dispatch(unmuteStatusSuccess(id));
dispatch({ type: STATUS_UNMUTE_SUCCESS, id });
}).catch(error => {
dispatch(unmuteStatusFail(id, error));
dispatch({ type: STATUS_UNMUTE_FAIL, id, error });
});
};
}
export function unmuteStatusRequest(id) {
return {
type: STATUS_UNMUTE_REQUEST,
id,
};
}
export function unmuteStatusSuccess(id) {
return {
type: STATUS_UNMUTE_SUCCESS,
id,
};
}
export function unmuteStatusFail(id, error) {
return {
type: STATUS_UNMUTE_FAIL,
id,
error,
};
}
export function hideStatus(ids) {
if (!Array.isArray(ids)) {
ids = [ids];

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';
/**

View File

@ -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) {

View File

@ -1,5 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import React from 'react';
export default function InlineSVG({ src }) {
return <svg id={src} />;

View File

@ -1,5 +1,6 @@
import React from 'react';
import renderer from 'react-test-renderer';
import AutosuggestEmoji from '../autosuggest_emoji';
describe('<AutosuggestEmoji />', () => {

View File

@ -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('<Avatar />', () => {

View File

@ -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('<AvatarOverlay', () => {

View File

@ -1,5 +1,6 @@
import React from 'react';
import renderer from 'react-test-renderer';
import Badge from '../badge';
describe('<Badge />', () => {

View File

@ -1,6 +1,7 @@
import { shallow } from 'enzyme';
import React from 'react';
import renderer from 'react-test-renderer';
import Button from '../button';
describe('<Button />', () => {

View File

@ -1,5 +1,6 @@
import React from 'react';
import renderer from 'react-test-renderer';
import Column from '../column';
describe('<Column />', () => {

View File

@ -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('<ColumnBackButton />', () => {
it('renders correctly', () => {
const component = createComponent(<ColumnBackButton />);

View File

@ -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('<DisplayName />', () => {
it('renders display name + account name', () => {
const account = fromJS({

View File

@ -1,5 +1,7 @@
import React from 'react';
import { createComponent } from 'soapbox/test_helpers';
import EmojiSelector from '../emoji_selector';
describe('<EmojiSelector />', () => {

View File

@ -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}}' },
});

View File

@ -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 {

View File

@ -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' },

View File

@ -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];

View File

@ -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 { connect } from 'react-redux';
import { openModal } from 'soapbox/actions/modal';
import Bundle from 'soapbox/features/ui/components/bundle';
import { MediaGallery } from 'soapbox/features/ui/util/async-components';
export default @connect()
class AttachmentThumbs extends ImmutablePureComponent {

View File

@ -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 = () => {};

View File

@ -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 = {

View File

@ -1,12 +1,16 @@
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;
@ -47,21 +51,28 @@ export default class AutosuggestInput extends ImmutablePureComponent {
onKeyUp: PropTypes.func,
onKeyDown: PropTypes.func,
autoFocus: PropTypes.bool,
autoSelect: PropTypes.bool,
className: PropTypes.string,
id: PropTypes.string,
searchTokens: PropTypes.arrayOf(PropTypes.string),
maxLength: PropTypes.number,
menu: PropTypes.arrayOf(PropTypes.object),
};
static defaultProps = {
autoFocus: false,
autoSelect: true,
searchTokens: ImmutableList(['@', ':', '#']),
};
getFirstIndex = () => {
return this.props.autoSelect ? 0 : -1;
}
state = {
suggestionsHidden: true,
focused: false,
selectedSuggestion: 0,
selectedSuggestion: this.getFirstIndex(),
lastToken: null,
tokenStart: 0,
};
@ -81,8 +92,10 @@ export default class AutosuggestInput extends ImmutablePureComponent {
}
onKeyDown = (e) => {
const { suggestions, disabled } = this.props;
const { suggestions, menu, disabled } = this.props;
const { selectedSuggestion, suggestionsHidden } = this.state;
const firstIndex = this.getFirstIndex();
const lastIndex = suggestions.size + (menu || []).length - 1;
if (disabled) {
e.preventDefault();
@ -106,26 +119,33 @@ export default class AutosuggestInput extends ImmutablePureComponent {
break;
case 'ArrowDown':
if (suggestions.size > 0 && !suggestionsHidden) {
if (!suggestionsHidden && (suggestions.size > 0 || menu)) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, lastIndex) });
}
break;
case 'ArrowUp':
if (suggestions.size > 0 && !suggestionsHidden) {
if (!suggestionsHidden && (suggestions.size > 0 || menu)) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, firstIndex) });
}
break;
case 'Enter':
case 'Tab':
// Select suggestion
if (suggestions.size > 0 && !suggestionsHidden) {
if (!suggestionsHidden && selectedSuggestion > -1 && (suggestions.size > 0 || menu)) {
e.preventDefault();
e.stopPropagation();
this.setState({ selectedSuggestion: firstIndex });
if (selectedSuggestion < suggestions.size) {
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
} else {
const item = menu[selectedSuggestion - suggestions.size];
this.handleMenuItemAction(item);
}
}
break;
@ -186,11 +206,51 @@ export default class AutosuggestInput extends ImmutablePureComponent {
);
}
handleMenuItemAction = item => {
this.onBlur();
item.action();
}
handleMenuItemClick = item => {
return e => {
e.preventDefault();
this.handleMenuItemAction(item);
};
}
renderMenu = () => {
const { menu, suggestions } = this.props;
const { selectedSuggestion } = this.state;
if (!menu) {
return null;
}
return menu.map((item, i) => (
<a
className={classNames('autosuggest-input__action', { selected: suggestions.size - selectedSuggestion === i })}
href='#'
role='button'
tabIndex='0'
onMouseDown={this.handleMenuItemClick(item)}
key={i}
>
{item.icon && (
<Icon src={item.icon} />
)}
<span>{item.text}</span>
</a>
));
};
render() {
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props;
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, menu } = this.props;
const { suggestionsHidden } = this.state;
const style = { direction: 'ltr' };
const visible = !suggestionsHidden && (!suggestions.isEmpty() || (menu && value));
if (isRtl(value)) {
style.direction = 'rtl';
}
@ -220,8 +280,9 @@ export default class AutosuggestInput extends ImmutablePureComponent {
/>
</label>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
<div className={classNames('autosuggest-textarea__suggestions', { 'autosuggest-textarea__suggestions--visible': visible })}>
{suggestions.map(this.renderSuggestion)}
{this.renderMenu()}
</div>
</div>
);

View File

@ -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;

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -1,5 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import React from 'react';
const Badge = (props) => (
<span className={'badge badge--' + props.slug}>{props.title}</span>

View File

@ -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

View File

@ -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 = (
<button
type={this.props.type}
className={className}
disabled={this.props.disabled}
onClick={this.handleClick}

View File

@ -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 Column extends React.PureComponent {
@ -11,6 +11,10 @@ export default class Column extends React.PureComponent {
label: PropTypes.string,
};
setRef = c => {
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}
</div>

View File

@ -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 {

View File

@ -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';

View File

@ -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 {

View File

@ -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 {
</span>
<div className='domain__buttons'>
<IconButton active icon='unlock' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} />
<IconButton active src={require('@tabler/icons/icons/lock-open.svg')} title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} />
</div>
</div>
</div>

View File

@ -1,11 +1,16 @@
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;
@ -146,10 +151,10 @@ class DropdownMenu extends React.PureComponent {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const { text, href, to, newTab, isLogout } = option;
const { text, href, to, newTab, isLogout, icon, destructive } = option;
return (
<li className='dropdown-menu__item' key={`${text}-${i}`}>
<li className={classNames('dropdown-menu__item', { destructive })} key={`${text}-${i}`}>
<a
href={href || to || '#'}
role='button'
@ -162,6 +167,7 @@ class DropdownMenu extends React.PureComponent {
target={newTab ? '_blank' : null}
data-method={isLogout ? 'delete' : null}
>
{icon && <Icon src={icon} />}
{text}
</a>
</li>
@ -201,6 +207,8 @@ export default class Dropdown extends React.PureComponent {
src: PropTypes.string,
items: PropTypes.array.isRequired,
size: PropTypes.number,
active: PropTypes.bool,
pressed: PropTypes.bool,
title: PropTypes.string,
disabled: PropTypes.bool,
status: ImmutablePropTypes.map,
@ -211,6 +219,7 @@ export default class Dropdown extends React.PureComponent {
dropdownPlacement: PropTypes.string,
openDropdownId: PropTypes.number,
openedViaKeyboard: PropTypes.bool,
text: PropTypes.string,
};
static defaultProps = {
@ -296,7 +305,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 (
@ -305,9 +314,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}

View File

@ -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'),

View File

@ -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 {

View File

@ -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 {

View File

@ -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 (
<div className='filter-bar__active' style={{ left, width }} />
);
}
renderItem(option, i) {
if (option === null) {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const { name, text, href, to, title } = option;
return (
<a
key={name}
href={href || to || '#'}
role='button'
tabIndex='0'
ref={i === 0 ? this.setFocusRef : null}
onClick={this.handleClick}
onKeyPress={this.handleItemKeyPress}
data-index={i}
title={title}
>
{text}
</a>
);
}
render() {
const { className, items } = this.props;
const { mounted } = this.state;
return (
<div className={classNames('filter-bar', className)} ref={this.setRef}>
{mounted && this.renderActiveTabIndicator()}
{items.map((option, i) => this.renderItem(option, i))}
</div>
);
}
}

View File

@ -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 {

View File

@ -1,30 +1,46 @@
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';
const Hashtag = ({ hashtag }) => (
import Permalink from './permalink';
const Hashtag = ({ hashtag }) => {
const count = Number(hashtag.getIn(['history', 0, 'accounts']));
return (
<div className='trends__item'>
<div className='trends__item__name'>
<Permalink href={hashtag.get('url')} to={`/tags/${hashtag.get('name')}`}>
#<span>{hashtag.get('name')}</span>
</Permalink>
{hashtag.get('history') && <div className='trends__item__count'>
<FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']), count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))}</strong> }} />
</div>}
{hashtag.get('history') && (
<div className='trends__item__count'>
<FormattedMessage
id='trends.count_by_accounts'
defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking'
values={{
rawCount: count,
count: <strong>{shortNumberFormat(count)}</strong>,
}}
/>
</div>
)}
</div>
{/* Pleroma doesn't support tag history yet */}
{/* hashtag.get('history') && <div className='trends__item__sparkline'>
{hashtag.get('history') && (
<div className='trends__item__sparkline'>
<Sparklines width={50} height={28} data={hashtag.get('history').reverse().map(day => day.get('uses')).toArray()}>
<SparklinesCurve style={{ fill: 'none' }} />
</Sparklines>
</div> */}
</div>
);
)}
</div>
);
};
Hashtag.propTypes = {
hashtag: ImmutablePropTypes.map.isRequired,

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