Merge branch 'develop' into 'icon_picker_admin_config'

# Conflicts:
#   app/soapbox/features/forms/index.js
#   app/soapbox/features/soapbox_config/index.js
icon_picker_admin_config
Sean King 4 years ago
commit 21f68bf623

@ -188,6 +188,8 @@ Customization details can be found in the [Customization doc](docs/customization
Soapbox FE is based on [Gab Social](https://code.gab.com/gab/social/gab-social)'s frontend which is in turn based on [Mastodon](https://github.com/tootsuite/mastodon/)'s frontend.
- `static/sounds/chat.mp3` and `static/sounds/chat.oga` are from [notificationsounds.com](https://notificationsounds.com/notification-sounds/intuition-561) licensed under CC BY 4.0.
Soapbox FE is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 69 KiB

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="63.161953mm"
height="181.12712mm"
viewBox="0 0 63.161953 181.12712"
version="1.1"
id="svg1199"
inkscape:version="0.92.4 (unknown)"
sodipodi:docname="spider.svg">
<defs
id="defs1193" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35355339"
inkscape:cx="188.63933"
inkscape:cy="154.00309"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="705"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1"
inkscape:snap-global="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata1196">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-54.223528,-39.965002)">
<path
style="stroke-width:0.99999994"
d="m 329.96094,151.04883 -7.95132,372.20898 c -30.02705,2.9243 -45.57271,12.92382 -64.25977,32.67188 -25.16762,33.38088 -18.43249,69.4298 -0.4707,100.66992 12.24879,17.03193 32.3984,27.97627 53.07033,34.15036 0,0 -5.52814,0.0857 -11.58984,9.46094 -18.91001,-5.43999 -38.07073,-9.95039 -57.14063,-14.82032 -10.49976,0.9523 -28.58163,18.8274 -36.67969,24.9004 0.27746,13.19067 1.67361,27.14135 3.33008,39.15039 1.1699,-1.57002 0.83916,-3.5804 1.03906,-5.40039 0.9,-10.79003 0.60034,-21.66016 1.99024,-32.41016 9.28,-6.03999 17.7906,-13.20072 26.7207,-19.7207 18.99884,1.97067 39.37112,9.36858 55.91016,13.35156 -0.49,2.41999 -1.38047,5.27974 -4.23047,5.67969 -18.4,4.60002 -36.81969,9.10007 -55.17969,13.83007 -4.86555,6.81697 -23.47884,41.76065 -22.16992,48 3.32807,9.25919 3.76668,29.35751 8.58984,35.70899 -0.65616,-11.27353 -1.26587,-23.12102 -2.88086,-33.41016 4.366,-15.53732 14.77165,-31.85507 21.21094,-44.72851 16.36837,-5.03014 33.6873,-8.93673 49.58008,-11.32032 -0.0299,3.31998 -0.081,6.66013 -0.20117,9.99024 -10.89612,8.5036 -30.45632,23.65603 -40.40821,30.44922 -2.57681,15.80044 -3.38605,33.75066 -4.2207,48.55078 2.50279,8.85582 13.19431,23.74406 18.17156,23.90823 -2.93816,-7.30216 -8.51629,-14.68425 -10.88086,-21.31836 -0.17087,-16.87764 2.99403,-32.98356 3.70114,-48.41015 11.61344,-9.80937 25.4679,-15.10577 35.89062,-24.25 2.26541,6.18864 7.32913,9.97253 10.32813,15.05859 -2.15,3.10001 -5.51922,5.79 -5.94922,9.75 2.88,4.37998 6.60955,8.25101 10.68945,11.54102 -0.85,-3.43 -2.26023,-6.68056 -3.24023,-10.06055 l 6.20117,-7.18945 c 10.18753,5.69922 19.39911,4.81707 28.78906,0.75976 2.12,2.45 4.30149,5.11952 5.27149,8.26953 -0.85,3.26 -2.7418,6.14966 -3.5918,9.42969 4.21,-3.40003 8.09071,-7.32883 11.4707,-11.54883 -0.72,-4.08 -4.4693,-6.80104 -5.27929,-10.79101 3.66,-4.43003 7.97023,-8.42941 10.24023,-13.85938 5.68622,5.4072 34.43902,22.24881 34.94922,26.88086 0.36518,16.19209 3.11897,31.74502 2,46.75 -4.46916,8.68536 -7.12999,16.57554 -14.39063,22.67969 9.90723,0.50906 17.4253,-14.74937 21.52152,-22.69328 -0.18697,-17.91233 -0.74645,-33.39521 -1.16992,-49.66992 -13.47001,-10.57002 -27.16094,-20.89017 -40.46094,-31.66016 0.59,-3.81003 0.49976,-7.6583 0.50977,-11.48828 15.73,4.66001 31.80992,8.14868 47.66992,12.38867 7.58475,10.99663 15.5151,31.43552 20.24023,42.75977 0.43698,13.66208 -3.68079,27.5449 -4.08008,40.14062 1.49998,-1.33999 1.6498,-3.42013 2.17969,-5.24023 1.88197,-11.16719 9.61842,-29.63645 8.13086,-37.92969 -6.21997,-14.23003 -11.95978,-28.75009 -18.42969,-42.83008 -19.30273,-6.68031 -40.27482,-12.85569 -58.39062,-17.73047 -0.65,-1.72002 -1.1801,-3.47951 -1.5,-5.26953 17.78,-3.66999 35.60009,-7.40034 53.33008,-11.32031 5.35892,-0.14205 29.14876,22.09172 28.98047,23.98047 1.30016,6.78634 -2.08415,29.71011 1.61914,33.13086 2.05988,-11.02999 3.41097,-22.17002 5.12109,-33.25 -0.32862,-6.33401 -29.16337,-28.29439 -33.91016,-30.79102 -20.42635,4.13166 -40.67884,9.74123 -59.80078,12.63086 -5.16629,-4.96887 -11.64306,-7.41991 -17.4707,-10.33984 26.33,-1.87998 52.09,-16.02008 66.25,-38.58008 9.5235,-13.96814 12.87637,-29.769 13.1992,-45.79102 0.33714,-20.46694 -8.12112,-40.39069 -21.6211,-55.4707 -18.78284,-17.43524 -31.48782,-23.12017 -55.43945,-26.73828 l 6.93151,-372.80078 z"
id="path1768"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
transform="scale(0.26458333)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="svg46269" viewBox="0 0 340.00001 394.2857" height="111.27618mm" width="95.955559mm">
<defs id="defs46271">
<linearGradient id="linearGradient46839">
<stop id="stop46841" offset="0" style="stop-color:#904700;stop-opacity:1;"/>
<stop id="stop46843" offset="1" style="stop-color:#904700;stop-opacity:0;"/>
</linearGradient>
<linearGradient id="linearGradient46831">
<stop id="stop46833" offset="0" style="stop-color:#904700;stop-opacity:1;"/>
<stop id="stop46835" offset="1" style="stop-color:#904700;stop-opacity:0;"/>
</linearGradient>
<linearGradient id="linearGradient46823">
<stop id="stop46825" offset="0" style="stop-color:#904700;stop-opacity:1;"/>
<stop id="stop46827" offset="1" style="stop-color:#904700;stop-opacity:0;"/>
</linearGradient>
<radialGradient gradientTransform="matrix(4.9019612,0,0,4.9019612,-600.72836,-1264.1473)" gradientUnits="userSpaceOnUse" r="72.85714" fy="330.93362" fx="152.85715" cy="330.93362" cx="152.85715" id="radialGradient46829" xlink:href="#linearGradient46823"/>
<radialGradient gradientTransform="matrix(3.3636365,0,0,3.3636365,-602.85717,-938.05096)" gradientUnits="userSpaceOnUse" r="62.857143" fy="429.50507" fx="251.42857" cy="429.50507" cx="251.42857" id="radialGradient46837" xlink:href="#linearGradient46831"/>
<radialGradient gradientTransform="matrix(1.7317072,0,0,1.7317072,-145.78397,-287.44272)" gradientUnits="userSpaceOnUse" r="58.57143" fy="470.93369" fx="132.85715" cy="470.93369" cx="132.85715" id="radialGradient46845" xlink:href="#linearGradient46839"/>
</defs>
<metadata id="metadata46274">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g transform="translate(-8.5714264,-218.07648)" id="layer1">
<circle r="140" cy="358.07648" cx="148.57143" id="path46817" style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#radialGradient46829);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:20, 5;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"/>
<circle r="105.71429" cy="506.64789" cx="242.85715" id="path46819" style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#radialGradient46837);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:20, 5;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"/>
<circle r="58.57143" cy="528.07654" cx="84.285713" id="path46821" style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#radialGradient46845);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:20, 5;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

@ -0,0 +1,739 @@
{
"ancestors": [
{
"account": {
"acct": "alex",
"avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
"avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
"bot": false,
"created_at": "2020-01-08T01:25:43.000Z",
"display_name": "Alex Gleason",
"emojis": [],
"fields": [
{
"name": "Website",
"value": "<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"
}
],
"follow_requests_count": 0,
"followers_count": 725,
"following_count": 1211,
"header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"id": "9v5bmRalQvjOy0ECcC",
"locked": false,
"note": "Fediverse developer. I come in peace.<br/><br/><a class=\"hashtag\" data-tag=\"vegan\" href=\"https://gleasonator.com/tag/vegan\">#vegan</a> <a class=\"hashtag\" data-tag=\"freeculture\" href=\"https://gleasonator.com/tag/freeculture\">#freeculture</a> <a class=\"hashtag\" data-tag=\"atheist\" href=\"https://gleasonator.com/tag/atheist\">#atheist</a> <a class=\"hashtag\" data-tag=\"antiporn\" href=\"https://gleasonator.com/tag/antiporn\">#antiporn</a> <a class=\"hashtag\" data-tag=\"gendercritical\" href=\"https://gleasonator.com/tag/gendercritical\">#gendercritical</a>.<br/><br/>Boosts ≠ endorsements.",
"pleroma": {
"accepts_chat_messages": true,
"allow_following_move": true,
"ap_id": "https://gleasonator.com/users/alex",
"background_image": null,
"confirmation_pending": false,
"deactivated": false,
"favicon": "https://gleasonator.com/favicon.png",
"hide_favorites": true,
"hide_followers": false,
"hide_followers_count": false,
"hide_follows": false,
"hide_follows_count": false,
"is_admin": true,
"is_moderator": false,
"notification_settings": {
"block_from_strangers": false,
"hide_notification_contents": false
},
"relationship": {},
"skip_thread_containment": false,
"tags": [],
"unread_conversation_count": 95,
"unread_notifications_count": 0
},
"source": {
"fields": [
{
"name": "Website",
"value": "https://alexgleason.me"
},
{
"name": "Pleroma+Soapbox",
"value": "https://soapbox.pub"
},
{
"name": "Email",
"value": "alex@alexgleason.me"
},
{
"name": "Gender identity",
"value": "Soyboy"
}
],
"note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.",
"pleroma": {
"actor_type": "Person",
"discoverable": false,
"no_rich_text": false,
"show_role": true
},
"privacy": "public",
"sensitive": false
},
"statuses_count": 9157,
"url": "https://gleasonator.com/users/alex",
"username": "alex"
},
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "<p>A</p>",
"created_at": "2020-09-18T20:07:10.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9zIH6kDXA10YqhMKqO",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [],
"mentions": [],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "A"
},
"conversation_id": 5089485,
"direct_conversation_id": null,
"emoji_reactions": [],
"expires_at": null,
"in_reply_to_account_acct": null,
"local": true,
"parent_visible": false,
"spoiler_text": {
"text/plain": ""
},
"thread_muted": false
},
"poll": null,
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"text": null,
"uri": "https://gleasonator.com/objects/9995c074-2ff6-4a01-b596-7ef6971ed5d2",
"url": "https://gleasonator.com/notice/9zIH6kDXA10YqhMKqO",
"visibility": "direct"
},
{
"account": {
"acct": "alex",
"avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
"avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
"bot": false,
"created_at": "2020-01-08T01:25:43.000Z",
"display_name": "Alex Gleason",
"emojis": [],
"fields": [
{
"name": "Website",
"value": "<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"
}
],
"follow_requests_count": 0,
"followers_count": 725,
"following_count": 1211,
"header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"id": "9v5bmRalQvjOy0ECcC",
"locked": false,
"note": "Fediverse developer. I come in peace.<br/><br/><a class=\"hashtag\" data-tag=\"vegan\" href=\"https://gleasonator.com/tag/vegan\">#vegan</a> <a class=\"hashtag\" data-tag=\"freeculture\" href=\"https://gleasonator.com/tag/freeculture\">#freeculture</a> <a class=\"hashtag\" data-tag=\"atheist\" href=\"https://gleasonator.com/tag/atheist\">#atheist</a> <a class=\"hashtag\" data-tag=\"antiporn\" href=\"https://gleasonator.com/tag/antiporn\">#antiporn</a> <a class=\"hashtag\" data-tag=\"gendercritical\" href=\"https://gleasonator.com/tag/gendercritical\">#gendercritical</a>.<br/><br/>Boosts ≠ endorsements.",
"pleroma": {
"accepts_chat_messages": true,
"allow_following_move": true,
"ap_id": "https://gleasonator.com/users/alex",
"background_image": null,
"confirmation_pending": false,
"deactivated": false,
"favicon": "https://gleasonator.com/favicon.png",
"hide_favorites": true,
"hide_followers": false,
"hide_followers_count": false,
"hide_follows": false,
"hide_follows_count": false,
"is_admin": true,
"is_moderator": false,
"notification_settings": {
"block_from_strangers": false,
"hide_notification_contents": false
},
"relationship": {},
"skip_thread_containment": false,
"tags": [],
"unread_conversation_count": 95,
"unread_notifications_count": 0
},
"source": {
"fields": [
{
"name": "Website",
"value": "https://alexgleason.me"
},
{
"name": "Pleroma+Soapbox",
"value": "https://soapbox.pub"
},
{
"name": "Email",
"value": "alex@alexgleason.me"
},
{
"name": "Gender identity",
"value": "Soyboy"
}
],
"note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.",
"pleroma": {
"actor_type": "Person",
"discoverable": false,
"no_rich_text": false,
"show_role": true
},
"privacy": "public",
"sensitive": false
},
"statuses_count": 9157,
"url": "https://gleasonator.com/users/alex",
"username": "alex"
},
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "<p>B</p>",
"created_at": "2020-09-18T20:07:18.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9zIH7PUdhK3Ircg4hM",
"in_reply_to_account_id": "9v5bmRalQvjOy0ECcC",
"in_reply_to_id": "9zIH6kDXA10YqhMKqO",
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "alex",
"id": "9v5bmRalQvjOy0ECcC",
"url": "https://gleasonator.com/users/alex",
"username": "alex"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "B"
},
"conversation_id": 5089485,
"direct_conversation_id": null,
"emoji_reactions": [],
"expires_at": null,
"in_reply_to_account_acct": "alex",
"local": true,
"parent_visible": true,
"spoiler_text": {
"text/plain": ""
},
"thread_muted": false
},
"poll": null,
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"text": null,
"uri": "https://gleasonator.com/objects/992ca99a-425d-46eb-b094-60412e9fb141",
"url": "https://gleasonator.com/notice/9zIH7PUdhK3Ircg4hM",
"visibility": "direct"
},
{
"account": {
"acct": "alex",
"avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
"avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
"bot": false,
"created_at": "2020-01-08T01:25:43.000Z",
"display_name": "Alex Gleason",
"emojis": [],
"fields": [
{
"name": "Website",
"value": "<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"
}
],
"follow_requests_count": 0,
"followers_count": 725,
"following_count": 1211,
"header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"id": "9v5bmRalQvjOy0ECcC",
"locked": false,
"note": "Fediverse developer. I come in peace.<br/><br/><a class=\"hashtag\" data-tag=\"vegan\" href=\"https://gleasonator.com/tag/vegan\">#vegan</a> <a class=\"hashtag\" data-tag=\"freeculture\" href=\"https://gleasonator.com/tag/freeculture\">#freeculture</a> <a class=\"hashtag\" data-tag=\"atheist\" href=\"https://gleasonator.com/tag/atheist\">#atheist</a> <a class=\"hashtag\" data-tag=\"antiporn\" href=\"https://gleasonator.com/tag/antiporn\">#antiporn</a> <a class=\"hashtag\" data-tag=\"gendercritical\" href=\"https://gleasonator.com/tag/gendercritical\">#gendercritical</a>.<br/><br/>Boosts ≠ endorsements.",
"pleroma": {
"accepts_chat_messages": true,
"allow_following_move": true,
"ap_id": "https://gleasonator.com/users/alex",
"background_image": null,
"confirmation_pending": false,
"deactivated": false,
"favicon": "https://gleasonator.com/favicon.png",
"hide_favorites": true,
"hide_followers": false,
"hide_followers_count": false,
"hide_follows": false,
"hide_follows_count": false,
"is_admin": true,
"is_moderator": false,
"notification_settings": {
"block_from_strangers": false,
"hide_notification_contents": false
},
"relationship": {},
"skip_thread_containment": false,
"tags": [],
"unread_conversation_count": 95,
"unread_notifications_count": 0
},
"source": {
"fields": [
{
"name": "Website",
"value": "https://alexgleason.me"
},
{
"name": "Pleroma+Soapbox",
"value": "https://soapbox.pub"
},
{
"name": "Email",
"value": "alex@alexgleason.me"
},
{
"name": "Gender identity",
"value": "Soyboy"
}
],
"note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.",
"pleroma": {
"actor_type": "Person",
"discoverable": false,
"no_rich_text": false,
"show_role": true
},
"privacy": "public",
"sensitive": false
},
"statuses_count": 9157,
"url": "https://gleasonator.com/users/alex",
"username": "alex"
},
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "<p>C</p>",
"created_at": "2020-09-18T20:07:22.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9zIH7mMGgc1RmJwDLM",
"in_reply_to_account_id": "9v5bmRalQvjOy0ECcC",
"in_reply_to_id": "9zIH6kDXA10YqhMKqO",
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "alex",
"id": "9v5bmRalQvjOy0ECcC",
"url": "https://gleasonator.com/users/alex",
"username": "alex"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "C"
},
"conversation_id": 5089485,
"direct_conversation_id": null,
"emoji_reactions": [],
"expires_at": null,
"in_reply_to_account_acct": "alex",
"local": true,
"parent_visible": true,
"spoiler_text": {
"text/plain": ""
},
"thread_muted": false
},
"poll": null,
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"text": null,
"uri": "https://gleasonator.com/objects/a2c25ef5-a40e-4098-b07e-b468989ef749",
"url": "https://gleasonator.com/notice/9zIH7mMGgc1RmJwDLM",
"visibility": "direct"
}
],
"descendants": [
{
"account": {
"acct": "alex",
"avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
"avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
"bot": false,
"created_at": "2020-01-08T01:25:43.000Z",
"display_name": "Alex Gleason",
"emojis": [],
"fields": [
{
"name": "Website",
"value": "<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"
}
],
"follow_requests_count": 0,
"followers_count": 725,
"following_count": 1211,
"header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"id": "9v5bmRalQvjOy0ECcC",
"locked": false,
"note": "Fediverse developer. I come in peace.<br/><br/><a class=\"hashtag\" data-tag=\"vegan\" href=\"https://gleasonator.com/tag/vegan\">#vegan</a> <a class=\"hashtag\" data-tag=\"freeculture\" href=\"https://gleasonator.com/tag/freeculture\">#freeculture</a> <a class=\"hashtag\" data-tag=\"atheist\" href=\"https://gleasonator.com/tag/atheist\">#atheist</a> <a class=\"hashtag\" data-tag=\"antiporn\" href=\"https://gleasonator.com/tag/antiporn\">#antiporn</a> <a class=\"hashtag\" data-tag=\"gendercritical\" href=\"https://gleasonator.com/tag/gendercritical\">#gendercritical</a>.<br/><br/>Boosts ≠ endorsements.",
"pleroma": {
"accepts_chat_messages": true,
"allow_following_move": true,
"ap_id": "https://gleasonator.com/users/alex",
"background_image": null,
"confirmation_pending": false,
"deactivated": false,
"favicon": "https://gleasonator.com/favicon.png",
"hide_favorites": true,
"hide_followers": false,
"hide_followers_count": false,
"hide_follows": false,
"hide_follows_count": false,
"is_admin": true,
"is_moderator": false,
"notification_settings": {
"block_from_strangers": false,
"hide_notification_contents": false
},
"relationship": {},
"skip_thread_containment": false,
"tags": [],
"unread_conversation_count": 95,
"unread_notifications_count": 0
},
"source": {
"fields": [
{
"name": "Website",
"value": "https://alexgleason.me"
},
{
"name": "Pleroma+Soapbox",
"value": "https://soapbox.pub"
},
{
"name": "Email",
"value": "alex@alexgleason.me"
},
{
"name": "Gender identity",
"value": "Soyboy"
}
],
"note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.",
"pleroma": {
"actor_type": "Person",
"discoverable": false,
"no_rich_text": false,
"show_role": true
},
"privacy": "public",
"sensitive": false
},
"statuses_count": 9157,
"url": "https://gleasonator.com/users/alex",
"username": "alex"
},
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "<p>E</p>",
"created_at": "2020-09-18T20:07:38.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9zIH9GTCDWEFSRt2um",
"in_reply_to_account_id": "9v5bmRalQvjOy0ECcC",
"in_reply_to_id": "9zIH7PUdhK3Ircg4hM",
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "alex",
"id": "9v5bmRalQvjOy0ECcC",
"url": "https://gleasonator.com/users/alex",
"username": "alex"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "E"
},
"conversation_id": 5089485,
"direct_conversation_id": null,
"emoji_reactions": [],
"expires_at": null,
"in_reply_to_account_acct": "alex",
"local": true,
"parent_visible": true,
"spoiler_text": {
"text/plain": ""
},
"thread_muted": false
},
"poll": null,
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"text": null,
"uri": "https://gleasonator.com/objects/a1e45493-2158-4f11-88ca-ba621429dbe5",
"url": "https://gleasonator.com/notice/9zIH9GTCDWEFSRt2um",
"visibility": "direct"
},
{
"account": {
"acct": "alex",
"avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
"avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
"bot": false,
"created_at": "2020-01-08T01:25:43.000Z",
"display_name": "Alex Gleason",
"emojis": [],
"fields": [
{
"name": "Website",
"value": "<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"
}
],
"follow_requests_count": 0,
"followers_count": 725,
"following_count": 1211,
"header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"id": "9v5bmRalQvjOy0ECcC",
"locked": false,
"note": "Fediverse developer. I come in peace.<br/><br/><a class=\"hashtag\" data-tag=\"vegan\" href=\"https://gleasonator.com/tag/vegan\">#vegan</a> <a class=\"hashtag\" data-tag=\"freeculture\" href=\"https://gleasonator.com/tag/freeculture\">#freeculture</a> <a class=\"hashtag\" data-tag=\"atheist\" href=\"https://gleasonator.com/tag/atheist\">#atheist</a> <a class=\"hashtag\" data-tag=\"antiporn\" href=\"https://gleasonator.com/tag/antiporn\">#antiporn</a> <a class=\"hashtag\" data-tag=\"gendercritical\" href=\"https://gleasonator.com/tag/gendercritical\">#gendercritical</a>.<br/><br/>Boosts ≠ endorsements.",
"pleroma": {
"accepts_chat_messages": true,
"allow_following_move": true,
"ap_id": "https://gleasonator.com/users/alex",
"background_image": null,
"confirmation_pending": false,
"deactivated": false,
"favicon": "https://gleasonator.com/favicon.png",
"hide_favorites": true,
"hide_followers": false,
"hide_followers_count": false,
"hide_follows": false,
"hide_follows_count": false,
"is_admin": true,
"is_moderator": false,
"notification_settings": {
"block_from_strangers": false,
"hide_notification_contents": false
},
"relationship": {},
"skip_thread_containment": false,
"tags": [],
"unread_conversation_count": 95,
"unread_notifications_count": 0
},
"source": {
"fields": [
{
"name": "Website",
"value": "https://alexgleason.me"
},
{
"name": "Pleroma+Soapbox",
"value": "https://soapbox.pub"
},
{
"name": "Email",
"value": "alex@alexgleason.me"
},
{
"name": "Gender identity",
"value": "Soyboy"
}
],
"note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.",
"pleroma": {
"actor_type": "Person",
"discoverable": false,
"no_rich_text": false,
"show_role": true
},
"privacy": "public",
"sensitive": false
},
"statuses_count": 9157,
"url": "https://gleasonator.com/users/alex",
"username": "alex"
},
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "<p>F</p>",
"created_at": "2020-09-18T20:07:42.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9zIH9fhaP9atiJoOJc",
"in_reply_to_account_id": "9v5bmRalQvjOy0ECcC",
"in_reply_to_id": "9zIH8WYwtnUx4yDzUm",
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "alex",
"id": "9v5bmRalQvjOy0ECcC",
"url": "https://gleasonator.com/users/alex",
"username": "alex"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "F"
},
"conversation_id": 5089485,
"direct_conversation_id": null,
"emoji_reactions": [],
"expires_at": null,
"in_reply_to_account_acct": "alex",
"local": true,
"parent_visible": true,
"spoiler_text": {
"text/plain": ""
},
"thread_muted": false
},
"poll": null,
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"text": null,
"uri": "https://gleasonator.com/objects/ee661cf9-35d4-4e84-88ff-13b5950f7556",
"url": "https://gleasonator.com/notice/9zIH9fhaP9atiJoOJc",
"visibility": "direct"
}
]
}

