Merge branch 'develop' into 'icon_picker_admin_config'

# Conflicts:
#   app/soapbox/features/forms/index.js
#   app/soapbox/features/soapbox_config/index.js
This commit is contained in:
Sean King 2020-09-26 19:52:52 +00:00
commit 21f68bf623
49 changed files with 2975 additions and 460 deletions

View File

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

View File

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

View File

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

View File

@ -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"
}
]
}

View File

@ -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"
}
]
}

View File

@ -23,6 +23,10 @@ export const CHAT_READ_REQUEST = 'CHAT_READ_REQUEST';
export const CHAT_READ_SUCCESS = 'CHAT_READ_SUCCESS'; export const CHAT_READ_SUCCESS = 'CHAT_READ_SUCCESS';
export const CHAT_READ_FAIL = 'CHAT_READ_FAIL'; 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() { export function fetchChats() {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ type: CHATS_FETCH_REQUEST }); 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 });
});
};
}

View File

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

View File

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

View File

@ -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() { export function cancelReport() {
return { return {
type: REPORT_CANCEL, type: REPORT_CANCEL,

View File

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

View File

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

View File

@ -55,7 +55,17 @@ export function connectTimelineStream(timelineId, path, pollingRefresh = null, a
dispatch(fetchFilters()); dispatch(fetchFilters());
break; break;
case 'pleroma:chat_update': 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; break;
} }
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@ import IconButton from 'soapbox/components/icon_button';
const messages = defineMessages({ const messages = defineMessages({
placeholder: { id: 'chat_box.input.placeholder', defaultMessage: 'Send a message…' }, placeholder: { id: 'chat_box.input.placeholder', defaultMessage: 'Send a message…' },
send: { id: 'chat_box.actions.send', defaultMessage: 'Send' },
}); });
const mapStateToProps = (state, { chatId }) => ({ const mapStateToProps = (state, { chatId }) => ({
@ -94,6 +95,7 @@ class ChatBox extends ImmutablePureComponent {
} }
handleKeyDown = (e) => { handleKeyDown = (e) => {
this.markRead();
if (e.key === 'Enter' && e.shiftKey) { if (e.key === 'Enter' && e.shiftKey) {
this.insertLine(); this.insertLine();
e.preventDefault(); e.preventDefault();
@ -122,17 +124,6 @@ class ChatBox extends ImmutablePureComponent {
onSetInputRef(el); 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) => { handleRemoveFile = (e) => {
this.setState({ attachment: undefined, resetFileKey: fileKeyGen() }); this.setState({ attachment: undefined, resetFileKey: fileKeyGen() });
} }
@ -174,11 +165,17 @@ class ChatBox extends ImmutablePureComponent {
} }
renderActionButton = () => { renderActionButton = () => {
const { intl } = this.props;
const { resetFileKey } = this.state; const { resetFileKey } = this.state;
return this.canSubmit() ? ( return this.canSubmit() ? (
<div className='chat-box__send'> <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> </div>
) : ( ) : (
<UploadButton onSelectFile={this.handleFiles} resetFileKey={resetFileKey} /> <UploadButton onSelectFile={this.handleFiles} resetFileKey={resetFileKey} />

View File

@ -2,16 +2,37 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; 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 ImmutablePureComponent from 'react-immutable-pure-component';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; 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 emojify from 'soapbox/features/emoji/emoji';
import classNames from 'classnames'; import classNames from 'classnames';
import { openModal } from 'soapbox/actions/modal'; import { openModal } from 'soapbox/actions/modal';
import { escape, throttle } from 'lodash'; import { escape, throttle } from 'lodash';
import { MediaGallery } from 'soapbox/features/ui/util/async-components'; import { MediaGallery } from 'soapbox/features/ui/util/async-components';
import Bundle from 'soapbox/features/ui/components/bundle'; 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) => { const makeEmojiMap = record => record.get('emojis', ImmutableList()).reduce((map, emoji) => {
return map.set(`:${emoji.get('shortcode')}:`, emoji); return map.set(`:${emoji.get('shortcode')}:`, emoji);
@ -89,11 +110,16 @@ class ChatMessageList extends ImmutablePureComponent {
return scrollBottom < elem.offsetHeight * 1.5; return scrollBottom < elem.offsetHeight * 1.5;
} }
handleResize = (e) => {
if (this.isNearBottom()) this.scrollToBottom();
}
componentDidMount() { componentDidMount() {
const { dispatch, chatId } = this.props; const { dispatch, chatId } = this.props;
dispatch(fetchChatMessages(chatId)); dispatch(fetchChatMessages(chatId));
this.node.addEventListener('scroll', this.handleScroll); this.node.addEventListener('scroll', this.handleScroll);
window.addEventListener('resize', this.handleResize);
this.scrollToBottom(); this.scrollToBottom();
} }
@ -125,6 +151,7 @@ class ChatMessageList extends ImmutablePureComponent {
componentWillUnmount() { componentWillUnmount() {
this.node.removeEventListener('scroll', this.handleScroll); this.node.removeEventListener('scroll', this.handleScroll);
window.removeEventListener('resize', this.handleResize);
} }
handleLoadMore = () => { handleLoadMore = () => {
@ -176,7 +203,8 @@ class ChatMessageList extends ImmutablePureComponent {
parseContent = chatMessage => { parseContent = chatMessage => {
const content = chatMessage.get('content') || ''; const content = chatMessage.get('content') || '';
const pending = chatMessage.get('pending', false); 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); const emojiMap = makeEmojiMap(chatMessage);
return emojify(formatted, emojiMap.toJS()); return emojify(formatted, emojiMap.toJS());
} }
@ -185,12 +213,30 @@ class ChatMessageList extends ImmutablePureComponent {
this.node = c; this.node = c;
} }
render() { renderDivider = (key, text) => (
const { chatMessages, me } = this.props; <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 ( return (
<div className='chat-messages' ref={this.setRef}>
{chatMessages.map(chatMessage => (
<div <div
className={classNames('chat-message', { className={classNames('chat-message', {
'chat-message--me': chatMessage.get('account_id') === me, 'chat-message--me': chatMessage.get('account_id') === me,
@ -202,15 +248,50 @@ class ChatMessageList extends ImmutablePureComponent {
title={this.getFormattedTimestamp(chatMessage)} title={this.getFormattedTimestamp(chatMessage)}
className='chat-message__bubble' className='chat-message__bubble'
ref={this.setBubbleRef} ref={this.setBubbleRef}
tabIndex={0}
> >
{this.maybeRenderMedia(chatMessage)} {this.maybeRenderMedia(chatMessage)}
<span <span
className='chat-message__content' className='chat-message__content'
dangerouslySetInnerHTML={{ __html: this.parseContent(chatMessage) }} 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> </div>
))} </div>
);
}
render() {
const { chatMessages, intl } = this.props;
return (
<div className='chat-messages' ref={this.setRef}>
{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 style={{ float: 'left', clear: 'both' }} ref={this.setMessageEndRef} />
</div> </div>
); );

View File

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

View File

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

View File

@ -3,14 +3,10 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { v4 as uuidv4 } from 'uuid'; 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 FontIconPicker from '@fonticonpicker/react-fonticonpicker';
import forkAwesomeIcons from './forkawesome.json'; import forkAwesomeIcons from './forkawesome.json';
const FormPropTypes = { export const FormPropTypes = {
label: PropTypes.oneOfType([ label: PropTypes.oneOfType([
PropTypes.string, PropTypes.string,
PropTypes.object, PropTypes.object,
@ -18,8 +14,6 @@ const FormPropTypes = {
]), ]),
}; };
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
export const InputContainer = (props) => { export const InputContainer = (props) => {
const containerClass = classNames('input', { const containerClass = classNames('input', {
'with_label': props.label, '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 { export class RadioItem extends ImmutablePureComponent {
static propTypes = { static propTypes = {

View File

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

View File

@ -185,6 +185,11 @@ class Preferences extends ImmutablePureComponent {
path={['dyslexicFont']} path={['dyslexicFont']}
/> />
</div> </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 <SettingsCheckbox
label={<FormattedMessage id='preferences.fields.demetricator_label' defaultMessage='Use Demetricator' />} 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.' />} hint={<FormattedMessage id='preferences.hints.demetricator' defaultMessage='Decrease social media anxiety by hiding all numbers from the site.' />}

View File

@ -6,14 +6,18 @@ import { Link } from 'react-router-dom';
import LoginForm from 'soapbox/features/auth_login/components/login_form'; import LoginForm from 'soapbox/features/auth_login/components/login_form';
import SiteLogo from './site_logo'; import SiteLogo from './site_logo';
import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types'; 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 { logIn } from 'soapbox/actions/auth';
import { fetchMe } from 'soapbox/actions/me'; import { fetchMe } from 'soapbox/actions/me';
import PropTypes from 'prop-types';
import OtpAuthForm from 'soapbox/features/auth_login/components/otp_auth_form'; import OtpAuthForm from 'soapbox/features/auth_login/components/otp_auth_form';
import IconButton from 'soapbox/components/icon_button'; import IconButton from 'soapbox/components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({ 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' }, close: { id: 'lightbox.close', defaultMessage: 'Close' },
}); });
@ -32,6 +36,12 @@ class Header extends ImmutablePureComponent {
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
} }
state = {
isLoading: false,
mfa_auth_needed: false,
mfa_token: '',
}
getFormData = (form) => { getFormData = (form) => {
return Object.fromEntries( return Object.fromEntries(
Array.from(form).map(i => [i.name, i.value]) Array.from(form).map(i => [i.name, i.value])
@ -64,16 +74,12 @@ class Header extends ImmutablePureComponent {
static propTypes = { static propTypes = {
me: SoapboxPropTypes.me, me: SoapboxPropTypes.me,
instance: ImmutablePropTypes.map, instance: ImmutablePropTypes.map,
} intl: PropTypes.object.isRequired,
state = {
mfa_auth_needed: false,
mfa_token: '',
} }
render() { render() {
const { me, instance, isLoading, intl } = this.props; const { me, instance, intl } = this.props;
const { mfa_auth_needed, mfa_token } = this.state; const { isLoading, mfa_auth_needed, mfa_token } = this.state;
return ( return (
<nav className='header'> <nav className='header'>
@ -90,21 +96,21 @@ class Header extends ImmutablePureComponent {
<Link className='brand' to='/'> <Link className='brand' to='/'>
<SiteLogo /> <SiteLogo />
</Link> </Link>
<Link className='nav-link optional' to='/'>Home</Link> <Link className='nav-link optional' to='/'>{intl.formatMessage(messages.home)}</Link>
<Link className='nav-link' to='/about'>About</Link> <Link className='nav-link' to='/about'>{intl.formatMessage(messages.about)}</Link>
</div> </div>
<div className='nav-center' /> <div className='nav-center' />
<div className='nav-right'> <div className='nav-right'>
<div className='hidden-sm'> <div className='hidden-sm'>
{me {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} /> : <LoginForm handleSubmit={this.handleSubmit} isLoading={isLoading} />
} }
</div> </div>
<div className='visible-sm'> <div className='visible-sm'>
{me {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>
: <Link className='nav-link nav-button webapp-btn' to='/auth/sign_in'>Log in</Link> : <Link className='nav-link nav-button webapp-btn' to='/auth/sign_in'>{intl.formatMessage(messages.login)}</Link>
} }
</div> </div>
</div> </div>

View File

@ -12,15 +12,19 @@ import {
Checkbox, Checkbox,
FileChooser, FileChooser,
SimpleTextarea, SimpleTextarea,
ColorWithPicker,
FileChooserLogo, FileChooserLogo,
IconPicker, IconPicker,
FormPropTypes,
} from 'soapbox/features/forms'; } from 'soapbox/features/forms';
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import { updateAdminConfig } from 'soapbox/actions/admin'; import { updateAdminConfig } from 'soapbox/actions/admin';
import Icon from 'soapbox/components/icon'; import Icon from 'soapbox/components/icon';
import { defaultConfig } from 'soapbox/actions/soapbox'; import { defaultConfig } from 'soapbox/actions/soapbox';
import { uploadMedia } from 'soapbox/actions/media'; 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({ const messages = defineMessages({
heading: { id: 'column.soapbox_config', defaultMessage: 'Soapbox config' }, 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.' }, rawJSONHint: { id: 'soapbox_config.raw_json_hint', defaultMessage: 'Advanced: Edit the settings data directly.' },
}); });
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
const templates = { const templates = {
promoPanelItem: ImmutableMap({ icon: '', text: '', url: '' }), promoPanelItem: ImmutableMap({ icon: '', text: '', url: '' }),
footerItem: ImmutableMap({ title: '', 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>
);
}
}

View File

@ -56,21 +56,21 @@ class UserPanel extends ImmutablePureComponent {
<div className='user-panel__stats-block'> <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'))}> <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> <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> <span className='user-panel-stats-item__label'><FormattedMessage className='user-panel-stats-item__label' id='account.posts' defaultMessage='Posts' /></span>
</Link> </Link>
</div>} </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'))}> <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> <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> <span className='user-panel-stats-item__label'><FormattedMessage id='account.followers' defaultMessage='Followers' /></span>
</Link> </Link>
</div>} </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'))}> <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> <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> <span className='user-panel-stats-item__label'><FormattedMessage className='user-panel-stats-item__label' id='account.follows' defaultMessage='Follows' /></span>

View File

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

View File

@ -1,5 +1,8 @@
import reducer from '../contexts'; 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', () => { describe('contexts reducer', () => {
it('should return the initial state', () => { it('should return the initial state', () => {
@ -8,4 +11,34 @@ describe('contexts reducer', () => {
replies: ImmutableMap(), 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',
]),
}),
}));
});
}); });

View File

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

View File

@ -3,6 +3,7 @@ import {
CHAT_MESSAGES_FETCH_SUCCESS, CHAT_MESSAGES_FETCH_SUCCESS,
CHAT_MESSAGE_SEND_REQUEST, CHAT_MESSAGE_SEND_REQUEST,
CHAT_MESSAGE_SEND_SUCCESS, CHAT_MESSAGE_SEND_SUCCESS,
CHAT_MESSAGE_DELETE_SUCCESS,
} from 'soapbox/actions/chats'; } from 'soapbox/actions/chats';
import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming'; import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; 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)); return updateList(state, action.chatId, action.chatMessages.map(chat => chat.id));
case CHAT_MESSAGE_SEND_SUCCESS: case CHAT_MESSAGE_SEND_SUCCESS:
return replaceMessage(state, action.chatId, action.uuid, action.chatMessage.id); 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: default:
return state; return state;
} }

View File

@ -3,6 +3,8 @@ import {
CHAT_MESSAGES_FETCH_SUCCESS, CHAT_MESSAGES_FETCH_SUCCESS,
CHAT_MESSAGE_SEND_REQUEST, CHAT_MESSAGE_SEND_REQUEST,
CHAT_MESSAGE_SEND_SUCCESS, CHAT_MESSAGE_SEND_SUCCESS,
CHAT_MESSAGE_DELETE_REQUEST,
CHAT_MESSAGE_DELETE_SUCCESS,
} from 'soapbox/actions/chats'; } from 'soapbox/actions/chats';
import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming'; import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
import { Map as ImmutableMap, fromJS } from 'immutable'; 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); return importMessage(state, fromJS(action.chatMessage)).delete(action.uuid);
case STREAMING_CHAT_UPDATE: case STREAMING_CHAT_UPDATE:
return importLastMessages(state, fromJS([action.chat])); 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: default:
return state; return state;
} }

View File

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

View File

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

View File

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

View File

@ -94,6 +94,41 @@
overflow: hidden; 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 { .chat-messages {
@ -111,14 +146,23 @@
max-width: 70%; max-width: 70%;
border-radius: 10px; border-radius: 10px;
background-color: var(--background-color); background-color: var(--background-color);
overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow-wrap: break-word; overflow-wrap: break-word;
white-space: break-spaces; white-space: break-spaces;
position: relative;
a { a {
color: var(--brand-color--hicontrast); color: var(--brand-color--hicontrast);
} }
&:hover,
&:focus,
&:active, {
.chat-message__menu {
opacity: 1;
pointer-events: all;
}
}
} }
&--me .chat-message__bubble { &--me .chat-message__bubble {
@ -129,6 +173,17 @@
&--pending .chat-message__bubble { &--pending .chat-message__bubble {
opacity: 0.5; 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 { .chat-list {
@ -152,6 +207,10 @@
.display-name { .display-name {
display: flex; display: flex;
.hover-ref-wrapper {
display: flex;
}
bdi { bdi {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -274,7 +333,38 @@
border-radius: 0 0 10px 10px; border-radius: 0 0 10px 10px;
&__actions textarea { &__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; margin-left: auto;
padding-right: 15px; padding-right: 15px;
overflow: hidden; overflow: hidden;
text-decoration: none;
.account__avatar { .account__avatar {
margin-right: 7px; margin-right: 7px;
@ -368,3 +459,11 @@
object-fit: contain; object-fit: contain;
} }
} }
.chat-messages__divider {
text-align: center;
text-transform: uppercase;
font-size: 13px;
padding: 14px 0 2px;
opacity: 0.8;
}

View File

@ -703,3 +703,16 @@
.column-link--transparent .icon-with-badge__badge { .column-link--transparent .icon-with-badge__badge {
border-color: var(--background-color); 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;
}
}

View File

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

View File

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

View File

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

View File

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

5
renovate.json Normal file
View File

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

BIN
static/sounds/chat.mp3 Normal file

Binary file not shown.

BIN
static/sounds/chat.oga Normal file

Binary file not shown.