Merge remote-tracking branch 'origin/develop' into typescript

This commit is contained in:
Alex Gleason 2022-02-19 01:25:06 -05:00
commit 2b235a80e4
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
713 changed files with 21581 additions and 6331 deletions

View File

@ -69,6 +69,10 @@ module.exports = {
eqeqeq: 'error',
indent: ['error', 2],
'jsx-quotes': ['error', 'prefer-single'],
'key-spacing': [
'error',
{ mode: 'minimum' },
],
'no-catch-shadow': 'error',
'no-cond-assign': 'error',
'no-console': [
@ -111,6 +115,13 @@ module.exports = {
'prefer-const': 'error',
quotes: ['error', 'single'],
semi: 'error',
'space-unary-ops': [
'error',
{
words: true,
nonwords: false,
},
],
strict: 'off',
'valid-typeof': 'error',
@ -212,6 +223,23 @@ module.exports = {
],
'import/no-unresolved': 'error',
'import/no-webpack-loader-syntax': 'error',
'import/order': [
'error',
{
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index',
'object',
'type',
],
'newlines-between': 'always',
alphabetize: { order: 'asc' },
},
],
'promise/catch-or-return': 'error',

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: 14 KiB

View File

@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
inkscape:export-ydpi="240"
inkscape:export-xdpi="240"
inkscape:export-filename="/home/alex/Projects/docker-tribe/avi.png"
sodipodi:docname="avatar.svg"
width="120"
height="120"
viewBox="0 0 31.75 31.750001"
version="1.1"
id="svg8"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
inkscape:window-maximized="1"
inkscape:window-y="30"
inkscape:window-x="0"
inkscape:window-height="1019"
inkscape:window-width="1920"
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4.3194033"
inkscape:cx="79.987899"
inkscape:cy="64.129228"
inkscape:document-units="px"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="false"
units="px"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:snap-bbox="true"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-object-midpoints="true"
inkscape:snap-center="true"
inkscape:snap-global="false"
inkscape:pagecheckerboard="0" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0.80272198,1.2346106)">
<rect
style="fill:#10b3d1;fill-opacity:1;stroke:none;stroke-width:1.53052;stroke-miterlimit:4;stroke-dasharray:none"
id="rect853"
width="31.75"
height="31.75"
x="-0.80272198"
y="-1.2346106" />
<path
id="path857"
style="fill:#f4962a;fill-opacity:1;stroke:#000000;stroke-width:1.48265;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 15.051525,3.9344041 A 10.434797,10.434797 0 0 0 4.6372555,14.368944 a 10.434797,10.434797 0 0 0 0,0.02075 v 0.03137 a 1.7839264,2.3625556 0 0 0 -0.9503056,-0.363423 1.7839264,2.3625556 0 0 0 -1.7838141,2.362493 1.7839264,2.3625556 0 0 0 1.7838141,2.362491 1.7839264,2.3625556 0 0 0 0.9503056,-0.363423 v 23.74268 H 25.507302 v -23.74268 a 1.7839264,2.3625556 0 0 0 0.950305,0.363423 1.7839264,2.3625556 0 0 0 1.783814,-2.362491 1.7839264,2.3625556 0 0 0 -1.783814,-2.362493 1.7839264,2.3625556 0 0 0 -0.950305,0.362941 v -0.05164 A 10.434797,10.434797 0 0 0 15.072278,3.9344041 a 10.434797,10.434797 0 0 0 -0.02075,0 z" />
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.900495;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path881-6"
cx="11.218504"
cy="13.292331"
r="1.3086123" />
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.900495;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path881-7"
cx="18.926052"
cy="13.292331"
r="1.3086123" />
<ellipse
style="fill:#f2f2f2;fill-opacity:1;stroke:#000000;stroke-width:0.88959;stroke-miterlimit:4;stroke-dasharray:none"
id="path917"
cx="15.072279"
cy="18.343904"
rx="7.1666903"
ry="4.0659413" />
<path
id="path957"
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.88959;stroke-miterlimit:4;stroke-dasharray:none"
d="m 21.900069,17.108362 a 7.1666903,4.0659412 0 0 1 -6.82779,2.8304 7.1666903,4.0659412 0 0 1 -6.8277959,-2.830413" />
<path
id="path1003"
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.88959;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:transform-center-y="-0.79831289"
d="m 13.062455,16.997512 c 0.457808,-1.227486 1.336486,-2.475611 2.009822,-2.475611 0.673338,0 1.552013,1.248125 2.009824,2.475611" />
<path
style="fill:none;stroke:#000000;stroke-width:0.79375;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 24.842957,6.3249745 c -1.070225,-0.4344716 -2.309802,0.1182843 -2.464817,1.0590207 0,0 -1.627232,-1.9230616 1.219705,-3.5847621"
id="path1030"
sodipodi:nodetypes="ccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 95 95" width="100" height="100"><path d="M94.909 19.374C94.909 8.674 86.235 0 75.534 0c-10.647 0-19.28 8.591-19.365 19.217l-15.631 2.09c-1.961-6.007-7.598-10.35-14.258-10.35-8.284 0-15.002 6.716-15.002 15.002 0 6.642 4.321 12.267 10.303 14.24l-2.205 16.056c-10.66.049-19.285 8.7-19.285 19.37C.091 86.325 8.765 95 19.466 95c10.677 0 19.332-8.638 19.37-19.304l18.093-2.501c1.979 5.972 7.598 10.285 14.234 10.285 8.284 0 15.002-6.716 15.002-15.002 0-6.891-4.652-12.682-10.983-14.441l1.365-15.339c10.229-.53 18.363-8.966 18.363-19.324zM56.194 67.8l-18.116 2.505a19.39 19.39 0 0 0-13.312-13.3l2.205-16.077a14.98 14.98 0 0 0 14.27-14.222l15.655-2.094c1.894 6.757 7.351 12.009 14.225 13.612l-1.365 15.322c-7.4.688-13.224 6.753-13.562 14.254z" fill="#0482d8"/></svg>

After

Width:  |  Height:  |  Size: 812 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,43 @@
{
"uri": "animalliberation.social",
"title": "Animal Liberation Network",
"short_description": "",
"description": "Animal Liberation Network is a community for <b>animal activists</b> on the Fediverse. You can connect with other activists through the <b>local timeline</b>, as well as spread your activism to the outside world with the <b>federated timeline</b>.",
"email": "alex@alexgleason.me",
"version": "3.0.0",
"urls": {
"streaming_api": "wss://animalliberation.social"
},
"stats": {
"user_count": 662,
"status_count": 2904,
"domain_count": 4003
},
"thumbnail": "https://animalliberation.social/packs/media/images/preview-9a17d32fc48369e8ccd910a75260e67d.jpg",
"languages": [
"en"
],
"registrations": true,
"approval_required": false,
"contact_account": {
"id": "1",
"username": "alex",
"acct": "alex",
"display_name": "Alex Gleason",
"locked": false,
"bot": false,
"created_at": "2016-11-30T22:19:42.956Z",
"note": "<p>Animal liberation free software Communist</p>",
"url": "https://animalliberation.social/@alex",
"avatar": "https://media.animalliberation.social/accounts/avatars/000/000/001/original/media.jpg",
"avatar_static": "https://media.animalliberation.social/accounts/avatars/000/000/001/original/media.jpg",
"header": "https://media.animalliberation.social/accounts/headers/000/000/001/original/09887023017e02c9.jpg",
"header_static": "https://media.animalliberation.social/accounts/headers/000/000/001/original/09887023017e02c9.jpg",
"followers_count": 236,
"following_count": 83,
"statuses_count": 357,
"last_status_at": "2021-02-20T19:28:24.353Z",
"emojis": [],
"fields": []
}
}

View File

@ -0,0 +1,128 @@
{
"uri": "mastodon.social",
"title": "Mastodon",
"short_description": "Server run by the main developers of the project <img draggable=\"false\" alt=\"🐘\" class=\"emojione\" src=\"https://mastodon.social/emoji/1f418.svg\" /> It is not focused on any particular niche interest - everyone is welcome as long as you follow our code of conduct!",
"description": "Server run by the main developers of the project <img draggable=\"false\" alt=\"🐘\" class=\"emojione\" src=\"https://mastodon.social/emoji/1f418.svg\" /> It is not focused on any particular niche interest - everyone is welcome as long as you follow our code of conduct!",
"email": "staff@mastodon.social",
"version": "3.4.3",
"urls": {
"streaming_api": "wss://mastodon.social"
},
"stats": {
"user_count": 619022,
"status_count": 33914684,
"domain_count": 21524
},
"thumbnail": "https://files.mastodon.social/site_uploads/files/000/000/001/original/vlcsnap-2018-08-27-16h43m11s127.png",
"languages": [
"en"
],
"registrations": true,
"approval_required": false,
"invites_enabled": true,
"configuration": {
"statuses": {
"max_characters": 500,
"max_media_attachments": 4,
"characters_reserved_per_url": 23
},
"media_attachments": {
"supported_mime_types": [
"image/jpeg",
"image/png",
"image/gif",
"video/webm",
"video/mp4",
"video/quicktime",
"video/ogg",
"audio/wave",
"audio/wav",
"audio/x-wav",
"audio/x-pn-wave",
"audio/ogg",
"audio/vorbis",
"audio/mpeg",
"audio/mp3",
"audio/webm",
"audio/flac",
"audio/aac",
"audio/m4a",
"audio/x-m4a",
"audio/mp4",
"audio/3gpp",
"video/x-ms-asf"
],
"image_size_limit": 10485760,
"image_matrix_limit": 16777216,
"video_size_limit": 41943040,
"video_frame_rate_limit": 60,
"video_matrix_limit": 2304000
},
"polls": {
"max_options": 4,
"max_characters_per_option": 50,
"min_expiration": 300,
"max_expiration": 2629746
}
},
"contact_account": {
"id": "1",
"username": "Gargron",
"acct": "Gargron",
"display_name": "Eugen 🎄",
"locked": false,
"bot": false,
"discoverable": true,
"group": false,
"created_at": "2016-03-16T00:00:00.000Z",
"note": "<p>Founder, CEO and lead developer <span class=\"h-card\"><a href=\"https://mastodon.social/@Mastodon\" class=\"u-url mention\">@<span>Mastodon</span></a></span>, Germany.</p>",
"url": "https://mastodon.social/@Gargron",
"avatar": "https://files.mastodon.social/accounts/avatars/000/000/001/original/ccb05a778962e171.png",
"avatar_static": "https://files.mastodon.social/accounts/avatars/000/000/001/original/ccb05a778962e171.png",
"header": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg",
"header_static": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg",
"followers_count": 98343,
"following_count": 271,
"statuses_count": 71288,
"last_status_at": "2022-01-31",
"emojis": [],
"fields": [
{
"name": "Patreon",
"value": "<a href=\"https://www.patreon.com/mastodon\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://www.</span><span class=\"\">patreon.com/mastodon</span><span class=\"invisible\"></span></a>",
"verified_at": null
},
{
"name": "Homepage",
"value": "<a href=\"https://zeonfederated.com\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">zeonfederated.com</span><span class=\"invisible\"></span></a>",
"verified_at": "2019-07-15T18:29:57.191+00:00"
}
]
},
"rules": [
{
"id": "1",
"text": "Sexually explicit or violent media must be marked as sensitive when posting"
},
{
"id": "2",
"text": "No racism, sexism, homophobia, transphobia, xenophobia, or casteism"
},
{
"id": "3",
"text": "No incitement of violence or promotion of violent ideologies"
},
{
"id": "4",
"text": "No harassment, dogpiling or doxxing of other users"
},
{
"id": "5",
"text": "No content illegal in Germany"
},
{
"id": "6",
"text": "No spam, advertising or bot accounts"
}
]
}

View File

@ -0,0 +1,107 @@
[
{
"id": "017ed503-bc96-301a-e871-2c23b30ddd05",
"uri": "https://mitra.social/objects/017ed503-bc96-301a-e871-2c23b30ddd05",
"created_at": "2022-02-07T16:28:18.966874Z",
"account": {
"id": "017ed4f9-c121-2ae6-0805-15516cce02c3",
"username": "alex",
"acct": "alex",
"url": "https://mitra.social/users/alex",
"display_name": null,
"created_at": "2022-02-07T16:17:24.769229Z",
"note": null,
"avatar": null,
"header": null,
"fields": [],
"followers_count": 1,
"following_count": 1,
"statuses_count": 3,
"source": null,
"wallet_address": null
},
"content": "<span class=\"h-card\"><a class=\"u-url mention\" href=\"https://mitra.social/users/silverpill\">@silverpill</a></span> sup!",
"in_reply_to_id": null,
"reblog": null,
"visibility": "public",
"replies_count": 1,
"favourites_count": 0,
"reblogs_count": 0,
"media_attachments": [],
"mentions": [
{
"id": "dd4ebc18-269d-4c7b-a310-03d29c6ab551",
"username": "silverpill",
"acct": "silverpill",
"url": "https://mitra.social/users/silverpill"
}
],
"tags": [],
"favourited": false,
"reblogged": false,
"ipfs_cid": null,
"token_id": null,
"token_tx_id": null
},
{
"id": "017ed505-5926-392f-256a-f86d5075df70",
"uri": "https://mitra.social/objects/017ed505-5926-392f-256a-f86d5075df70",
"created_at": "2022-02-07T16:30:04.582771Z",
"account": {
"id": "dd4ebc18-269d-4c7b-a310-03d29c6ab551",
"username": "silverpill",
"acct": "silverpill",
"url": "https://mitra.social/users/silverpill",
"display_name": "silverpill",
"created_at": "2021-11-06T21:08:57.441927Z",
"note": "Admin of <a href=\"https://mitra.social/\" rel=\"noopener noreferrer\">mitra.social</a> instance. It is running experimental ActivityPub server <a href=\"https://codeberg.org/silverpill/mitra\" rel=\"noopener noreferrer\">Mitra</a>.",
"avatar": "https://mitra.social/media/6a785bf7dd05f61c3590e8935aa49156a499ac30fd1e402f79e7e164adb36e2c.png",
"header": null,
"fields": [
{
"name": "Matrix",
"value": "@silverpill:poa.st"
},
{
"name": "Alt",
"value": "@silverpill@poa.st"
},
{
"name": "Code",
"value": "<a href=\"https://codeberg.org/silverpill/\" rel=\"noopener noreferrer\">https://codeberg.org/silverpill/</a>"
},
{
"name": "$XMR",
"value": "884y9LmsWY7PQNsyR7bJy1dvj91tuF5spVabyCnPk4KfQtSuzFbQobTFC7xSemJgVW1FWAwnJbjTZX5zZWbBrfkv62DB62d"
}
],
"followers_count": 27,
"following_count": 15,
"statuses_count": 110,
"source": null,
"wallet_address": null
},
"content": "<span class=\"h-card\"><a class=\"u-url mention\" href=\"https://mitra.social/users/alex\">@alex</a></span> welcome",
"in_reply_to_id": "017ed503-bc96-301a-e871-2c23b30ddd05",
"reblog": null,
"visibility": "public",
"replies_count": 0,
"favourites_count": 1,
"reblogs_count": 0,
"media_attachments": [],
"mentions": [
{
"id": "017ed4f9-c121-2ae6-0805-15516cce02c3",
"username": "alex",
"acct": "alex",
"url": "https://mitra.social/users/alex"
}
],
"tags": [],
"favourited": true,
"reblogged": false,
"ipfs_cid": null,
"token_id": null,
"token_tx_id": null
}
]

View File

@ -0,0 +1,95 @@
{
"id": "017eeb0e-e5e7-98fe-6b2b-ad02349251fb",
"uri": "https://gleasonator.com/objects/aa5e66c9-0a10-4167-9c80-f40d9574aaec",
"created_at": "2022-02-11T23:11:59.891770Z",
"account": {
"id": "8fe4d6ed-3a99-43e1-a7d4-66b4e635f756",
"username": "alex",
"acct": "alex@gleasonator.com",
"url": "https://gleasonator.com/users/alex",
"display_name": "Alex Gleason",
"created_at": "2021-11-14T17:01:17.446307Z",
"note": "I create Fediverse software that empowers people online.<br><br>I'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.",
"avatar": "https://mitra.social/media/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
"header": "https://mitra.social/media/bdfb009adac0e31257e9fe527d3844a7234cc71f6e06dff2bec94354639555dd.png",
"fields": [
{
"name": "Website",
"value": "<a href=\"https://alexgleason.me\" rel=\"noopener noreferrer\">https://alexgleason.me</a>"
},
{
"name": "Pleroma+Soapbox",
"value": "<a href=\"https://soapbox.pub\" rel=\"noopener noreferrer\">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=\"noopener noreferrer\">https://paypal.me/gleasonator</a>"
},
{
"name": "$BTC",
"value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
},
{
"name": "$ETH",
"value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
},
{
"name": "$DOGE",
"value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
},
{
"name": "$XMR",
"value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
}
],
"followers_count": 2,
"following_count": 2,
"statuses_count": 970,
"source": null,
"wallet_address": null
},
"content": "<p>Test</p>",
"in_reply_to_id": null,
"reblog": null,
"visibility": "public",
"replies_count": 0,
"favourites_count": 0,
"reblogs_count": 0,
"media_attachments": [
{
"id": "017eeb0e-e5df-30a4-77a7-a929145cb836",
"type": "image",
"url": "https://mitra.social/media/8e04e6091bbbac79641b5812508683ce72c38693661c18d16040553f2371e18d.png"
},
{
"id": "017eeb0e-e5e4-2a48-2889-afdebf368a54",
"type": "unknown",
"url": "https://mitra.social/media/8f72dc2e98572eb4ba7c3a902bca5f69c448fc4391837e5f8f0d4556280440ac"
},
{
"id": "017eeb0e-e5e5-79fd-6054-8b6869b1db49",
"type": "unknown",
"url": "https://mitra.social/media/55a81a090247cc4fc127e5716bcf7964f6e0df9b584f85f4696c0b994747a4d0.oga"
},
{
"id": "017eeb0e-e5e6-c416-a444-21e560c47839",
"type": "unknown",
"url": "https://mitra.social/media/0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0"
}
],
"mentions": [],
"tags": [],
"favourited": false,
"reblogged": false,
"ipfs_cid": null,
"token_id": null,
"token_tx_id": null
}

View File

@ -0,0 +1,131 @@
{
"approval_required": true,
"avatar_upload_limit": 2000000,
"background_image": "https://gleasonator.com/images/city.jpg",
"background_upload_limit": 4000000,
"banner_upload_limit": 4000000,
"description": "Building the next generation of the Fediverse. Speak freely.",
"description_limit": 5000,
"email": "alex@alexgleason.me",
"languages": [
"en"
],
"max_toot_chars": 5000,
"pleroma": {
"metadata": {
"account_activation_required": false,
"birthday_min_age": 0,
"birthday_required": false,
"features": [
"pleroma_api",
"mastodon_api",
"mastodon_api_streaming",
"polls",
"v2_suggestions",
"pleroma_explicit_addressing",
"shareable_emoji_packs",
"multifetch",
"pleroma:api/v1/notifications:include_types_filter",
"quote_posting",
"media_proxy",
"relay",
"pleroma_emoji_reactions",
"pleroma_chat_messages",
"email_list",
"profile_directory"
],
"federation": {
"enabled": true,
"exclusions": false,
"mrf_hashtag": {
"federated_timeline_removal": [],
"reject": [],
"sensitive": [
"nsfw"
]
},
"mrf_policies": [
"TagPolicy",
"SimplePolicy",
"InlineQuotePolicy",
"HashtagPolicy"
],
"mrf_simple": {
"accept": [],
"avatar_removal": [
"pawoo.net",
"sinblr.com",
"dajiaweibo.com",
"baraag.net"
],
"banner_removal": [
"pawoo.net",
"sinblr.com",
"dajiaweibo.com",
"baraag.net"
],
"federated_timeline_removal": [],
"followers_only": [],
"media_nsfw": [],
"media_removal": [
"pawoo.net",
"sinblr.com",
"dajiaweibo.com",
"baraag.net"
],
"reject": [
"solagg.com"
],
"reject_deletes": [],
"report_removal": []
},
"mrf_simple_info": {},
"quarantined_instances": [],
"quarantined_instances_info": {
"quarantined_instances": {}
}
},
"fields_limits": {
"max_fields": 15,
"max_remote_fields": 20,
"name_length": 512,
"value_length": 2048
},
"post_formats": [
"text/plain",
"text/html",
"text/markdown",
"text/bbcode"
],
"privileged_staff": true
},
"stats": {
"mau": 71
},
"vapid_public_key": "BLElLQVJVmY_e4F5JoYxI5jXiVOYNsJ9p-amkykc9NcI-jwa9T1Y2GIbDqbY-HqC6ayPkfW4K4o9vgBFKYmkuS4"
},
"poll_limits": {
"max_expiration": 31536000,
"max_option_chars": 200,
"max_options": 20,
"min_expiration": 0
},
"registrations": true,
"shout_limit": 5000,
"soapbox": {
"version": "1.1.1"
},
"stats": {
"domain_count": 8140,
"status_count": 101956,
"user_count": 421
},
"thumbnail": "https://media.gleasonator.com/c0d38bde6ef0b3baa483f574797662ebd83ef9e1a1162e8e4fcd930bb4b3c068.png",
"title": "Gleasonator",
"upload_limit": 100000000,
"uri": "https://gleasonator.com",
"urls": {
"streaming_api": "wss://gleasonator.com"
},
"version": "2.7.2 (compatible; Pleroma 2.4.51-1129-gf2cfef09-gleasonator)"
}

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,238 @@
{
"account": {
"acct": "alex",
"avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
"avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
"bot": false,
"created_at": "2020-01-08T01:25:43.000Z",
"display_name": "Alex Gleason",
"emojis": [],
"fields": [
{
"name": "Website",
"value": "<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": 2344,
"following_count": 1564,
"fqn": "alex@gleasonator.com",
"header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"id": "9v5bmRalQvjOy0ECcC",
"last_status_at": "2022-02-11T23:12:00",
"locked": false,
"note": "I create Fediverse software that empowers people online.<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,
"birthday": "1993-07-03",
"favicon": "https://gleasonator.com/favicon.png",
"hide_favorites": true,
"hide_followers": false,
"hide_followers_count": false,
"hide_follows": false,
"hide_follows_count": false,
"is_admin": true,
"is_confirmed": true,
"is_moderator": false,
"is_suggested": true,
"relationship": {},
"skip_thread_containment": false,
"tags": []
},
"source": {
"fields": [
{
"name": "Website",
"value": "https://alexgleason.me"
},
{
"name": "Pleroma+Soapbox",
"value": "https://soapbox.pub"
},
{
"name": "Email",
"value": "alex@alexgleason.me"
},
{
"name": "Gender identity",
"value": "Soyboy"
},
{
"name": "Donate (PayPal)",
"value": "https://paypal.me/gleasonator"
},
{
"name": "$BTC",
"value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
},
{
"name": "$ETH",
"value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
},
{
"name": "$DOGE",
"value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
},
{
"name": "$XMR",
"value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
}
],
"note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
"pleroma": {
"actor_type": "Person",
"discoverable": false
},
"sensitive": false
},
"statuses_count": 23357,
"url": "https://gleasonator.com/users/alex",
"username": "alex"
},
"application": {
"name": "Soapbox FE",
"website": "https://soapbox.pub/"
},
"bookmarked": false,
"card": null,
"content": "<p>Test</p>",
"created_at": "2022-02-11T23:11:59.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 1,
"id": "AGNkA21auFR5lnEAHw",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [
{
"blurhash": "emLqe9t7~q%M%M-;WBt7ofRj%Moft7ofoft7ayWBj[of-;j[ayofM{",
"description": "",
"id": "974611173",
"meta": {
"original": {
"aspect": 0.9944598337950139,
"height": 1444,
"width": 1436
}
},
"pleroma": {
"mime_type": "image/png"
},
"preview_url": "https://media.gleasonator.com/8e04e6091bbbac79641b5812508683ce72c38693661c18d16040553f2371e18d.png",
"remote_url": "https://media.gleasonator.com/8e04e6091bbbac79641b5812508683ce72c38693661c18d16040553f2371e18d.png",
"text_url": "https://media.gleasonator.com/8e04e6091bbbac79641b5812508683ce72c38693661c18d16040553f2371e18d.png",
"type": "image",
"url": "https://media.gleasonator.com/8e04e6091bbbac79641b5812508683ce72c38693661c18d16040553f2371e18d.png"
},
{
"blurhash": null,
"description": "",
"id": "-1764036199",
"pleroma": {
"mime_type": "application/x-nes-rom"
},
"preview_url": "https://media.gleasonator.com/8f72dc2e98572eb4ba7c3a902bca5f69c448fc4391837e5f8f0d4556280440ac.nes",
"remote_url": "https://media.gleasonator.com/8f72dc2e98572eb4ba7c3a902bca5f69c448fc4391837e5f8f0d4556280440ac.nes",
"text_url": "https://media.gleasonator.com/8f72dc2e98572eb4ba7c3a902bca5f69c448fc4391837e5f8f0d4556280440ac.nes",
"type": "unknown",
"url": "https://media.gleasonator.com/8f72dc2e98572eb4ba7c3a902bca5f69c448fc4391837e5f8f0d4556280440ac.nes"
},
{
"blurhash": null,
"description": "",
"id": "-636167741",
"pleroma": {
"mime_type": "audio/ogg"
},
"preview_url": "https://media.gleasonator.com/55a81a090247cc4fc127e5716bcf7964f6e0df9b584f85f4696c0b994747a4d0.ogg",
"remote_url": "https://media.gleasonator.com/55a81a090247cc4fc127e5716bcf7964f6e0df9b584f85f4696c0b994747a4d0.ogg",
"text_url": "https://media.gleasonator.com/55a81a090247cc4fc127e5716bcf7964f6e0df9b584f85f4696c0b994747a4d0.ogg",
"type": "audio",
"url": "https://media.gleasonator.com/55a81a090247cc4fc127e5716bcf7964f6e0df9b584f85f4696c0b994747a4d0.ogg"
},
{
"blurhash": null,
"description": "",
"id": "517941208",
"pleroma": {
"mime_type": "text/plain"
},
"preview_url": "https://media.gleasonator.com/0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0.LICENSE",
"remote_url": "https://media.gleasonator.com/0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0.LICENSE",
"text_url": "https://media.gleasonator.com/0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0.LICENSE",
"type": "unknown",
"url": "https://media.gleasonator.com/0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0.LICENSE"
}
],
"mentions": [],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "Test"
},
"conversation_id": "AGNkA1yP66srbtjcJc",
"direct_conversation_id": null,
"emoji_reactions": [],
"expires_at": null,
"in_reply_to_account_acct": null,
"local": true,
"parent_visible": false,
"pinned_at": null,
"quote": null,
"quote_url": null,
"quote_visible": false,
"spoiler_text": {
"text/plain": ""
},
"thread_muted": false
},
"poll": null,
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"text": null,
"uri": "https://gleasonator.com/objects/aa5e66c9-0a10-4167-9c80-f40d9574aaec",
"url": "https://gleasonator.com/notice/AGNkA21auFR5lnEAHw",
"visibility": "public"
}

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