@ -0,0 +1,739 @@
{
"ancestors": [
{
"account": {
"acct": "alex",
"avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
"avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
"bot": false,
"created_at": "2020-01-08T01:25:43.000Z",
"display_name": "Alex Gleason",
"emojis": [],
"fields": [
{
"name": "Website",
"value": "<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"
}
],
"follow_requests_count": 0,
"followers_count": 725,
"following_count": 1211,
"header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"id": "9v5bmRalQvjOy0ECcC",
"locked": false,
"note": "Fediverse developer. I come in peace.<br/><br/><a class=\"hashtag\" data-tag=\"vegan\" href=\"https://gleasonator.com/tag/vegan\">#vegan</a> <a class=\"hashtag\" data-tag=\"freeculture\" href=\"https://gleasonator.com/tag/freeculture\">#freeculture</a> <a class=\"hashtag\" data-tag=\"atheist\" href=\"https://gleasonator.com/tag/atheist\">#atheist</a> <a class=\"hashtag\" data-tag=\"antiporn\" href=\"https://gleasonator.com/tag/antiporn\">#antiporn</a> <a class=\"hashtag\" data-tag=\"gendercritical\" href=\"https://gleasonator.com/tag/gendercritical\">#gendercritical</a>.<br/><br/>Boosts ≠ endorsements.",
"pleroma": {
"accepts_chat_messages": true,
"allow_following_move": true,
"ap_id": "https://gleasonator.com/users/alex",
"background_image": null,
"confirmation_pending": false,
"deactivated": false,
"favicon": "https://gleasonator.com/favicon.png",
"hide_favorites": true,
"hide_followers": false,
"hide_followers_count": false,
"hide_follows": false,
"hide_follows_count": false,
"is_admin": true,
"is_moderator": false,
"notification_settings": {
"block_from_strangers": false,
"hide_notification_contents": false
},
"relationship": {},
"skip_thread_containment": false,
"tags": [],
"unread_conversation_count": 95,
"unread_notifications_count": 0
},
"source": {
"fields": [
{
"name": "Website",
"value": "https://alexgleason.me"
},
{
"name": "Pleroma+Soapbox",
"value": "https://soapbox.pub"
},
{
"name": "Email",
"value": "alex@alexgleason.me"
},
{
"name": "Gender identity",
"value": "Soyboy"
}
],
"note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.",
"pleroma": {
"actor_type": "Person",
"discoverable": false,
"no_rich_text": false,
"show_role": true
},
"privacy": "public",
"sensitive": false
},
"statuses_count": 9157,
"url": "https://gleasonator.com/users/alex",
"username": "alex"
},
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "<p>A</p>",
"created_at": "2020-09-18T20:07:10.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9zIH6kDXA10YqhMKqO",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [],
"mentions": [],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "A"
},
"conversation_id": 5089485,
"direct_conversation_id": null,
"emoji_reactions": [],
"expires_at": null,
"in_reply_to_account_acct": null,
"local": true,
"parent_visible": false,
"spoiler_text": {
"text/plain": ""
},
"thread_muted": false
},
"poll": null,
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"text": null,
"uri": "https://gleasonator.com/objects/9995c074-2ff6-4a01-b596-7ef6971ed5d2",
"url": "https://gleasonator.com/notice/9zIH6kDXA10YqhMKqO",
"visibility": "direct"
}
],
"descendants": [
{
"account": {
"acct": "alex",
"avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
"avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
"bot": false,
"created_at": "2020-01-08T01:25:43.000Z",
"display_name": "Alex Gleason",
"emojis": [],
"fields": [
{
"name": "Website",
"value": "<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"
}
],
"follow_requests_count": 0,
"followers_count": 725,
"following_count": 1211,
"header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"id": "9v5bmRalQvjOy0ECcC",
"locked": false,
"note": "Fediverse developer. I come in peace.<br/><br/><a class=\"hashtag\" data-tag=\"vegan\" href=\"https://gleasonator.com/tag/vegan\">#vegan</a> <a class=\"hashtag\" data-tag=\"freeculture\" href=\"https://gleasonator.com/tag/freeculture\">#freeculture</a> <a class=\"hashtag\" data-tag=\"atheist\" href=\"https://gleasonator.com/tag/atheist\">#atheist</a> <a class=\"hashtag\" data-tag=\"antiporn\" href=\"https://gleasonator.com/tag/antiporn\">#antiporn</a> <a class=\"hashtag\" data-tag=\"gendercritical\" href=\"https://gleasonator.com/tag/gendercritical\">#gendercritical</a>.<br/><br/>Boosts ≠ endorsements.",
"pleroma": {
"accepts_chat_messages": true,
"allow_following_move": true,
"ap_id": "https://gleasonator.com/users/alex",
"background_image": null,
"confirmation_pending": false,
"deactivated": false,
"favicon": "https://gleasonator.com/favicon.png",
"hide_favorites": true,
"hide_followers": false,
"hide_followers_count": false,
"hide_follows": false,
"hide_follows_count": false,
"is_admin": true,
"is_moderator": false,
"notification_settings": {
"block_from_strangers": false,
"hide_notification_contents": false
},
"relationship": {},
"skip_thread_containment": false,
"tags": [],
"unread_conversation_count": 95,
"unread_notifications_count": 0
},
"source": {
"fields": [
{
"name": "Website",
"value": "https://alexgleason.me"
},
{
"name": "Pleroma+Soapbox",
"value": "https://soapbox.pub"
},
{
"name": "Email",
"value": "alex@alexgleason.me"
},
{
"name": "Gender identity",
"value": "Soyboy"
}
],
"note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.",
"pleroma": {
"actor_type": "Person",
"discoverable": false,
"no_rich_text": false,
"show_role": true
},
"privacy": "public",
"sensitive": false
},
"statuses_count": 9157,
"url": "https://gleasonator.com/users/alex",
"username": "alex"
},
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "<p>C</p>",
"created_at": "2020-09-18T20:07:22.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9zIH7mMGgc1RmJwDLM",
"in_reply_to_account_id": "9v5bmRalQvjOy0ECcC",
"in_reply_to_id": "9zIH6kDXA10YqhMKqO",
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "alex",
"id": "9v5bmRalQvjOy0ECcC",
"url": "https://gleasonator.com/users/alex",
"username": "alex"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "C"
},
"conversation_id": 5089485,
"direct_conversation_id": null,
"emoji_reactions": [],
"expires_at": null,
"in_reply_to_account_acct": "alex",
"local": true,
"parent_visible": true,
"spoiler_text": {
"text/plain": ""
},
"thread_muted": false
},
"poll": null,
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"text": null,
"uri": "https://gleasonator.com/objects/a2c25ef5-a40e-4098-b07e-b468989ef749",
"url": "https://gleasonator.com/notice/9zIH7mMGgc1RmJwDLM",
"visibility": "direct"
},
{
"account": {
"acct": "alex",
"avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
"avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
"bot": false,
"created_at": "2020-01-08T01:25:43.000Z",
"display_name": "Alex Gleason",
"emojis": [],
"fields": [
{
"name": "Website",
"value": "<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"
}
],
"follow_requests_count": 0,
"followers_count": 725,
"following_count": 1211,
"header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"id": "9v5bmRalQvjOy0ECcC",
"locked": false,
"note": "Fediverse developer. I come in peace.<br/><br/><a class=\"hashtag\" data-tag=\"vegan\" href=\"https://gleasonator.com/tag/vegan\">#vegan</a> <a class=\"hashtag\" data-tag=\"freeculture\" href=\"https://gleasonator.com/tag/freeculture\">#freeculture</a> <a class=\"hashtag\" data-tag=\"atheist\" href=\"https://gleasonator.com/tag/atheist\">#atheist</a> <a class=\"hashtag\" data-tag=\"antiporn\" href=\"https://gleasonator.com/tag/antiporn\">#antiporn</a> <a class=\"hashtag\" data-tag=\"gendercritical\" href=\"https://gleasonator.com/tag/gendercritical\">#gendercritical</a>.<br/><br/>Boosts ≠ endorsements.",
"pleroma": {
"accepts_chat_messages": true,
"allow_following_move": true,
"ap_id": "https://gleasonator.com/users/alex",
"background_image": null,
"confirmation_pending": false,
"deactivated": false,
"favicon": "https://gleasonator.com/favicon.png",
"hide_favorites": true,
"hide_followers": false,
"hide_followers_count": false,
"hide_follows": false,
"hide_follows_count": false,
"is_admin": true,
"is_moderator": false,
"notification_settings": {
"block_from_strangers": false,
"hide_notification_contents": false
},
"relationship": {},
"skip_thread_containment": false,
"tags": [],
"unread_conversation_count": 95,
"unread_notifications_count": 0
},
"source": {
"fields": [
{
"name": "Website",
"value": "https://alexgleason.me"
},
{
"name": "Pleroma+Soapbox",
"value": "https://soapbox.pub"
},
{
"name": "Email",
"value": "alex@alexgleason.me"
},
{
"name": "Gender identity",
"value": "Soyboy"
}
],
"note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.",
"pleroma": {
"actor_type": "Person",
"discoverable": false,
"no_rich_text": false,
"show_role": true
},
"privacy": "public",
"sensitive": false
},
"statuses_count": 9157,
"url": "https://gleasonator.com/users/alex",
"username": "alex"
},
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "<p>D</p>",
"created_at": "2020-09-18T20:07:30.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9zIH8WYwtnUx4yDzUm",
"in_reply_to_account_id": "9v5bmRalQvjOy0ECcC",
"in_reply_to_id": "9zIH7PUdhK3Ircg4hM",
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "alex",
"id": "9v5bmRalQvjOy0ECcC",
"url": "https://gleasonator.com/users/alex",
"username": "alex"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "D"
},
"conversation_id": 5089485,
"direct_conversation_id": null,
"emoji_reactions": [],
"expires_at": null,
"in_reply_to_account_acct": "alex",
"local": true,
"parent_visible": true,
"spoiler_text": {
"text/plain": ""
},
"thread_muted": false
},
"poll": null,
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"text": null,
"uri": "https://gleasonator.com/objects/bb423adc-ed86-42d8-942e-84efbe7b1acf",
"url": "https://gleasonator.com/notice/9zIH8WYwtnUx4yDzUm",
"visibility": "direct"
},
{
"account": {
"acct": "alex",
"avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
"avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
"bot": false,
"created_at": "2020-01-08T01:25:43.000Z",
"display_name": "Alex Gleason",
"emojis": [],
"fields": [
{
"name": "Website",
"value": "<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"
}
],
"follow_requests_count": 0,
"followers_count": 725,
"following_count": 1211,
"header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"id": "9v5bmRalQvjOy0ECcC",
"locked": false,
"note": "Fediverse developer. I come in peace.<br/><br/><a class=\"hashtag\" data-tag=\"vegan\" href=\"https://gleasonator.com/tag/vegan\">#vegan</a> <a class=\"hashtag\" data-tag=\"freeculture\" href=\"https://gleasonator.com/tag/freeculture\">#freeculture</a> <a class=\"hashtag\" data-tag=\"atheist\" href=\"https://gleasonator.com/tag/atheist\">#atheist</a> <a class=\"hashtag\" data-tag=\"antiporn\" href=\"https://gleasonator.com/tag/antiporn\">#antiporn</a> <a class=\"hashtag\" data-tag=\"gendercritical\" href=\"https://gleasonator.com/tag/gendercritical\">#gendercritical</a>.<br/><br/>Boosts ≠ endorsements.",
"pleroma": {
"accepts_chat_messages": true,
"allow_following_move": true,
"ap_id": "https://gleasonator.com/users/alex",
"background_image": null,
"confirmation_pending": false,
"deactivated": false,
"favicon": "https://gleasonator.com/favicon.png",
"hide_favorites": true,
"hide_followers": false,
"hide_followers_count": false,
"hide_follows": false,
"hide_follows_count": false,
"is_admin": true,
"is_moderator": false,
"notification_settings": {
"block_from_strangers": false,
"hide_notification_contents": false
},
"relationship": {},
"skip_thread_containment": false,
"tags": [],
"unread_conversation_count": 95,
"unread_notifications_count": 0
},
"source": {
"fields": [
{
"name": "Website",
"value": "https://alexgleason.me"
},
{
"name": "Pleroma+Soapbox",
"value": "https://soapbox.pub"
},
{
"name": "Email",
"value": "alex@alexgleason.me"
},
{
"name": "Gender identity",
"value": "Soyboy"
}
],
"note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.",
"pleroma": {
"actor_type": "Person",
"discoverable": false,
"no_rich_text": false,
"show_role": true
},
"privacy": "public",
"sensitive": false
},
"statuses_count": 9157,
"url": "https://gleasonator.com/users/alex",
"username": "alex"
},
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "<p>E</p>",
"created_at": "2020-09-18T20:07:38.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9zIH9GTCDWEFSRt2um",
"in_reply_to_account_id": "9v5bmRalQvjOy0ECcC",
"in_reply_to_id": "9zIH7PUdhK3Ircg4hM",
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "alex",
"id": "9v5bmRalQvjOy0ECcC",
"url": "https://gleasonator.com/users/alex",
"username": "alex"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "E"
},
"conversation_id": 5089485,
"direct_conversation_id": null,
"emoji_reactions": [],
"expires_at": null,
"in_reply_to_account_acct": "alex",
"local": true,
"parent_visible": true,
"spoiler_text": {
"text/plain": ""
},
"thread_muted": false
},
"poll": null,
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"text": null,
"uri": "https://gleasonator.com/objects/a1e45493-2158-4f11-88ca-ba621429dbe5",
"url": "https://gleasonator.com/notice/9zIH9GTCDWEFSRt2um",
"visibility": "direct"
},
{
"account": {
"acct": "alex",
"avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
"avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
"bot": false,
"created_at": "2020-01-08T01:25:43.000Z",
"display_name": "Alex Gleason",
"emojis": [],
"fields": [
{
"name": "Website",
"value": "<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"
}
],
"follow_requests_count": 0,
"followers_count": 725,
"following_count": 1211,
"header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
"id": "9v5bmRalQvjOy0ECcC",
"locked": false,
"note": "Fediverse developer. I come in peace.<br/><br/><a class=\"hashtag\" data-tag=\"vegan\" href=\"https://gleasonator.com/tag/vegan\">#vegan</a> <a class=\"hashtag\" data-tag=\"freeculture\" href=\"https://gleasonator.com/tag/freeculture\">#freeculture</a> <a class=\"hashtag\" data-tag=\"atheist\" href=\"https://gleasonator.com/tag/atheist\">#atheist</a> <a class=\"hashtag\" data-tag=\"antiporn\" href=\"https://gleasonator.com/tag/antiporn\">#antiporn</a> <a class=\"hashtag\" data-tag=\"gendercritical\" href=\"https://gleasonator.com/tag/gendercritical\">#gendercritical</a>.<br/><br/>Boosts ≠ endorsements.",
"pleroma": {
"accepts_chat_messages": true,
"allow_following_move": true,
"ap_id": "https://gleasonator.com/users/alex",
"background_image": null,
"confirmation_pending": false,
"deactivated": false,
"favicon": "https://gleasonator.com/favicon.png",
"hide_favorites": true,
"hide_followers": false,
"hide_followers_count": false,
"hide_follows": false,
"hide_follows_count": false,
"is_admin": true,
"is_moderator": false,
"notification_settings": {
"block_from_strangers": false,
"hide_notification_contents": false
},
"relationship": {},
"skip_thread_containment": false,
"tags": [],
"unread_conversation_count": 95,
"unread_notifications_count": 0
},
"source": {
"fields": [
{
"name": "Website",
"value": "https://alexgleason.me"
},
{
"name": "Pleroma+Soapbox",
"value": "https://soapbox.pub"
},
{
"name": "Email",
"value": "alex@alexgleason.me"
},
{
"name": "Gender identity",
"value": "Soyboy"
}
],
"note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.",
"pleroma": {
"actor_type": "Person",
"discoverable": false,
"no_rich_text": false,
"show_role": true
},
"privacy": "public",
"sensitive": false
},
"statuses_count": 9157,
"url": "https://gleasonator.com/users/alex",
"username": "alex"
},
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "<p>F</p>",
"created_at": "2020-09-18T20:07:42.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9zIH9fhaP9atiJoOJc",
"in_reply_to_account_id": "9v5bmRalQvjOy0ECcC",
"in_reply_to_id": "9zIH8WYwtnUx4yDzUm",
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "alex",
"id": "9v5bmRalQvjOy0ECcC",
"url": "https://gleasonator.com/users/alex",
"username": "alex"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "F"
},
"conversation_id": 5089485,
"direct_conversation_id": null,
"emoji_reactions": [],
"expires_at": null,
"in_reply_to_account_acct": "alex",
"local": true,
"parent_visible": true,
"spoiler_text": {
"text/plain": ""
},
"thread_muted": false
},
"poll": null,
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"text": null,
"uri": "https://gleasonator.com/objects/ee661cf9-35d4-4e84-88ff-13b5950f7556",
"url": "https://gleasonator.com/notice/9zIH9fhaP9atiJoOJc",
"visibility": "direct"
}
]
}

@ -23,6 +23,10 @@ export const CHAT_READ_REQUEST = 'CHAT_READ_REQUEST';
export const CHAT_READ_SUCCESS = 'CHAT_READ_SUCCESS';
export const CHAT_READ_FAIL = 'CHAT_READ_FAIL';
export const CHAT_MESSAGE_DELETE_REQUEST = 'CHAT_MESSAGE_DELETE_REQUEST';
export const CHAT_MESSAGE_DELETE_SUCCESS = 'CHAT_MESSAGE_DELETE_SUCCESS';
export const CHAT_MESSAGE_DELETE_FAIL = 'CHAT_MESSAGE_DELETE_FAIL';
export function fetchChats() {
return (dispatch, getState) => {
dispatch({ type: CHATS_FETCH_REQUEST });
@ -150,3 +154,14 @@ export function markChatRead(chatId, lastReadId) {
});
};
}
export function deleteChatMessage(chatId, messageId) {
return (dispatch, getState) => {
dispatch({ type: CHAT_MESSAGE_DELETE_REQUEST, chatId, messageId });
api(getState).delete(`/api/v1/pleroma/chats/${chatId}/messages/${messageId}`).then(({ data }) => {
dispatch({ type: CHAT_MESSAGE_DELETE_SUCCESS, chatId, messageId, chatMessage: data });
}).catch(error => {
dispatch({ type: CHAT_MESSAGE_DELETE_FAIL, chatId, messageId, error });
});
};
}