@ -0,0 +1,29 @@
import { Map as ImmutableMap } from 'immutable';
import { STATUSES_IMPORT } from 'soapbox/actions/importer';
import { __stub } from 'soapbox/api';
import { mockStore } from 'soapbox/test_helpers';
import { fetchContext } from '../statuses';
describe('fetchContext()', () => {
it('handles Mitra context', done => {
const statuses = require('soapbox/__fixtures__/mitra-context.json');
__stub(mock => {
mock.onGet('/api/v1/statuses/017ed505-5926-392f-256a-f86d5075df70/context')
.reply(200, statuses);
});
const store = mockStore(ImmutableMap());
store.dispatch(fetchContext('017ed505-5926-392f-256a-f86d5075df70')).then(context => {
const actions = store.getActions();
expect(actions[3].type).toEqual(STATUSES_IMPORT);
expect(actions[3].statuses[0].id).toEqual('017ed503-bc96-301a-e871-2c23b30ddd05');
done();
}).catch(console.error);
});
});

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';
@ -99,6 +109,10 @@ export const NOTIFICATION_SETTINGS_REQUEST = 'NOTIFICATION_SETTINGS_REQUEST';
export const NOTIFICATION_SETTINGS_SUCCESS = 'NOTIFICATION_SETTINGS_SUCCESS';
export const NOTIFICATION_SETTINGS_FAIL = 'NOTIFICATION_SETTINGS_FAIL';
export const BIRTHDAY_REMINDERS_FETCH_REQUEST = 'BIRTHDAY_REMINDERS_FETCH_REQUEST';
export const BIRTHDAY_REMINDERS_FETCH_SUCCESS = 'BIRTHDAY_REMINDERS_FETCH_SUCCESS';
export const BIRTHDAY_REMINDERS_FETCH_FAIL = 'BIRTHDAY_REMINDERS_FETCH_FAIL';
export function createAccount(params) {
return (dispatch, getState) => {
dispatch({ type: ACCOUNT_CREATE_REQUEST, params });
@ -144,8 +158,9 @@ export function fetchAccountByUsername(username) {
const instance = state.get('instance');
const features = getFeatures(instance);
const me = state.get('me');
if (features.accountByUsername) {
if (features.accountByUsername && (me || !features.accountLookup)) {
api(getState).get(`/api/v1/accounts/${username}`).then(response => {
dispatch(fetchRelationships([response.data.id]));
dispatch(importFetchedAccount(response.data));
@ -154,6 +169,13 @@ export function fetchAccountByUsername(username) {
dispatch(fetchAccountFail(null, error));
dispatch(importErrorWhileFetchingAccountByUsername(username));
});
} else if (features.accountLookup) {
dispatch(accountLookup(username)).then(account => {
dispatch(fetchAccountSuccess(account));
}).catch(error => {
dispatch(fetchAccountFail(null, error));
dispatch(importErrorWhileFetchingAccountByUsername(username));
});
} else {
dispatch(accountSearch({
q: username,
@ -199,7 +221,7 @@ export function fetchAccountFail(id, error) {
};
}
export function followAccount(id, reblogs = true) {
export function followAccount(id, options = { reblogs: true }) {
return (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
@ -208,7 +230,7 @@ export function followAccount(id, reblogs = true) {
dispatch(followAccountRequest(id, locked));
api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => {
api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => {
dispatch(followAccountSuccess(response.data, alreadyFollowing));
}).catch(error => {
dispatch(followAccountFail(error, locked));
@ -948,6 +970,43 @@ export function unpinAccountFail(error) {
};
}
export function fetchPinnedAccounts(id) {
return (dispatch, getState) => {
dispatch(fetchPinnedAccountsRequest(id));
api(getState).get(`/api/v1/pleroma/accounts/${id}/endorsements`).then(response => {
dispatch(importFetchedAccounts(response.data));
dispatch(fetchPinnedAccountsSuccess(id, response.data, null));
}).catch(error => {
dispatch(fetchPinnedAccountsFail(id, error));
});
};
}
export function fetchPinnedAccountsRequest(id) {
return {
type: PINNED_ACCOUNTS_FETCH_REQUEST,
id,
};
}
export function fetchPinnedAccountsSuccess(id, accounts, next) {
return {
type: PINNED_ACCOUNTS_FETCH_SUCCESS,
id,
accounts,
next,
};
}
export function fetchPinnedAccountsFail(id, error) {
return {
type: PINNED_ACCOUNTS_FETCH_FAIL,
id,
error,
};
}
export function accountSearch(params, cancelToken) {
return (dispatch, getState) => {
dispatch({ type: ACCOUNT_SEARCH_REQUEST, params });
@ -961,3 +1020,40 @@ export function accountSearch(params, cancelToken) {
});
};
}
export function accountLookup(acct, cancelToken) {
return (dispatch, getState) => {
dispatch({ type: ACCOUNT_LOOKUP_REQUEST, acct });
return api(getState).get('/api/v1/accounts/lookup', { params: { acct }, cancelToken }).then(({ data: account }) => {
if (account && account.id) dispatch(importFetchedAccount(account));
dispatch({ type: ACCOUNT_LOOKUP_SUCCESS, account });
return account;
}).catch(error => {
dispatch({ type: ACCOUNT_LOOKUP_FAIL });
throw error;
});
};
}
export function fetchBirthdayReminders(day, month) {
return (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
const me = getState().get('me');
dispatch({ type: BIRTHDAY_REMINDERS_FETCH_REQUEST, day, month, id: me });
api(getState).get('/api/v1/pleroma/birthdays', { params: { day, month } }).then(response => {
dispatch(importFetchedAccounts(response.data));
dispatch({
type: BIRTHDAY_REMINDERS_FETCH_SUCCESS,
accounts: response.data,
day,
month,
id: me,
});
}).catch(error => {
dispatch({ type: BIRTHDAY_REMINDERS_FETCH_FAIL, day, month, id: me });
});
};
}

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,18 @@
import { defineMessages } from 'react-intl';
import api from '../api';
import { importFetchedAccount, importFetchedAccounts } from './importer';
import { showAlertForError } from './alerts';
import snackbar from './snackbar';
import { isLoggedIn } from 'soapbox/utils/auth';
import { ME_PATCH_SUCCESS } from './me';
import { getFeatures } from 'soapbox/utils/features';
import api from '../api';
import { showAlertForError } from './alerts';
import { importFetchedAccounts } from './importer';
import { patchMeSuccess } from './me';
import snackbar from './snackbar';
export const ALIASES_FETCH_REQUEST = 'ALIASES_FETCH_REQUEST';
export const ALIASES_FETCH_SUCCESS = 'ALIASES_FETCH_SUCCESS';
export const ALIASES_FETCH_FAIL = 'ALIASES_FETCH_FAIL';
export const ALIASES_SUGGESTIONS_CHANGE = 'ALIASES_SUGGESTIONS_CHANGE';
export const ALIASES_SUGGESTIONS_READY = 'ALIASES_SUGGESTIONS_READY';
@ -23,6 +31,38 @@ const messages = defineMessages({
removeSuccess: { id: 'aliases.success.remove', defaultMessage: 'Account alias removed successfully' },
});
export const fetchAliases = (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
const state = getState();
const instance = state.get('instance');
const features = getFeatures(instance);
if (!features.accountMoving) return;
dispatch(fetchAliasesRequest());
api(getState).get('/api/pleroma/aliases')
.then(response => {
dispatch(fetchAliasesSuccess(response.data.aliases));
})
.catch(err => dispatch(fetchAliasesFail(err)));
};
export const fetchAliasesRequest = () => ({
type: ALIASES_FETCH_REQUEST,
});
export const fetchAliasesSuccess = aliases => ({
type: ALIASES_FETCH_SUCCESS,
value: aliases,
});
export const fetchAliasesFail = error => ({
type: ALIASES_FETCH_FAIL,
error,
});
export const fetchAliasesSuggestions = q => (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
@ -53,80 +93,104 @@ export const changeAliasesSuggestions = value => ({
value,
});
export const addToAliases = (intl, apId) => (dispatch, getState) => {
export const addToAliases = (intl, account) => (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
const state = getState();
const me = state.get('me');
const alsoKnownAs = state.getIn(['accounts_meta', me, 'pleroma', 'also_known_as']);
const instance = state.get('instance');
const features = getFeatures(instance);
dispatch(addToAliasesRequest(apId));
if (!features.accountMoving) {
const me = state.get('me');
const alsoKnownAs = state.getIn(['accounts_meta', me, 'pleroma', 'also_known_as']);
api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: [...alsoKnownAs, apId] })
.then((response => {
dispatch(addToAliasesRequest());
api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: [...alsoKnownAs, account.getIn(['pleroma', 'ap_id'])] })
.then((response => {
dispatch(snackbar.success(intl.formatMessage(messages.createSuccess)));
dispatch(addToAliasesSuccess);
dispatch(patchMeSuccess(response.data));
}))
.catch(err => dispatch(addToAliasesFail(err)));
return;
}
dispatch(addToAliasesRequest());
api(getState).put('/api/pleroma/aliases', {
alias: account.get('acct'),
})
.then(response => {
dispatch(snackbar.success(intl.formatMessage(messages.createSuccess)));
dispatch(addToAliasesSuccess(response.data));
}))
.catch(err => dispatch(addToAliasesFail(err)));
dispatch(addToAliasesSuccess);
dispatch(fetchAliases);
})
.catch(err => dispatch(fetchAliasesFail(err)));
};
export const addToAliasesRequest = (apId) => ({
export const addToAliasesRequest = () => ({
type: ALIASES_ADD_REQUEST,
apId,
});
export const addToAliasesSuccess = me => dispatch => {
dispatch(importFetchedAccount(me));
dispatch({
type: ME_PATCH_SUCCESS,
me,
});
dispatch({
type: ALIASES_ADD_SUCCESS,
});
};
export const addToAliasesSuccess = () => ({
type: ALIASES_ADD_SUCCESS,
});
export const addToAliasesFail = (apId, error) => ({
export const addToAliasesFail = error => ({
type: ALIASES_ADD_FAIL,
apId,
error,
});
export const removeFromAliases = (intl, apId) => (dispatch, getState) => {
export const removeFromAliases = (intl, account) => (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
const state = getState();
const me = state.get('me');
const alsoKnownAs = state.getIn(['accounts_meta', me, 'pleroma', 'also_known_as']);
const instance = state.get('instance');
const features = getFeatures(instance);
dispatch(removeFromAliasesRequest(apId));
if (!features.accountMoving) {
const me = state.get('me');
const alsoKnownAs = state.getIn(['accounts_meta', me, 'pleroma', 'also_known_as']);
api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: alsoKnownAs.filter(id => id !== apId) })
dispatch(removeFromAliasesRequest());
api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: alsoKnownAs.filter(id => id !== account) })
.then(response => {
dispatch(snackbar.success(intl.formatMessage(messages.removeSuccess)));
dispatch(removeFromAliasesSuccess);
dispatch(patchMeSuccess(response.data));
})
.catch(err => dispatch(removeFromAliasesFail(err)));
return;
}
dispatch(addToAliasesRequest());
api(getState).delete('/api/pleroma/aliases', {
data: {
alias: account,
},
})
.then(response => {
dispatch(snackbar.success(intl.formatMessage(messages.removeSuccess)));
dispatch(removeFromAliasesSuccess(response.data));
dispatch(removeFromAliasesSuccess);
dispatch(fetchAliases);
})
.catch(err => dispatch(removeFromAliasesFail(apId, err)));
.catch(err => dispatch(fetchAliasesFail(err)));
};
export const removeFromAliasesRequest = (apId) => ({
export const removeFromAliasesRequest = () => ({
type: ALIASES_REMOVE_REQUEST,
apId,
});
export const removeFromAliasesSuccess = me => dispatch => {
dispatch(importFetchedAccount(me));
dispatch({
type: ME_PATCH_SUCCESS,
me,
});
dispatch({
type: ALIASES_REMOVE_SUCCESS,
});
};
export const removeFromAliasesSuccess = () => ({
type: ALIASES_REMOVE_SUCCESS,
});
export const removeFromAliasesFail = (apId, error) => ({
export const removeFromAliasesFail = error => ({
type: ALIASES_REMOVE_FAIL,
apId,
error,
});

View File

@ -8,18 +8,21 @@
*/
import { defineMessages } from 'react-intl';
import api, { baseClient } from '../api';
import { importFetchedAccount } from './importer';
import snackbar from 'soapbox/actions/snackbar';
import { createAccount } from 'soapbox/actions/accounts';
import { fetchMeSuccess, fetchMeFail } from 'soapbox/actions/me';
import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth';
import { createApp } from 'soapbox/actions/apps';
import { fetchMeSuccess, fetchMeFail } from 'soapbox/actions/me';
import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth';
import snackbar from 'soapbox/actions/snackbar';
import KVStore from 'soapbox/storage/kv_store';
import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth';
import sourceCode from 'soapbox/utils/code';
import { getFeatures } from 'soapbox/utils/features';
import { isStandalone } from 'soapbox/utils/state';
import KVStore from 'soapbox/storage/kv_store';
import api, { baseClient } from '../api';
import { importFetchedAccount } from './importer';
export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT';
@ -140,6 +143,7 @@ export function otpVerify(code, mfa_token) {
code: code,
challenge_type: 'totp',
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
scope: getScopes(getState()),
}).then(({ data: token }) => dispatch(authLoggedIn(token)));
};
}
@ -186,7 +190,7 @@ export function loadCredentials(token, accountUrl) {
export function logIn(intl, username, password) {
return (dispatch, getState) => {
return dispatch(createAppAndToken()).then(() => {
return dispatch(createAuthApp()).then(() => {
return dispatch(createUserToken(username, password));
}).catch(error => {
if (error.response.data.error === 'mfa_required') {

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

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 { openModal, closeModal } from './modal';
import { getSettings } from './settings';
import { getFeatures } from 'soapbox/utils/features';
import { useEmoji } from './emojis';
import { importFetchedAccounts } from './importer';
import { uploadMedia, fetchMedia, updateMedia } from './media';
import { isLoggedIn } from 'soapbox/utils/auth';
import { openModal, closeModal } from './modals';
import { getSettings } from './settings';
import { createStatus } from './statuses';
import snackbar from 'soapbox/actions/snackbar';
let cancelFetchComposeSuggestionsAccounts;
@ -24,6 +27,8 @@ export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
export const COMPOSE_REPLY = 'COMPOSE_REPLY';
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
export const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
export const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL';
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
export const COMPOSE_RESET = 'COMPOSE_RESET';
@ -68,11 +73,15 @@ export const COMPOSE_SCHEDULE_ADD = 'COMPOSE_SCHEDULE_ADD';
export const COMPOSE_SCHEDULE_SET = 'COMPOSE_SCHEDULE_SET';
export const COMPOSE_SCHEDULE_REMOVE = 'COMPOSE_SCHEDULE_REMOVE';
export const COMPOSE_ADD_TO_MENTIONS = 'COMPOSE_ADD_TO_MENTIONS';
export const COMPOSE_REMOVE_FROM_MENTIONS = 'COMPOSE_REMOVE_FROM_MENTIONS';
const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' },
success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent' },
view: { id: 'snackbar.view', defaultMessage: 'View' },
});
const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1);
@ -93,10 +102,14 @@ export function changeCompose(text) {
export function replyCompose(status, routerHistory) {
return (dispatch, getState) => {
const state = getState();
const instance = state.get('instance');
const { explicitAddressing } = getFeatures(instance);
dispatch({
type: COMPOSE_REPLY,
status: status,
account: state.getIn(['accounts', state.get('me')]),
explicitAddressing,
});
dispatch(openModal('COMPOSE'));
@ -109,6 +122,29 @@ export function cancelReplyCompose() {
};
}
export function quoteCompose(status, routerHistory) {
return (dispatch, getState) => {
const state = getState();
const instance = state.get('instance');
const { explicitAddressing } = getFeatures(instance);
dispatch({
type: COMPOSE_QUOTE,
status: status,
account: state.getIn(['accounts', state.get('me')]),
explicitAddressing,
});
dispatch(openModal('COMPOSE'));
};
}
export function cancelQuoteCompose() {
return {
type: COMPOSE_QUOTE_CANCEL,
};
}
export function resetCompose() {
return {
type: COMPOSE_RESET,
@ -155,7 +191,7 @@ export function handleComposeSubmit(dispatch, getState, data, status) {
dispatch(insertIntoTagHistory(data.tags || [], status));
dispatch(submitComposeSuccess({ ...data }));
dispatch(snackbar.success(messages.success));
dispatch(snackbar.success(messages.success, messages.view, `/@${data.account.acct}/posts/${data.id}`));
}
const needsDescriptions = state => {
@ -183,6 +219,7 @@ export function submitCompose(routerHistory, force = false) {
const status = state.getIn(['compose', 'text'], '');
const media = state.getIn(['compose', 'media_attachments']);
let to = state.getIn(['compose', 'to'], null);
if (!validateSchedule(state)) {
dispatch(snackbar.error(messages.scheduleError));
@ -200,6 +237,13 @@ export function submitCompose(routerHistory, force = false) {
return;
}
if (to && status) {
const mentions = status.match(/(?:^|\s|\.)@([a-z0-9_]+(?:@[a-z0-9\.\-]+)?)/gi); // not a perfect regex
if (mentions)
to = to.union(mentions.map(mention => mention.trim().slice(1)));
}
dispatch(submitComposeRequest());
dispatch(closeModal());
@ -208,6 +252,7 @@ export function submitCompose(routerHistory, force = false) {
const params = {
status,
in_reply_to_id: state.getIn(['compose', 'in_reply_to'], null),
quote_id: state.getIn(['compose', 'quote'], null),
media_ids: media.map(item => item.get('id')),
sensitive: state.getIn(['compose', 'sensitive']),
spoiler_text: state.getIn(['compose', 'spoiler_text'], ''),
@ -215,6 +260,7 @@ export function submitCompose(routerHistory, force = false) {
content_type: state.getIn(['compose', 'content_type']),
poll: state.getIn(['compose', 'poll'], null),
scheduled_at: state.getIn(['compose', 'schedule'], null),
to,
};
dispatch(createStatus(params, idempotencyKey)).then(function(data) {
@ -251,8 +297,7 @@ export function submitComposeFail(error) {
export function uploadCompose(files) {
return function(dispatch, getState) {
if (!isLoggedIn(getState)) return;
const instance = getState().get('instance');
const { attachmentLimit } = getFeatures(instance);
const attachmentLimit = getState().getIn(['instance', 'configuration', 'statuses', 'max_media_attachments']);
const media = getState().getIn(['compose', 'media_attachments']);
const progress = new Array(files.length).fill(0);
@ -643,3 +688,27 @@ export function openComposeWithText(text = '') {
dispatch(changeCompose(text));
};
}
export function addToMentions(accountId) {
return (dispatch, getState) => {
const state = getState();
const acct = state.getIn(['accounts', accountId, 'acct']);
return dispatch({
type: COMPOSE_ADD_TO_MENTIONS,
account: acct,
});
};
}
export function removeFromMentions(accountId) {
return (dispatch, getState) => {
const state = getState();
const acct = state.getIn(['accounts', accountId, 'acct']);
return dispatch({
type: COMPOSE_REMOVE_FROM_MENTIONS,
account: acct,
});
};
}

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,25 @@
* @see module:soapbox/actions/oauth
*/
import { baseClient } from '../api';
import { createApp } from 'soapbox/actions/apps';
import { obtainOAuthToken } from 'soapbox/actions/oauth';
import { authLoggedIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth';
import { parseBaseURL } from 'soapbox/utils/auth';
import { getFeatures } from 'soapbox/utils/features';
import sourceCode from 'soapbox/utils/code';
import { Map as ImmutableMap, fromJS } from 'immutable';
import { createApp } from 'soapbox/actions/apps';
import { authLoggedIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth';
import { obtainOAuthToken } from 'soapbox/actions/oauth';
import { parseBaseURL } from 'soapbox/utils/auth';
import sourceCode from 'soapbox/utils/code';
import { getWalletAndSign } from 'soapbox/utils/ethereum';
import { getFeatures } from 'soapbox/utils/features';
import { getQuirks } from 'soapbox/utils/quirks';
import { baseClient } from '../api';
const fetchExternalInstance = baseURL => {
return baseClient(null, baseURL)
.get('/api/v1/instance')
.then(({ data: instance }) => fromJS(instance))
.catch(error => {
if (error.response && error.response.status === 401) {
if (error.response?.status === 401) {
// Authenticated fetch is enabled.
// Continue with a limited featureset.
return ImmutableMap({ version: '0.0.0' });
@ -30,36 +34,86 @@ const fetchExternalInstance = baseURL => {
});
};
export function createAppAndRedirect(host) {
function createExternalApp(instance, baseURL) {
return (dispatch, getState) => {
// Mitra: skip creating the auth app
if (getQuirks(instance).noApps) return new Promise(f => f({}));
const { scopes } = getFeatures(instance);
const params = {
client_name: sourceCode.displayName,
redirect_uris: `${window.location.origin}/auth/external`,
website: sourceCode.homepage,
scopes,
};
return dispatch(createApp(params, baseURL));
};
}
function externalAuthorize(instance, baseURL) {
return (dispatch, getState) => {
const { scopes } = getFeatures(instance);
return dispatch(createExternalApp(instance, baseURL)).then(app => {
const { client_id, redirect_uri } = app;
const query = new URLSearchParams({
client_id,
redirect_uri,
response_type: 'code',
scope: scopes,
});
localStorage.setItem('soapbox:external:app', JSON.stringify(app));
localStorage.setItem('soapbox:external:baseurl', baseURL);
localStorage.setItem('soapbox:external:scopes', scopes);
window.location.href = `${baseURL}/oauth/authorize?${query.toString()}`;
});
};
}
export function externalEthereumLogin(instance, baseURL) {
return (dispatch, getState) => {
const loginMessage = instance.get('login_message');
return getWalletAndSign(loginMessage).then(({ wallet, signature }) => {
return dispatch(createExternalApp(instance, baseURL)).then(app => {
const params = {
grant_type: 'ethereum',
wallet_address: wallet.toLowerCase(),
client_id: app.client_id,
client_secret: app.client_secret,
password: signature,
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
scope: getFeatures(instance).scopes,
};
return dispatch(obtainOAuthToken(params, baseURL))
.then(token => dispatch(authLoggedIn(token)))
.then(({ access_token }) => dispatch(verifyCredentials(access_token, baseURL)))
.then(account => dispatch(switchAccount(account.id)))
.then(() => window.location.href = '/');
});
});
};
}
export function externalLogin(host) {
return (dispatch, getState) => {
const baseURL = parseBaseURL(host) || parseBaseURL(`https://${host}`);
return fetchExternalInstance(baseURL).then(instance => {
const { scopes } = getFeatures(instance);
const features = getFeatures(instance);
const quirks = getQuirks(instance);
const params = {
client_name: sourceCode.displayName,
redirect_uris: `${window.location.origin}/auth/external`,
website: sourceCode.homepage,
scopes,
};
return dispatch(createApp(params, baseURL)).then(app => {
const { client_id, redirect_uri } = app;
const query = new URLSearchParams({
client_id,
redirect_uri,
response_type: 'code',
scope: scopes,
});
localStorage.setItem('soapbox:external:app', JSON.stringify(app));
localStorage.setItem('soapbox:external:baseurl', baseURL);
localStorage.setItem('soapbox:external:scopes', scopes);
window.location.href = `${baseURL}/oauth/authorize?${query.toString()}`;
});
if (features.ethereumLogin && quirks.noOAuthForm) {
return dispatch(externalEthereumLogin(instance, baseURL));
} else {
return dispatch(externalAuthorize(instance, baseURL));
}
});
};
}

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);
@ -70,11 +65,20 @@ export function importFetchedStatus(status, idempotencyKey) {
const normalizedStatus = normalizeStatus(status, normalOldStatus, expandSpoilers);
if (status.reblog && status.reblog.id) {
if (status.reblog?.id) {
dispatch(importFetchedStatus(status.reblog));
}
if (status.poll && status.poll.id) {
// Fedibird quotes
if (status.quote?.id) {
dispatch(importFetchedStatus(status.quote));
}
if (status.pleroma?.quote?.id) {
dispatch(importFetchedStatus(status.pleroma.quote));
}
if (status.poll?.id) {
dispatch(importFetchedPoll(status.poll));
}
@ -112,15 +116,24 @@ export function importFetchedStatuses(statuses) {
const normalOldStatus = getState().getIn(['statuses', status.id]);
const expandSpoilers = getSettings(getState()).get('expandSpoilers');
pushUnique(normalStatuses, normalizeStatus(status, normalOldStatus, expandSpoilers));
pushUnique(accounts, status.account);
normalStatuses.push(normalizeStatus(status, normalOldStatus, expandSpoilers));
accounts.push(status.account);
if (status.reblog && status.reblog.id) {
if (status.reblog?.id) {
processStatus(status.reblog);
}
if (status.poll && status.poll.id) {
pushUnique(polls, normalizePoll(status.poll));
// Fedibird quotes
if (status.quote?.id) {
processStatus(status.quote);
}
if (status.pleroma?.quote?.id) {
processStatus(status.pleroma.quote);
}
if (status.poll?.id) {
polls.push(normalizePoll(status.poll));
}
}

View File

@ -1,4 +1,7 @@
import escapeTextContentForBrowser from 'escape-html';
import { stripCompatibilityFeatures } from 'soapbox/utils/html';
import emojify from '../../features/emoji/emoji';
import { unescapeHTML } from '../../utils/html';
@ -12,6 +15,13 @@ const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => {
export function normalizeAccount(account) {
account = { ...account };
// Some backends can return null, or omit these required fields
if (!account.emojis) account.emojis = [];
if (!account.display_name) account.display_name = '';
if (!account.note) account.note = '';
if (!account.avatar) account.avatar = account.avatar_static || require('images/avatar-missing.png');
if (!account.avatar_static) account.avatar_static = account.avatar;
const emojiMap = makeEmojiMap(account);
const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name;
@ -36,18 +46,39 @@ export function normalizeAccount(account) {
}
export function normalizeStatus(status, normalOldStatus, expandSpoilers) {
const normalStatus = { ...status };
const normalStatus = { ...status };
// Some backends can return null, or omit these required fields
if (!normalStatus.emojis) normalStatus.emojis = [];
if (!normalStatus.spoiler_text) normalStatus.spoiler_text = '';
// Copy the pleroma object too, so we can modify our copy
if (status.pleroma) {
normalStatus.pleroma = { ...status.pleroma };
}
normalStatus.account = status.account.id;
if (status.reblog && status.reblog.id) {
if (status.reblog?.id) {
normalStatus.reblog = status.reblog.id;
}
if (status.poll && status.poll.id) {
if (status.poll?.id) {
normalStatus.poll = status.poll.id;
}
if (status.pleroma?.quote?.id) {
// Normalize quote to the top-level, so delete the original for performance
normalStatus.quote = status.pleroma.quote.id;
delete normalStatus.pleroma.quote;
} else if (status.quote?.id) {
// Fedibird compatibility, because why not
normalStatus.quote = status.quote.id;
} else if (status.quote_id) {
// Fedibird: fall back to quote_id
normalStatus.quote = status.quote_id;
}
// Only calculate these values when status first encountered
// Otherwise keep the ones already in the reducer
if (normalOldStatus) {
@ -57,11 +88,11 @@ export function normalizeStatus(status, normalOldStatus, expandSpoilers) {
normalStatus.hidden = normalOldStatus.get('hidden');
} else {
const spoilerText = normalStatus.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
const searchContent = ([spoilerText, status.content].concat((status.poll?.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
const emojiMap = makeEmojiMap(normalStatus);
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.contentHtml = stripCompatibilityFeatures(emojify(normalStatus.content, emojiMap));
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
}
@ -76,7 +107,7 @@ export function normalizePoll(poll) {
normalPoll.options = poll.options.map((option, index) => ({
...option,
voted: poll.own_votes && poll.own_votes.includes(index),
voted: Boolean(poll.own_votes?.includes(index)),
title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap),
}));

View File

@ -1,8 +1,10 @@
import api from '../api';
import { get } from 'lodash';
import { parseVersion } from 'soapbox/utils/features';
import { getAuthUserUrl } from 'soapbox/utils/auth';
import KVStore from 'soapbox/storage/kv_store';
import { getAuthUserUrl } from 'soapbox/utils/auth';
import { parseVersion } from 'soapbox/utils/features';
import api from '../api';
export const INSTANCE_FETCH_REQUEST = 'INSTANCE_FETCH_REQUEST';
export const INSTANCE_FETCH_SUCCESS = 'INSTANCE_FETCH_SUCCESS';
@ -22,7 +24,7 @@ const getMeUrl = state => {
};
// Figure out the appropriate instance to fetch depending on the state
const getHost = state => {
export const getHost = state => {
const accountUrl = getMeUrl(state) || getAuthUserUrl(state);
try {
@ -59,6 +61,7 @@ export function fetchInstance() {
dispatch(fetchNodeinfo()); // Pleroma < 2.1 backwards compatibility
}
}).catch(error => {
console.error(error);
dispatch({ type: INSTANCE_FETCH_FAIL, error, skipAlert: true });
});
};

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,9 +51,14 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
export const REMOTE_INTERACTION_REQUEST = 'REMOTE_INTERACTION_REQUEST';
export const REMOTE_INTERACTION_SUCCESS = 'REMOTE_INTERACTION_SUCCESS';
export const REMOTE_INTERACTION_FAIL = 'REMOTE_INTERACTION_FAIL';
const messages = defineMessages({
bookmarkAdded: { id: 'status.bookmarked', defaultMessage: 'Bookmark added.' },
bookmarkRemoved: { id: 'status.unbookmarked', defaultMessage: 'Bookmark removed.' },
view: { id: 'snackbar.view', defaultMessage: 'View' },
});
export function reblog(status) {
@ -77,7 +85,6 @@ export function unreblog(status) {
dispatch(unreblogRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(unreblogSuccess(status));
}).catch(error => {
dispatch(unreblogFail(status, error));
@ -157,7 +164,6 @@ export function unfavourite(status) {
dispatch(unfavouriteRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(unfavouriteSuccess(status));
}).catch(error => {
dispatch(unfavouriteFail(status, error));
@ -215,28 +221,28 @@ export function unfavouriteFail(status, error) {
};
}
export function bookmark(intl, status) {
export function bookmark(status) {
return function(dispatch, getState) {
dispatch(bookmarkRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function(response) {
dispatch(importFetchedStatus(response.data));
dispatch(bookmarkSuccess(status, response.data));
dispatch(snackbar.success(intl.formatMessage(messages.bookmarkAdded)));
dispatch(snackbar.success(messages.bookmarkAdded, messages.view, '/bookmarks'));
}).catch(function(error) {
dispatch(bookmarkFail(status, error));
});
};
}
export function unbookmark(intl, status) {
export function unbookmark(status) {
return (dispatch, getState) => {
dispatch(unbookmarkRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(unbookmarkSuccess(status, response.data));
dispatch(snackbar.success(intl.formatMessage(messages.bookmarkRemoved)));
dispatch(snackbar.success(messages.bookmarkRemoved));
}).catch(error => {
dispatch(unbookmarkFail(status, error));
});
@ -477,3 +483,46 @@ export function unpinFail(status, error) {
skipLoading: true,
};
}
export function remoteInteraction(ap_id, profile) {
return (dispatch, getState) => {
dispatch(remoteInteractionRequest(ap_id, profile));
return api(getState).post('/api/v1/pleroma/remote_interaction', { ap_id, profile }).then(({ data }) => {
if (data.error) throw new Error(data.error);
dispatch(remoteInteractionSuccess(ap_id, profile, data.url));
return data.url;
}).catch(error => {
dispatch(remoteInteractionFail(ap_id, profile, error));
throw error;
});
};
}
export function remoteInteractionRequest(ap_id, profile) {
return {
type: REMOTE_INTERACTION_REQUEST,
ap_id,
profile,
};
}
export function remoteInteractionSuccess(ap_id, profile, url) {
return {
type: REMOTE_INTERACTION_SUCCESS,
ap_id,
profile,
url,
};
}
export function remoteInteractionFail(ap_id, profile, error) {
return {
type: REMOTE_INTERACTION_FAIL,
ap_id,
profile,
error,
};
}

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 { loadCredentials } from './auth';
import { getAuthUserId, getAuthUserUrl } from 'soapbox/utils/auth';
import api from '../api';
import { loadCredentials } from './auth';
import { importFetchedAccount } from './importer';
export const ME_FETCH_REQUEST = 'ME_FETCH_REQUEST';
export const ME_FETCH_SUCCESS = 'ME_FETCH_SUCCESS';
export const ME_FETCH_FAIL = 'ME_FETCH_FAIL';

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

@ -1,23 +1,32 @@
import React from 'react';
import { defineMessages } from 'react-intl';
import { openModal } from 'soapbox/actions/modal';
import { deactivateUsers, deleteUsers, deleteStatus, toggleStatusSensitivity } from 'soapbox/actions/admin';
import { fetchAccountByUsername } from 'soapbox/actions/accounts';
import { deactivateUsers, deleteUsers, deleteStatus, toggleStatusSensitivity } from 'soapbox/actions/admin';
import { openModal } from 'soapbox/actions/modals';
import snackbar from 'soapbox/actions/snackbar';
import AccountContainer from 'soapbox/containers/account_container';
import { isLocal } from 'soapbox/utils/accounts';
const messages = defineMessages({
deactivateUserHeading: { id: 'confirmations.admin.deactivate_user.heading', defaultMessage: 'Deactivate @{acct}' },
deactivateUserPrompt: { id: 'confirmations.admin.deactivate_user.message', defaultMessage: 'You are about to deactivate @{acct}. Deactivating a user is a reversible action.' },
deactivateUserConfirm: { id: 'confirmations.admin.deactivate_user.confirm', defaultMessage: 'Deactivate @{name}' },
userDeactivated: { id: 'admin.users.user_deactivated_message', defaultMessage: '@{acct} was deactivated' },
deleteUserHeading: { id: 'confirmations.admin.delete_user.heading', defaultMessage: 'Delete @{acct}' },
deleteUserPrompt: { id: 'confirmations.admin.delete_user.message', defaultMessage: 'You are about to delete @{acct}. THIS IS A DESTRUCTIVE ACTION THAT CANNOT BE UNDONE.' },
deleteUserConfirm: { id: 'confirmations.admin.delete_user.confirm', defaultMessage: 'Delete @{name}' },
deleteLocalUserCheckbox: { id: 'confirmations.admin.delete_local_user.checkbox', defaultMessage: 'I understand that I am about to delete a local user.' },
userDeleted: { id: 'admin.users.user_deleted_message', defaultMessage: '@{acct} was deleted' },
deleteStatusHeading: { id: 'confirmations.admin.delete_status.heading', defaultMessage: 'Delete post' },
deleteStatusPrompt: { id: 'confirmations.admin.delete_status.message', defaultMessage: 'You are about to delete a post by @{acct}. This action cannot be undone.' },
deleteStatusConfirm: { id: 'confirmations.admin.delete_status.confirm', defaultMessage: 'Delete post' },
rejectUserHeading: { id: 'confirmations.admin.reject_user.heading', defaultMessage: 'Reject @{acct}' },
rejectUserPrompt: { id: 'confirmations.admin.reject_user.message', defaultMessage: 'You are about to reject @{acct} registration request. This action cannot be undone.' },
rejectUserConfirm: { id: 'confirmations.admin.reject_user.confirm', defaultMessage: 'Reject @{name}' },
statusDeleted: { id: 'admin.statuses.status_deleted_message', defaultMessage: 'Post by @{acct} was deleted' },
markStatusSensitiveHeading: { id: 'confirmations.admin.mark_status_sensitive.heading', defaultMessage: 'Mark post sensitive' },
markStatusNotSensitiveHeading: { id: 'confirmations.admin.mark_status_not_sensitive.heading', defaultMessage: 'Mark post not sensitive.' },
markStatusSensitivePrompt: { id: 'confirmations.admin.mark_status_sensitive.message', defaultMessage: 'You are about to mark a post by @{acct} sensitive.' },
markStatusNotSensitivePrompt: { id: 'confirmations.admin.mark_status_not_sensitive.message', defaultMessage: 'You are about to mark a post by @{acct} not sensitive.' },
markStatusSensitiveConfirm: { id: 'confirmations.admin.mark_status_sensitive.confirm', defaultMessage: 'Mark post sensitive' },
@ -33,6 +42,8 @@ export function deactivateUserModal(intl, accountId, afterConfirm = () => {}) {
const name = state.getIn(['accounts', accountId, 'username']);
dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/icons/user-off.svg'),
heading: intl.formatMessage(messages.deactivateUserHeading, { acct }),
message: intl.formatMessage(messages.deactivateUserPrompt, { acct }),
confirm: intl.formatMessage(messages.deactivateUserConfirm, { name }),
onConfirm: () => {
@ -70,6 +81,8 @@ export function deleteUserModal(intl, accountId, afterConfirm = () => {}) {
const checkbox = local ? intl.formatMessage(messages.deleteLocalUserCheckbox) : false;
dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/icons/user-minus.svg'),
heading: intl.formatMessage(messages.deleteUserHeading, { acct }),
message,
confirm,
checkbox,
@ -85,6 +98,28 @@ export function deleteUserModal(intl, accountId, afterConfirm = () => {}) {
};
}
export function rejectUserModal(intl, accountId, afterConfirm = () => {}) {
return function(dispatch, getState) {
const state = getState();
const acct = state.getIn(['accounts', accountId, 'acct']);
const name = state.getIn(['accounts', accountId, 'username']);
dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/icons/user-off.svg'),
heading: intl.formatMessage(messages.rejectUserHeading, { acct }),
message: intl.formatMessage(messages.rejectUserPrompt, { acct }),
confirm: intl.formatMessage(messages.rejectUserConfirm, { name }),
onConfirm: () => {
dispatch(deleteUsers([accountId]))
.then(() => {
afterConfirm();
})
.catch(() => {});
},
}));
};
}
export function toggleStatusSensitivityModal(intl, statusId, sensitive, afterConfirm = () => {}) {
return function(dispatch, getState) {
const state = getState();
@ -92,6 +127,8 @@ export function toggleStatusSensitivityModal(intl, statusId, sensitive, afterCon
const acct = state.getIn(['accounts', accountId, 'acct']);
dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/icons/alert-triangle.svg'),
heading: intl.formatMessage(sensitive === false ? messages.markStatusSensitiveHeading : messages.markStatusNotSensitiveHeading),
message: intl.formatMessage(sensitive === false ? messages.markStatusSensitivePrompt : messages.markStatusNotSensitivePrompt, { acct }),
confirm: intl.formatMessage(sensitive === false ? messages.markStatusSensitiveConfirm : messages.markStatusNotSensitiveConfirm),
onConfirm: () => {
@ -112,6 +149,8 @@ export function deleteStatusModal(intl, statusId, afterConfirm = () => {}) {
const acct = state.getIn(['accounts', accountId, 'acct']);
dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/icons/trash.svg'),
heading: intl.formatMessage(messages.deleteStatusHeading),
message: intl.formatMessage(messages.deleteStatusPrompt, { acct }),
confirm: intl.formatMessage(messages.deleteStatusConfirm),
onConfirm: () => {

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,10 +1,12 @@
import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts } from './importer';
import { openModal } from './modal';
import { isLoggedIn } from 'soapbox/utils/auth';
import { getNextLinkName } from 'soapbox/utils/quirks';
import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts } from './importer';
import { openModal } from './modals';
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
export const MUTES_FETCH_FAIL = 'MUTES_FETCH_FAIL';

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,17 +24,6 @@ import {
} from './importer';
import { saveMarker } from './markers';
import { getSettings, saveSettings } from './settings';
import { defineMessages } from 'react-intl';
import {
List as ImmutableList,
Map as ImmutableMap,
OrderedMap as ImmutableOrderedMap,
} from 'immutable';
import { unescapeHTML } from '../utils/html';
import { getFilters, regexFromFilters } from '../selectors';
import { isLoggedIn } from 'soapbox/utils/auth';
import { parseVersion, PLEROMA } from 'soapbox/utils/features';
import { joinPublicPath } from 'soapbox/utils/static';
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
@ -115,7 +118,7 @@ export function updateNotificationsQueue(notification, intlMessages, intlLocale,
data: {
url: joinPublicPath('/notifications'),
},
});
}).catch(console.error);
}).catch(console.error);
}
} catch(e) {
@ -203,16 +206,16 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
const next = getLinks(response).refs.find(link => link.rel === 'next');
const entries = response.data.reduce((acc, item) => {
if (item.account && item.account.id) {
if (item.account?.id) {
acc.accounts[item.account.id] = item.account;
}
// Used by Move notification
if (item.target && item.target.id) {
if (item.target?.id) {
acc.accounts[item.target.id] = item.target;
}
if (item.status && item.status.id) {
if (item.status?.id) {
acc.statuses[item.status.id] = item.status;
}

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,7 +1,10 @@
import { decode as decodeBase64 } from '../../utils/base64';
import { pushNotificationsSetting } from '../../settings';
import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
import { createPushSubsription, updatePushSubscription } from 'soapbox/actions/push_subscriptions';
import { getVapidKey } from 'soapbox/utils/auth';
import { pushNotificationsSetting } from '../../settings';
import { decode as decodeBase64 } from '../../utils/base64';
import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
// Taken from https://www.npmjs.com/package/web-push
const urlBase64ToUint8Array = (base64String) => {
@ -13,11 +16,6 @@ const urlBase64ToUint8Array = (base64String) => {
return decodeBase64(base64);
};
const getVapidKey = getState => {
const state = getState();
return state.getIn(['auth', 'app', 'vapid_key']) || state.getIn(['instance', 'pleroma', 'vapid_public_key']);
};
const getRegistration = () => navigator.serviceWorker.ready;
const getPushSubscription = (registration) =>
@ -27,7 +25,7 @@ const getPushSubscription = (registration) =>
const subscribe = (registration, getState) =>
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(getVapidKey(getState)),
applicationServerKey: urlBase64ToUint8Array(getVapidKey(getState())),
});
const unsubscribe = ({ registration, subscription }) =>
@ -35,7 +33,8 @@ const unsubscribe = ({ registration, subscription }) =>
const sendSubscriptionToBackend = (subscription, me) => {
return (dispatch, getState) => {
const params = { subscription };
const alerts = getState().getIn(['push_notifications', 'alerts']).toJS();
const params = { subscription, data: { alerts } };
if (me) {
const data = pushNotificationsSetting.get(me);
@ -54,7 +53,7 @@ const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager'
export function register() {
return (dispatch, getState) => {
const me = getState().get('me');
const vapidKey = getVapidKey(getState);
const vapidKey = getVapidKey(getState());
dispatch(setBrowserSupport(supportsPushNotifications));
@ -105,6 +104,7 @@ export function register() {
}
})
.catch(error => {
console.error(error);
if (error.code === 20 && error.name === 'AbortError') {
console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.');
} else if (error.code === 5 && error.name === 'InvalidCharacterError') {

View File

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

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 {
type: SEARCH_CHANGE,
value,
return (dispatch, getState) => {
// If backspaced all the way, clear the search
if (value.length === 0) {
return dispatch(clearSearch());
} else {
return dispatch({
type: SEARCH_CHANGE,
value,
});
}
};
}
@ -29,10 +37,12 @@ export function clearSearch() {
};
}
export function submitSearch() {
export function submitSearch(filter) {
return (dispatch, getState) => {
const value = getState().getIn(['search', 'value']);
const type = filter || getState().getIn(['search', 'filter'], 'accounts');
// An empty search doesn't return any results
if (value.length === 0) {
return;
}
@ -44,6 +54,7 @@ export function submitSearch() {
q: value,
resolve: true,
limit: 20,
type,
},
}).then(response => {
if (response.data.accounts) {
@ -54,7 +65,7 @@ export function submitSearch() {
dispatch(importFetchedStatuses(response.data.statuses));
}
dispatch(fetchSearchSuccess(response.data));
dispatch(fetchSearchSuccess(response.data, value, type));
dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
}).catch(error => {
dispatch(fetchSearchFail(error));
@ -69,10 +80,12 @@ export function fetchSearchRequest(value) {
};
}
export function fetchSearchSuccess(results) {
export function fetchSearchSuccess(results, searchTerm, searchType) {
return {
type: SEARCH_FETCH_SUCCESS,
results,
searchTerm,
searchType,
};
}
@ -83,13 +96,17 @@ export function fetchSearchFail(error) {
};
}
export const setFilter = filterType => dispatch => {
dispatch({
type: SEARCH_FILTER_SET,
path: ['search', 'filter'],
value: filterType,
});
};
export function setFilter(filterType) {
return (dispatch) => {
dispatch(submitSearch(filterType));
dispatch({
type: SEARCH_FILTER_SET,
path: ['search', 'filter'],
value: filterType,
});
};
}
export const expandSearch = type => (dispatch, getState) => {
const value = getState().getIn(['search', 'value']);

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';
@ -33,6 +35,10 @@ export const DELETE_ACCOUNT_REQUEST = 'DELETE_ACCOUNT_REQUEST';
export const DELETE_ACCOUNT_SUCCESS = 'DELETE_ACCOUNT_SUCCESS';
export const DELETE_ACCOUNT_FAIL = 'DELETE_ACCOUNT_FAIL';
export const MOVE_ACCOUNT_REQUEST = 'MOVE_ACCOUNT_REQUEST';
export const MOVE_ACCOUNT_SUCCESS = 'MOVE_ACCOUNT_SUCCESS';
export const MOVE_ACCOUNT_FAIL = 'MOVE_ACCOUNT_FAIL';
export function fetchOAuthTokens() {
return (dispatch, getState) => {
dispatch({ type: FETCH_TOKENS_REQUEST });
@ -122,3 +128,19 @@ export function deleteAccount(intl, password) {
});
};
}
export function moveAccount(targetAccount, password) {
return (dispatch, getState) => {
dispatch({ type: MOVE_ACCOUNT_REQUEST });
return api(getState).post('/api/pleroma/move_account', {
password,
target_account: targetAccount,
}).then(response => {
if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure
dispatch({ type: MOVE_ACCOUNT_SUCCESS, response });
}).catch(error => {
dispatch({ type: MOVE_ACCOUNT_FAIL, error, skipAlert: true });
throw error;
});
};
}

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,7 +34,6 @@ export const defaultSettings = ImmutableMap({
locale: navigator.language.split(/[-_]/)[0] || 'en',
showExplanationBox: true,
explanationBox: true,
otpEnabled: false,
autoloadTimelines: true,
autoloadMore: true,
@ -97,12 +100,17 @@ export const defaultSettings = ImmutableMap({
move: false,
'pleroma:emoji_reaction': false,
}),
birthdays: ImmutableMap({
show: true,
}),
}),
community: ImmutableMap({
shows: ImmutableMap({
reblog: false,
reply: true,
direct: false,
}),
other: ImmutableMap({
onlyMedia: false,
@ -116,6 +124,7 @@ export const defaultSettings = ImmutableMap({
shows: ImmutableMap({
reblog: true,
reply: true,
direct: false,
}),
other: ImmutableMap({
onlyMedia: false,
@ -135,6 +144,7 @@ export const defaultSettings = ImmutableMap({
shows: ImmutableMap({
reblog: true,
pinned: true,
direct: false,
}),
}),
@ -162,6 +172,18 @@ export const getSettings = createSelector([
.mergeDeep(settings);
});
export function changeSettingImmediate(path, value) {
return dispatch => {
dispatch({
type: SETTING_CHANGE,
path,
value,
});
dispatch(saveSettingsImmediate());
};
}
export function changeSetting(path, value) {
return dispatch => {
dispatch({
@ -174,23 +196,29 @@ export function changeSetting(path, value) {
};
}
export function saveSettingsImmediate() {
return (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
const state = getState();
if (getSettings(state).getIn(['saved'])) return;
const data = state.get('settings').delete('saved').toJS();
dispatch(patchMe({
pleroma_settings_store: {
[FE_NAME]: data,
},
})).then(response => {
dispatch({ type: SETTING_SAVE });
}).catch(error => {
dispatch(showAlertForError(error));
});
};
}
const debouncedSave = debounce((dispatch, getState) => {
if (!isLoggedIn(getState)) return;
const state = getState();
if (getSettings(state).getIn(['saved'])) return;
const data = state.get('settings').delete('saved').toJS();
dispatch(patchMe({
pleroma_settings_store: {
[FE_NAME]: data,
},
})).then(response => {
dispatch({ type: SETTING_SAVE });
}).catch(error => {
dispatch(showAlertForError(error));
});
dispatch(saveSettingsImmediate());
}, 5000, { trailing: true });
export function saveSettings() {

View File

@ -1,21 +1,23 @@
import { ALERT_SHOW } from './alerts';
export const show = (severity, message) => ({
export const show = (severity, message, actionLabel, actionLink) => ({
type: ALERT_SHOW,
message,
actionLabel,
actionLink,
severity,
});
export function info(message) {
return show('info', message);
export function info(message, actionLabel, actionLink) {
return show('info', message, actionLabel, actionLink);
}
export function success(message) {
return show('success', message);
export function success(message, actionLabel, actionLink) {
return show('success', message, actionLabel, actionLink);
}
export function error(message) {
return show('error', message);
export function error(message, actionLabel, actionLink) {
return show('error', message, actionLabel, actionLink);
}
export default {

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 api from '../api';
import { deleteFromTimelines } from './timelines';
import { importFetchedStatus, importFetchedStatuses } from './importer';
import { openModal } from './modal';
import { isLoggedIn } from 'soapbox/utils/auth';
import { getFeatures } from 'soapbox/utils/features';
import { shouldHaveCard } from 'soapbox/utils/status';
import api from '../api';
import { importFetchedStatus, importFetchedStatuses } from './importer';
import { openModal } from './modals';
import { deleteFromTimelines } from './timelines';
export const STATUS_CREATE_REQUEST = 'STATUS_CREATE_REQUEST';
export const STATUS_CREATE_SUCCESS = 'STATUS_CREATE_SUCCESS';
@ -44,8 +48,31 @@ export function createStatus(params, idempotencyKey) {
return api(getState).post('/api/v1/statuses', params, {
headers: { 'Idempotency-Key': idempotencyKey },
}).then(({ data: status }) => {
// The backend might still be processing the rich media attachment
if (!status.card && shouldHaveCard(status)) {
status.expectsCard = true;
}
dispatch(importFetchedStatus(status, idempotencyKey));
dispatch({ type: STATUS_CREATE_SUCCESS, status, params, idempotencyKey });
// Poll the backend for the updated card
if (status.expectsCard) {
const delay = 1000;
const poll = (retries = 5) => {
api(getState).get(`/api/v1/statuses/${status.id}`).then(response => {
if (response.data?.card) {
dispatch(importFetchedStatus(response.data));
} else if (retries > 0 && response.status === 200) {
setTimeout(() => poll(retries - 1), delay);
}
}).catch(console.error);
};
setTimeout(() => poll(), delay);
}
return status;
}).catch(error => {
dispatch({ type: STATUS_CREATE_FAIL, error, params, idempotencyKey });
@ -71,10 +98,17 @@ export function fetchStatus(id) {
}
export function redraft(status, raw_text) {
return {
type: REDRAFT,
status,
raw_text,
return (dispatch, getState) => {
const state = getState();
const instance = state.get('instance');
const { explicitAddressing } = getFeatures(instance);
dispatch({
type: REDRAFT,
status,
raw_text,
explicitAddressing,
});
};
}
@ -109,13 +143,21 @@ export function fetchContext(id) {
dispatch({ type: CONTEXT_FETCH_REQUEST, id });
return api(getState).get(`/api/v1/statuses/${id}/context`).then(({ data: context }) => {
const { ancestors, descendants } = context;
const statuses = ancestors.concat(descendants);
dispatch(importFetchedStatuses(statuses));
dispatch({ type: CONTEXT_FETCH_SUCCESS, id, ancestors, descendants });
if (Array.isArray(context)) {
// Mitra: returns a list of statuses
dispatch(importFetchedStatuses(context));
} else if (typeof context === 'object') {
// Standard Mastodon API returns a map with `ancestors` and `descendants`
const { ancestors, descendants } = context;
const statuses = ancestors.concat(descendants);
dispatch(importFetchedStatuses(statuses));
dispatch({ type: CONTEXT_FETCH_SUCCESS, id, ancestors, descendants });
} else {
throw context;
}
return context;
}).catch(error => {
if (error.response && error.response.status === 404) {
if (error.response?.status === 404) {
dispatch(deleteFromTimelines(id));
}

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';
@ -23,7 +26,7 @@ export const MAX_QUEUED_ITEMS = 40;
export function processTimelineUpdate(timeline, status, accept) {
return (dispatch, getState) => {
const me = getState().get('me');
const ownStatus = status.account && status.account.id === me;
const ownStatus = status.account?.id === me;
const hasPendingStatuses = !getState().get('pending_statuses').isEmpty();
const columnSettings = getSettings(getState()).get(timeline, ImmutableMap());

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

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,14 +1,17 @@
import React from 'react';
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
import AutosuggestEmoji from './autosuggest_emoji';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { isRtl } from '../rtl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import classNames from 'classnames';
import { List as ImmutableList } from 'immutable';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'soapbox/components/icon';
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
import { isRtl } from '../rtl';
import AutosuggestEmoji from './autosuggest_emoji';
const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
let word;

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

@ -0,0 +1,130 @@
import PropTypes from 'prop-types';
import React from 'react';
import DatePicker from 'react-datepicker';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import 'react-datepicker/dist/react-datepicker.css';
import IconButton from 'soapbox/components/icon_button';
import { getFeatures } from 'soapbox/utils/features';
const messages = defineMessages({
birthdayPlaceholder: { id: 'edit_profile.fields.birthday_placeholder', defaultMessage: 'Your birthday' },
previousMonth: { id: 'datepicker.previous_month', defaultMessage: 'Previous month' },
nextMonth: { id: 'datepicker.next_month', defaultMessage: 'Next month' },
previousYear: { id: 'datepicker.previous_year', defaultMessage: 'Previous year' },
nextYear: { id: 'datepicker.next_year', defaultMessage: 'Next year' },
});
const mapStateToProps = state => {
const features = getFeatures(state.get('instance'));
return {
supportsBirthdays: features.birthdays,
minAge: state.getIn(['instance', 'pleroma', 'metadata', 'birthday_min_age']),
};
};
export default @connect(mapStateToProps)
@injectIntl
class BirthdayInput extends ImmutablePureComponent {
static propTypes = {
hint: PropTypes.node,
required: PropTypes.bool,
supportsBirthdays: PropTypes.bool,
minAge: PropTypes.number,
onChange: PropTypes.func.isRequired,
value: PropTypes.instanceOf(Date),
};
renderHeader = ({
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
decreaseYear,
increaseYear,
prevYearButtonDisabled,
nextYearButtonDisabled,
date,
}) => {
const { intl } = this.props;
return (
<div className='datepicker__header'>
<div className='datepicker__months'>
<IconButton
className='datepicker__button'
src={require('@tabler/icons/icons/chevron-left.svg')}
onClick={decreaseMonth}
disabled={prevMonthButtonDisabled}
aria-label={intl.formatMessage(messages.previousMonth)}
title={intl.formatMessage(messages.previousMonth)}
/>
{intl.formatDate(date, { month: 'long' })}
<IconButton
className='datepicker__button'
src={require('@tabler/icons/icons/chevron-right.svg')}
onClick={increaseMonth}
disabled={nextMonthButtonDisabled}
aria-label={intl.formatMessage(messages.nextMonth)}
title={intl.formatMessage(messages.nextMonth)}
/>
</div>
<div className='datepicker__years'>
<IconButton
className='datepicker__button'
src={require('@tabler/icons/icons/chevron-left.svg')}
onClick={decreaseYear}
disabled={prevYearButtonDisabled}
aria-label={intl.formatMessage(messages.previousYear)}
title={intl.formatMessage(messages.previousYear)}
/>
{intl.formatDate(date, { year: 'numeric' })}
<IconButton
className='datepicker__button'
src={require('@tabler/icons/icons/chevron-right.svg')}
onClick={increaseYear}
disabled={nextYearButtonDisabled}
aria-label={intl.formatMessage(messages.nextYear)}
title={intl.formatMessage(messages.nextYear)}
/>
</div>
</div>
);
}
render() {
const { intl, value, onChange, supportsBirthdays, hint, required, minAge } = this.props;
if (!supportsBirthdays) return null;
let maxDate = new Date();
maxDate = new Date(maxDate.getTime() - minAge * 1000 * 60 * 60 * 24 + maxDate.getTimezoneOffset() * 1000 * 60);
return (
<div className='datepicker'>
{hint && (
<div className='datepicker__hint'>
{hint}
</div>
)}
<div className='datepicker__input'>
<DatePicker
selected={value}
wrapperClassName='react-datepicker-wrapper'
onChange={onChange}
placeholderText={intl.formatMessage(messages.birthdayPlaceholder)}
minDate={new Date('1900-01-01')}
maxDate={maxDate}
required={required}
renderCustomHeader={this.renderHeader}
/>
</div>
</div>
);
}
}

View File

@ -0,0 +1,154 @@
import PropTypes from 'prop-types';
import React from 'react';
import { HotKeys } from 'react-hotkeys';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { fetchBirthdayReminders } from 'soapbox/actions/accounts';
import { openModal } from 'soapbox/actions/modals';
import Icon from 'soapbox/components/icon';
import { makeGetAccount } from 'soapbox/selectors';
const mapStateToProps = (state, props) => {
const me = state.get('me');
const getAccount = makeGetAccount();
const birthdays = state.getIn(['user_lists', 'birthday_reminders', me]);
if (birthdays?.size > 0) {
return {
birthdays,
account: getAccount(state, birthdays.first()),
};
}
return {
birthdays,
};
};
export default @connect(mapStateToProps)
@injectIntl
class BirthdayReminders extends ImmutablePureComponent {
static propTypes = {
birthdays: ImmutablePropTypes.orderedSet,
intl: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
onMoveDown: PropTypes.func,
};
componentDidMount() {
const { dispatch } = this.props;
const date = new Date();
const day = date.getDate();
const month = date.getMonth() + 1;
dispatch(fetchBirthdayReminders(day, month));
}
getHandlers() {
return {
open: this.handleOpenBirthdaysModal,
moveDown: this.props.onMoveDown,
};
}
handleOpenBirthdaysModal = () => {
const { dispatch } = this.props;
dispatch(openModal('BIRTHDAYS'));
}
renderMessage() {
const { birthdays, account } = this.props;
const link = (
<bdi>
<Link
className='notification__display-name'
title={account.get('acct')}
to={`/@${account.get('acct')}`}
dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }}
/>
</bdi>
);
if (birthdays.size === 1) {
return <FormattedMessage id='notification.birthday' defaultMessage='{name} has birthday today' values={{ name: link }} />;
}
return (
<FormattedMessage
id='notification.birthday_plural'
defaultMessage='{name} and {more} have birthday today'
values={{
name: link,
more: (
<span type='button' role='presentation' onClick={this.handleOpenBirthdaysModal}>
<FormattedMessage
id='notification.birthday.more'
defaultMessage='{count} more {count, plural, one {friend} other {friends}}'
values={{ count: birthdays.size - 1 }}
/>
</span>
),
}}
/>
);
}
renderMessageForScreenReader = () => {
const { intl, birthdays, account } = this.props;
if (birthdays.size === 1) {
return intl.formatMessage({ id: 'notification.birthday', defaultMessage: '{name} has birthday today' }, { name: account.get('display_name') });
}
return intl.formatMessage(
{
id: 'notification.birthday_plural',
defaultMessage: '{name} and {more} have birthday today',
},
{
name: account.get('display_name'),
more: intl.formatMessage(
{
id: 'notification.birthday.more',
defaultMessage: '{count} more {count, plural, one {friend} other {friends}}',
},
{ count: birthdays.size - 1 },
),
},
);
}
render() {
const { birthdays } = this.props;
if (!birthdays || birthdays.size === 0) return null;
return (
<HotKeys handlers={this.getHandlers()}>
<div className='notification notification-birthday focusable' tabIndex='0' title={this.renderMessageForScreenReader()}>
<div className='notification__message'>
<div className='notification__icon-wrapper'>
<Icon src={require('@tabler/icons/icons/ballon.svg')} />
</div>
<span>
{this.renderMessage()}
</span>
</div>
</div>
</HotKeys>
);
}
}

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 {
@ -16,7 +17,7 @@ export default class ColumnBackButton extends React.PureComponent {
handleClick = () => {
const { to } = this.props;
if (window.history && window.history.length === 1) {
if (window.history?.length === 1) {
this.context.router.history.push(to ? to : '/');
} else {
this.context.router.history.goBack();

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';
@ -33,7 +34,7 @@ export default class ColumnHeader extends React.PureComponent {
};
historyBack = () => {
if (window.history && window.history.length === 1) {
if (window.history?.length === 1) {
this.context.router.history.push('/');
} else {
this.context.router.history.goBack();

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