@ -46,6 +46,8 @@ export function importFetchedAccounts(accounts) {
const normalAccounts = [];
function processAccount(account) {
if (!account.id) return;
pushUnique(normalAccounts, normalizeAccount(account));
if (account.moved) {
@ -69,6 +71,8 @@ export function importFetchedStatuses(statuses) {
const polls = [];
function processStatus(status) {
if (!status.account.id) return;
const normalOldStatus = getState().getIn(['statuses', status.id]);
const expandSpoilers = getSettings(getState()).get('expandSpoilers');

@ -10,7 +10,11 @@ import {
} from './importer';
import { getSettings, saveSettings } from './settings';
import { defineMessages } from 'react-intl';
import { List as ImmutableList } from 'immutable';
import {
List as ImmutableList,
Map as ImmutableMap,
OrderedMap as ImmutableOrderedMap,
} from 'immutable';
import { unescapeHTML } from '../utils/html';
import { getFilters, regexFromFilters } from '../selectors';
@ -121,7 +125,7 @@ export function updateNotificationsQueue(notification, intlMessages, intlLocale,
export function dequeueNotifications() {
return (dispatch, getState) => {
const queuedNotifications = getState().getIn(['notifications', 'queuedNotifications'], ImmutableList());
const queuedNotifications = getState().getIn(['notifications', 'queuedNotifications'], ImmutableOrderedMap());
const totalQueuedNotificationsCount = getState().getIn(['notifications', 'totalQueuedNotificationsCount'], 0);
if (totalQueuedNotificationsCount === 0) {
@ -252,9 +256,12 @@ export function setFilter(filterType) {
export function markReadNotifications() {
return (dispatch, getState) => {
if (!getState().get('me')) return;
const topNotification = parseInt(getState().getIn(['notifications', 'items', 0, 'id']));
const lastRead = getState().getIn(['notifications', 'lastRead']);
const state = getState();
if (!state.get('me')) return;
const topNotification = state.getIn(['notifications', 'items'], ImmutableOrderedMap()).first(ImmutableMap()).get('id');
const lastRead = state.getIn(['notifications', 'lastRead']);
if (!(topNotification && topNotification > lastRead)) return;
dispatch({

@ -25,6 +25,17 @@ export function initReport(account, status) {
};
};
export function initReportById(accountId) {
return (dispatch, getState) => {
dispatch({
type: REPORT_INIT,
account: getState().getIn(['accounts', accountId]),
});
dispatch(openModal('REPORT'));
};
};
export function cancelReport() {
return {
type: REPORT_CANCEL,

@ -32,6 +32,7 @@ const defaultSettings = ImmutableMap({
chats: ImmutableMap({
panes: ImmutableList(),
mainWindow: 'minimized',
sound: true,
}),
home: ImmutableMap({

@ -219,7 +219,6 @@ export function fetchContextSuccess(id, ancestors, descendants) {
id,
ancestors,
descendants,
statuses: ancestors.concat(descendants),
};
};

@ -55,7 +55,17 @@ export function connectTimelineStream(timelineId, path, pollingRefresh = null, a
dispatch(fetchFilters());
break;
case 'pleroma:chat_update':
dispatch({ type: STREAMING_CHAT_UPDATE, chat: JSON.parse(data.payload), me: getState().get('me') });
dispatch((dispatch, getState) => {
const chat = JSON.parse(data.payload);
const messageOwned = !(chat.last_message && chat.last_message.account_id !== getState().get('me'));
dispatch({
type: STREAMING_CHAT_UPDATE,
chat,
// Only play sounds for recipient messages
meta: !messageOwned && getSettings(getState()).getIn(['chats', 'sound']) && { sound: 'chat' },
});
});
break;
}
},

@ -6,6 +6,7 @@ exports[`<DisplayName /> renders display name + account name 1`] = `
>
<span
className="hover-ref-wrapper"
onClick={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>

@ -36,6 +36,7 @@ class SoapboxHelmet extends React.Component {
<Helmet
titleTemplate={this.addCounter(`%s | ${siteTitle}`)}
defaultTitle={this.addCounter(siteTitle)}
defer={false}
>
{children}
</Helmet>

@ -26,6 +26,13 @@ const handleMouseLeave = (dispatch) => {
};
};
const handleClick = (dispatch) => {
return e => {
showProfileHoverCard.cancel();
dispatch(closeProfileHoverCard(true));
};
};
export const HoverRefWrapper = ({ accountId, children, inline }) => {
const dispatch = useDispatch();
const ref = useRef();
@ -37,6 +44,7 @@ export const HoverRefWrapper = ({ accountId, children, inline }) => {
className='hover-ref-wrapper'
onMouseEnter={handleMouseEnter(dispatch, ref, accountId)}
onMouseLeave={handleMouseLeave(dispatch)}
onClick={handleClick(dispatch)}
>
{children}
</Elem>

@ -59,6 +59,7 @@ const mapStateToProps = (state) => {
locale: validLocale(locale) ? locale : 'en',
themeCss: generateThemeCss(soapboxConfig.get('brandColor')),
themeMode: settings.get('themeMode'),
halloween: settings.get('halloween'),
customCss: soapboxConfig.get('customCss'),
};
};
@ -77,6 +78,7 @@ class SoapboxMount extends React.PureComponent {
themeCss: PropTypes.string,
themeMode: PropTypes.string,
customCss: ImmutablePropTypes.list,
halloween: PropTypes.bool,
dispatch: PropTypes.func,
};
@ -122,6 +124,7 @@ class SoapboxMount extends React.PureComponent {
'no-reduce-motion': !this.props.reduceMotion,
'dyslexic': this.props.dyslexicFont,
'demetricator': this.props.demetricator,
'halloween': this.props.halloween,
});
return (

@ -20,17 +20,18 @@ class LoginPage extends ImmutablePureComponent {
this.handleSubmit = this.handleSubmit.bind(this);
}
state = {
isLoading: false,
mfa_auth_needed: false,
mfa_token: '',
}
getFormData = (form) => {
return Object.fromEntries(
Array.from(form).map(i => [i.name, i.value])
);
}
state = {
mfa_auth_needed: false,
mfa_token: '',
}
handleSubmit = (event) => {
const { dispatch } = this.props;
const { username, password } = this.getFormData(event.target);
@ -47,8 +48,8 @@ class LoginPage extends ImmutablePureComponent {
}
render() {
const { me, isLoading } = this.props;
const { mfa_auth_needed, mfa_token } = this.state;
const { me } = this.props;
const { isLoading, mfa_auth_needed, mfa_token } = this.state;
if (me) return <Redirect to='/' />;
if (mfa_auth_needed) return <OtpAuthForm mfa_token={mfa_token} />;

@ -0,0 +1,61 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, defineMessages } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Icon from 'soapbox/components/icon';
import { changeSetting, getSettings } from 'soapbox/actions/settings';
import SettingToggle from 'soapbox/features/notifications/components/setting_toggle';
const messages = defineMessages({
switchToOn: { id: 'chats.audio_toggle_on', defaultMessage: 'Audio notification on' },
switchToOff: { id: 'chats.audio_toggle_off', defaultMessage: 'Audio notification off' },
});
const mapStateToProps = state => {
return {
settings: getSettings(state),
};
};
const mapDispatchToProps = (dispatch) => ({
toggleAudio(setting) {
dispatch(changeSetting(['chats', 'sound'], setting));
},
});
export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class AudioToggle extends React.PureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
settings: ImmutablePropTypes.map.isRequired,
toggleAudio: PropTypes.func,
showLabel: PropTypes.bool,
};
handleToggleAudio = () => {
this.props.toggleAudio(this.props.settings.getIn(['chats', 'sound']) === true ? false : true);
}
render() {
const { intl, settings, showLabel } = this.props;
let toggle = (
<SettingToggle settings={settings} settingPath={['chats', 'sound']} onChange={this.handleToggleAudio} icons={{ checked: <Icon id='volume-up' />, unchecked: <Icon id='volume-off' /> }} ariaLabel={settings.get('chats', 'sound') === true ? intl.formatMessage(messages.switchToOff) : intl.formatMessage(messages.switchToOn)} />
);
if (showLabel) {
toggle = (
<SettingToggle settings={settings} settingPath={['chats', 'sound']} onChange={this.handleToggleAudio} icons={{ checked: <Icon id='volume-up' />, unchecked: <Icon id='volume-off' /> }} label={settings.get('chats', 'sound') === true ? intl.formatMessage(messages.switchToOff) : intl.formatMessage(messages.switchToOn)} />
);
}
return (
<div className='audio-toggle react-toggle--mini'>
{toggle}
</div>
);
}
}

@ -18,6 +18,7 @@ import IconButton from 'soapbox/components/icon_button';
const messages = defineMessages({
placeholder: { id: 'chat_box.input.placeholder', defaultMessage: 'Send a message…' },
send: { id: 'chat_box.actions.send', defaultMessage: 'Send' },
});
const mapStateToProps = (state, { chatId }) => ({
@ -94,6 +95,7 @@ class ChatBox extends ImmutablePureComponent {
}
handleKeyDown = (e) => {
this.markRead();
if (e.key === 'Enter' && e.shiftKey) {
this.insertLine();
e.preventDefault();
@ -122,17 +124,6 @@ class ChatBox extends ImmutablePureComponent {
onSetInputRef(el);
};
componentDidUpdate(prevProps) {
const markReadConditions = [
() => this.props.chat !== undefined,
() => document.activeElement === this.inputElem,
() => this.props.chat.get('unread') > 0,
];
if (markReadConditions.every(c => c() === true))
this.markRead();
}
handleRemoveFile = (e) => {
this.setState({ attachment: undefined, resetFileKey: fileKeyGen() });
}
@ -174,11 +165,17 @@ class ChatBox extends ImmutablePureComponent {
}
renderActionButton = () => {
const { intl } = this.props;
const { resetFileKey } = this.state;
return this.canSubmit() ? (
<div className='chat-box__send'>
<IconButton icon='send' size={16} onClick={this.sendMessage} />
<IconButton
icon='send'
title={intl.formatMessage(messages.send)}
size={16}
onClick={this.sendMessage}
/>
</div>
) : (
<UploadButton onSelectFile={this.handleFiles} resetFileKey={resetFileKey} />

@ -2,16 +2,37 @@ import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl } from 'react-intl';
import { injectIntl, defineMessages } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { fetchChatMessages } from 'soapbox/actions/chats';
import { fetchChatMessages, deleteChatMessage } from 'soapbox/actions/chats';
import emojify from 'soapbox/features/emoji/emoji';
import classNames from 'classnames';
import { openModal } from 'soapbox/actions/modal';
import { escape, throttle } from 'lodash';
import { MediaGallery } from 'soapbox/features/ui/util/async-components';
import Bundle from 'soapbox/features/ui/components/bundle';
import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
import { initReportById } from 'soapbox/actions/reports';
const messages = defineMessages({
today: { id: 'chats.dividers.today', defaultMessage: 'Today' },
more: { id: 'chats.actions.more', defaultMessage: 'More' },
delete: { id: 'chats.actions.delete', defaultMessage: 'Delete message' },
report: { id: 'chats.actions.report', defaultMessage: 'Report user' },
});
const timeChange = (prev, curr) => {
const prevDate = new Date(prev.get('created_at')).getDate();
const currDate = new Date(curr.get('created_at')).getDate();
const nowDate = new Date().getDate();
if (prevDate !== currDate) {
return currDate === nowDate ? 'today' : 'date';
};
return null;
};
const makeEmojiMap = record => record.get('emojis', ImmutableList()).reduce((map, emoji) => {
return map.set(`:${emoji.get('shortcode')}:`, emoji);
@ -89,11 +110,16 @@ class ChatMessageList extends ImmutablePureComponent {
return scrollBottom < elem.offsetHeight * 1.5;
}
handleResize = (e) => {
if (this.isNearBottom()) this.scrollToBottom();
}
componentDidMount() {
const { dispatch, chatId } = this.props;
dispatch(fetchChatMessages(chatId));
this.node.addEventListener('scroll', this.handleScroll);
window.addEventListener('resize', this.handleResize);
this.scrollToBottom();
}
@ -125,6 +151,7 @@ class ChatMessageList extends ImmutablePureComponent {
componentWillUnmount() {
this.node.removeEventListener('scroll', this.handleScroll);
window.removeEventListener('resize', this.handleResize);
}
handleLoadMore = () => {
@ -176,7 +203,8 @@ class ChatMessageList extends ImmutablePureComponent {
parseContent = chatMessage => {
const content = chatMessage.get('content') || '';
const pending = chatMessage.get('pending', false);
const formatted = pending ? this.parsePendingContent(content) : content;
const deleting = chatMessage.get('deleting', false);
const formatted = (pending && !deleting) ? this.parsePendingContent(content) : content;
const emojiMap = makeEmojiMap(chatMessage);
return emojify(formatted, emojiMap.toJS());
}
@ -185,32 +213,85 @@ class ChatMessageList extends ImmutablePureComponent {
this.node = c;
}
renderDivider = (key, text) => (
<div className='chat-messages__divider' key={key}>{text}</div>
)
handleDeleteMessage = (chatId, messageId) => {
return () => {
this.props.dispatch(deleteChatMessage(chatId, messageId));
};
}
handleReportUser = (userId) => {
return () => {
this.props.dispatch(initReportById(userId));
};
}
renderMessage = (chatMessage) => {
const { me, intl } = this.props;
const menu = [
{ text: intl.formatMessage(messages.delete), action: this.handleDeleteMessage(chatMessage.get('chat_id'), chatMessage.get('id')) },
{ text: intl.formatMessage(messages.report), action: this.handleReportUser(chatMessage.get('account_id')) },
];
return (
<div
className={classNames('chat-message', {
'chat-message--me': chatMessage.get('account_id') === me,
'chat-message--pending': chatMessage.get('pending', false) === true,
})}
key={chatMessage.get('id')}
>
<div
title={this.getFormattedTimestamp(chatMessage)}
className='chat-message__bubble'
ref={this.setBubbleRef}
tabIndex={0}
>
{this.maybeRenderMedia(chatMessage)}
<span
className='chat-message__content'
dangerouslySetInnerHTML={{ __html: this.parseContent(chatMessage) }}
/>
<div className='chat-message__menu'>
<DropdownMenuContainer
items={menu}
icon='ellipsis-h'
size={18}
direction='top'
title={intl.formatMessage(messages.more)}
/>
</div>
</div>
</div>
);
}
render() {
const { chatMessages, me } = this.props;
const { chatMessages, intl } = this.props;
return (
<div className='chat-messages' ref={this.setRef}>
{chatMessages.map(chatMessage => (
<div
className={classNames('chat-message', {
'chat-message--me': chatMessage.get('account_id') === me,
'chat-message--pending': chatMessage.get('pending', false) === true,
})}
key={chatMessage.get('id')}
>
<div
title={this.getFormattedTimestamp(chatMessage)}
className='chat-message__bubble'
ref={this.setBubbleRef}
>
{this.maybeRenderMedia(chatMessage)}
<span
className='chat-message__content'
dangerouslySetInnerHTML={{ __html: this.parseContent(chatMessage) }}
/>
</div>
</div>
))}
{chatMessages.reduce((acc, curr, idx) => {
const lastMessage = chatMessages.get(idx-1);
if (lastMessage) {
const key = `${curr.get('id')}_divider`;
switch(timeChange(lastMessage, curr)) {
case 'today':
acc.push(this.renderDivider(key, intl.formatMessage(messages.today)));
break;
case 'date':
acc.push(this.renderDivider(key, new Date(curr.get('created_at')).toDateString()));
break;
}
}
acc.push(this.renderMessage(curr));
return acc;
}, [])}
<div style={{ float: 'left', clear: 'both' }} ref={this.setMessageEndRef} />
</div>
);

@ -11,6 +11,7 @@ import { makeGetChat } from 'soapbox/selectors';
import { openChat, toggleMainWindow } from 'soapbox/actions/chats';
import ChatWindow from './chat_window';
import { shortNumberFormat } from 'soapbox/utils/numbers';
import AudioToggle from 'soapbox/features/chats/components/audio_toggle';
const addChatsToPanes = (state, panesData) => {
const getChat = makeGetChat();
@ -62,6 +63,7 @@ class ChatPanes extends ImmutablePureComponent {
<button className='pane__title' onClick={this.handleMainWindowToggle}>
<FormattedMessage id='chat_panels.main_window.title' defaultMessage='Chats' />
</button>
<AudioToggle />
</div>
<div className='pane__content'>
<ChatList

@ -4,6 +4,7 @@ import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ChatList from './components/chat_list';
import AudioToggle from 'soapbox/features/chats/components/audio_toggle';
const messages = defineMessages({
title: { id: 'column.chats', defaultMessage: 'Chats' },
@ -33,6 +34,7 @@ class ChatIndex extends React.PureComponent {
icon='comment'
title={intl.formatMessage(messages.title)}
/>
<div className='column__switch'><AudioToggle /></div>
<ChatList
onClickChat={this.handleClickChat}

@ -3,14 +3,10 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { v4 as uuidv4 } from 'uuid';
import { SketchPicker } from 'react-color';
import Overlay from 'react-overlays/lib/Overlay';
import { isMobile } from '../../is_mobile';
import detectPassiveEvents from 'detect-passive-events';
import FontIconPicker from '@fonticonpicker/react-fonticonpicker';
import forkAwesomeIcons from './forkawesome.json';
const FormPropTypes = {
export const FormPropTypes = {
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
@ -18,8 +14,6 @@ const FormPropTypes = {
]),
};
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
export const InputContainer = (props) => {
const containerClass = classNames('input', {
'with_label': props.label,
@ -226,98 +220,6 @@ export class IconPicker extends ImmutablePureComponent {
}
export class ColorPicker extends React.PureComponent {
static propTypes = {
style: PropTypes.object,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
onClose: PropTypes.func,
}
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
}
}
componentDidMount() {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
setRef = c => {
this.node = c;
}
render() {
const { style, value, onChange } = this.props;
let margin_left_picker = isMobile(window.innerWidth) ? '20px' : '12px';
return (
<div id='SketchPickerContainer' ref={this.setRef} style={{ ...style, marginLeft: margin_left_picker, position: 'absolute', zIndex: 1000 }}>
<SketchPicker color={value} disableAlpha onChange={onChange} />
</div>
);
}
}
export class ColorWithPicker extends ImmutablePureComponent {
static propTypes = {
buttonId: PropTypes.string.isRequired,
label: FormPropTypes.label,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
}
onToggle = (e) => {
if (!e.key || e.key === 'Enter') {
if (this.state.active) {
this.onHidePicker();
} else {
this.onShowPicker(e);
}
}
}
state = {
active: false,
placement: null,
}
onHidePicker = () => {
this.setState({ active: false });
}
onShowPicker = ({ target }) => {
this.setState({ active: true });
this.setState({ placement: isMobile(window.innerWidth) ? 'bottom' : 'right' });
}
render() {
const { buttonId, label, value, onChange } = this.props;
const { active, placement } = this.state;
return (
<div className='label_input__color'>
<label>{label}</label>
<div id={buttonId} className='color-swatch' role='presentation' style={{ background: value }} title={value} value={value} onClick={this.onToggle} />
<Overlay show={active} placement={placement} target={this}>
<ColorPicker value={value} onChange={onChange} onClose={this.onHidePicker} />
</Overlay>
</div>
);
}
}
export class RadioItem extends ImmutablePureComponent {
static propTypes = {

@ -30,7 +30,7 @@ const getNotifications = createSelector([
state => getSettings(state).getIn(['notifications', 'quickFilter', 'show']),
state => getSettings(state).getIn(['notifications', 'quickFilter', 'active']),
state => ImmutableList(getSettings(state).getIn(['notifications', 'shows']).filter(item => !item).keys()),
state => state.getIn(['notifications', 'items']),
state => state.getIn(['notifications', 'items']).toList(),
], (showFilterBar, allowedType, excludedTypes, notifications) => {
if (!showFilterBar || allowedType === 'all') {
// used if user changed the notification settings after loading the notifications from the server

@ -185,6 +185,11 @@ class Preferences extends ImmutablePureComponent {
path={['dyslexicFont']}
/>
</div>
<SettingsCheckbox
label={<FormattedMessage id='preferences.fields.halloween_label' defaultMessage='Halloween mode' />}
hint={<FormattedMessage id='preferences.hints.halloween' defaultMessage='Beware: SPOOKY! Supports light/dark toggle.' />}
path={['halloween']}
/>
<SettingsCheckbox
label={<FormattedMessage id='preferences.fields.demetricator_label' defaultMessage='Use Demetricator' />}
hint={<FormattedMessage id='preferences.hints.demetricator' defaultMessage='Decrease social media anxiety by hiding all numbers from the site.' />}

@ -6,14 +6,18 @@ import { Link } from 'react-router-dom';
import LoginForm from 'soapbox/features/auth_login/components/login_form';
import SiteLogo from './site_logo';
import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
import { defineMessages, injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import { logIn } from 'soapbox/actions/auth';
import { fetchMe } from 'soapbox/actions/me';
import PropTypes from 'prop-types';
import OtpAuthForm from 'soapbox/features/auth_login/components/otp_auth_form';
import IconButton from 'soapbox/components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
home: { id: 'header.home.label', defaultMessage: 'Home' },
about: { id: 'header.about.label', defaultMessage: 'About' },
backTo: { id: 'header.back_to.label', defaultMessage: 'Back to {siteTitle}' },
login: { id: 'header.login.label', defaultMessage: 'Log in' },
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
@ -32,48 +36,50 @@ class Header extends ImmutablePureComponent {
this.handleSubmit = this.handleSubmit.bind(this);
}
getFormData = (form) => {
return Object.fromEntries(
Array.from(form).map(i => [i.name, i.value])
);
}
state = {
isLoading: false,
mfa_auth_needed: false,
mfa_token: '',
}
static contextTypes = {
router: PropTypes.object,
};
getFormData = (form) => {
return Object.fromEntries(
Array.from(form).map(i => [i.name, i.value])
);
}
handleSubmit = (event) => {
const { dispatch } = this.props;
const { username, password } = this.getFormData(event.target);
dispatch(logIn(username, password)).then(() => {
return dispatch(fetchMe());
}).catch(error => {
if (error.response.data.error === 'mfa_required') {
this.setState({ mfa_auth_needed: true, mfa_token: error.response.data.mfa_token });
}
this.setState({ isLoading: false });
});
this.setState({ isLoading: true });
event.preventDefault();
}
static contextTypes = {
router: PropTypes.object,
};
onClickClose = (event) => {
this.setState({ mfa_auth_needed: false, mfa_token: '' });
}
handleSubmit = (event) => {
const { dispatch } = this.props;
const { username, password } = this.getFormData(event.target);
dispatch(logIn(username, password)).then(() => {
return dispatch(fetchMe());
}).catch(error => {
if (error.response.data.error === 'mfa_required') {
this.setState({ mfa_auth_needed: true, mfa_token: error.response.data.mfa_token });
}
this.setState({ isLoading: false });
});
this.setState({ isLoading: true });
event.preventDefault();
}
onClickClose = (event) => {
this.setState({ mfa_auth_needed: false, mfa_token: '' });
}
static propTypes = {
me: SoapboxPropTypes.me,
instance: ImmutablePropTypes.map,
}
state = {
mfa_auth_needed: false,
mfa_token: '',
intl: PropTypes.object.isRequired,
}
render() {
const { me, instance, isLoading, intl } = this.props;
const { mfa_auth_needed, mfa_token } = this.state;
const { me, instance, intl } = this.props;
const { isLoading, mfa_auth_needed, mfa_token } = this.state;
return (
<nav className='header'>
@ -90,21 +96,21 @@ class Header extends ImmutablePureComponent {
<Link className='brand' to='/'>
<SiteLogo />
</Link>
<Link className='nav-link optional' to='/'>Home</Link>
<Link className='nav-link' to='/about'>About</Link>
<Link className='nav-link optional' to='/'>{intl.formatMessage(messages.home)}</Link>
<Link className='nav-link' to='/about'>{intl.formatMessage(messages.about)}</Link>
</div>
<div className='nav-center' />
<div className='nav-right'>
<div className='hidden-sm'>
{me
? <Link className='nav-link nav-button webapp-btn' to='/'>Back to {instance.get('title')}</Link>
? <Link className='nav-link nav-button webapp-btn' to='/'>{intl.formatMessage(messages.backTo, { siteTitle: instance.get('title') })}</Link>
: <LoginForm handleSubmit={this.handleSubmit} isLoading={isLoading} />
}
</div>
<div className='visible-sm'>
{me
? <Link className='nav-link nav-button webapp-btn' to='/'>Back to {instance.get('title')}</Link>
: <Link className='nav-link nav-button webapp-btn' to='/auth/sign_in'>Log in</Link>
? <Link className='nav-link nav-button webapp-btn' to='/'>{intl.formatMessage(messages.backTo, { siteTitle: instance.get('title') })}</Link>
: <Link className='nav-link nav-button webapp-btn' to='/auth/sign_in'>{intl.formatMessage(messages.login)}</Link>
}
</div>
</div>

@ -12,15 +12,19 @@ import {
Checkbox,
FileChooser,
SimpleTextarea,
ColorWithPicker,
FileChooserLogo,
IconPicker,
FormPropTypes,
} from 'soapbox/features/forms';
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import { updateAdminConfig } from 'soapbox/actions/admin';
import Icon from 'soapbox/components/icon';
import { defaultConfig } from 'soapbox/actions/soapbox';
import { uploadMedia } from 'soapbox/actions/media';
import { SketchPicker } from 'react-color';
import Overlay from 'react-overlays/lib/Overlay';
import { isMobile } from 'soapbox/is_mobile';
import detectPassiveEvents from 'detect-passive-events';
const messages = defineMessages({
heading: { id: 'column.soapbox_config', defaultMessage: 'Soapbox config' },
@ -35,6 +39,8 @@ const messages = defineMessages({
rawJSONHint: { id: 'soapbox_config.raw_json_hint', defaultMessage: 'Advanced: Edit the settings data directly.' },
});
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
const templates = {
promoPanelItem: ImmutableMap({ icon: '', text: '', url: '' }),
footerItem: ImmutableMap({ title: '', url: '' }),
@ -364,3 +370,95 @@ class SoapboxConfig extends ImmutablePureComponent {
}
}
class ColorPicker extends React.PureComponent {
static propTypes = {
style: PropTypes.object,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
onClose: PropTypes.func,
}
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
}
}
componentDidMount() {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
setRef = c => {
this.node = c;
}
render() {
const { style, value, onChange } = this.props;
let margin_left_picker = isMobile(window.innerWidth) ? '20px' : '12px';
return (
<div id='SketchPickerContainer' ref={this.setRef} style={{ ...style, marginLeft: margin_left_picker, position: 'absolute', zIndex: 1000 }}>
<SketchPicker color={value} disableAlpha onChange={onChange} />
</div>
);
}
}
class ColorWithPicker extends ImmutablePureComponent {
static propTypes = {
buttonId: PropTypes.string.isRequired,
label: FormPropTypes.label,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
}
onToggle = (e) => {
if (!e.key || e.key === 'Enter') {
if (this.state.active) {
this.onHidePicker();
} else {
this.onShowPicker(e);
}
}
}
state = {
active: false,
placement: null,
}
onHidePicker = () => {
this.setState({ active: false });
}
onShowPicker = ({ target }) => {
this.setState({ active: true });
this.setState({ placement: isMobile(window.innerWidth) ? 'bottom' : 'right' });
}
render() {
const { buttonId, label, value, onChange } = this.props;
const { active, placement } = this.state;
return (
<div className='label_input__color'>
<label>{label}</label>
<div id={buttonId} className='color-swatch' role='presentation' style={{ background: value }} title={value} value={value} onClick={this.onToggle} />
<Overlay show={active} placement={placement} target={this}>
<ColorPicker value={value} onChange={onChange} onClose={this.onHidePicker} />
</Overlay>
</div>
);
}
}

@ -56,21 +56,21 @@ class UserPanel extends ImmutablePureComponent {
<div className='user-panel__stats-block'>
{account.get('statuses_count') && <div className='user-panel-stats-item'>
{account.get('statuses_count') >= 0 && <div className='user-panel-stats-item'>
<Link to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}>
<strong className='user-panel-stats-item__value'>{shortNumberFormat(account.get('statuses_count'))}</strong>
<span className='user-panel-stats-item__label'><FormattedMessage className='user-panel-stats-item__label' id='account.posts' defaultMessage='Posts' /></span>
</Link>
</div>}
{account.get('followers_count') && <div className='user-panel-stats-item'>
{account.get('followers_count') >= 0 && <div className='user-panel-stats-item'>
<Link to={`/@${account.get('acct')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
<strong className='user-panel-stats-item__value'>{shortNumberFormat(account.get('followers_count'))}</strong>
<span className='user-panel-stats-item__label'><FormattedMessage id='account.followers' defaultMessage='Followers' /></span>
</Link>
</div>}
{account.get('following_count') && <div className='user-panel-stats-item'>
{account.get('following_count') >= 0 && <div className='user-panel-stats-item'>
<Link to={`/@${account.get('acct')}/following`} title={intl.formatNumber(account.get('following_count'))}>
<strong className='user-panel-stats-item__value'>{shortNumberFormat(account.get('following_count'))}</strong>
<span className='user-panel-stats-item__label'><FormattedMessage className='user-panel-stats-item__label' id='account.follows' defaultMessage='Follows' /></span>

@ -36,6 +36,16 @@ export default function soundsMiddleware() {
type: 'audio/mpeg',
},
]),
chat: createAudio([
{
src: '/sounds/chat.oga',
type: 'audio/ogg',
},
{
src: '/sounds/chat.mp3',
type: 'audio/mpeg',
},
]),
};
return () => next => action => {

@ -1,5 +1,8 @@
import reducer from '../contexts';
import { Map as ImmutableMap } from 'immutable';
import { CONTEXT_FETCH_SUCCESS } from 'soapbox/actions/statuses';
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
import context1 from 'soapbox/__fixtures__/context_1.json';
import context2 from 'soapbox/__fixtures__/context_2.json';
describe('contexts reducer', () => {
it('should return the initial state', () => {
@ -8,4 +11,34 @@ describe('contexts reducer', () => {
replies: ImmutableMap(),
}));
});
it('should support rendering a complete tree', () => {
// https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/422
let result;
result = reducer(result, { type: CONTEXT_FETCH_SUCCESS, id: '9zIH8WYwtnUx4yDzUm', ancestors: context1.ancestors, descendants: context1.descendants });
result = reducer(result, { type: CONTEXT_FETCH_SUCCESS, id: '9zIH7PUdhK3Ircg4hM', ancestors: context2.ancestors, descendants: context2.descendants });
expect(result).toEqual(ImmutableMap({
inReplyTos: ImmutableMap({
'9zIH7PUdhK3Ircg4hM': '9zIH6kDXA10YqhMKqO',
'9zIH7mMGgc1RmJwDLM': '9zIH6kDXA10YqhMKqO',
'9zIH9GTCDWEFSRt2um': '9zIH7PUdhK3Ircg4hM',
'9zIH9fhaP9atiJoOJc': '9zIH8WYwtnUx4yDzUm',
'9zIH8WYwtnUx4yDzUm': '9zIH7PUdhK3Ircg4hM',
}),
replies: ImmutableMap({
'9zIH6kDXA10YqhMKqO': ImmutableOrderedSet([
'9zIH7PUdhK3Ircg4hM',
'9zIH7mMGgc1RmJwDLM',
]),
'9zIH7PUdhK3Ircg4hM': ImmutableOrderedSet([
'9zIH8WYwtnUx4yDzUm',
'9zIH9GTCDWEFSRt2um',
]),
'9zIH8WYwtnUx4yDzUm': ImmutableOrderedSet([
'9zIH9fhaP9atiJoOJc',
]),
}),
}));
});
});

@ -1,23 +1,23 @@
import * as actions from 'soapbox/actions/notifications';
import reducer from '../notifications';
import notifications from 'soapbox/__fixtures__/notifications.json';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { Map as ImmutableMap, OrderedMap as ImmutableOrderedMap } from 'immutable';
import { take } from 'lodash';
import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'soapbox/actions/accounts';
import notification from 'soapbox/__fixtures__/notification.json';
import intlMessages from 'soapbox/__fixtures__/intlMessages.json';
import relationship from 'soapbox/__fixtures__/relationship.json';
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from 'soapbox/actions/timelines';
import { TIMELINE_DELETE } from 'soapbox/actions/timelines';
describe('notifications reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(ImmutableMap({
items: ImmutableList(),
items: ImmutableOrderedMap(),
hasMore: true,
top: false,
unread: 0,
isLoading: false,
queuedNotifications: ImmutableList(),
queuedNotifications: ImmutableOrderedMap(),
totalQueuedNotificationsCount: 0,
lastRead: -1,
}));
@ -32,8 +32,8 @@ describe('notifications reducer', () => {
skipLoading: true,
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10744', ImmutableMap({
id: '10744',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
@ -42,8 +42,8 @@ describe('notifications reducer', () => {
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
})],
['10743', ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
@ -52,8 +52,8 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
})],
['10741', ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
@ -62,13 +62,13 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
})],
]),
hasMore: false,
top: false,
unread: 1,
isLoading: false,
queuedNotifications: ImmutableList(),
queuedNotifications: ImmutableOrderedMap(),
totalQueuedNotificationsCount: 0,
lastRead: -1,
}));
@ -100,8 +100,8 @@ describe('notifications reducer', () => {
it('should handle NOTIFICATIONS_FILTER_SET', () => {
const state = ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10744', ImmutableMap({
id: '10744',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
@ -110,8 +110,8 @@ describe('notifications reducer', () => {
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
})],
['10743', ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
@ -120,8 +120,8 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
})],
['10741', ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
@ -130,13 +130,13 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
})],
]),
hasMore: false,
top: false,
unread: 1,
isLoading: false,
queuedNotifications: ImmutableList(),
queuedNotifications: ImmutableOrderedMap(),
totalQueuedNotificationsCount: 0,
lastRead: -1,
});
@ -144,12 +144,12 @@ describe('notifications reducer', () => {
type: actions.NOTIFICATIONS_FILTER_SET,
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList(),
items: ImmutableOrderedMap(),
hasMore: true,
top: false,
unread: 1,
isLoading: false,
queuedNotifications: ImmutableList(),
queuedNotifications: ImmutableOrderedMap(),
totalQueuedNotificationsCount: 0,
lastRead: -1,
}));
@ -185,7 +185,7 @@ describe('notifications reducer', () => {
it('should handle NOTIFICATIONS_UPDATE, when top = false, increment unread', () => {
const state = ImmutableMap({
items: ImmutableList(),
items: ImmutableOrderedMap(),
top: false,
unread: 1,
});
@ -194,8 +194,8 @@ describe('notifications reducer', () => {
notification: notification,
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10743', ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
@ -204,7 +204,7 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
})],
]),
top: false,
unread: 2,
@ -213,8 +213,8 @@ describe('notifications reducer', () => {
it('should handle NOTIFICATIONS_UPDATE_QUEUE', () => {
const state = ImmutableMap({
items: ImmutableList([]),
queuedNotifications: ImmutableList([]),
items: ImmutableOrderedMap(),
queuedNotifications: ImmutableOrderedMap(),
totalQueuedNotificationsCount: 0,
});
const action = {
@ -224,19 +224,19 @@ describe('notifications reducer', () => {
intlLocale: 'en',
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([]),
queuedNotifications: ImmutableList([{
items: ImmutableOrderedMap(),
queuedNotifications: ImmutableOrderedMap([[notification.id, {
notification: notification,
intlMessages: intlMessages,
intlLocale: 'en',
}]),
}]]),
totalQueuedNotificationsCount: 1,
}));
});
it('should handle NOTIFICATIONS_DEQUEUE', () => {
const state = ImmutableMap({
items: ImmutableList([]),
items: ImmutableOrderedMap(),
queuedNotifications: take(notifications, 1),
totalQueuedNotificationsCount: 1,
});
@ -244,16 +244,16 @@ describe('notifications reducer', () => {
type: actions.NOTIFICATIONS_DEQUEUE,
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([]),
queuedNotifications: ImmutableList([]),
items: ImmutableOrderedMap(),
queuedNotifications: ImmutableOrderedMap(),
totalQueuedNotificationsCount: 0,
}));
});
it('should handle NOTIFICATIONS_EXPAND_SUCCESS with non-empty items and next set true', () => {
const state = ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10734', ImmutableMap({
id: '10734',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
@ -262,7 +262,7 @@ describe('notifications reducer', () => {
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
})],
]),
unread: 1,
hasMore: true,
@ -274,8 +274,8 @@ describe('notifications reducer', () => {
next: true,
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10744', ImmutableMap({
id: '10744',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
@ -284,8 +284,8 @@ describe('notifications reducer', () => {
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
})],
['10743', ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
@ -294,8 +294,8 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
})],
['10741', ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
@ -304,8 +304,8 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
})],
['10734', ImmutableMap({
id: '10734',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
@ -314,7 +314,7 @@ describe('notifications reducer', () => {
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
})],
]),
unread: 1,
hasMore: true,
@ -324,7 +324,7 @@ describe('notifications reducer', () => {
it('should handle NOTIFICATIONS_EXPAND_SUCCESS with empty items and next set true', () => {
const state = ImmutableMap({
items: ImmutableList([]),
items: ImmutableOrderedMap(),
unread: 1,
hasMore: true,
isLoading: false,
@ -335,8 +335,8 @@ describe('notifications reducer', () => {
next: true,
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10744', ImmutableMap({
id: '10744',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
@ -345,8 +345,8 @@ describe('notifications reducer', () => {
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
})],
['10743', ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
@ -355,8 +355,8 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
})],
['10741', ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
@ -365,7 +365,7 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
})],
]),
unread: 1,
hasMore: true,
@ -375,8 +375,8 @@ describe('notifications reducer', () => {
it('should handle ACCOUNT_BLOCK_SUCCESS', () => {
const state = ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10744', ImmutableMap({
id: '10744',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
@ -385,8 +385,8 @@ describe('notifications reducer', () => {
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
})],
['10743', ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
@ -395,8 +395,8 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
})],
['10741', ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
@ -405,7 +405,7 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
})],
]),
});
const action = {
@ -413,8 +413,8 @@ describe('notifications reducer', () => {
relationship: relationship,
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10743', ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
@ -423,8 +423,8 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
})],
['10741', ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
@ -433,15 +433,15 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
})],
]),
}));
});
it('should handle ACCOUNT_MUTE_SUCCESS', () => {
const state = ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10744', ImmutableMap({
id: '10744',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
@ -450,8 +450,8 @@ describe('notifications reducer', () => {
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
})],
['10743', ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
@ -460,8 +460,8 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
})],
['10741', ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
@ -470,7 +470,7 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
})],
]),
});
const action = {
@ -478,8 +478,8 @@ describe('notifications reducer', () => {
relationship: relationship,
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10743', ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
@ -488,8 +488,8 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
})],
['10741', ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
@ -498,43 +498,43 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
})],
]),
}));
});
it('should handle NOTIFICATIONS_CLEAR', () => {
const state = ImmutableMap({
items: ImmutableList([]),
items: ImmutableOrderedMap(),
hasMore: true,
});
const action = {
type: actions.NOTIFICATIONS_CLEAR,
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([]),
items: ImmutableOrderedMap(),
hasMore: false,
}));
});
it('should handle NOTIFICATIONS_MARK_READ_REQUEST', () => {
const state = ImmutableMap({
items: ImmutableList([]),
items: ImmutableOrderedMap(),
});
const action = {
type: actions.NOTIFICATIONS_MARK_READ_REQUEST,
lastRead: 35098814,
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([]),
items: ImmutableOrderedMap(),
lastRead: 35098814,
}));
});
it('should handle TIMELINE_DELETE', () => {
const state = ImmutableMap({
items: ImmutableList([
ImmutableMap({
items: ImmutableOrderedMap([
['10744', ImmutableMap({
id: '10744',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
@ -543,8 +543,8 @@ describe('notifications reducer', () => {
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
})],
['10743', ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
@ -553,8 +553,8 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
})],
['10741', ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
@ -563,7 +563,7 @@ describe('notifications reducer', () => {
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
})],
]),
});
const action = {
@ -571,84 +571,87 @@ describe('notifications reducer', () => {
id: '9vvNxoo5EFbbnfdXQu',
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([]),
items: ImmutableOrderedMap(),
}));
});
it('should handle TIMELINE_DISCONNECT', () => {
const state = ImmutableMap({
items: ImmutableList([
ImmutableMap({
id: '10744',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
created_at: '2020-06-10T02:54:39.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
created_at: '2020-06-10T02:51:05.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
created_at: '2020-06-10T02:05:06.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
]),
});
const action = {
type: TIMELINE_DISCONNECT,
timeline: 'home',
};
expect(reducer(state, action)).toEqual(ImmutableMap({
items: ImmutableList([
null,
ImmutableMap({
id: '10744',
type: 'pleroma:emoji_reaction',
account: '9vMAje101ngtjlMj7w',
created_at: '2020-06-10T02:54:39.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: '😢',
chat_message: undefined,
is_seen: false,
}),
ImmutableMap({
id: '10743',
type: 'favourite',
account: '9v5c6xSEgAi3Zu1Lv6',
created_at: '2020-06-10T02:51:05.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
ImmutableMap({
id: '10741',
type: 'favourite',
account: '9v5cKMOPGqPcgfcWp6',
created_at: '2020-06-10T02:05:06.000Z',
status: '9vvNxoo5EFbbnfdXQu',
emoji: undefined,
chat_message: undefined,
is_seen: true,
}),
]),
}));
});
// Disable for now
// https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/432
//
// it('should handle TIMELINE_DISCONNECT', () => {
// const state = ImmutableMap({
// items: ImmutableOrderedSet([
// ImmutableMap({
// id: '10744',
// type: 'pleroma:emoji_reaction',
// account: '9vMAje101ngtjlMj7w',
// created_at: '2020-06-10T02:54:39.000Z',
// status: '9vvNxoo5EFbbnfdXQu',
// emoji: '😢',
// chat_message: undefined,
// is_seen: false,
// }),
// ImmutableMap({
// id: '10743',
// type: 'favourite',
// account: '9v5c6xSEgAi3Zu1Lv6',
// created_at: '2020-06-10T02:51:05.000Z',
// status: '9vvNxoo5EFbbnfdXQu',
// emoji: undefined,
// chat_message: undefined,
// is_seen: true,
// }),
// ImmutableMap({
// id: '10741',
// type: 'favourite',
// account: '9v5cKMOPGqPcgfcWp6',
// created_at: '2020-06-10T02:05:06.000Z',
// status: '9vvNxoo5EFbbnfdXQu',
// emoji: undefined,
// chat_message: undefined,
// is_seen: true,
// }),
// ]),
// });
// const action = {
// type: TIMELINE_DISCONNECT,
// timeline: 'home',
// };
// expect(reducer(state, action)).toEqual(ImmutableMap({
// items: ImmutableOrderedSet([
// null,
// ImmutableMap({
// id: '10744',
// type: 'pleroma:emoji_reaction',
// account: '9vMAje101ngtjlMj7w',
// created_at: '2020-06-10T02:54:39.000Z',
// status: '9vvNxoo5EFbbnfdXQu',
// emoji: '😢',
// chat_message: undefined,
// is_seen: false,
// }),
// ImmutableMap({
// id: '10743',
// type: 'favourite',
// account: '9v5c6xSEgAi3Zu1Lv6',
// created_at: '2020-06-10T02:51:05.000Z',
// status: '9vvNxoo5EFbbnfdXQu',
// emoji: undefined,
// chat_message: undefined,
// is_seen: true,
// }),
// ImmutableMap({
// id: '10741',
// type: 'favourite',
// account: '9v5cKMOPGqPcgfcWp6',
// created_at: '2020-06-10T02:05:06.000Z',
// status: '9vvNxoo5EFbbnfdXQu',
// emoji: undefined,
// chat_message: undefined,
// is_seen: true,
// }),
// ]),
// }));
// });
});

@ -3,6 +3,7 @@ import {
CHAT_MESSAGES_FETCH_SUCCESS,
CHAT_MESSAGE_SEND_REQUEST,
CHAT_MESSAGE_SEND_SUCCESS,
CHAT_MESSAGE_DELETE_SUCCESS,
} from 'soapbox/actions/chats';
import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
@ -59,6 +60,8 @@ export default function chatMessageLists(state = initialState, action) {
return updateList(state, action.chatId, action.chatMessages.map(chat => chat.id));
case CHAT_MESSAGE_SEND_SUCCESS:
return replaceMessage(state, action.chatId, action.uuid, action.chatMessage.id);
case CHAT_MESSAGE_DELETE_SUCCESS:
return state.update(action.chatId, chat => chat.delete(action.messageId));
default:
return state;
}

@ -3,6 +3,8 @@ import {
CHAT_MESSAGES_FETCH_SUCCESS,
CHAT_MESSAGE_SEND_REQUEST,
CHAT_MESSAGE_SEND_SUCCESS,
CHAT_MESSAGE_DELETE_REQUEST,
CHAT_MESSAGE_DELETE_SUCCESS,
} from 'soapbox/actions/chats';
import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
import { Map as ImmutableMap, fromJS } from 'immutable';
@ -43,6 +45,11 @@ export default function chatMessages(state = initialState, action) {
return importMessage(state, fromJS(action.chatMessage)).delete(action.uuid);
case STREAMING_CHAT_UPDATE:
return importLastMessages(state, fromJS([action.chat]));
case CHAT_MESSAGE_DELETE_REQUEST:
return state.update(action.messageId, chatMessage =>
chatMessage.set('pending', true).set('deleting', true));
case CHAT_MESSAGE_DELETE_SUCCESS:
return state.delete(action.messageId);
default:
return state;
}

@ -4,8 +4,7 @@ import {
} from '../actions/accounts';
import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses';
import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from '../compare_id';
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
const initialState = ImmutableMap({
inReplyTos: ImmutableMap(),
@ -16,26 +15,16 @@ const normalizeContext = (immutableState, id, ancestors, descendants) => immutab
state.update('inReplyTos', immutableAncestors => immutableAncestors.withMutations(inReplyTos => {
state.update('replies', immutableDescendants => immutableDescendants.withMutations(replies => {
function addReply({ id, in_reply_to_id }) {
if (in_reply_to_id && !inReplyTos.has(id)) {
replies.update(in_reply_to_id, ImmutableList(), siblings => {
const index = siblings.findLastIndex(sibling => compareId(sibling, id) < 0);
return siblings.insert(index + 1, id);
if (in_reply_to_id) {
replies.update(in_reply_to_id, ImmutableOrderedSet(), siblings => {
return siblings.add(id).sort();
});
inReplyTos.set(id, in_reply_to_id);
}
}
// We know in_reply_to_id of statuses but `id` itself.
// So we assume that the status of the id replies to last ancestors.
ancestors.forEach(addReply);
if (ancestors[0]) {
addReply({ id, in_reply_to_id: ancestors[ancestors.length - 1].id });
}
descendants.forEach(addReply);
}));
}));
@ -76,12 +65,12 @@ const filterContexts = (state, relationship, statuses) => {
const updateContext = (state, status) => {
if (status.in_reply_to_id) {
return state.withMutations(mutable => {
const replies = mutable.getIn(['replies', status.in_reply_to_id], ImmutableList());
const replies = mutable.getIn(['replies', status.in_reply_to_id], ImmutableOrderedSet());
mutable.setIn(['inReplyTos', status.id], status.in_reply_to_id);
if (!replies.includes(status.id)) {
mutable.setIn(['replies', status.in_reply_to_id], replies.push(status.id));
mutable.setIn(['replies', status.in_reply_to_id], replies.add(status.id).sort());
}
});
}

@ -15,22 +15,28 @@ import {
ACCOUNT_BLOCK_SUCCESS,
ACCOUNT_MUTE_SUCCESS,
} from '../actions/accounts';
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from '../compare_id';
import { TIMELINE_DELETE } from '../actions/timelines';
import { Map as ImmutableMap, OrderedMap as ImmutableOrderedMap } from 'immutable';
import { get } from 'lodash';
const initialState = ImmutableMap({
items: ImmutableList(),
items: ImmutableOrderedMap(),
hasMore: true,
top: false,
unread: 0,
isLoading: false,
queuedNotifications: ImmutableList(), //max = MAX_QUEUED_NOTIFICATIONS
queuedNotifications: ImmutableOrderedMap(), //max = MAX_QUEUED_NOTIFICATIONS
totalQueuedNotificationsCount: 0, //used for queuedItems overflow for MAX_QUEUED_NOTIFICATIONS+
lastRead: -1,
});
// For sorting the notifications
const comparator = (a, b) => {
if (a.get('id') < b.get('id')) return 1;
if (a.get('id') > b.get('id')) return -1;
return 0;
};
const notificationToMap = notification => ImmutableMap({
id: notification.id,
type: notification.type,
@ -42,85 +48,67 @@ const notificationToMap = notification => ImmutableMap({
is_seen: get(notification, ['pleroma', 'is_seen'], true),
});
// https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/424
const isValid = notification => Boolean(notification.account.id);
const normalizeNotification = (state, notification) => {
const top = state.get('top');
if (!top) {
state = state.update('unread', unread => unread + 1);
}
if (!top) state = state.update('unread', unread => unread + 1);
return state.update('items', list => {
if (top && list.size > 40) {
list = list.take(20);
return state.update('items', map => {
if (top && map.size > 40) {
map = map.take(20);
}
return list.unshift(notificationToMap(notification));
return map.set(notification.id, notificationToMap(notification)).sort(comparator);
});
};
const expandNormalizedNotifications = (state, notifications, next) => {
let items = ImmutableList();
const processRawNotifications = notifications => (
ImmutableOrderedMap(
notifications
.filter(isValid)
.map(n => [n.id, notificationToMap(n)])
));
notifications.forEach((n, i) => {
items = items.set(i, notificationToMap(n));
});
const expandNormalizedNotifications = (state, notifications, next) => {
const items = processRawNotifications(notifications);
return state.withMutations(mutable => {
if (!items.isEmpty()) {
mutable.update('items', list => {
const lastIndex = 1 + list.findLastIndex(
item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id'))
);
const firstIndex = 1 + list.take(lastIndex).findLastIndex(
item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0
);
return list.take(firstIndex).concat(items, list.skip(lastIndex));
});
}
if (!next) {
mutable.set('hasMore', false);
}
mutable.update('items', map => map.merge(items).sort(comparator));
if (!next) mutable.set('hasMore', false);
mutable.set('isLoading', false);
});
};
const filterNotifications = (state, relationship) => {
return state.update('items', list => list.filterNot(item => item !== null && item.get('account') === relationship.id));
return state.update('items', map => map.filterNot(item => item !== null && item.get('account') === relationship.id));
};
const updateTop = (state, top) => {
if (top) {
state = state.set('unread', 0);
}
if (top) state = state.set('unread', 0);
return state.set('top', top);
};
const deleteByStatus = (state, statusId) => {
return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId));
return state.update('items', map => map.filterNot(item => item !== null && item.get('status') === statusId));
};
const updateNotificationsQueue = (state, notification, intlMessages, intlLocale) => {
const queuedNotifications = state.getIn(['queuedNotifications'], ImmutableList());
const listedNotifications = state.getIn(['items'], ImmutableList());
const queuedNotifications = state.getIn(['queuedNotifications'], ImmutableOrderedMap());
const listedNotifications = state.getIn(['items'], ImmutableOrderedMap());
const totalQueuedNotificationsCount = state.getIn(['totalQueuedNotificationsCount'], 0);
let alreadyExists = queuedNotifications.find(existingQueuedNotification => existingQueuedNotification.id === notification.id);
if (!alreadyExists) alreadyExists = listedNotifications.find(existingListedNotification => existingListedNotification.get('id') === notification.id);
if (alreadyExists) {
return state;
}
const alreadyExists = queuedNotifications.has(notification.id) || listedNotifications.has(notification.id);
if (alreadyExists) return state;
let newQueuedNotifications = queuedNotifications;
return state.withMutations(mutable => {
if (totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) {
mutable.set('queuedNotifications', newQueuedNotifications.push({
mutable.set('queuedNotifications', newQueuedNotifications.set(notification.id, {
notification,
intlMessages,
intlLocale,
@ -130,6 +118,9 @@ const updateNotificationsQueue = (state, notification, intlMessages, intlLocale)
});
};
const countUnseen = notifications => notifications.reduce((acc, cur) =>
get(cur, ['pleroma', 'is_seen'], false) === false ? acc + 1 : acc, 0);
export default function notifications(state = initialState, action) {
switch(action.type) {
case NOTIFICATIONS_EXPAND_REQUEST:
@ -137,7 +128,7 @@ export default function notifications(state = initialState, action) {
case NOTIFICATIONS_EXPAND_FAIL:
return state.set('isLoading', false);
case NOTIFICATIONS_FILTER_SET:
return state.set('items', ImmutableList()).set('hasMore', true);
return state.set('items', ImmutableOrderedMap()).set('hasMore', true);
case NOTIFICATIONS_SCROLL_TOP:
return updateTop(state, action.top);
case NOTIFICATIONS_UPDATE:
@ -146,12 +137,11 @@ export default function notifications(state = initialState, action) {
return updateNotificationsQueue(state, action.notification, action.intlMessages, action.intlLocale);
case NOTIFICATIONS_DEQUEUE:
return state.withMutations(mutable => {
mutable.set('queuedNotifications', ImmutableList());
mutable.set('queuedNotifications', ImmutableOrderedMap());
mutable.set('totalQueuedNotificationsCount', 0);
});
case NOTIFICATIONS_EXPAND_SUCCESS:
const legacyUnread = action.notifications.reduce((acc, cur) =>
get(cur, ['pleroma', 'is_seen'], false) === false ? acc + 1 : acc, 0);
const legacyUnread = countUnseen(action.notifications);
return expandNormalizedNotifications(state, action.notifications, action.next)
.merge({ unread: Math.max(legacyUnread, state.get('unread')) });
case ACCOUNT_BLOCK_SUCCESS:
@ -159,15 +149,21 @@ export default function notifications(state = initialState, action) {
case ACCOUNT_MUTE_SUCCESS:
return action.relationship.muting_notifications ? filterNotifications(state, action.relationship) : state;
case NOTIFICATIONS_CLEAR:
return state.set('items', ImmutableList()).set('hasMore', false);
return state.set('items', ImmutableOrderedMap()).set('hasMore', false);
case NOTIFICATIONS_MARK_READ_REQUEST:
return state.set('lastRead', action.lastRead);
case TIMELINE_DELETE:
return deleteByStatus(state, action.id);
case TIMELINE_DISCONNECT:
return action.timeline === 'home' ?
state.update('items', items => items.first() ? items.unshift(null) : items) :
state;
// Disable for now
// https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/432
//
// case TIMELINE_DISCONNECT:
// // This is kind of a hack - `null` renders a LoadGap in the component
// // https://github.com/tootsuite/mastodon/pull/6886
// return action.timeline === 'home' ?
// state.update('items', items => items.first() ? ImmutableOrderedSet([null]).union(items) : items) :
// state;
default:
return state;
}

@ -76,3 +76,6 @@
@import 'components/profile_hover_card';
@import 'components/filters';
@import 'components/mfa_form';
// Holiday
@import 'holiday/halloween';

@ -94,6 +94,41 @@
overflow: hidden;
}
}
.audio-toggle .react-toggle-thumb {
height: 14px;
width: 14px;
border: 1px solid var(--brand-color--med);
}
.audio-toggle .react-toggle {
height: 16px;
top: 4px;
}
.audio-toggle .react-toggle-track {
height: 16px;
width: 34px;
background-color: var(--accent-color);
}
.audio-toggle .react-toggle-track-check {
left: 4px;
bottom: 4px;
}
.react-toggle--checked .react-toggle-thumb {
left: 19px;
}
.audio-toggle .react-toggle-track-x {
right: 4px;
bottom: 4px;
}
.fa {
font-size: 14px;
}
}
.chat-messages {
@ -111,14 +146,23 @@
max-width: 70%;
border-radius: 10px;
background-color: var(--background-color);
overflow: hidden;
text-overflow: ellipsis;
overflow-wrap: break-word;
white-space: break-spaces;
position: relative;
a {
color: var(--brand-color--hicontrast);
}
&:hover,
&:focus,
&:active, {
.chat-message__menu {
opacity: 1;
pointer-events: all;
}
}
}
&--me .chat-message__bubble {
@ -129,6 +173,17 @@
&--pending .chat-message__bubble {
opacity: 0.5;
}
&__menu {
position: absolute;
top: -8px;
right: -8px;
background: var(--background-color);
border-radius: 999px;
opacity: 0;
pointer-events: none;
transition: 0.2s;
}
}
.chat-list {
@ -152,6 +207,10 @@
.display-name {
display: flex;
.hover-ref-wrapper {
display: flex;
}
bdi {
overflow: hidden;
text-overflow: ellipsis;
@ -274,7 +333,38 @@
border-radius: 0 0 10px 10px;
&__actions textarea {
padding: 10px;
padding: 10px 40px 10px 10px;
}
}
}
@media(max-width: 630px) {
.columns-area__panels__main .columns-area {
padding: 0;
}
.columns-area__panels__main {
padding: 0;
max-width: none;
}
.columns-area--mobile .column {
border-radius: 0;
}
.page {
.chat-box {
border-radius: 0;
border: 2px solid var(--foreground-color);
&__actions {
padding: 0;
textarea {
height: 4em;
border-radius: 0;
}
}
}
}
}
@ -297,6 +387,7 @@
margin-left: auto;
padding-right: 15px;
overflow: hidden;
text-decoration: none;
.account__avatar {
margin-right: 7px;
@ -368,3 +459,11 @@
object-fit: contain;
}
}
.chat-messages__divider {
text-align: center;
text-transform: uppercase;
font-size: 13px;
padding: 14px 0 2px;
opacity: 0.8;
}

@ -703,3 +703,16 @@
.column-link--transparent .icon-with-badge__badge {
border-color: var(--background-color);
}
.column__switch .audio-toggle {
position: absolute;
z-index: 4;
top: 12px;
right: 14px;
.react-toggle-track-check,
.react-toggle-track-x {
height: 16px;
color: white;
}
}

@ -105,16 +105,3 @@
}
}
}
.detailed-status {
.profile-hover-card {
top: 0;
left: 0;
}
}
/* Hide the popper when the reference is hidden */
#popper[data-popper-reference-hidden] {
visibility: hidden;
pointer-events: none;
}

@ -3,7 +3,6 @@
position: fixed;
flex-direction: column;
width: 275px;
height: 100vh;
top: 0;
bottom: 0;
left: 0;
@ -30,12 +29,10 @@
}
&__content {
display: flex;
flex: 1 1;
flex-direction: column;
padding-bottom: 40px;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
overflow: auto;
height: 100%;
width: 100%;
}
&__section {

@ -0,0 +1,158 @@
body.halloween {
// Set brand color to orange
--brand-color_h: 29.727272727272727;
--brand-color_s: 100%;
--brand-color_l: 43.13725490196079%;
// Stars BG
background-color: #904700; // Color matches twinkle.svg
background-image: url('../images/halloween/starfield.png');
background-size: cover;
background-attachment: fixed;
background-position: center;
// Full-screen pseudo-elements to hold BG graphics
&::before,
&::after,
.app-holder::before,
.app-holder::after {
content: '';
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-size: cover;
background-position: center;
width: 100%;
height: 100%;
z-index: -100;
}
// Spiderweb BG
&::before {
background-image: url('../images/halloween/spiderweb.svg');
}
// Twinkle effect by masking with semi-transparent animated circles
&::after {
z-index: -101;
background: transparent url("../images/halloween/twinkle.svg") repeat top center;
animation: halloween-twinkle 200s linear infinite;
}
.app-holder {
// Black vignette
&::before {
background-image: radial-gradient(
circle,
transparent 0%,
transparent 60%,
#000 100%
);
}
// Floating clouds BG
&::after {
background: transparent url("../images/halloween/clouds.png") repeat top center;
animation: halloween-clouds 200s linear infinite;
}
}
// Dangling spider
.ui .page__top::after,
.ui .page__columns::after {
content: '';
display: block;
width: 100px;
height: 100px;
right: 20px;
background-image: url('../images/halloween/spider.svg');
background-size: contain;
background-repeat: no-repeat;
background-position: top right;
z-index: -1;
pointer-events: none;
}
.ui .page__columns::after {
position: fixed;
top: 50px;
}
.ui .page__top::after {
position: absolute;
bottom: -100px;
}
.ui .page__top + .page__columns::after {
display: none;
}
// Witch emblem
.getting-started__footer::before {
content: '';
display: block;
background-image: url('../images/halloween/halloween-emblem.svg');
background-size: contain;
background-position: left;
background-repeat: no-repeat;
width: 100%;
height: 100px;
margin-bottom: 20px;
}
// Color fixes
// Elements directly over the BG need static colors that don't change
// regardless of the theme-mode
.getting-started__footer {
color: #fff;
a {
color: hsla(0, 0%, 100%, 0.4);
}
p {
color: hsla(0, 0%, 100%, 0.8);
}
}
.profile-info-panel {
color: #fff;
&-content__name h1 {
span:first-of-type {
color: hsla(0, 0%, 100%, 0.6);
}
small {
color: #fff;
}
}
&-content__bio {
color: #fff;
}
&-content__bio a,
&-content__fields a {
color: hsl(
var(--brand-color_h),
var(--brand-color_s),
calc(var(--brand-color_l) + 8%)
);
}
}
}
// Animations
@keyframes halloween-twinkle {
from { background-position: 0 0; }
to { background-position: -10000px 5000px; }
}
@keyframes halloween-clouds {
from { background-position: 0 0; }
to { background-position: 10000px 0; }
}

@ -0,0 +1,25 @@
# Installing Soapbox FE via YunoHost
If you want to install Soapbox FE to a Pleroma instance installed using [YunoHost](https://yunohost.org), you can do so by following these steps.
## 1. Download the build
First, download the latest build of Soapbox FE from GitLab.
```sh
curl -L https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/v1.0.0/download?job=build-production -o soapbox-fe.zip
```
## 2. Unzip the build
Then, unzip the build to the Pleroma directory under YunoHost's directory:
```sh
busybox unzip soapbox-fe.zip -o -d /home/yunohost.app/pleroma/
```
**That's it! 🎉 Soapbox FE is installed.** The change will take effect immediately, just refresh your browser tab. It's not necessary to restart the Pleroma service.
---
Thank you to [@jeroen@social.franssen.xyz](https://social.franssen.xyz/@jeroen) for discovering this method.

@ -0,0 +1,5 @@
{
"extends": [
"config:base"
]
}

Binary file not shown.

Binary file not shown.
Loading…
Cancel
Save