commit b1b5dc62dbe8a5110d7471b5d4b773dd590f8bc8 Author: Alex Gleason Date: Fri Mar 27 15:59:38 2020 -0500 Initial commit diff --git a/app/fonts/OpenDyslexic/OFL.txt b/app/fonts/OpenDyslexic/OFL.txt new file mode 100644 index 000000000..bb867823f --- /dev/null +++ b/app/fonts/OpenDyslexic/OFL.txt @@ -0,0 +1,94 @@ +Copyright (c) 2019-07-29, Abbie Gonzalez (https://abbiecod.es|support@abbiecod.es), +with Reserved Font Name OpenDyslexic. +Copyright (c) 12/2012 - 2019 +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/app/fonts/OpenDyslexic/OpenDyslexic-Bold-Italic.woff2 b/app/fonts/OpenDyslexic/OpenDyslexic-Bold-Italic.woff2 new file mode 100644 index 000000000..aa7bcdea9 Binary files /dev/null and b/app/fonts/OpenDyslexic/OpenDyslexic-Bold-Italic.woff2 differ diff --git a/app/fonts/OpenDyslexic/OpenDyslexic-Bold.woff2 b/app/fonts/OpenDyslexic/OpenDyslexic-Bold.woff2 new file mode 100644 index 000000000..2f04ad119 Binary files /dev/null and b/app/fonts/OpenDyslexic/OpenDyslexic-Bold.woff2 differ diff --git a/app/fonts/OpenDyslexic/OpenDyslexic-Italic.woff2 b/app/fonts/OpenDyslexic/OpenDyslexic-Italic.woff2 new file mode 100644 index 000000000..00c19082d Binary files /dev/null and b/app/fonts/OpenDyslexic/OpenDyslexic-Italic.woff2 differ diff --git a/app/fonts/OpenDyslexic/OpenDyslexic-Regular.woff2 b/app/fonts/OpenDyslexic/OpenDyslexic-Regular.woff2 new file mode 100644 index 000000000..47e26d82a Binary files /dev/null and b/app/fonts/OpenDyslexic/OpenDyslexic-Regular.woff2 differ diff --git a/app/fonts/montserrat/montserrat-extra-bold-800.eot b/app/fonts/montserrat/montserrat-extra-bold-800.eot new file mode 100644 index 000000000..85c4c6f7a Binary files /dev/null and b/app/fonts/montserrat/montserrat-extra-bold-800.eot differ diff --git a/app/fonts/montserrat/montserrat-extra-bold-800.svg b/app/fonts/montserrat/montserrat-extra-bold-800.svg new file mode 100644 index 000000000..350e53f53 --- /dev/null +++ b/app/fonts/montserrat/montserrat-extra-bold-800.svg @@ -0,0 +1,327 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/fonts/montserrat/montserrat-extra-bold-800.ttf b/app/fonts/montserrat/montserrat-extra-bold-800.ttf new file mode 100644 index 000000000..94514b21a Binary files /dev/null and b/app/fonts/montserrat/montserrat-extra-bold-800.ttf differ diff --git a/app/fonts/montserrat/montserrat-extra-bold-800.woff b/app/fonts/montserrat/montserrat-extra-bold-800.woff new file mode 100644 index 000000000..f236d6c99 Binary files /dev/null and b/app/fonts/montserrat/montserrat-extra-bold-800.woff differ diff --git a/app/fonts/montserrat/montserrat-extra-bold-800.woff2 b/app/fonts/montserrat/montserrat-extra-bold-800.woff2 new file mode 100644 index 000000000..887107632 Binary files /dev/null and b/app/fonts/montserrat/montserrat-extra-bold-800.woff2 differ diff --git a/app/fonts/roboto/roboto-bold-700.eot b/app/fonts/roboto/roboto-bold-700.eot new file mode 100644 index 000000000..f23076570 Binary files /dev/null and b/app/fonts/roboto/roboto-bold-700.eot differ diff --git a/app/fonts/roboto/roboto-bold-700.svg b/app/fonts/roboto/roboto-bold-700.svg new file mode 100644 index 000000000..11db87dd0 --- /dev/null +++ b/app/fonts/roboto/roboto-bold-700.svg @@ -0,0 +1,309 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/fonts/roboto/roboto-bold-700.ttf b/app/fonts/roboto/roboto-bold-700.ttf new file mode 100644 index 000000000..333022da4 Binary files /dev/null and b/app/fonts/roboto/roboto-bold-700.ttf differ diff --git a/app/fonts/roboto/roboto-bold-700.woff b/app/fonts/roboto/roboto-bold-700.woff new file mode 100644 index 000000000..8c9b02410 Binary files /dev/null and b/app/fonts/roboto/roboto-bold-700.woff differ diff --git a/app/fonts/roboto/roboto-bold-700.woff2 b/app/fonts/roboto/roboto-bold-700.woff2 new file mode 100644 index 000000000..3768f0182 Binary files /dev/null and b/app/fonts/roboto/roboto-bold-700.woff2 differ diff --git a/app/fonts/roboto/roboto-bold-italic-700.eot b/app/fonts/roboto/roboto-bold-italic-700.eot new file mode 100644 index 000000000..77866f2d3 Binary files /dev/null and b/app/fonts/roboto/roboto-bold-italic-700.eot differ diff --git a/app/fonts/roboto/roboto-bold-italic-700.svg b/app/fonts/roboto/roboto-bold-italic-700.svg new file mode 100644 index 000000000..050bee0e4 --- /dev/null +++ b/app/fonts/roboto/roboto-bold-italic-700.svg @@ -0,0 +1,325 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/fonts/roboto/roboto-bold-italic-700.ttf b/app/fonts/roboto/roboto-bold-italic-700.ttf new file mode 100644 index 000000000..51df87709 Binary files /dev/null and b/app/fonts/roboto/roboto-bold-italic-700.ttf differ diff --git a/app/fonts/roboto/roboto-bold-italic-700.woff b/app/fonts/roboto/roboto-bold-italic-700.woff new file mode 100644 index 000000000..55befb695 Binary files /dev/null and b/app/fonts/roboto/roboto-bold-italic-700.woff differ diff --git a/app/fonts/roboto/roboto-bold-italic-700.woff2 b/app/fonts/roboto/roboto-bold-italic-700.woff2 new file mode 100644 index 000000000..93ee346e5 Binary files /dev/null and b/app/fonts/roboto/roboto-bold-italic-700.woff2 differ diff --git a/app/fonts/roboto/roboto-light-300.eot b/app/fonts/roboto/roboto-light-300.eot new file mode 100644 index 000000000..efa769588 Binary files /dev/null and b/app/fonts/roboto/roboto-light-300.eot differ diff --git a/app/fonts/roboto/roboto-light-300.svg b/app/fonts/roboto/roboto-light-300.svg new file mode 100644 index 000000000..4ded944a8 --- /dev/null +++ b/app/fonts/roboto/roboto-light-300.svg @@ -0,0 +1,312 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/fonts/roboto/roboto-light-300.ttf b/app/fonts/roboto/roboto-light-300.ttf new file mode 100644 index 000000000..89f0e4627 Binary files /dev/null and b/app/fonts/roboto/roboto-light-300.ttf differ diff --git a/app/fonts/roboto/roboto-light-300.woff b/app/fonts/roboto/roboto-light-300.woff new file mode 100644 index 000000000..983083c7f Binary files /dev/null and b/app/fonts/roboto/roboto-light-300.woff differ diff --git a/app/fonts/roboto/roboto-light-300.woff2 b/app/fonts/roboto/roboto-light-300.woff2 new file mode 100644 index 000000000..7438daebe Binary files /dev/null and b/app/fonts/roboto/roboto-light-300.woff2 differ diff --git a/app/fonts/roboto/roboto-light-italic-300.eot b/app/fonts/roboto/roboto-light-italic-300.eot new file mode 100644 index 000000000..a431b1166 Binary files /dev/null and b/app/fonts/roboto/roboto-light-italic-300.eot differ diff --git a/app/fonts/roboto/roboto-light-italic-300.svg b/app/fonts/roboto/roboto-light-italic-300.svg new file mode 100644 index 000000000..758402b65 --- /dev/null +++ b/app/fonts/roboto/roboto-light-italic-300.svg @@ -0,0 +1,329 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/fonts/roboto/roboto-light-italic-300.ttf b/app/fonts/roboto/roboto-light-italic-300.ttf new file mode 100644 index 000000000..712f868c4 Binary files /dev/null and b/app/fonts/roboto/roboto-light-italic-300.ttf differ diff --git a/app/fonts/roboto/roboto-light-italic-300.woff b/app/fonts/roboto/roboto-light-italic-300.woff new file mode 100644 index 000000000..1d4ce5212 Binary files /dev/null and b/app/fonts/roboto/roboto-light-italic-300.woff differ diff --git a/app/fonts/roboto/roboto-light-italic-300.woff2 b/app/fonts/roboto/roboto-light-italic-300.woff2 new file mode 100644 index 000000000..2e45b8676 Binary files /dev/null and b/app/fonts/roboto/roboto-light-italic-300.woff2 differ diff --git a/app/fonts/roboto/roboto-regular-400.eot b/app/fonts/roboto/roboto-regular-400.eot new file mode 100644 index 000000000..cdf3f0079 Binary files /dev/null and b/app/fonts/roboto/roboto-regular-400.eot differ diff --git a/app/fonts/roboto/roboto-regular-400.svg b/app/fonts/roboto/roboto-regular-400.svg new file mode 100644 index 000000000..627f5a368 --- /dev/null +++ b/app/fonts/roboto/roboto-regular-400.svg @@ -0,0 +1,308 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/fonts/roboto/roboto-regular-400.ttf b/app/fonts/roboto/roboto-regular-400.ttf new file mode 100644 index 000000000..a36cdeed5 Binary files /dev/null and b/app/fonts/roboto/roboto-regular-400.ttf differ diff --git a/app/fonts/roboto/roboto-regular-400.woff b/app/fonts/roboto/roboto-regular-400.woff new file mode 100644 index 000000000..7245f5cae Binary files /dev/null and b/app/fonts/roboto/roboto-regular-400.woff differ diff --git a/app/fonts/roboto/roboto-regular-400.woff2 b/app/fonts/roboto/roboto-regular-400.woff2 new file mode 100644 index 000000000..2e3eefc89 Binary files /dev/null and b/app/fonts/roboto/roboto-regular-400.woff2 differ diff --git a/app/fonts/roboto/roboto-regular-italic-400.eot b/app/fonts/roboto/roboto-regular-italic-400.eot new file mode 100644 index 000000000..e25ecbe0d Binary files /dev/null and b/app/fonts/roboto/roboto-regular-italic-400.eot differ diff --git a/app/fonts/roboto/roboto-regular-italic-400.svg b/app/fonts/roboto/roboto-regular-italic-400.svg new file mode 100644 index 000000000..4d5979710 --- /dev/null +++ b/app/fonts/roboto/roboto-regular-italic-400.svg @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/fonts/roboto/roboto-regular-italic-400.ttf b/app/fonts/roboto/roboto-regular-italic-400.ttf new file mode 100644 index 000000000..0d2131121 Binary files /dev/null and b/app/fonts/roboto/roboto-regular-italic-400.ttf differ diff --git a/app/fonts/roboto/roboto-regular-italic-400.woff b/app/fonts/roboto/roboto-regular-italic-400.woff new file mode 100644 index 000000000..cd91cce9c Binary files /dev/null and b/app/fonts/roboto/roboto-regular-italic-400.woff differ diff --git a/app/fonts/roboto/roboto-regular-italic-400.woff2 b/app/fonts/roboto/roboto-regular-italic-400.woff2 new file mode 100644 index 000000000..bdeb9f5d7 Binary files /dev/null and b/app/fonts/roboto/roboto-regular-italic-400.woff2 differ diff --git a/app/fonts/spinster/spinster.eot b/app/fonts/spinster/spinster.eot new file mode 100644 index 000000000..d66bb100a Binary files /dev/null and b/app/fonts/spinster/spinster.eot differ diff --git a/app/fonts/spinster/spinster.svg b/app/fonts/spinster/spinster.svg new file mode 100644 index 000000000..20d08a586 --- /dev/null +++ b/app/fonts/spinster/spinster.svg @@ -0,0 +1,12 @@ + + + +Generated by IcoMoon + + + + + + + + \ No newline at end of file diff --git a/app/fonts/spinster/spinster.ttf b/app/fonts/spinster/spinster.ttf new file mode 100644 index 000000000..df64210fb Binary files /dev/null and b/app/fonts/spinster/spinster.ttf differ diff --git a/app/fonts/spinster/spinster.woff b/app/fonts/spinster/spinster.woff new file mode 100644 index 000000000..1902dbc36 Binary files /dev/null and b/app/fonts/spinster/spinster.woff differ diff --git a/app/gabsocial/actions/accounts.js b/app/gabsocial/actions/accounts.js new file mode 100644 index 000000000..8dcd8e052 --- /dev/null +++ b/app/gabsocial/actions/accounts.js @@ -0,0 +1,833 @@ +import api, { getLinks } from '../api'; +import openDB from '../storage/db'; +import { + importAccount, + importFetchedAccount, + importFetchedAccounts, + importErrorWhileFetchingAccountByUsername, +} from './importer'; +import { me } from 'gabsocial/initial_state'; + +export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; +export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS'; +export const ACCOUNT_FETCH_FAIL = 'ACCOUNT_FETCH_FAIL'; + +export const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST'; +export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS'; +export const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL'; + +export const ACCOUNT_UNFOLLOW_REQUEST = 'ACCOUNT_UNFOLLOW_REQUEST'; +export const ACCOUNT_UNFOLLOW_SUCCESS = 'ACCOUNT_UNFOLLOW_SUCCESS'; +export const ACCOUNT_UNFOLLOW_FAIL = 'ACCOUNT_UNFOLLOW_FAIL'; + +export const ACCOUNT_BLOCK_REQUEST = 'ACCOUNT_BLOCK_REQUEST'; +export const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS'; +export const ACCOUNT_BLOCK_FAIL = 'ACCOUNT_BLOCK_FAIL'; + +export const ACCOUNT_UNBLOCK_REQUEST = 'ACCOUNT_UNBLOCK_REQUEST'; +export const ACCOUNT_UNBLOCK_SUCCESS = 'ACCOUNT_UNBLOCK_SUCCESS'; +export const ACCOUNT_UNBLOCK_FAIL = 'ACCOUNT_UNBLOCK_FAIL'; + +export const ACCOUNT_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST'; +export const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS'; +export const ACCOUNT_MUTE_FAIL = 'ACCOUNT_MUTE_FAIL'; + +export const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST'; +export const ACCOUNT_UNMUTE_SUCCESS = 'ACCOUNT_UNMUTE_SUCCESS'; +export const ACCOUNT_UNMUTE_FAIL = 'ACCOUNT_UNMUTE_FAIL'; + +export const ACCOUNT_PIN_REQUEST = 'ACCOUNT_PIN_REQUEST'; +export const ACCOUNT_PIN_SUCCESS = 'ACCOUNT_PIN_SUCCESS'; +export const ACCOUNT_PIN_FAIL = 'ACCOUNT_PIN_FAIL'; + +export const ACCOUNT_UNPIN_REQUEST = 'ACCOUNT_UNPIN_REQUEST'; +export const ACCOUNT_UNPIN_SUCCESS = 'ACCOUNT_UNPIN_SUCCESS'; +export const ACCOUNT_UNPIN_FAIL = 'ACCOUNT_UNPIN_FAIL'; + +export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST'; +export const FOLLOWERS_FETCH_SUCCESS = 'FOLLOWERS_FETCH_SUCCESS'; +export const FOLLOWERS_FETCH_FAIL = 'FOLLOWERS_FETCH_FAIL'; + +export const FOLLOWERS_EXPAND_REQUEST = 'FOLLOWERS_EXPAND_REQUEST'; +export const FOLLOWERS_EXPAND_SUCCESS = 'FOLLOWERS_EXPAND_SUCCESS'; +export const FOLLOWERS_EXPAND_FAIL = 'FOLLOWERS_EXPAND_FAIL'; + +export const FOLLOWING_FETCH_REQUEST = 'FOLLOWING_FETCH_REQUEST'; +export const FOLLOWING_FETCH_SUCCESS = 'FOLLOWING_FETCH_SUCCESS'; +export const FOLLOWING_FETCH_FAIL = 'FOLLOWING_FETCH_FAIL'; + +export const FOLLOWING_EXPAND_REQUEST = 'FOLLOWING_EXPAND_REQUEST'; +export const FOLLOWING_EXPAND_SUCCESS = 'FOLLOWING_EXPAND_SUCCESS'; +export const FOLLOWING_EXPAND_FAIL = 'FOLLOWING_EXPAND_FAIL'; + +export const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST'; +export const RELATIONSHIPS_FETCH_SUCCESS = 'RELATIONSHIPS_FETCH_SUCCESS'; +export const RELATIONSHIPS_FETCH_FAIL = 'RELATIONSHIPS_FETCH_FAIL'; + +export const FOLLOW_REQUESTS_FETCH_REQUEST = 'FOLLOW_REQUESTS_FETCH_REQUEST'; +export const FOLLOW_REQUESTS_FETCH_SUCCESS = 'FOLLOW_REQUESTS_FETCH_SUCCESS'; +export const FOLLOW_REQUESTS_FETCH_FAIL = 'FOLLOW_REQUESTS_FETCH_FAIL'; + +export const FOLLOW_REQUESTS_EXPAND_REQUEST = 'FOLLOW_REQUESTS_EXPAND_REQUEST'; +export const FOLLOW_REQUESTS_EXPAND_SUCCESS = 'FOLLOW_REQUESTS_EXPAND_SUCCESS'; +export const FOLLOW_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL'; + +export const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST'; +export const FOLLOW_REQUEST_AUTHORIZE_SUCCESS = 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS'; +export const FOLLOW_REQUEST_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL'; + +export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST'; +export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS'; +export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL'; + +function getFromDB(dispatch, getState, index, id) { + return new Promise((resolve, reject) => { + const request = index.get(id); + + request.onerror = reject; + + request.onsuccess = () => { + if (!request.result) { + reject(); + return; + } + + dispatch(importAccount(request.result)); + resolve(request.result.moved && getFromDB(dispatch, getState, index, request.result.moved)); + }; + }); +} + +export function fetchAccount(id) { + return (dispatch, getState) => { + dispatch(fetchRelationships([id])); + + if (getState().getIn(['accounts', id], null) !== null) { + return; + } + + dispatch(fetchAccountRequest(id)); + + openDB().then(db => getFromDB( + dispatch, + getState, + db.transaction('accounts', 'read').objectStore('accounts').index('id'), + id + ).then(() => db.close(), error => { + db.close(); + throw error; + })).catch(() => api(getState).get(`/api/v1/accounts/${id}`).then(response => { + dispatch(importFetchedAccount(response.data)); + })).then(() => { + dispatch(fetchAccountSuccess()); + }).catch(error => { + dispatch(fetchAccountFail(id, error)); + }); + }; +}; + +export function fetchAccountByUsername(username) { + return (dispatch, getState) => { + api(getState).get(`/api/v1/account_by_username/${username}`).then(response => { + dispatch(importFetchedAccount(response.data)); + }).then(() => { + dispatch(fetchAccountSuccess()); + }).catch(error => { + dispatch(fetchAccountFail(null, error)); + dispatch(importErrorWhileFetchingAccountByUsername(username)); + }); + }; +}; + +export function fetchAccountRequest(id) { + return { + type: ACCOUNT_FETCH_REQUEST, + id, + }; +}; + +export function fetchAccountSuccess() { + return { + type: ACCOUNT_FETCH_SUCCESS, + }; +}; + +export function fetchAccountFail(id, error) { + return { + type: ACCOUNT_FETCH_FAIL, + id, + error, + skipAlert: true, + }; +}; + +export function followAccount(id, reblogs = true) { + return (dispatch, getState) => { + if (!me) return; + + const alreadyFollowing = getState().getIn(['relationships', id, 'following']); + const locked = getState().getIn(['accounts', id, 'locked'], false); + + dispatch(followAccountRequest(id, locked)); + + api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => { + dispatch(followAccountSuccess(response.data, alreadyFollowing)); + }).catch(error => { + dispatch(followAccountFail(error, locked)); + }); + }; +}; + +export function unfollowAccount(id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(unfollowAccountRequest(id)); + + api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => { + dispatch(unfollowAccountSuccess(response.data, getState().get('statuses'))); + }).catch(error => { + dispatch(unfollowAccountFail(error)); + }); + }; +}; + +export function followAccountRequest(id, locked) { + return { + type: ACCOUNT_FOLLOW_REQUEST, + id, + locked, + skipLoading: true, + }; +}; + +export function followAccountSuccess(relationship, alreadyFollowing) { + return { + type: ACCOUNT_FOLLOW_SUCCESS, + relationship, + alreadyFollowing, + skipLoading: true, + }; +}; + +export function followAccountFail(error, locked) { + return { + type: ACCOUNT_FOLLOW_FAIL, + error, + locked, + skipLoading: true, + }; +}; + +export function unfollowAccountRequest(id) { + return { + type: ACCOUNT_UNFOLLOW_REQUEST, + id, + skipLoading: true, + }; +}; + +export function unfollowAccountSuccess(relationship, statuses) { + return { + type: ACCOUNT_UNFOLLOW_SUCCESS, + relationship, + statuses, + skipLoading: true, + }; +}; + +export function unfollowAccountFail(error) { + return { + type: ACCOUNT_UNFOLLOW_FAIL, + error, + skipLoading: true, + }; +}; + +export function blockAccount(id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(blockAccountRequest(id)); + + api(getState).post(`/api/v1/accounts/${id}/block`).then(response => { + // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers + dispatch(blockAccountSuccess(response.data, getState().get('statuses'))); + }).catch(error => { + dispatch(blockAccountFail(id, error)); + }); + }; +}; + +export function unblockAccount(id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(unblockAccountRequest(id)); + + api(getState).post(`/api/v1/accounts/${id}/unblock`).then(response => { + dispatch(unblockAccountSuccess(response.data)); + }).catch(error => { + dispatch(unblockAccountFail(id, error)); + }); + }; +}; + +export function blockAccountRequest(id) { + return { + type: ACCOUNT_BLOCK_REQUEST, + id, + }; +}; + +export function blockAccountSuccess(relationship, statuses) { + return { + type: ACCOUNT_BLOCK_SUCCESS, + relationship, + statuses, + }; +}; + +export function blockAccountFail(error) { + return { + type: ACCOUNT_BLOCK_FAIL, + error, + }; +}; + +export function unblockAccountRequest(id) { + return { + type: ACCOUNT_UNBLOCK_REQUEST, + id, + }; +}; + +export function unblockAccountSuccess(relationship) { + return { + type: ACCOUNT_UNBLOCK_SUCCESS, + relationship, + }; +}; + +export function unblockAccountFail(error) { + return { + type: ACCOUNT_UNBLOCK_FAIL, + error, + }; +}; + + +export function muteAccount(id, notifications) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(muteAccountRequest(id)); + + api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications }).then(response => { + // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers + dispatch(muteAccountSuccess(response.data, getState().get('statuses'))); + }).catch(error => { + dispatch(muteAccountFail(id, error)); + }); + }; +}; + +export function unmuteAccount(id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(unmuteAccountRequest(id)); + + api(getState).post(`/api/v1/accounts/${id}/unmute`).then(response => { + dispatch(unmuteAccountSuccess(response.data)); + }).catch(error => { + dispatch(unmuteAccountFail(id, error)); + }); + }; +}; + +export function muteAccountRequest(id) { + return { + type: ACCOUNT_MUTE_REQUEST, + id, + }; +}; + +export function muteAccountSuccess(relationship, statuses) { + return { + type: ACCOUNT_MUTE_SUCCESS, + relationship, + statuses, + }; +}; + +export function muteAccountFail(error) { + return { + type: ACCOUNT_MUTE_FAIL, + error, + }; +}; + +export function unmuteAccountRequest(id) { + return { + type: ACCOUNT_UNMUTE_REQUEST, + id, + }; +}; + +export function unmuteAccountSuccess(relationship) { + return { + type: ACCOUNT_UNMUTE_SUCCESS, + relationship, + }; +}; + +export function unmuteAccountFail(error) { + return { + type: ACCOUNT_UNMUTE_FAIL, + error, + }; +}; + + +export function fetchFollowers(id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(fetchFollowersRequest(id)); + + api(getState).get(`/api/v1/accounts/${id}/followers`).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(importFetchedAccounts(response.data)); + dispatch(fetchFollowersSuccess(id, response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => { + dispatch(fetchFollowersFail(id, error)); + }); + }; +}; + +export function fetchFollowersRequest(id) { + return { + type: FOLLOWERS_FETCH_REQUEST, + id, + }; +}; + +export function fetchFollowersSuccess(id, accounts, next) { + return { + type: FOLLOWERS_FETCH_SUCCESS, + id, + accounts, + next, + }; +}; + +export function fetchFollowersFail(id, error) { + return { + type: FOLLOWERS_FETCH_FAIL, + id, + error, + }; +}; + +export function expandFollowers(id) { + return (dispatch, getState) => { + if (!me) return; + + const url = getState().getIn(['user_lists', 'followers', id, 'next']); + + if (url === null) { + return; + } + + dispatch(expandFollowersRequest(id)); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(importFetchedAccounts(response.data)); + dispatch(expandFollowersSuccess(id, response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => { + dispatch(expandFollowersFail(id, error)); + }); + }; +}; + +export function expandFollowersRequest(id) { + return { + type: FOLLOWERS_EXPAND_REQUEST, + id, + }; +}; + +export function expandFollowersSuccess(id, accounts, next) { + return { + type: FOLLOWERS_EXPAND_SUCCESS, + id, + accounts, + next, + }; +}; + +export function expandFollowersFail(id, error) { + return { + type: FOLLOWERS_EXPAND_FAIL, + id, + error, + }; +}; + +export function fetchFollowing(id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(fetchFollowingRequest(id)); + + api(getState).get(`/api/v1/accounts/${id}/following`).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(importFetchedAccounts(response.data)); + dispatch(fetchFollowingSuccess(id, response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => { + dispatch(fetchFollowingFail(id, error)); + }); + }; +}; + +export function fetchFollowingRequest(id) { + return { + type: FOLLOWING_FETCH_REQUEST, + id, + }; +}; + +export function fetchFollowingSuccess(id, accounts, next) { + return { + type: FOLLOWING_FETCH_SUCCESS, + id, + accounts, + next, + }; +}; + +export function fetchFollowingFail(id, error) { + return { + type: FOLLOWING_FETCH_FAIL, + id, + error, + }; +}; + +export function expandFollowing(id) { + return (dispatch, getState) => { + if (!me) return; + + const url = getState().getIn(['user_lists', 'following', id, 'next']); + + if (url === null) { + return; + } + + dispatch(expandFollowingRequest(id)); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(importFetchedAccounts(response.data)); + dispatch(expandFollowingSuccess(id, response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => { + dispatch(expandFollowingFail(id, error)); + }); + }; +}; + +export function expandFollowingRequest(id) { + return { + type: FOLLOWING_EXPAND_REQUEST, + id, + }; +}; + +export function expandFollowingSuccess(id, accounts, next) { + return { + type: FOLLOWING_EXPAND_SUCCESS, + id, + accounts, + next, + }; +}; + +export function expandFollowingFail(id, error) { + return { + type: FOLLOWING_EXPAND_FAIL, + id, + error, + }; +}; + +export function fetchRelationships(accountIds) { + return (dispatch, getState) => { + if (!me) return; + + const loadedRelationships = getState().get('relationships'); + const newAccountIds = accountIds.filter(id => loadedRelationships.get(id, null) === null); + + if (newAccountIds.length === 0) { + return; + } + + dispatch(fetchRelationshipsRequest(newAccountIds)); + + api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => { + dispatch(fetchRelationshipsSuccess(response.data)); + }).catch(error => { + dispatch(fetchRelationshipsFail(error)); + }); + }; +}; + +export function fetchRelationshipsRequest(ids) { + return { + type: RELATIONSHIPS_FETCH_REQUEST, + ids, + skipLoading: true, + }; +}; + +export function fetchRelationshipsSuccess(relationships) { + return { + type: RELATIONSHIPS_FETCH_SUCCESS, + relationships, + skipLoading: true, + }; +}; + +export function fetchRelationshipsFail(error) { + return { + type: RELATIONSHIPS_FETCH_FAIL, + error, + skipLoading: true, + }; +}; + +export function fetchFollowRequests() { + return (dispatch, getState) => { + if (!me) return; + + dispatch(fetchFollowRequestsRequest()); + + api(getState).get('/api/v1/follow_requests').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(response.data)); + dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null)); + }).catch(error => dispatch(fetchFollowRequestsFail(error))); + }; +}; + +export function fetchFollowRequestsRequest() { + return { + type: FOLLOW_REQUESTS_FETCH_REQUEST, + }; +}; + +export function fetchFollowRequestsSuccess(accounts, next) { + return { + type: FOLLOW_REQUESTS_FETCH_SUCCESS, + accounts, + next, + }; +}; + +export function fetchFollowRequestsFail(error) { + return { + type: FOLLOW_REQUESTS_FETCH_FAIL, + error, + }; +}; + +export function expandFollowRequests() { + return (dispatch, getState) => { + if (!me) return; + + const url = getState().getIn(['user_lists', 'follow_requests', 'next']); + + if (url === null) { + return; + } + + dispatch(expandFollowRequestsRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(response.data)); + dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null)); + }).catch(error => dispatch(expandFollowRequestsFail(error))); + }; +}; + +export function expandFollowRequestsRequest() { + return { + type: FOLLOW_REQUESTS_EXPAND_REQUEST, + }; +}; + +export function expandFollowRequestsSuccess(accounts, next) { + return { + type: FOLLOW_REQUESTS_EXPAND_SUCCESS, + accounts, + next, + }; +}; + +export function expandFollowRequestsFail(error) { + return { + type: FOLLOW_REQUESTS_EXPAND_FAIL, + error, + }; +}; + +export function authorizeFollowRequest(id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(authorizeFollowRequestRequest(id)); + + api(getState) + .post(`/api/v1/follow_requests/${id}/authorize`) + .then(() => dispatch(authorizeFollowRequestSuccess(id))) + .catch(error => dispatch(authorizeFollowRequestFail(id, error))); + }; +}; + +export function authorizeFollowRequestRequest(id) { + return { + type: FOLLOW_REQUEST_AUTHORIZE_REQUEST, + id, + }; +}; + +export function authorizeFollowRequestSuccess(id) { + return { + type: FOLLOW_REQUEST_AUTHORIZE_SUCCESS, + id, + }; +}; + +export function authorizeFollowRequestFail(id, error) { + return { + type: FOLLOW_REQUEST_AUTHORIZE_FAIL, + id, + error, + }; +}; + + +export function rejectFollowRequest(id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(rejectFollowRequestRequest(id)); + + api(getState) + .post(`/api/v1/follow_requests/${id}/reject`) + .then(() => dispatch(rejectFollowRequestSuccess(id))) + .catch(error => dispatch(rejectFollowRequestFail(id, error))); + }; +}; + +export function rejectFollowRequestRequest(id) { + return { + type: FOLLOW_REQUEST_REJECT_REQUEST, + id, + }; +}; + +export function rejectFollowRequestSuccess(id) { + return { + type: FOLLOW_REQUEST_REJECT_SUCCESS, + id, + }; +}; + +export function rejectFollowRequestFail(id, error) { + return { + type: FOLLOW_REQUEST_REJECT_FAIL, + id, + error, + }; +}; + +export function pinAccount(id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(pinAccountRequest(id)); + + api(getState).post(`/api/v1/accounts/${id}/pin`).then(response => { + dispatch(pinAccountSuccess(response.data)); + }).catch(error => { + dispatch(pinAccountFail(error)); + }); + }; +}; + +export function unpinAccount(id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(unpinAccountRequest(id)); + + api(getState).post(`/api/v1/accounts/${id}/unpin`).then(response => { + dispatch(unpinAccountSuccess(response.data)); + }).catch(error => { + dispatch(unpinAccountFail(error)); + }); + }; +}; + +export function pinAccountRequest(id) { + return { + type: ACCOUNT_PIN_REQUEST, + id, + }; +}; + +export function pinAccountSuccess(relationship) { + return { + type: ACCOUNT_PIN_SUCCESS, + relationship, + }; +}; + +export function pinAccountFail(error) { + return { + type: ACCOUNT_PIN_FAIL, + error, + }; +}; + +export function unpinAccountRequest(id) { + return { + type: ACCOUNT_UNPIN_REQUEST, + id, + }; +}; + +export function unpinAccountSuccess(relationship) { + return { + type: ACCOUNT_UNPIN_SUCCESS, + relationship, + }; +}; + +export function unpinAccountFail(error) { + return { + type: ACCOUNT_UNPIN_FAIL, + error, + }; +}; diff --git a/app/gabsocial/actions/alerts.js b/app/gabsocial/actions/alerts.js new file mode 100644 index 000000000..33791369f --- /dev/null +++ b/app/gabsocial/actions/alerts.js @@ -0,0 +1,55 @@ +//test +import { defineMessages } from 'react-intl'; + +const messages = defineMessages({ + unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' }, + unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' }, +}); + +export const ALERT_SHOW = 'ALERT_SHOW'; +export const ALERT_DISMISS = 'ALERT_DISMISS'; +export const ALERT_CLEAR = 'ALERT_CLEAR'; + +export function dismissAlert(alert) { + return { + type: ALERT_DISMISS, + alert, + }; +}; + +export function clearAlert() { + return { + type: ALERT_CLEAR, + }; +}; + +export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) { + return { + type: ALERT_SHOW, + title, + message, + }; +}; + +export function showAlertForError(error) { + if (error.response) { + const { data, status, statusText } = error.response; + + if (status === 404 || status === 410) { + // Skip these errors as they are reflected in the UI + return {}; + } + + let message = statusText; + let title = `${status}`; + + if (data.error) { + message = data.error; + } + + return showAlert(title, message); + } else { + console.error(error); + return showAlert(); + } +} diff --git a/app/gabsocial/actions/blocks.js b/app/gabsocial/actions/blocks.js new file mode 100644 index 000000000..69db572a3 --- /dev/null +++ b/app/gabsocial/actions/blocks.js @@ -0,0 +1,90 @@ +import api, { getLinks } from '../api'; +import { fetchRelationships } from './accounts'; +import { importFetchedAccounts } from './importer'; +import { me } from 'gabsocial/initial_state'; + +export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST'; +export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS'; +export const BLOCKS_FETCH_FAIL = 'BLOCKS_FETCH_FAIL'; + +export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST'; +export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS'; +export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL'; + +export function fetchBlocks() { + return (dispatch, getState) => { + if (!me) return; + + dispatch(fetchBlocksRequest()); + + api(getState).get('/api/v1/blocks').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(response.data)); + dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => dispatch(fetchBlocksFail(error))); + }; +}; + +export function fetchBlocksRequest() { + return { + type: BLOCKS_FETCH_REQUEST, + }; +}; + +export function fetchBlocksSuccess(accounts, next) { + return { + type: BLOCKS_FETCH_SUCCESS, + accounts, + next, + }; +}; + +export function fetchBlocksFail(error) { + return { + type: BLOCKS_FETCH_FAIL, + error, + }; +}; + +export function expandBlocks() { + return (dispatch, getState) => { + if (!me) return; + + const url = getState().getIn(['user_lists', 'blocks', 'next']); + + if (url === null) { + return; + } + + dispatch(expandBlocksRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(response.data)); + dispatch(expandBlocksSuccess(response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => dispatch(expandBlocksFail(error))); + }; +}; + +export function expandBlocksRequest() { + return { + type: BLOCKS_EXPAND_REQUEST, + }; +}; + +export function expandBlocksSuccess(accounts, next) { + return { + type: BLOCKS_EXPAND_SUCCESS, + accounts, + next, + }; +}; + +export function expandBlocksFail(error) { + return { + type: BLOCKS_EXPAND_FAIL, + error, + }; +}; diff --git a/app/gabsocial/actions/bundles.js b/app/gabsocial/actions/bundles.js new file mode 100644 index 000000000..ecc9c8f7d --- /dev/null +++ b/app/gabsocial/actions/bundles.js @@ -0,0 +1,25 @@ +export const BUNDLE_FETCH_REQUEST = 'BUNDLE_FETCH_REQUEST'; +export const BUNDLE_FETCH_SUCCESS = 'BUNDLE_FETCH_SUCCESS'; +export const BUNDLE_FETCH_FAIL = 'BUNDLE_FETCH_FAIL'; + +export function fetchBundleRequest(skipLoading) { + return { + type: BUNDLE_FETCH_REQUEST, + skipLoading, + }; +} + +export function fetchBundleSuccess(skipLoading) { + return { + type: BUNDLE_FETCH_SUCCESS, + skipLoading, + }; +} + +export function fetchBundleFail(error, skipLoading) { + return { + type: BUNDLE_FETCH_FAIL, + error, + skipLoading, + }; +} diff --git a/app/gabsocial/actions/compose.js b/app/gabsocial/actions/compose.js new file mode 100644 index 000000000..1c815c044 --- /dev/null +++ b/app/gabsocial/actions/compose.js @@ -0,0 +1,563 @@ +import api from '../api'; +import { CancelToken, isCancel } from 'axios'; +import { throttle } from 'lodash'; +import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light'; +import { tagHistory } from '../settings'; +import { useEmoji } from './emojis'; +import resizeImage from '../utils/resize_image'; +import { importFetchedAccounts } from './importer'; +import { updateTimeline, dequeueTimeline } from './timelines'; +import { showAlertForError } from './alerts'; +import { showAlert } from './alerts'; +import { defineMessages } from 'react-intl'; +import { openModal, closeModal } from './modal'; +import { me } from 'gabsocial/initial_state'; + +let cancelFetchComposeSuggestionsAccounts; + +export const COMPOSE_CHANGE = 'COMPOSE_CHANGE'; +export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST'; +export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS'; +export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL'; +export const COMPOSE_REPLY = 'COMPOSE_REPLY'; +export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL'; +export const COMPOSE_DIRECT = 'COMPOSE_DIRECT'; +export const COMPOSE_MENTION = 'COMPOSE_MENTION'; +export const COMPOSE_RESET = 'COMPOSE_RESET'; +export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST'; +export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS'; +export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL'; +export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS'; +export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO'; + +export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR'; +export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY'; +export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT'; +export const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE'; + +export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE'; + +export const COMPOSE_MOUNT = 'COMPOSE_MOUNT'; +export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT'; + +export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE'; +export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE'; +export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE'; +export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE'; +export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE'; +export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE'; + +export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT'; + +export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST'; +export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS'; +export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL'; + +export const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD'; +export const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE'; +export const COMPOSE_POLL_OPTION_ADD = 'COMPOSE_POLL_OPTION_ADD'; +export const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE'; +export const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE'; +export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE'; + +const messages = defineMessages({ + uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, + uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, +}); + +const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1); + +export const ensureComposeIsVisible = (getState, routerHistory) => { + if (!getState().getIn(['compose', 'mounted']) && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) { + routerHistory.push('/posts/new'); + } +}; + +export function changeCompose(text) { + return { + type: COMPOSE_CHANGE, + text: text, + }; +}; + +export function replyCompose(status, routerHistory) { + return (dispatch, getState) => { + dispatch({ + type: COMPOSE_REPLY, + status: status, + }); + + dispatch(openModal('COMPOSE')); + }; +}; + +export function cancelReplyCompose() { + return { + type: COMPOSE_REPLY_CANCEL, + }; +}; + +export function resetCompose() { + return { + type: COMPOSE_RESET, + }; +}; + +export function mentionCompose(account, routerHistory) { + return (dispatch, getState) => { + dispatch({ + type: COMPOSE_MENTION, + account: account, + }); + + dispatch(openModal('COMPOSE')); + }; +}; + +export function directCompose(account, routerHistory) { + return (dispatch, getState) => { + dispatch({ + type: COMPOSE_DIRECT, + account: account, + }); + + dispatch(openModal('COMPOSE')); + }; +}; + +export function handleComposeSubmit(dispatch, getState, response, status) { + if (!dispatch || !getState) return; + + dispatch(insertIntoTagHistory(response.data.tags, status)); + dispatch(submitComposeSuccess({ ...response.data })); + + // To make the app more responsive, immediately push the status into the columns + const insertIfOnline = timelineId => { + const timeline = getState().getIn(['timelines', timelineId]); + + if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) { + let dequeueArgs = {}; + if (timelineId === 'community') dequeueArgs.onlyMedia = getState().getIn(['settings', 'community', 'other', 'onlyMedia']); + dispatch(dequeueTimeline(timelineId, null, dequeueArgs)); + dispatch(updateTimeline(timelineId, { ...response.data })); + } + }; + + if (response.data.visibility !== 'direct') { + insertIfOnline('home'); + } else if (response.data.in_reply_to_id === null && response.data.visibility === 'public') { + insertIfOnline('community'); + insertIfOnline('public'); + } +} + +export function submitCompose(routerHistory, group) { + return function (dispatch, getState) { + if (!me) return; + + const status = getState().getIn(['compose', 'text'], ''); + const media = getState().getIn(['compose', 'media_attachments']); + + if ((!status || !status.length) && media.size === 0) { + return; + } + + dispatch(submitComposeRequest()); + dispatch(closeModal()); + + api(getState).post('/api/v1/statuses', { + status, + in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), + media_ids: media.map(item => item.get('id')), + sensitive: getState().getIn(['compose', 'sensitive']), + spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''), + visibility: getState().getIn(['compose', 'privacy']), + poll: getState().getIn(['compose', 'poll'], null), + group_id: group ? group.get('id') : null, + }, { + headers: { + 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), + }, + }).then(function (response) { + if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) { + routerHistory.push('/messages'); + } + handleComposeSubmit(dispatch, getState, response, status); + }).catch(function (error) { + dispatch(submitComposeFail(error)); + }); + }; +}; + +export function submitComposeRequest() { + return { + type: COMPOSE_SUBMIT_REQUEST, + }; +}; + +export function submitComposeSuccess(status) { + return { + type: COMPOSE_SUBMIT_SUCCESS, + status: status, + }; +}; + +export function submitComposeFail(error) { + return { + type: COMPOSE_SUBMIT_FAIL, + error: error, + }; +}; + +export function uploadCompose(files) { + return function (dispatch, getState) { + if (!me) return; + + const uploadLimit = 4; + const media = getState().getIn(['compose', 'media_attachments']); + const progress = new Array(files.length).fill(0); + let total = Array.from(files).reduce((a, v) => a + v.size, 0); + + if (files.length + media.size > uploadLimit) { + dispatch(showAlert(undefined, messages.uploadErrorLimit)); + return; + } + + if (getState().getIn(['compose', 'poll'])) { + dispatch(showAlert(undefined, messages.uploadErrorPoll)); + return; + } + + dispatch(uploadComposeRequest()); + + for (const [i, f] of Array.from(files).entries()) { + if (media.size + i > 3) break; + + resizeImage(f).then(file => { + const data = new FormData(); + data.append('file', file); + // Account for disparity in size of original image and resized data + total += file.size - f.size; + + return api(getState).post('/api/v1/media', data, { + onUploadProgress: function({ loaded }){ + progress[i] = loaded; + dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total)); + }, + }).then(({ data }) => dispatch(uploadComposeSuccess(data))); + }).catch(error => dispatch(uploadComposeFail(error))); + }; + }; +}; + +export function changeUploadCompose(id, params) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(changeUploadComposeRequest()); + + api(getState).put(`/api/v1/media/${id}`, params).then(response => { + dispatch(changeUploadComposeSuccess(response.data)); + }).catch(error => { + dispatch(changeUploadComposeFail(id, error)); + }); + }; +}; + +export function changeUploadComposeRequest() { + return { + type: COMPOSE_UPLOAD_CHANGE_REQUEST, + skipLoading: true, + }; +}; +export function changeUploadComposeSuccess(media) { + return { + type: COMPOSE_UPLOAD_CHANGE_SUCCESS, + media: media, + skipLoading: true, + }; +}; + +export function changeUploadComposeFail(error) { + return { + type: COMPOSE_UPLOAD_CHANGE_FAIL, + error: error, + skipLoading: true, + }; +}; + +export function uploadComposeRequest() { + return { + type: COMPOSE_UPLOAD_REQUEST, + skipLoading: true, + }; +}; + +export function uploadComposeProgress(loaded, total) { + return { + type: COMPOSE_UPLOAD_PROGRESS, + loaded: loaded, + total: total, + }; +}; + +export function uploadComposeSuccess(media) { + return { + type: COMPOSE_UPLOAD_SUCCESS, + media: media, + skipLoading: true, + }; +}; + +export function uploadComposeFail(error) { + return { + type: COMPOSE_UPLOAD_FAIL, + error: error, + skipLoading: true, + }; +}; + +export function undoUploadCompose(media_id) { + return { + type: COMPOSE_UPLOAD_UNDO, + media_id: media_id, + }; +}; + +export function clearComposeSuggestions() { + if (cancelFetchComposeSuggestionsAccounts) { + cancelFetchComposeSuggestionsAccounts(); + } + return { + type: COMPOSE_SUGGESTIONS_CLEAR, + }; +}; + +const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => { + if (cancelFetchComposeSuggestionsAccounts) { + cancelFetchComposeSuggestionsAccounts(); + } + api(getState).get('/api/v1/accounts/search', { + cancelToken: new CancelToken(cancel => { + cancelFetchComposeSuggestionsAccounts = cancel; + }), + params: { + q: token.slice(1), + resolve: false, + limit: 4, + }, + }).then(response => { + dispatch(importFetchedAccounts(response.data)); + dispatch(readyComposeSuggestionsAccounts(token, response.data)); + }).catch(error => { + if (!isCancel(error)) { + dispatch(showAlertForError(error)); + } + }); +}, 200, { leading: true, trailing: true }); + +const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => { + const results = emojiSearch(token.replace(':', ''), { maxResults: 5 }); + dispatch(readyComposeSuggestionsEmojis(token, results)); +}; + +const fetchComposeSuggestionsTags = (dispatch, getState, token) => { + dispatch(updateSuggestionTags(token)); +}; + +export function fetchComposeSuggestions(token) { + return (dispatch, getState) => { + switch (token[0]) { + case ':': + fetchComposeSuggestionsEmojis(dispatch, getState, token); + break; + case '#': + fetchComposeSuggestionsTags(dispatch, getState, token); + break; + default: + fetchComposeSuggestionsAccounts(dispatch, getState, token); + break; + } + }; +}; + +export function readyComposeSuggestionsEmojis(token, emojis) { + return { + type: COMPOSE_SUGGESTIONS_READY, + token, + emojis, + }; +}; + +export function readyComposeSuggestionsAccounts(token, accounts) { + return { + type: COMPOSE_SUGGESTIONS_READY, + token, + accounts, + }; +}; + +export function selectComposeSuggestion(position, token, suggestion, path) { + return (dispatch, getState) => { + let completion, startPosition; + + if (typeof suggestion === 'object' && suggestion.id) { + completion = suggestion.native || suggestion.colons; + startPosition = position - 1; + + dispatch(useEmoji(suggestion)); + } else if (suggestion[0] === '#') { + completion = suggestion; + startPosition = position - 1; + } else { + completion = getState().getIn(['accounts', suggestion, 'acct']); + startPosition = position; + } + + dispatch({ + type: COMPOSE_SUGGESTION_SELECT, + position: startPosition, + token, + completion, + path, + }); + }; +}; + +export function updateSuggestionTags(token) { + return { + type: COMPOSE_SUGGESTION_TAGS_UPDATE, + token, + }; +} + +export function updateTagHistory(tags) { + return { + type: COMPOSE_TAG_HISTORY_UPDATE, + tags, + }; +} + +export function hydrateCompose() { + return (dispatch, getState) => { + const me = getState().getIn(['meta', 'me']); + const history = tagHistory.get(me); + + if (history !== null) { + dispatch(updateTagHistory(history)); + } + }; +} + +function insertIntoTagHistory(recognizedTags, text) { + return (dispatch, getState) => { + const state = getState(); + const oldHistory = state.getIn(['compose', 'tagHistory']); + const me = state.getIn(['meta', 'me']); + const names = recognizedTags.map(tag => text.match(new RegExp(`#${tag.name}`, 'i'))[0].slice(1)); + const intersectedOldHistory = oldHistory.filter(name => names.findIndex(newName => newName.toLowerCase() === name.toLowerCase()) === -1); + + names.push(...intersectedOldHistory.toJS()); + + const newHistory = names.slice(0, 1000); + + tagHistory.set(me, newHistory); + dispatch(updateTagHistory(newHistory)); + }; +} + +export function mountCompose() { + return { + type: COMPOSE_MOUNT, + }; +}; + +export function unmountCompose() { + return { + type: COMPOSE_UNMOUNT, + }; +}; + +export function changeComposeSensitivity() { + return { + type: COMPOSE_SENSITIVITY_CHANGE, + }; +}; + +export function changeComposeSpoilerness() { + return { + type: COMPOSE_SPOILERNESS_CHANGE, + }; +}; + +export function changeComposeSpoilerText(text) { + return { + type: COMPOSE_SPOILER_TEXT_CHANGE, + text, + }; +}; + +export function changeComposeVisibility(value) { + return { + type: COMPOSE_VISIBILITY_CHANGE, + value, + }; +}; + +export function insertEmojiCompose(position, emoji, needsSpace) { + return { + type: COMPOSE_EMOJI_INSERT, + position, + emoji, + needsSpace, + }; +}; + +export function changeComposing(value) { + return { + type: COMPOSE_COMPOSING_CHANGE, + value, + }; +}; + +export function addPoll() { + return { + type: COMPOSE_POLL_ADD, + }; +}; + +export function removePoll() { + return { + type: COMPOSE_POLL_REMOVE, + }; +}; + +export function addPollOption(title) { + return { + type: COMPOSE_POLL_OPTION_ADD, + title, + }; +}; + +export function changePollOption(index, title) { + return { + type: COMPOSE_POLL_OPTION_CHANGE, + index, + title, + }; +}; + +export function removePollOption(index) { + return { + type: COMPOSE_POLL_OPTION_REMOVE, + index, + }; +}; + +export function changePollSettings(expiresIn, isMultiple) { + return { + type: COMPOSE_POLL_SETTINGS_CHANGE, + expiresIn, + isMultiple, + }; +}; diff --git a/app/gabsocial/actions/conversations.js b/app/gabsocial/actions/conversations.js new file mode 100644 index 000000000..d69dcb08c --- /dev/null +++ b/app/gabsocial/actions/conversations.js @@ -0,0 +1,89 @@ +import api, { getLinks } from '../api'; +import { + importFetchedAccounts, + importFetchedStatuses, + importFetchedStatus, +} from './importer'; +import { me } from 'gabsocial/initial_state'; + +export const CONVERSATIONS_MOUNT = 'CONVERSATIONS_MOUNT'; +export const CONVERSATIONS_UNMOUNT = 'CONVERSATIONS_UNMOUNT'; + +export const CONVERSATIONS_FETCH_REQUEST = 'CONVERSATIONS_FETCH_REQUEST'; +export const CONVERSATIONS_FETCH_SUCCESS = 'CONVERSATIONS_FETCH_SUCCESS'; +export const CONVERSATIONS_FETCH_FAIL = 'CONVERSATIONS_FETCH_FAIL'; +export const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE'; + +export const CONVERSATIONS_READ = 'CONVERSATIONS_READ'; + +export const mountConversations = () => ({ + type: CONVERSATIONS_MOUNT, +}); + +export const unmountConversations = () => ({ + type: CONVERSATIONS_UNMOUNT, +}); + +export const markConversationRead = conversationId => (dispatch, getState) => { + if (!me) return; + + dispatch({ + type: CONVERSATIONS_READ, + id: conversationId, + }); + + api(getState).post(`/api/v1/conversations/${conversationId}/read`); +}; + +export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => { + if (!me) return; + + dispatch(expandConversationsRequest()); + + const params = { max_id: maxId }; + + if (!maxId) { + params.since_id = getState().getIn(['conversations', 'items', 0, 'last_status']); + } + + const isLoadingRecent = !!params.since_id; + + api(getState).get('/api/v1/conversations', { params }) + .then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(importFetchedAccounts(response.data.reduce((aggr, item) => aggr.concat(item.accounts), []))); + dispatch(importFetchedStatuses(response.data.map(item => item.last_status).filter(x => !!x))); + dispatch(expandConversationsSuccess(response.data, next ? next.uri : null, isLoadingRecent)); + }) + .catch(err => dispatch(expandConversationsFail(err))); +}; + +export const expandConversationsRequest = () => ({ + type: CONVERSATIONS_FETCH_REQUEST, +}); + +export const expandConversationsSuccess = (conversations, next, isLoadingRecent) => ({ + type: CONVERSATIONS_FETCH_SUCCESS, + conversations, + next, + isLoadingRecent, +}); + +export const expandConversationsFail = error => ({ + type: CONVERSATIONS_FETCH_FAIL, + error, +}); + +export const updateConversations = conversation => dispatch => { + dispatch(importFetchedAccounts(conversation.accounts)); + + if (conversation.last_status) { + dispatch(importFetchedStatus(conversation.last_status)); + } + + dispatch({ + type: CONVERSATIONS_UPDATE, + conversation, + }); +}; diff --git a/app/gabsocial/actions/custom_emojis.js b/app/gabsocial/actions/custom_emojis.js new file mode 100644 index 000000000..7b7d0091b --- /dev/null +++ b/app/gabsocial/actions/custom_emojis.js @@ -0,0 +1,40 @@ +import api from '../api'; + +export const CUSTOM_EMOJIS_FETCH_REQUEST = 'CUSTOM_EMOJIS_FETCH_REQUEST'; +export const CUSTOM_EMOJIS_FETCH_SUCCESS = 'CUSTOM_EMOJIS_FETCH_SUCCESS'; +export const CUSTOM_EMOJIS_FETCH_FAIL = 'CUSTOM_EMOJIS_FETCH_FAIL'; + +export function fetchCustomEmojis() { + return (dispatch, getState) => { + dispatch(fetchCustomEmojisRequest()); + + api(getState).get('/api/v1/custom_emojis').then(response => { + dispatch(fetchCustomEmojisSuccess(response.data)); + }).catch(error => { + dispatch(fetchCustomEmojisFail(error)); + }); + }; +}; + +export function fetchCustomEmojisRequest() { + return { + type: CUSTOM_EMOJIS_FETCH_REQUEST, + skipLoading: true, + }; +}; + +export function fetchCustomEmojisSuccess(custom_emojis) { + return { + type: CUSTOM_EMOJIS_FETCH_SUCCESS, + custom_emojis, + skipLoading: true, + }; +}; + +export function fetchCustomEmojisFail(error) { + return { + type: CUSTOM_EMOJIS_FETCH_FAIL, + error, + skipLoading: true, + }; +}; diff --git a/app/gabsocial/actions/domain_blocks.js b/app/gabsocial/actions/domain_blocks.js new file mode 100644 index 000000000..e7aadb559 --- /dev/null +++ b/app/gabsocial/actions/domain_blocks.js @@ -0,0 +1,174 @@ +import api, { getLinks } from '../api'; +import { me } from 'gabsocial/initial_state'; + +export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST'; +export const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS'; +export const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL'; + +export const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST'; +export const DOMAIN_UNBLOCK_SUCCESS = 'DOMAIN_UNBLOCK_SUCCESS'; +export const DOMAIN_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL'; + +export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST'; +export const DOMAIN_BLOCKS_FETCH_SUCCESS = 'DOMAIN_BLOCKS_FETCH_SUCCESS'; +export const DOMAIN_BLOCKS_FETCH_FAIL = 'DOMAIN_BLOCKS_FETCH_FAIL'; + +export const DOMAIN_BLOCKS_EXPAND_REQUEST = 'DOMAIN_BLOCKS_EXPAND_REQUEST'; +export const DOMAIN_BLOCKS_EXPAND_SUCCESS = 'DOMAIN_BLOCKS_EXPAND_SUCCESS'; +export const DOMAIN_BLOCKS_EXPAND_FAIL = 'DOMAIN_BLOCKS_EXPAND_FAIL'; + +export function blockDomain(domain) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(blockDomainRequest(domain)); + + api(getState).post('/api/v1/domain_blocks', { domain }).then(() => { + const at_domain = '@' + domain; + const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id')); + dispatch(blockDomainSuccess(domain, accounts)); + }).catch(err => { + dispatch(blockDomainFail(domain, err)); + }); + }; +}; + +export function blockDomainRequest(domain) { + return { + type: DOMAIN_BLOCK_REQUEST, + domain, + }; +}; + +export function blockDomainSuccess(domain, accounts) { + return { + type: DOMAIN_BLOCK_SUCCESS, + domain, + accounts, + }; +}; + +export function blockDomainFail(domain, error) { + return { + type: DOMAIN_BLOCK_FAIL, + domain, + error, + }; +}; + +export function unblockDomain(domain) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(unblockDomainRequest(domain)); + + api(getState).delete('/api/v1/domain_blocks', { params: { domain } }).then(() => { + const at_domain = '@' + domain; + const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id')); + dispatch(unblockDomainSuccess(domain, accounts)); + }).catch(err => { + dispatch(unblockDomainFail(domain, err)); + }); + }; +}; + +export function unblockDomainRequest(domain) { + return { + type: DOMAIN_UNBLOCK_REQUEST, + domain, + }; +}; + +export function unblockDomainSuccess(domain, accounts) { + return { + type: DOMAIN_UNBLOCK_SUCCESS, + domain, + accounts, + }; +}; + +export function unblockDomainFail(domain, error) { + return { + type: DOMAIN_UNBLOCK_FAIL, + domain, + error, + }; +}; + +export function fetchDomainBlocks() { + return (dispatch, getState) => { + if (!me) return; + + dispatch(fetchDomainBlocksRequest()); + + api(getState).get('/api/v1/domain_blocks').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null)); + }).catch(err => { + dispatch(fetchDomainBlocksFail(err)); + }); + }; +}; + +export function fetchDomainBlocksRequest() { + return { + type: DOMAIN_BLOCKS_FETCH_REQUEST, + }; +}; + +export function fetchDomainBlocksSuccess(domains, next) { + return { + type: DOMAIN_BLOCKS_FETCH_SUCCESS, + domains, + next, + }; +}; + +export function fetchDomainBlocksFail(error) { + return { + type: DOMAIN_BLOCKS_FETCH_FAIL, + error, + }; +}; + +export function expandDomainBlocks() { + return (dispatch, getState) => { + if (!me) return; + + const url = getState().getIn(['domain_lists', 'blocks', 'next']); + + if (!url) { + return; + } + + dispatch(expandDomainBlocksRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(expandDomainBlocksSuccess(response.data, next ? next.uri : null)); + }).catch(err => { + dispatch(expandDomainBlocksFail(err)); + }); + }; +}; + +export function expandDomainBlocksRequest() { + return { + type: DOMAIN_BLOCKS_EXPAND_REQUEST, + }; +}; + +export function expandDomainBlocksSuccess(domains, next) { + return { + type: DOMAIN_BLOCKS_EXPAND_SUCCESS, + domains, + next, + }; +}; + +export function expandDomainBlocksFail(error) { + return { + type: DOMAIN_BLOCKS_EXPAND_FAIL, + error, + }; +}; diff --git a/app/gabsocial/actions/dropdown_menu.js b/app/gabsocial/actions/dropdown_menu.js new file mode 100644 index 000000000..14f2939c7 --- /dev/null +++ b/app/gabsocial/actions/dropdown_menu.js @@ -0,0 +1,10 @@ +export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN'; +export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE'; + +export function openDropdownMenu(id, placement, keyboard) { + return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard }; +} + +export function closeDropdownMenu(id) { + return { type: DROPDOWN_MENU_CLOSE, id }; +} diff --git a/app/gabsocial/actions/emojis.js b/app/gabsocial/actions/emojis.js new file mode 100644 index 000000000..7cd9d4b7b --- /dev/null +++ b/app/gabsocial/actions/emojis.js @@ -0,0 +1,14 @@ +import { saveSettings } from './settings'; + +export const EMOJI_USE = 'EMOJI_USE'; + +export function useEmoji(emoji) { + return dispatch => { + dispatch({ + type: EMOJI_USE, + emoji, + }); + + dispatch(saveSettings()); + }; +}; diff --git a/app/gabsocial/actions/favourites.js b/app/gabsocial/actions/favourites.js new file mode 100644 index 000000000..849981e47 --- /dev/null +++ b/app/gabsocial/actions/favourites.js @@ -0,0 +1,98 @@ +import api, { getLinks } from '../api'; +import { importFetchedStatuses } from './importer'; +import { me } from 'gabsocial/initial_state'; + +export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST'; +export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS'; +export const FAVOURITED_STATUSES_FETCH_FAIL = 'FAVOURITED_STATUSES_FETCH_FAIL'; + +export const FAVOURITED_STATUSES_EXPAND_REQUEST = 'FAVOURITED_STATUSES_EXPAND_REQUEST'; +export const FAVOURITED_STATUSES_EXPAND_SUCCESS = 'FAVOURITED_STATUSES_EXPAND_SUCCESS'; +export const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FAIL'; + +export function fetchFavouritedStatuses() { + return (dispatch, getState) => { + if (!me) return; + + if (getState().getIn(['status_lists', 'favourites', 'isLoading'])) { + return; + } + + dispatch(fetchFavouritedStatusesRequest()); + + api(getState).get('/api/v1/favourites').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); + dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(fetchFavouritedStatusesFail(error)); + }); + }; +}; + +export function fetchFavouritedStatusesRequest() { + return { + type: FAVOURITED_STATUSES_FETCH_REQUEST, + skipLoading: true, + }; +}; + +export function fetchFavouritedStatusesSuccess(statuses, next) { + return { + type: FAVOURITED_STATUSES_FETCH_SUCCESS, + statuses, + next, + skipLoading: true, + }; +}; + +export function fetchFavouritedStatusesFail(error) { + return { + type: FAVOURITED_STATUSES_FETCH_FAIL, + error, + skipLoading: true, + }; +}; + +export function expandFavouritedStatuses() { + return (dispatch, getState) => { + if (!me) return; + + const url = getState().getIn(['status_lists', 'favourites', 'next'], null); + + if (url === null || getState().getIn(['status_lists', 'favourites', 'isLoading'])) { + return; + } + + dispatch(expandFavouritedStatusesRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); + dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(expandFavouritedStatusesFail(error)); + }); + }; +}; + +export function expandFavouritedStatusesRequest() { + return { + type: FAVOURITED_STATUSES_EXPAND_REQUEST, + }; +}; + +export function expandFavouritedStatusesSuccess(statuses, next) { + return { + type: FAVOURITED_STATUSES_EXPAND_SUCCESS, + statuses, + next, + }; +}; + +export function expandFavouritedStatusesFail(error) { + return { + type: FAVOURITED_STATUSES_EXPAND_FAIL, + error, + }; +}; diff --git a/app/gabsocial/actions/filters.js b/app/gabsocial/actions/filters.js new file mode 100644 index 000000000..0f8dd0eac --- /dev/null +++ b/app/gabsocial/actions/filters.js @@ -0,0 +1,29 @@ +import api from '../api'; +import { me } from 'gabsocial/initial_state'; + +export const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST'; +export const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS'; +export const FILTERS_FETCH_FAIL = 'FILTERS_FETCH_FAIL'; + +export const fetchFilters = () => (dispatch, getState) => { + if (!me) return; + + dispatch({ + type: FILTERS_FETCH_REQUEST, + skipLoading: true, + }); + + api(getState) + .get('/api/v1/filters') + .then(({ data }) => dispatch({ + type: FILTERS_FETCH_SUCCESS, + filters: data, + skipLoading: true, + })) + .catch(err => dispatch({ + type: FILTERS_FETCH_FAIL, + err, + skipLoading: true, + skipAlert: true, + })); +}; diff --git a/app/gabsocial/actions/group_editor.js b/app/gabsocial/actions/group_editor.js new file mode 100644 index 000000000..e464c1c6e --- /dev/null +++ b/app/gabsocial/actions/group_editor.js @@ -0,0 +1,113 @@ +import api from '../api'; +import { me } from 'gabsocial/initial_state'; + +export const GROUP_CREATE_REQUEST = 'GROUP_CREATE_REQUEST'; +export const GROUP_CREATE_SUCCESS = 'GROUP_CREATE_SUCCESS'; +export const GROUP_CREATE_FAIL = 'GROUP_CREATE_FAIL'; + +export const GROUP_UPDATE_REQUEST = 'GROUP_UPDATE_REQUEST'; +export const GROUP_UPDATE_SUCCESS = 'GROUP_UPDATE_SUCCESS'; +export const GROUP_UPDATE_FAIL = 'GROUP_UPDATE_FAIL'; + +export const GROUP_EDITOR_VALUE_CHANGE = 'GROUP_EDITOR_VALUE_CHANGE'; +export const GROUP_EDITOR_RESET = 'GROUP_EDITOR_RESET'; +export const GROUP_EDITOR_SETUP = 'GROUP_EDITOR_SETUP'; + +export const submit = (routerHistory) => (dispatch, getState) => { + const groupId = getState().getIn(['group_editor', 'groupId']); + const title = getState().getIn(['group_editor', 'title']); + const description = getState().getIn(['group_editor', 'description']); + const coverImage = getState().getIn(['group_editor', 'coverImage']); + + if (groupId === null) { + dispatch(create(title, description, coverImage, routerHistory)); + } else { + dispatch(update(groupId, title, description, coverImage, routerHistory)); + } +}; + + +export const create = (title, description, coverImage, routerHistory) => (dispatch, getState) => { + if (!me) return; + + dispatch(createRequest()); + + const formData = new FormData(); + formData.append('title', title); + formData.append('description', description); + + if (coverImage !== null) { + formData.append('cover_image', coverImage); + } + + api(getState).post('/api/v1/groups', formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(({ data }) => { + dispatch(createSuccess(data)); + routerHistory.push(`/groups/${data.id}`); + }).catch(err => dispatch(createFail(err))); + }; + + +export const createRequest = id => ({ + type: GROUP_CREATE_REQUEST, + id, +}); + +export const createSuccess = group => ({ + type: GROUP_CREATE_SUCCESS, + group, +}); + +export const createFail = error => ({ + type: GROUP_CREATE_FAIL, + error, +}); + +export const update = (groupId, title, description, coverImage, routerHistory) => (dispatch, getState) => { + if (!me) return; + + dispatch(updateRequest()); + + const formData = new FormData(); + formData.append('title', title); + formData.append('description', description); + + if (coverImage !== null) { + formData.append('cover_image', coverImage); + } + + api(getState).put(`/api/v1/groups/${groupId}`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(({ data }) => { + dispatch(updateSuccess(data)); + routerHistory.push(`/groups/${data.id}`); + }).catch(err => dispatch(updateFail(err))); + }; + + +export const updateRequest = id => ({ + type: GROUP_UPDATE_REQUEST, + id, +}); + +export const updateSuccess = group => ({ + type: GROUP_UPDATE_SUCCESS, + group, +}); + +export const updateFail = error => ({ + type: GROUP_UPDATE_FAIL, + error, +}); + +export const changeValue = (field, value) => ({ + type: GROUP_EDITOR_VALUE_CHANGE, + field, + value, +}); + +export const reset = () => ({ + type: GROUP_EDITOR_RESET +}); + +export const setUp = (group) => ({ + type: GROUP_EDITOR_SETUP, + group, +}); \ No newline at end of file diff --git a/app/gabsocial/actions/groups.js b/app/gabsocial/actions/groups.js new file mode 100644 index 000000000..eac343718 --- /dev/null +++ b/app/gabsocial/actions/groups.js @@ -0,0 +1,524 @@ +import api, { getLinks } from '../api'; +import { me } from 'gabsocial/initial_state'; +import { importFetchedAccounts } from './importer'; +import { fetchRelationships } from './accounts'; + +export const GROUP_FETCH_REQUEST = 'GROUP_FETCH_REQUEST'; +export const GROUP_FETCH_SUCCESS = 'GROUP_FETCH_SUCCESS'; +export const GROUP_FETCH_FAIL = 'GROUP_FETCH_FAIL'; + +export const GROUP_RELATIONSHIPS_FETCH_REQUEST = 'GROUP_RELATIONSHIPS_FETCH_REQUEST'; +export const GROUP_RELATIONSHIPS_FETCH_SUCCESS = 'GROUP_RELATIONSHIPS_FETCH_SUCCESS'; +export const GROUP_RELATIONSHIPS_FETCH_FAIL = 'GROUP_RELATIONSHIPS_FETCH_FAIL'; + +export const GROUPS_FETCH_REQUEST = 'GROUPS_FETCH_REQUEST'; +export const GROUPS_FETCH_SUCCESS = 'GROUPS_FETCH_SUCCESS'; +export const GROUPS_FETCH_FAIL = 'GROUPS_FETCH_FAIL'; + +export const GROUP_JOIN_REQUEST = 'GROUP_JOIN_REQUEST'; +export const GROUP_JOIN_SUCCESS = 'GROUP_JOIN_SUCCESS'; +export const GROUP_JOIN_FAIL = 'GROUP_JOIN_FAIL'; + +export const GROUP_LEAVE_REQUEST = 'GROUP_LEAVE_REQUEST'; +export const GROUP_LEAVE_SUCCESS = 'GROUP_LEAVE_SUCCESS'; +export const GROUP_LEAVE_FAIL = 'GROUP_LEAVE_FAIL'; + +export const GROUP_MEMBERS_FETCH_REQUEST = 'GROUP_MEMBERS_FETCH_REQUEST'; +export const GROUP_MEMBERS_FETCH_SUCCESS = 'GROUP_MEMBERS_FETCH_SUCCESS'; +export const GROUP_MEMBERS_FETCH_FAIL = 'GROUP_MEMBERS_FETCH_FAIL'; + +export const GROUP_MEMBERS_EXPAND_REQUEST = 'GROUP_MEMBERS_EXPAND_REQUEST'; +export const GROUP_MEMBERS_EXPAND_SUCCESS = 'GROUP_MEMBERS_EXPAND_SUCCESS'; +export const GROUP_MEMBERS_EXPAND_FAIL = 'GROUP_MEMBERS_EXPAND_FAIL'; + +export const GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST = 'GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST'; +export const GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS'; +export const GROUP_REMOVED_ACCOUNTS_FETCH_FAIL = 'GROUP_REMOVED_ACCOUNTS_FETCH_FAIL'; + +export const GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST = 'GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST'; +export const GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS'; +export const GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL = 'GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL'; + +export const GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST = 'GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST'; +export const GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS'; +export const GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL = 'GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL'; + +export const GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST = 'GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST'; +export const GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS'; +export const GROUP_REMOVED_ACCOUNTS_CREATE_FAIL = 'GROUP_REMOVED_ACCOUNTS_CREATE_FAIL'; + +export const GROUP_REMOVE_STATUS_REQUEST = 'GROUP_REMOVE_STATUS_REQUEST'; +export const GROUP_REMOVE_STATUS_SUCCESS = 'GROUP_REMOVE_STATUS_SUCCESS'; +export const GROUP_REMOVE_STATUS_FAIL = 'GROUP_REMOVE_STATUS_FAIL'; + +export const fetchGroup = id => (dispatch, getState) => { + if (!me) return; + + dispatch(fetchGroupRelationships([id])); + + if (getState().getIn(['groups', id])) { + return; + } + + dispatch(fetchGroupRequest(id)); + + api(getState).get(`/api/v1/groups/${id}`) + .then(({ data }) => dispatch(fetchGroupSuccess(data))) + .catch(err => dispatch(fetchGroupFail(id, err))); +}; + +export const fetchGroupRequest = id => ({ + type: GROUP_FETCH_REQUEST, + id, +}); + +export const fetchGroupSuccess = group => ({ + type: GROUP_FETCH_SUCCESS, + group, +}); + +export const fetchGroupFail = (id, error) => ({ + type: GROUP_FETCH_FAIL, + id, + error, +}); + +export function fetchGroupRelationships(groupIds) { + return (dispatch, getState) => { + if (!me) return; + + const loadedRelationships = getState().get('group_relationships'); + const newGroupIds = groupIds.filter(id => loadedRelationships.get(id, null) === null); + + if (newGroupIds.length === 0) { + return; + } + + dispatch(fetchGroupRelationshipsRequest(newGroupIds)); + + api(getState).get(`/api/v1/groups/${newGroupIds[0]}/relationships?${newGroupIds.map(id => `id[]=${id}`).join('&')}`).then(response => { + dispatch(fetchGroupRelationshipsSuccess(response.data)); + }).catch(error => { + dispatch(fetchGroupRelationshipsFail(error)); + }); + }; +}; + +export function fetchGroupRelationshipsRequest(ids) { + return { + type: GROUP_RELATIONSHIPS_FETCH_REQUEST, + ids, + skipLoading: true, + }; +}; + +export function fetchGroupRelationshipsSuccess(relationships) { + return { + type: GROUP_RELATIONSHIPS_FETCH_SUCCESS, + relationships, + skipLoading: true, + }; +}; + +export function fetchGroupRelationshipsFail(error) { + return { + type: GROUP_RELATIONSHIPS_FETCH_FAIL, + error, + skipLoading: true, + }; +}; + +export const fetchGroups = (tab) => (dispatch, getState) => { + if (!me) return; + + dispatch(fetchGroupsRequest()); + + api(getState).get('/api/v1/groups?tab=' + tab) + .then(({ data }) => { + dispatch(fetchGroupsSuccess(data, tab)); + dispatch(fetchGroupRelationships(data.map(item => item.id))); + }) + .catch(err => dispatch(fetchGroupsFail(err))); +}; + +export const fetchGroupsRequest = () => ({ + type: GROUPS_FETCH_REQUEST, +}); + +export const fetchGroupsSuccess = (groups, tab) => ({ + type: GROUPS_FETCH_SUCCESS, + groups, + tab, +}); + +export const fetchGroupsFail = error => ({ + type: GROUPS_FETCH_FAIL, + error, +}); + +export function joinGroup(id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(joinGroupRequest(id)); + + api(getState).post(`/api/v1/groups/${id}/accounts`).then(response => { + dispatch(joinGroupSuccess(response.data)); + }).catch(error => { + dispatch(joinGroupFail(id, error)); + }); + }; +}; + +export function leaveGroup(id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(leaveGroupRequest(id)); + + api(getState).delete(`/api/v1/groups/${id}/accounts`).then(response => { + dispatch(leaveGroupSuccess(response.data)); + }).catch(error => { + dispatch(leaveGroupFail(id, error)); + }); + }; +}; + +export function joinGroupRequest(id) { + return { + type: GROUP_JOIN_REQUEST, + id, + }; +}; + +export function joinGroupSuccess(relationship) { + return { + type: GROUP_JOIN_SUCCESS, + relationship + }; +}; + +export function joinGroupFail(error) { + return { + type: GROUP_JOIN_FAIL, + error, + }; +}; + +export function leaveGroupRequest(id) { + return { + type: GROUP_LEAVE_REQUEST, + id, + }; +}; + +export function leaveGroupSuccess(relationship) { + return { + type: GROUP_LEAVE_SUCCESS, + relationship, + }; +}; + +export function leaveGroupFail(error) { + return { + type: GROUP_LEAVE_FAIL, + error, + }; +}; + +export function fetchMembers(id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(fetchMembersRequest(id)); + + api(getState).get(`/api/v1/groups/${id}/accounts`).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(importFetchedAccounts(response.data)); + dispatch(fetchMembersSuccess(id, response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => { + dispatch(fetchMembersFail(id, error)); + }); + }; +}; + +export function fetchMembersRequest(id) { + return { + type: GROUP_MEMBERS_FETCH_REQUEST, + id, + }; +}; + +export function fetchMembersSuccess(id, accounts, next) { + return { + type: GROUP_MEMBERS_FETCH_SUCCESS, + id, + accounts, + next, + }; +}; + +export function fetchMembersFail(id, error) { + return { + type: GROUP_MEMBERS_FETCH_FAIL, + id, + error, + }; +}; + +export function expandMembers(id) { + return (dispatch, getState) => { + if (!me) return; + + const url = getState().getIn(['user_lists', 'groups', id, 'next']); + + if (url === null) { + return; + } + + dispatch(expandMembersRequest(id)); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(importFetchedAccounts(response.data)); + dispatch(expandMembersSuccess(id, response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => { + dispatch(expandMembersFail(id, error)); + }); + }; +}; + +export function expandMembersRequest(id) { + return { + type: GROUP_MEMBERS_EXPAND_REQUEST, + id, + }; +}; + +export function expandMembersSuccess(id, accounts, next) { + return { + type: GROUP_MEMBERS_EXPAND_SUCCESS, + id, + accounts, + next, + }; +}; + +export function expandMembersFail(id, error) { + return { + type: GROUP_MEMBERS_EXPAND_FAIL, + id, + error, + }; +}; + +export function fetchRemovedAccounts(id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(fetchRemovedAccountsRequest(id)); + + api(getState).get(`/api/v1/groups/${id}/removed_accounts`).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(importFetchedAccounts(response.data)); + dispatch(fetchRemovedAccountsSuccess(id, response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => { + dispatch(fetchRemovedAccountsFail(id, error)); + }); + }; +}; + +export function fetchRemovedAccountsRequest(id) { + return { + type: GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST, + id, + }; +}; + +export function fetchRemovedAccountsSuccess(id, accounts, next) { + return { + type: GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS, + id, + accounts, + next, + }; +}; + +export function fetchRemovedAccountsFail(id, error) { + return { + type: GROUP_REMOVED_ACCOUNTS_FETCH_FAIL, + id, + error, + }; +}; + +export function expandRemovedAccounts(id) { + return (dispatch, getState) => { + if (!me) return; + + const url = getState().getIn(['user_lists', 'groups_removed_accounts', id, 'next']); + + if (url === null) { + return; + } + + dispatch(expandRemovedAccountsRequest(id)); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(importFetchedAccounts(response.data)); + dispatch(expandRemovedAccountsSuccess(id, response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => { + dispatch(expandRemovedAccountsFail(id, error)); + }); + }; +}; + +export function expandRemovedAccountsRequest(id) { + return { + type: GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST, + id, + }; +}; + +export function expandRemovedAccountsSuccess(id, accounts, next) { + return { + type: GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS, + id, + accounts, + next, + }; +}; + +export function expandRemovedAccountsFail(id, error) { + return { + type: GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL, + id, + error, + }; +}; + +export function removeRemovedAccount(groupId, id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(removeRemovedAccountRequest(groupId, id)); + + api(getState).delete(`/api/v1/groups/${groupId}/removed_accounts?account_id=${id}`).then(response => { + dispatch(removeRemovedAccountSuccess(groupId, id)); + }).catch(error => { + dispatch(removeRemovedAccountFail(groupId, id, error)); + }); + }; +}; + +export function removeRemovedAccountRequest(groupId, id) { + return { + type: GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST, + groupId, + id, + }; +}; + +export function removeRemovedAccountSuccess(groupId, id) { + return { + type: GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS, + groupId, + id, + }; +}; + +export function removeRemovedAccountFail(groupId, id, error) { + return { + type: GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL, + groupId, + id, + error, + }; +}; + +export function createRemovedAccount(groupId, id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(createRemovedAccountRequest(groupId, id)); + + api(getState).post(`/api/v1/groups/${groupId}/removed_accounts?account_id=${id}`).then(response => { + dispatch(createRemovedAccountSuccess(groupId, id)); + }).catch(error => { + dispatch(createRemovedAccountFail(groupId, id, error)); + }); + }; +}; + +export function createRemovedAccountRequest(groupId, id) { + return { + type: GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST, + groupId, + id, + }; +}; + +export function createRemovedAccountSuccess(groupId, id) { + return { + type: GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS, + groupId, + id, + }; +}; + +export function createRemovedAccountFail(groupId, id, error) { + return { + type: GROUP_REMOVED_ACCOUNTS_CREATE_FAIL, + groupId, + id, + error, + }; +}; + +export function groupRemoveStatus(groupId, id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(groupRemoveStatusRequest(groupId, id)); + + api(getState).delete(`/api/v1/groups/${groupId}/statuses/${id}`).then(response => { + dispatch(groupRemoveStatusSuccess(groupId, id)); + }).catch(error => { + dispatch(groupRemoveStatusFail(groupId, id, error)); + }); + }; +}; + +export function groupRemoveStatusRequest(groupId, id) { + return { + type: GROUP_REMOVE_STATUS_REQUEST, + groupId, + id, + }; +}; + +export function groupRemoveStatusSuccess(groupId, id) { + return { + type: GROUP_REMOVE_STATUS_SUCCESS, + groupId, + id, + }; +}; + +export function groupRemoveStatusFail(groupId, id, error) { + return { + type: GROUP_REMOVE_STATUS_FAIL, + groupId, + id, + error, + }; +}; \ No newline at end of file diff --git a/app/gabsocial/actions/height_cache.js b/app/gabsocial/actions/height_cache.js new file mode 100644 index 000000000..4c752993f --- /dev/null +++ b/app/gabsocial/actions/height_cache.js @@ -0,0 +1,17 @@ +export const HEIGHT_CACHE_SET = 'HEIGHT_CACHE_SET'; +export const HEIGHT_CACHE_CLEAR = 'HEIGHT_CACHE_CLEAR'; + +export function setHeight (key, id, height) { + return { + type: HEIGHT_CACHE_SET, + key, + id, + height, + }; +}; + +export function clearHeight () { + return { + type: HEIGHT_CACHE_CLEAR, + }; +}; diff --git a/app/gabsocial/actions/identity_proofs.js b/app/gabsocial/actions/identity_proofs.js new file mode 100644 index 000000000..449debf61 --- /dev/null +++ b/app/gabsocial/actions/identity_proofs.js @@ -0,0 +1,30 @@ +import api from '../api'; + +export const IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST = 'IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST'; +export const IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS = 'IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS'; +export const IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL = 'IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL'; + +export const fetchAccountIdentityProofs = accountId => (dispatch, getState) => { + dispatch(fetchAccountIdentityProofsRequest(accountId)); + + api(getState).get(`/api/v1/accounts/${accountId}/identity_proofs`) + .then(({ data }) => dispatch(fetchAccountIdentityProofsSuccess(accountId, data))) + .catch(err => dispatch(fetchAccountIdentityProofsFail(accountId, err))); +}; + +export const fetchAccountIdentityProofsRequest = id => ({ + type: IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST, + id, +}); + +export const fetchAccountIdentityProofsSuccess = (accountId, identity_proofs) => ({ + type: IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS, + accountId, + identity_proofs, +}); + +export const fetchAccountIdentityProofsFail = (accountId, err) => ({ + type: IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL, + accountId, + err, +}); diff --git a/app/gabsocial/actions/importer/index.js b/app/gabsocial/actions/importer/index.js new file mode 100644 index 000000000..bf0e6d358 --- /dev/null +++ b/app/gabsocial/actions/importer/index.js @@ -0,0 +1,95 @@ +import { normalizeAccount, normalizeStatus, normalizePoll } from './normalizer'; + +export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; +export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT'; +export const STATUS_IMPORT = 'STATUS_IMPORT'; +export const STATUSES_IMPORT = 'STATUSES_IMPORT'; +export const POLLS_IMPORT = 'POLLS_IMPORT'; +export const ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP = 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP'; + +function pushUnique(array, object) { + if (array.every(element => element.id !== object.id)) { + array.push(object); + } +} + +export function importAccount(account) { + return { type: ACCOUNT_IMPORT, account }; +} + +export function importAccounts(accounts) { + return { type: ACCOUNTS_IMPORT, accounts }; +} + +export function importStatus(status) { + return { type: STATUS_IMPORT, status }; +} + +export function importStatuses(statuses) { + return { type: STATUSES_IMPORT, statuses }; +} + +export function importPolls(polls) { + return { type: POLLS_IMPORT, polls }; +} + +export function importFetchedAccount(account) { + return importFetchedAccounts([account]); +} + +export function importFetchedAccounts(accounts) { + const normalAccounts = []; + + function processAccount(account) { + pushUnique(normalAccounts, normalizeAccount(account)); + + if (account.moved) { + processAccount(account.moved); + } + } + + accounts.forEach(processAccount); + + return importAccounts(normalAccounts); +} + +export function importFetchedStatus(status) { + return importFetchedStatuses([status]); +} + +export function importFetchedStatuses(statuses) { + return (dispatch, getState) => { + const accounts = []; + const normalStatuses = []; + const polls = []; + + function processStatus(status) { + pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]))); + pushUnique(accounts, status.account); + + if (status.reblog && status.reblog.id) { + processStatus(status.reblog); + } + + if (status.poll && status.poll.id) { + pushUnique(polls, normalizePoll(status.poll)); + } + } + + statuses.forEach(processStatus); + + dispatch(importPolls(polls)); + dispatch(importFetchedAccounts(accounts)); + dispatch(importStatuses(normalStatuses)); + }; +} + +export function importFetchedPoll(poll) { + return dispatch => { + dispatch(importPolls([normalizePoll(poll)])); + }; +} + +export function importErrorWhileFetchingAccountByUsername(username) { + return { type: ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP, username }; +}; diff --git a/app/gabsocial/actions/importer/normalizer.js b/app/gabsocial/actions/importer/normalizer.js new file mode 100644 index 000000000..5badb0c49 --- /dev/null +++ b/app/gabsocial/actions/importer/normalizer.js @@ -0,0 +1,82 @@ +import escapeTextContentForBrowser from 'escape-html'; +import emojify from '../../features/emoji/emoji'; +import { unescapeHTML } from '../../utils/html'; +import { expandSpoilers } from '../../initial_state'; + +const domParser = new DOMParser(); + +const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { + obj[`:${emoji.shortcode}:`] = emoji; + return obj; +}, {}); + +export function normalizeAccount(account) { + account = { ...account }; + + const emojiMap = makeEmojiMap(account); + const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name; + + account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap); + account.note_emojified = emojify(account.note, emojiMap); + + if (account.fields) { + account.fields = account.fields.map(pair => ({ + ...pair, + name_emojified: emojify(escapeTextContentForBrowser(pair.name)), + value_emojified: emojify(pair.value, emojiMap), + value_plain: unescapeHTML(pair.value), + })); + } + + if (account.moved) { + account.moved = account.moved.id; + } + + return account; +} + +export function normalizeStatus(status, normalOldStatus) { + const normalStatus = { ...status }; + normalStatus.account = status.account.id; + + if (status.reblog && status.reblog.id) { + normalStatus.reblog = status.reblog.id; + } + + if (status.poll && status.poll.id) { + normalStatus.poll = status.poll.id; + } + + // Only calculate these values when status first encountered + // Otherwise keep the ones already in the reducer + if (normalOldStatus) { + normalStatus.search_index = normalOldStatus.get('search_index'); + normalStatus.contentHtml = normalOldStatus.get('contentHtml'); + normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml'); + normalStatus.hidden = normalOldStatus.get('hidden'); + } else { + const spoilerText = normalStatus.spoiler_text || ''; + const searchContent = [spoilerText, status.content].join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); + const emojiMap = makeEmojiMap(normalStatus); + + normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; + normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); + normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap); + normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive; + } + + return normalStatus; +} + +export function normalizePoll(poll) { + const normalPoll = { ...poll }; + + const emojiMap = makeEmojiMap(normalPoll); + + normalPoll.options = poll.options.map(option => ({ + ...option, + title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap), + })); + + return normalPoll; +} diff --git a/app/gabsocial/actions/interactions.js b/app/gabsocial/actions/interactions.js new file mode 100644 index 000000000..542b96b57 --- /dev/null +++ b/app/gabsocial/actions/interactions.js @@ -0,0 +1,351 @@ +import api from '../api'; +import { importFetchedAccounts, importFetchedStatus } from './importer'; +import { me } from 'gabsocial/initial_state'; + +export const REBLOG_REQUEST = 'REBLOG_REQUEST'; +export const REBLOG_SUCCESS = 'REBLOG_SUCCESS'; +export const REBLOG_FAIL = 'REBLOG_FAIL'; + +export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST'; +export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS'; +export const FAVOURITE_FAIL = 'FAVOURITE_FAIL'; + +export const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST'; +export const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS'; +export const UNREBLOG_FAIL = 'UNREBLOG_FAIL'; + +export const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST'; +export const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS'; +export const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL'; + +export const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST'; +export const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS'; +export const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL'; + +export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST'; +export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS'; +export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL'; + +export const PIN_REQUEST = 'PIN_REQUEST'; +export const PIN_SUCCESS = 'PIN_SUCCESS'; +export const PIN_FAIL = 'PIN_FAIL'; + +export const UNPIN_REQUEST = 'UNPIN_REQUEST'; +export const UNPIN_SUCCESS = 'UNPIN_SUCCESS'; +export const UNPIN_FAIL = 'UNPIN_FAIL'; + +export function reblog(status) { + return function (dispatch, getState) { + if (!me) return; + + dispatch(reblogRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`).then(function (response) { + // The reblog API method returns a new status wrapped around the original. In this case we are only + // interested in how the original is modified, hence passing it skipping the wrapper + dispatch(importFetchedStatus(response.data.reblog)); + dispatch(reblogSuccess(status)); + }).catch(function (error) { + dispatch(reblogFail(status, error)); + }); + }; +}; + +export function unreblog(status) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(unreblogRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => { + dispatch(importFetchedStatus(response.data)); + dispatch(unreblogSuccess(status)); + }).catch(error => { + dispatch(unreblogFail(status, error)); + }); + }; +}; + +export function reblogRequest(status) { + return { + type: REBLOG_REQUEST, + status: status, + skipLoading: true, + }; +}; + +export function reblogSuccess(status) { + return { + type: REBLOG_SUCCESS, + status: status, + skipLoading: true, + }; +}; + +export function reblogFail(status, error) { + return { + type: REBLOG_FAIL, + status: status, + error: error, + skipLoading: true, + }; +}; + +export function unreblogRequest(status) { + return { + type: UNREBLOG_REQUEST, + status: status, + skipLoading: true, + }; +}; + +export function unreblogSuccess(status) { + return { + type: UNREBLOG_SUCCESS, + status: status, + skipLoading: true, + }; +}; + +export function unreblogFail(status, error) { + return { + type: UNREBLOG_FAIL, + status: status, + error: error, + skipLoading: true, + }; +}; + +export function favourite(status) { + return function (dispatch, getState) { + if (!me) return; + + dispatch(favouriteRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function (response) { + dispatch(importFetchedStatus(response.data)); + dispatch(favouriteSuccess(status)); + }).catch(function (error) { + dispatch(favouriteFail(status, error)); + }); + }; +}; + +export function unfavourite(status) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(unfavouriteRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => { + dispatch(importFetchedStatus(response.data)); + dispatch(unfavouriteSuccess(status)); + }).catch(error => { + dispatch(unfavouriteFail(status, error)); + }); + }; +}; + +export function favouriteRequest(status) { + return { + type: FAVOURITE_REQUEST, + status: status, + skipLoading: true, + }; +}; + +export function favouriteSuccess(status) { + return { + type: FAVOURITE_SUCCESS, + status: status, + skipLoading: true, + }; +}; + +export function favouriteFail(status, error) { + return { + type: FAVOURITE_FAIL, + status: status, + error: error, + skipLoading: true, + }; +}; + +export function unfavouriteRequest(status) { + return { + type: UNFAVOURITE_REQUEST, + status: status, + skipLoading: true, + }; +}; + +export function unfavouriteSuccess(status) { + return { + type: UNFAVOURITE_SUCCESS, + status: status, + skipLoading: true, + }; +}; + +export function unfavouriteFail(status, error) { + return { + type: UNFAVOURITE_FAIL, + status: status, + error: error, + skipLoading: true, + }; +}; + +export function fetchReblogs(id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(fetchReblogsRequest(id)); + + api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => { + dispatch(importFetchedAccounts(response.data)); + dispatch(fetchReblogsSuccess(id, response.data)); + }).catch(error => { + dispatch(fetchReblogsFail(id, error)); + }); + }; +}; + +export function fetchReblogsRequest(id) { + return { + type: REBLOGS_FETCH_REQUEST, + id, + }; +}; + +export function fetchReblogsSuccess(id, accounts) { + return { + type: REBLOGS_FETCH_SUCCESS, + id, + accounts, + }; +}; + +export function fetchReblogsFail(id, error) { + return { + type: REBLOGS_FETCH_FAIL, + error, + }; +}; + +export function fetchFavourites(id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(fetchFavouritesRequest(id)); + + api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => { + dispatch(importFetchedAccounts(response.data)); + dispatch(fetchFavouritesSuccess(id, response.data)); + }).catch(error => { + dispatch(fetchFavouritesFail(id, error)); + }); + }; +}; + +export function fetchFavouritesRequest(id) { + return { + type: FAVOURITES_FETCH_REQUEST, + id, + }; +}; + +export function fetchFavouritesSuccess(id, accounts) { + return { + type: FAVOURITES_FETCH_SUCCESS, + id, + accounts, + }; +}; + +export function fetchFavouritesFail(id, error) { + return { + type: FAVOURITES_FETCH_FAIL, + error, + }; +}; + +export function pin(status) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(pinRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => { + dispatch(importFetchedStatus(response.data)); + dispatch(pinSuccess(status)); + }).catch(error => { + dispatch(pinFail(status, error)); + }); + }; +}; + +export function pinRequest(status) { + return { + type: PIN_REQUEST, + status, + skipLoading: true, + }; +}; + +export function pinSuccess(status) { + return { + type: PIN_SUCCESS, + status, + skipLoading: true, + }; +}; + +export function pinFail(status, error) { + return { + type: PIN_FAIL, + status, + error, + skipLoading: true, + }; +}; + +export function unpin (status) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(unpinRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => { + dispatch(importFetchedStatus(response.data)); + dispatch(unpinSuccess(status)); + }).catch(error => { + dispatch(unpinFail(status, error)); + }); + }; +}; + +export function unpinRequest(status) { + return { + type: UNPIN_REQUEST, + status, + skipLoading: true, + }; +}; + +export function unpinSuccess(status) { + return { + type: UNPIN_SUCCESS, + status, + skipLoading: true, + }; +}; + +export function unpinFail(status, error) { + return { + type: UNPIN_FAIL, + status, + error, + skipLoading: true, + }; +}; diff --git a/app/gabsocial/actions/lists.js b/app/gabsocial/actions/lists.js new file mode 100644 index 000000000..3461c6b33 --- /dev/null +++ b/app/gabsocial/actions/lists.js @@ -0,0 +1,392 @@ +import api from '../api'; +import { importFetchedAccounts } from './importer'; +import { showAlertForError } from './alerts'; +import { me } from 'gabsocial/initial_state' + +export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST'; +export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS'; +export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL'; + +export const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST'; +export const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS'; +export const LISTS_FETCH_FAIL = 'LISTS_FETCH_FAIL'; + +export const LIST_EDITOR_TITLE_CHANGE = 'LIST_EDITOR_TITLE_CHANGE'; +export const LIST_EDITOR_RESET = 'LIST_EDITOR_RESET'; +export const LIST_EDITOR_SETUP = 'LIST_EDITOR_SETUP'; + +export const LIST_CREATE_REQUEST = 'LIST_CREATE_REQUEST'; +export const LIST_CREATE_SUCCESS = 'LIST_CREATE_SUCCESS'; +export const LIST_CREATE_FAIL = 'LIST_CREATE_FAIL'; + +export const LIST_UPDATE_REQUEST = 'LIST_UPDATE_REQUEST'; +export const LIST_UPDATE_SUCCESS = 'LIST_UPDATE_SUCCESS'; +export const LIST_UPDATE_FAIL = 'LIST_UPDATE_FAIL'; + +export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST'; +export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS'; +export const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL'; + +export const LIST_ACCOUNTS_FETCH_REQUEST = 'LIST_ACCOUNTS_FETCH_REQUEST'; +export const LIST_ACCOUNTS_FETCH_SUCCESS = 'LIST_ACCOUNTS_FETCH_SUCCESS'; +export const LIST_ACCOUNTS_FETCH_FAIL = 'LIST_ACCOUNTS_FETCH_FAIL'; + +export const LIST_EDITOR_SUGGESTIONS_CHANGE = 'LIST_EDITOR_SUGGESTIONS_CHANGE'; +export const LIST_EDITOR_SUGGESTIONS_READY = 'LIST_EDITOR_SUGGESTIONS_READY'; +export const LIST_EDITOR_SUGGESTIONS_CLEAR = 'LIST_EDITOR_SUGGESTIONS_CLEAR'; + +export const LIST_EDITOR_ADD_REQUEST = 'LIST_EDITOR_ADD_REQUEST'; +export const LIST_EDITOR_ADD_SUCCESS = 'LIST_EDITOR_ADD_SUCCESS'; +export const LIST_EDITOR_ADD_FAIL = 'LIST_EDITOR_ADD_FAIL'; + +export const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST'; +export const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS'; +export const LIST_EDITOR_REMOVE_FAIL = 'LIST_EDITOR_REMOVE_FAIL'; + +export const LIST_ADDER_RESET = 'LIST_ADDER_RESET'; +export const LIST_ADDER_SETUP = 'LIST_ADDER_SETUP'; + +export const LIST_ADDER_LISTS_FETCH_REQUEST = 'LIST_ADDER_LISTS_FETCH_REQUEST'; +export const LIST_ADDER_LISTS_FETCH_SUCCESS = 'LIST_ADDER_LISTS_FETCH_SUCCESS'; +export const LIST_ADDER_LISTS_FETCH_FAIL = 'LIST_ADDER_LISTS_FETCH_FAIL'; + +export const fetchList = id => (dispatch, getState) => { + if (!me) return; + + if (getState().getIn(['lists', id])) { + return; + } + + dispatch(fetchListRequest(id)); + + api(getState).get(`/api/v1/lists/${id}`) + .then(({ data }) => dispatch(fetchListSuccess(data))) + .catch(err => dispatch(fetchListFail(id, err))); +}; + +export const fetchListRequest = id => ({ + type: LIST_FETCH_REQUEST, + id, +}); + +export const fetchListSuccess = list => ({ + type: LIST_FETCH_SUCCESS, + list, +}); + +export const fetchListFail = (id, error) => ({ + type: LIST_FETCH_FAIL, + id, + error, +}); + +export const fetchLists = () => (dispatch, getState) => { + if (!me) return; + + dispatch(fetchListsRequest()); + + api(getState).get('/api/v1/lists') + .then(({ data }) => dispatch(fetchListsSuccess(data))) + .catch(err => dispatch(fetchListsFail(err))); +}; + +export const fetchListsRequest = () => ({ + type: LISTS_FETCH_REQUEST, +}); + +export const fetchListsSuccess = lists => ({ + type: LISTS_FETCH_SUCCESS, + lists, +}); + +export const fetchListsFail = error => ({ + type: LISTS_FETCH_FAIL, + error, +}); + +export const submitListEditor = shouldReset => (dispatch, getState) => { + const listId = getState().getIn(['listEditor', 'listId']); + const title = getState().getIn(['listEditor', 'title']); + + if (listId === null) { + dispatch(createList(title, shouldReset)); + } else { + dispatch(updateList(listId, title, shouldReset)); + } +}; + +export const setupListEditor = listId => (dispatch, getState) => { + dispatch({ + type: LIST_EDITOR_SETUP, + list: getState().getIn(['lists', listId]), + }); + + dispatch(fetchListAccounts(listId)); +}; + +export const changeListEditorTitle = value => ({ + type: LIST_EDITOR_TITLE_CHANGE, + value, +}); + +export const createList = (title, shouldReset) => (dispatch, getState) => { + if (!me) return; + + dispatch(createListRequest()); + + api(getState).post('/api/v1/lists', { title }).then(({ data }) => { + dispatch(createListSuccess(data)); + + if (shouldReset) { + dispatch(resetListEditor()); + } + }).catch(err => dispatch(createListFail(err))); +}; + +export const createListRequest = () => ({ + type: LIST_CREATE_REQUEST, +}); + +export const createListSuccess = list => ({ + type: LIST_CREATE_SUCCESS, + list, +}); + +export const createListFail = error => ({ + type: LIST_CREATE_FAIL, + error, +}); + +export const updateList = (id, title, shouldReset) => (dispatch, getState) => { + if (!me) return; + + dispatch(updateListRequest(id)); + + api(getState).put(`/api/v1/lists/${id}`, { title }).then(({ data }) => { + dispatch(updateListSuccess(data)); + + if (shouldReset) { + dispatch(resetListEditor()); + } + }).catch(err => dispatch(updateListFail(id, err))); +}; + +export const updateListRequest = id => ({ + type: LIST_UPDATE_REQUEST, + id, +}); + +export const updateListSuccess = list => ({ + type: LIST_UPDATE_SUCCESS, + list, +}); + +export const updateListFail = (id, error) => ({ + type: LIST_UPDATE_FAIL, + id, + error, +}); + +export const resetListEditor = () => ({ + type: LIST_EDITOR_RESET, +}); + +export const deleteList = id => (dispatch, getState) => { + if (!me) return; + + dispatch(deleteListRequest(id)); + + api(getState).delete(`/api/v1/lists/${id}`) + .then(() => dispatch(deleteListSuccess(id))) + .catch(err => dispatch(deleteListFail(id, err))); +}; + +export const deleteListRequest = id => ({ + type: LIST_DELETE_REQUEST, + id, +}); + +export const deleteListSuccess = id => ({ + type: LIST_DELETE_SUCCESS, + id, +}); + +export const deleteListFail = (id, error) => ({ + type: LIST_DELETE_FAIL, + id, + error, +}); + +export const fetchListAccounts = listId => (dispatch, getState) => { + if (!me) return; + + dispatch(fetchListAccountsRequest(listId)); + + api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchListAccountsSuccess(listId, data)); + }).catch(err => dispatch(fetchListAccountsFail(listId, err))); +}; + +export const fetchListAccountsRequest = id => ({ + type: LIST_ACCOUNTS_FETCH_REQUEST, + id, +}); + +export const fetchListAccountsSuccess = (id, accounts, next) => ({ + type: LIST_ACCOUNTS_FETCH_SUCCESS, + id, + accounts, + next, +}); + +export const fetchListAccountsFail = (id, error) => ({ + type: LIST_ACCOUNTS_FETCH_FAIL, + id, + error, +}); + +export const fetchListSuggestions = q => (dispatch, getState) => { + if (!me) return; + + const params = { + q, + resolve: false, + limit: 4, + following: true, + }; + + api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchListSuggestionsReady(q, data)); + }).catch(error => dispatch(showAlertForError(error))); +}; + +export const fetchListSuggestionsReady = (query, accounts) => ({ + type: LIST_EDITOR_SUGGESTIONS_READY, + query, + accounts, +}); + +export const clearListSuggestions = () => ({ + type: LIST_EDITOR_SUGGESTIONS_CLEAR, +}); + +export const changeListSuggestions = value => ({ + type: LIST_EDITOR_SUGGESTIONS_CHANGE, + value, +}); + +export const addToListEditor = accountId => (dispatch, getState) => { + dispatch(addToList(getState().getIn(['listEditor', 'listId']), accountId)); +}; + +export const addToList = (listId, accountId) => (dispatch, getState) => { + if (!me) return; + + dispatch(addToListRequest(listId, accountId)); + + api(getState).post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] }) + .then(() => dispatch(addToListSuccess(listId, accountId))) + .catch(err => dispatch(addToListFail(listId, accountId, err))); +}; + +export const addToListRequest = (listId, accountId) => ({ + type: LIST_EDITOR_ADD_REQUEST, + listId, + accountId, +}); + +export const addToListSuccess = (listId, accountId) => ({ + type: LIST_EDITOR_ADD_SUCCESS, + listId, + accountId, +}); + +export const addToListFail = (listId, accountId, error) => ({ + type: LIST_EDITOR_ADD_FAIL, + listId, + accountId, + error, +}); + +export const removeFromListEditor = accountId => (dispatch, getState) => { + dispatch(removeFromList(getState().getIn(['listEditor', 'listId']), accountId)); +}; + +export const removeFromList = (listId, accountId) => (dispatch, getState) => { + if (!me) return; + + dispatch(removeFromListRequest(listId, accountId)); + + api(getState).delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } }) + .then(() => dispatch(removeFromListSuccess(listId, accountId))) + .catch(err => dispatch(removeFromListFail(listId, accountId, err))); +}; + +export const removeFromListRequest = (listId, accountId) => ({ + type: LIST_EDITOR_REMOVE_REQUEST, + listId, + accountId, +}); + +export const removeFromListSuccess = (listId, accountId) => ({ + type: LIST_EDITOR_REMOVE_SUCCESS, + listId, + accountId, +}); + +export const removeFromListFail = (listId, accountId, error) => ({ + type: LIST_EDITOR_REMOVE_FAIL, + listId, + accountId, + error, +}); + +export const resetListAdder = () => ({ + type: LIST_ADDER_RESET, +}); + +export const setupListAdder = accountId => (dispatch, getState) => { + dispatch({ + type: LIST_ADDER_SETUP, + account: getState().getIn(['accounts', accountId]), + }); + dispatch(fetchLists()); + dispatch(fetchAccountLists(accountId)); +}; + +export const fetchAccountLists = accountId => (dispatch, getState) => { + if (!me) return; + + dispatch(fetchAccountListsRequest(accountId)); + + api(getState).get(`/api/v1/accounts/${accountId}/lists`) + .then(({ data }) => dispatch(fetchAccountListsSuccess(accountId, data))) + .catch(err => dispatch(fetchAccountListsFail(accountId, err))); +}; + +export const fetchAccountListsRequest = id => ({ + type:LIST_ADDER_LISTS_FETCH_REQUEST, + id, +}); + +export const fetchAccountListsSuccess = (id, lists) => ({ + type: LIST_ADDER_LISTS_FETCH_SUCCESS, + id, + lists, +}); + +export const fetchAccountListsFail = (id, err) => ({ + type: LIST_ADDER_LISTS_FETCH_FAIL, + id, + err, +}); + +export const addToListAdder = listId => (dispatch, getState) => { + dispatch(addToList(listId, getState().getIn(['listAdder', 'accountId']))); +}; + +export const removeFromListAdder = listId => (dispatch, getState) => { + dispatch(removeFromList(listId, getState().getIn(['listAdder', 'accountId']))); +}; diff --git a/app/gabsocial/actions/modal.js b/app/gabsocial/actions/modal.js new file mode 100644 index 000000000..80e15c28e --- /dev/null +++ b/app/gabsocial/actions/modal.js @@ -0,0 +1,16 @@ +export const MODAL_OPEN = 'MODAL_OPEN'; +export const MODAL_CLOSE = 'MODAL_CLOSE'; + +export function openModal(type, props) { + return { + type: MODAL_OPEN, + modalType: type, + modalProps: props, + }; +}; + +export function closeModal() { + return { + type: MODAL_CLOSE, + }; +}; diff --git a/app/gabsocial/actions/mutes.js b/app/gabsocial/actions/mutes.js new file mode 100644 index 000000000..4e2ae15d3 --- /dev/null +++ b/app/gabsocial/actions/mutes.js @@ -0,0 +1,111 @@ +import api, { getLinks } from '../api'; +import { fetchRelationships } from './accounts'; +import { importFetchedAccounts } from './importer'; +import { openModal } from './modal'; +import { me } from 'gabsocial/initial_state'; + +export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST'; +export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS'; +export const MUTES_FETCH_FAIL = 'MUTES_FETCH_FAIL'; + +export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST'; +export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS'; +export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL'; + +export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL'; +export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS'; + +export function fetchMutes() { + return (dispatch, getState) => { + if (!me) return; + + dispatch(fetchMutesRequest()); + + api(getState).get('/api/v1/mutes').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(response.data)); + dispatch(fetchMutesSuccess(response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => dispatch(fetchMutesFail(error))); + }; +}; + +export function fetchMutesRequest() { + return { + type: MUTES_FETCH_REQUEST, + }; +}; + +export function fetchMutesSuccess(accounts, next) { + return { + type: MUTES_FETCH_SUCCESS, + accounts, + next, + }; +}; + +export function fetchMutesFail(error) { + return { + type: MUTES_FETCH_FAIL, + error, + }; +}; + +export function expandMutes() { + return (dispatch, getState) => { + if (!me) return; + + const url = getState().getIn(['user_lists', 'mutes', 'next']); + + if (url === null) { + return; + } + + dispatch(expandMutesRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(response.data)); + dispatch(expandMutesSuccess(response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => dispatch(expandMutesFail(error))); + }; +}; + +export function expandMutesRequest() { + return { + type: MUTES_EXPAND_REQUEST, + }; +}; + +export function expandMutesSuccess(accounts, next) { + return { + type: MUTES_EXPAND_SUCCESS, + accounts, + next, + }; +}; + +export function expandMutesFail(error) { + return { + type: MUTES_EXPAND_FAIL, + error, + }; +}; + +export function initMuteModal(account) { + return dispatch => { + dispatch({ + type: MUTES_INIT_MODAL, + account, + }); + + dispatch(openModal('MUTE')); + }; +} + +export function toggleHideNotifications() { + return dispatch => { + dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS }); + }; +} diff --git a/app/gabsocial/actions/notifications.js b/app/gabsocial/actions/notifications.js new file mode 100644 index 000000000..5d44ed224 --- /dev/null +++ b/app/gabsocial/actions/notifications.js @@ -0,0 +1,274 @@ +import api, { getLinks } from '../api'; +import IntlMessageFormat from 'intl-messageformat'; +import 'intl-pluralrules'; +import { fetchRelationships } from './accounts'; +import { + importFetchedAccount, + importFetchedAccounts, + importFetchedStatus, + importFetchedStatuses, +} from './importer'; +import { saveSettings } from './settings'; +import { defineMessages } from 'react-intl'; +import { List as ImmutableList } from 'immutable'; +import { unescapeHTML } from '../utils/html'; +import { getFilters, regexFromFilters } from '../selectors'; +import { me } from 'gabsocial/initial_state'; + +export const NOTIFICATIONS_INITIALIZE = 'NOTIFICATIONS_INITIALIZE'; +export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; +export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; +export const NOTIFICATIONS_UPDATE_QUEUE = 'NOTIFICATIONS_UPDATE_QUEUE'; +export const NOTIFICATIONS_DEQUEUE = 'NOTIFICATIONS_DEQUEUE'; + +export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST'; +export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS'; +export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; + +export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET'; + +export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; +export const NOTIFICATIONS_MARK_READ = 'NOTIFICATIONS_MARK_READ'; +export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; + +export const MAX_QUEUED_NOTIFICATIONS = 40; + +defineMessages({ + mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, + group: { id: 'notifications.group', defaultMessage: '{count} notifications' }, +}); + +const fetchRelatedRelationships = (dispatch, notifications) => { + const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id); + + if (accountIds.length > 0) { + dispatch(fetchRelationships(accountIds)); + } +}; + +export function initializeNotifications() { + return { + type: NOTIFICATIONS_INITIALIZE + }; +} + +export function updateNotifications(notification, intlMessages, intlLocale) { + return (dispatch, getState) => { + const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true); + + if (showInColumn) { + dispatch(importFetchedAccount(notification.account)); + + if (notification.status) { + dispatch(importFetchedStatus(notification.status)); + } + + dispatch({ + type: NOTIFICATIONS_UPDATE, + notification, + }); + + fetchRelatedRelationships(dispatch, [notification]); + } + }; +}; + +export function updateNotificationsQueue(notification, intlMessages, intlLocale, curPath) { + return (dispatch, getState) => { + const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true); + const filters = getFilters(getState(), { contextType: 'notifications' }); + const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true); + + let filtered = false; + + const isOnNotificationsPage = curPath === '/notifications'; + + if (notification.type === 'mention') { + const regex = regexFromFilters(filters); + const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content); + filtered = regex && regex.test(searchIndex); + } + + // Desktop notifications + if (typeof window.Notification !== 'undefined' && showAlert && !filtered) { + const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username }); + const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : ''); + + const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id }); + + notify.addEventListener('click', () => { + window.focus(); + notify.close(); + }); + } + + if (playSound && !filtered) { + dispatch({ + type: NOTIFICATIONS_UPDATE_NOOP, + meta: { sound: 'ribbit' }, + }); + } + + if (isOnNotificationsPage) { + dispatch({ + type: NOTIFICATIONS_UPDATE_QUEUE, + notification, + intlMessages, + intlLocale, + }); + } + else { + dispatch(updateNotifications(notification, intlMessages, intlLocale)); + } + } +}; + +export function dequeueNotifications() { + return (dispatch, getState) => { + const queuedNotifications = getState().getIn(['notifications', 'queuedNotifications'], ImmutableList()); + const totalQueuedNotificationsCount = getState().getIn(['notifications', 'totalQueuedNotificationsCount'], 0); + + if (totalQueuedNotificationsCount == 0) { + return; + } + else if (totalQueuedNotificationsCount > 0 && totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) { + queuedNotifications.forEach(block => { + dispatch(updateNotifications(block.notification, block.intlMessages, block.intlLocale)); + }); + } + else { + dispatch(expandNotifications()); + } + + dispatch({ + type: NOTIFICATIONS_DEQUEUE, + }); + dispatch(markReadNotifications()); + } +}; + +const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); + +const excludeTypesFromFilter = filter => { + const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention', 'poll']); + return allTypes.filterNot(item => item === filter).toJS(); +}; + +const noOp = () => {}; + +export function expandNotifications({ maxId } = {}, done = noOp) { + return (dispatch, getState) => { + if (!me) return; + + const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']); + const notifications = getState().get('notifications'); + const isLoadingMore = !!maxId; + + if (notifications.get('isLoading')) { + done(); + return; + } + + const params = { + max_id: maxId, + exclude_types: activeFilter === 'all' + ? excludeTypesFromSettings(getState()) + : excludeTypesFromFilter(activeFilter), + }; + + if (!maxId && notifications.get('items').size > 0) { + params.since_id = notifications.getIn(['items', 0, 'id']); + } + + dispatch(expandNotificationsRequest(isLoadingMore)); + + api(getState).get('/api/v1/notifications', { params }).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(importFetchedAccounts(response.data.map(item => item.account))); + dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); + + dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore)); + fetchRelatedRelationships(dispatch, response.data); + done(); + }).catch(error => { + dispatch(expandNotificationsFail(error, isLoadingMore)); + done(); + }); + }; +}; + +export function expandNotificationsRequest(isLoadingMore) { + return { + type: NOTIFICATIONS_EXPAND_REQUEST, + skipLoading: !isLoadingMore, + }; +}; + +export function expandNotificationsSuccess(notifications, next, isLoadingMore) { + return { + type: NOTIFICATIONS_EXPAND_SUCCESS, + notifications, + next, + skipLoading: !isLoadingMore, + }; +}; + +export function expandNotificationsFail(error, isLoadingMore) { + return { + type: NOTIFICATIONS_EXPAND_FAIL, + error, + skipLoading: !isLoadingMore, + }; +}; + +export function clearNotifications() { + return (dispatch, getState) => { + if (!me) return; + + dispatch({ + type: NOTIFICATIONS_CLEAR, + }); + + api(getState).post('/api/v1/notifications/clear'); + }; +}; + +export function scrollTopNotifications(top) { + return (dispatch, getState) => { + dispatch({ + type: NOTIFICATIONS_SCROLL_TOP, + top, + }); + dispatch(markReadNotifications()); + } +} + +export function setFilter (filterType) { + return dispatch => { + dispatch({ + type: NOTIFICATIONS_FILTER_SET, + path: ['notifications', 'quickFilter', 'active'], + value: filterType, + }); + dispatch(expandNotifications()); + dispatch(saveSettings()); + }; +} + +export function markReadNotifications() { + return (dispatch, getState) => { + if (!me) return; + const top_notification = parseInt(getState().getIn(['notifications', 'items', 0, 'id'])); + const last_read = getState().getIn(['notifications', 'lastRead']); + + if (top_notification && top_notification > last_read) { + api(getState).post('/api/v1/notifications/mark_read', {id: top_notification}).then(response => { + dispatch({ + type: NOTIFICATIONS_MARK_READ, + notification: top_notification, + }); + }); + } + } +} diff --git a/app/gabsocial/actions/onboarding.js b/app/gabsocial/actions/onboarding.js new file mode 100644 index 000000000..a1dd3a731 --- /dev/null +++ b/app/gabsocial/actions/onboarding.js @@ -0,0 +1,8 @@ +import { changeSetting, saveSettings } from './settings'; + +export const INTRODUCTION_VERSION = 20181216044202; + +export const closeOnboarding = () => dispatch => { + dispatch(changeSetting(['introductionVersion'], INTRODUCTION_VERSION)); + dispatch(saveSettings()); +}; diff --git a/app/gabsocial/actions/pin_statuses.js b/app/gabsocial/actions/pin_statuses.js new file mode 100644 index 000000000..79a5d8140 --- /dev/null +++ b/app/gabsocial/actions/pin_statuses.js @@ -0,0 +1,43 @@ +import api from '../api'; +import { importFetchedStatuses } from './importer'; +import { me } from 'gabsocial/initial_state'; + +export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST'; +export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS'; +export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL'; + +export function fetchPinnedStatuses() { + return (dispatch, getState) => { + if (!me) return; + + dispatch(fetchPinnedStatusesRequest()); + + api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => { + dispatch(importFetchedStatuses(response.data)); + dispatch(fetchPinnedStatusesSuccess(response.data, null)); + }).catch(error => { + dispatch(fetchPinnedStatusesFail(error)); + }); + }; +}; + +export function fetchPinnedStatusesRequest() { + return { + type: PINNED_STATUSES_FETCH_REQUEST, + }; +}; + +export function fetchPinnedStatusesSuccess(statuses, next) { + return { + type: PINNED_STATUSES_FETCH_SUCCESS, + statuses, + next, + }; +}; + +export function fetchPinnedStatusesFail(error) { + return { + type: PINNED_STATUSES_FETCH_FAIL, + error, + }; +}; diff --git a/app/gabsocial/actions/polls.js b/app/gabsocial/actions/polls.js new file mode 100644 index 000000000..8e8b82df5 --- /dev/null +++ b/app/gabsocial/actions/polls.js @@ -0,0 +1,60 @@ +import api from '../api'; +import { importFetchedPoll } from './importer'; + +export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST'; +export const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS'; +export const POLL_VOTE_FAIL = 'POLL_VOTE_FAIL'; + +export const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST'; +export const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS'; +export const POLL_FETCH_FAIL = 'POLL_FETCH_FAIL'; + +export const vote = (pollId, choices) => (dispatch, getState) => { + dispatch(voteRequest()); + + api(getState).post(`/api/v1/polls/${pollId}/votes`, { choices }) + .then(({ data }) => { + dispatch(importFetchedPoll(data)); + dispatch(voteSuccess(data)); + }) + .catch(err => dispatch(voteFail(err))); +}; + +export const fetchPoll = pollId => (dispatch, getState) => { + dispatch(fetchPollRequest()); + + api(getState).get(`/api/v1/polls/${pollId}`) + .then(({ data }) => { + dispatch(importFetchedPoll(data)); + dispatch(fetchPollSuccess(data)); + }) + .catch(err => dispatch(fetchPollFail(err))); +}; + +export const voteRequest = () => ({ + type: POLL_VOTE_REQUEST, +}); + +export const voteSuccess = poll => ({ + type: POLL_VOTE_SUCCESS, + poll, +}); + +export const voteFail = error => ({ + type: POLL_VOTE_FAIL, + error, +}); + +export const fetchPollRequest = () => ({ + type: POLL_FETCH_REQUEST, +}); + +export const fetchPollSuccess = poll => ({ + type: POLL_FETCH_SUCCESS, + poll, +}); + +export const fetchPollFail = error => ({ + type: POLL_FETCH_FAIL, + error, +}); diff --git a/app/gabsocial/actions/push_notifications/index.js b/app/gabsocial/actions/push_notifications/index.js new file mode 100644 index 000000000..2ffec500a --- /dev/null +++ b/app/gabsocial/actions/push_notifications/index.js @@ -0,0 +1,23 @@ +import { + SET_BROWSER_SUPPORT, + SET_SUBSCRIPTION, + CLEAR_SUBSCRIPTION, + SET_ALERTS, + setAlerts, +} from './setter'; +import { register, saveSettings } from './registerer'; + +export { + SET_BROWSER_SUPPORT, + SET_SUBSCRIPTION, + CLEAR_SUBSCRIPTION, + SET_ALERTS, + register, +}; + +export function changeAlerts(path, value) { + return dispatch => { + dispatch(setAlerts(path, value)); + dispatch(saveSettings()); + }; +} diff --git a/app/gabsocial/actions/push_notifications/registerer.js b/app/gabsocial/actions/push_notifications/registerer.js new file mode 100644 index 000000000..b0f42b6a2 --- /dev/null +++ b/app/gabsocial/actions/push_notifications/registerer.js @@ -0,0 +1,133 @@ +import api from '../../api'; +import { decode as decodeBase64 } from '../../utils/base64'; +import { pushNotificationsSetting } from '../../settings'; +import { setBrowserSupport, setSubscription, clearSubscription } from './setter'; +import { me } from '../../initial_state'; + +// Taken from https://www.npmjs.com/package/web-push +const urlBase64ToUint8Array = (base64String) => { + const padding = '='.repeat((4 - base64String.length % 4) % 4); + const base64 = (base64String + padding) + .replace(/\-/g, '+') + .replace(/_/g, '/'); + + return decodeBase64(base64); +}; + +const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content'); + +const getRegistration = () => navigator.serviceWorker.ready; + +const getPushSubscription = (registration) => + registration.pushManager.getSubscription() + .then(subscription => ({ registration, subscription })); + +const subscribe = (registration) => + registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey()), + }); + +const unsubscribe = ({ registration, subscription }) => + subscription ? subscription.unsubscribe().then(() => registration) : registration; + +const sendSubscriptionToBackend = (subscription) => { + const params = { subscription }; + + if (me) { + const data = pushNotificationsSetting.get(me); + if (data) { + params.data = data; + } + } + + return api().post('/api/web/push_subscriptions', params).then(response => response.data); +}; + +// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload +const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype); + +export function register () { + return (dispatch, getState) => { + dispatch(setBrowserSupport(supportsPushNotifications)); + + if (supportsPushNotifications) { + if (!getApplicationServerKey()) { + console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.'); + return; + } + + getRegistration() + .then(getPushSubscription) + .then(({ registration, subscription }) => { + if (subscription !== null) { + // We have a subscription, check if it is still valid + const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString(); + const subscriptionServerKey = urlBase64ToUint8Array(getApplicationServerKey()).toString(); + const serverEndpoint = getState().getIn(['push_notifications', 'subscription', 'endpoint']); + + // If the VAPID public key did not change and the endpoint corresponds + // to the endpoint saved in the backend, the subscription is valid + if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) { + return subscription; + } else { + // Something went wrong, try to subscribe again + return unsubscribe({ registration, subscription }).then(subscribe).then( + subscription => sendSubscriptionToBackend(subscription)); + } + } + + // No subscription, try to subscribe + return subscribe(registration).then( + subscription => sendSubscriptionToBackend(subscription)); + }) + .then(subscription => { + // If we got a PushSubscription (and not a subscription object from the backend) + // it means that the backend subscription is valid (and was set during hydration) + if (!(subscription instanceof PushSubscription)) { + dispatch(setSubscription(subscription)); + if (me) { + pushNotificationsSetting.set(me, { alerts: subscription.alerts }); + } + } + }) + .catch(error => { + if (error.code === 20 && error.name === 'AbortError') { + console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.'); + } else if (error.code === 5 && error.name === 'InvalidCharacterError') { + console.error('The VAPID public key seems to be invalid:', getApplicationServerKey()); + } + + // Clear alerts and hide UI settings + dispatch(clearSubscription()); + if (me) { + pushNotificationsSetting.remove(me); + } + + return getRegistration() + .then(getPushSubscription) + .then(unsubscribe); + }) + .catch(console.warn); + } else { + console.warn('Your browser does not support Web Push Notifications.'); + } + }; +} + +export function saveSettings() { + return (_, getState) => { + const state = getState().get('push_notifications'); + const subscription = state.get('subscription'); + const alerts = state.get('alerts'); + const data = { alerts }; + + api().put(`/api/web/push_subscriptions/${subscription.get('id')}`, { + data, + }).then(() => { + if (me) { + pushNotificationsSetting.set(me, data); + } + }).catch(console.warn); + }; +} diff --git a/app/gabsocial/actions/push_notifications/setter.js b/app/gabsocial/actions/push_notifications/setter.js new file mode 100644 index 000000000..5561766bf --- /dev/null +++ b/app/gabsocial/actions/push_notifications/setter.js @@ -0,0 +1,34 @@ +export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT'; +export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION'; +export const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION'; +export const SET_ALERTS = 'PUSH_NOTIFICATIONS_SET_ALERTS'; + +export function setBrowserSupport (value) { + return { + type: SET_BROWSER_SUPPORT, + value, + }; +} + +export function setSubscription (subscription) { + return { + type: SET_SUBSCRIPTION, + subscription, + }; +} + +export function clearSubscription () { + return { + type: CLEAR_SUBSCRIPTION, + }; +} + +export function setAlerts (path, value) { + return dispatch => { + dispatch({ + type: SET_ALERTS, + path, + value, + }); + }; +} diff --git a/app/gabsocial/actions/reports.js b/app/gabsocial/actions/reports.js new file mode 100644 index 000000000..afa0c3412 --- /dev/null +++ b/app/gabsocial/actions/reports.js @@ -0,0 +1,89 @@ +import api from '../api'; +import { openModal, closeModal } from './modal'; + +export const REPORT_INIT = 'REPORT_INIT'; +export const REPORT_CANCEL = 'REPORT_CANCEL'; + +export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST'; +export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS'; +export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL'; + +export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE'; +export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE'; +export const REPORT_FORWARD_CHANGE = 'REPORT_FORWARD_CHANGE'; + +export function initReport(account, status) { + return dispatch => { + dispatch({ + type: REPORT_INIT, + account, + status, + }); + + dispatch(openModal('REPORT')); + }; +}; + +export function cancelReport() { + return { + type: REPORT_CANCEL, + }; +}; + +export function toggleStatusReport(statusId, checked) { + return { + type: REPORT_STATUS_TOGGLE, + statusId, + checked, + }; +}; + +export function submitReport() { + return (dispatch, getState) => { + dispatch(submitReportRequest()); + + api(getState).post('/api/v1/reports', { + account_id: getState().getIn(['reports', 'new', 'account_id']), + status_ids: getState().getIn(['reports', 'new', 'status_ids']), + comment: getState().getIn(['reports', 'new', 'comment']), + forward: getState().getIn(['reports', 'new', 'forward']), + }).then(response => { + dispatch(closeModal()); + dispatch(submitReportSuccess(response.data)); + }).catch(error => dispatch(submitReportFail(error))); + }; +}; + +export function submitReportRequest() { + return { + type: REPORT_SUBMIT_REQUEST, + }; +}; + +export function submitReportSuccess(report) { + return { + type: REPORT_SUBMIT_SUCCESS, + report, + }; +}; + +export function submitReportFail(error) { + return { + type: REPORT_SUBMIT_FAIL, + error, + }; +}; + +export function changeReportComment(comment) { + return { + type: REPORT_COMMENT_CHANGE, + comment, + }; +}; + +export function changeReportForward(forward) { + return { + type: REPORT_FORWARD_CHANGE, + forward, + }; +}; diff --git a/app/gabsocial/actions/search.js b/app/gabsocial/actions/search.js new file mode 100644 index 000000000..ee7bc1657 --- /dev/null +++ b/app/gabsocial/actions/search.js @@ -0,0 +1,83 @@ +import api from '../api'; +import { fetchRelationships } from './accounts'; +import { importFetchedAccounts, importFetchedStatuses } from './importer'; + +export const SEARCH_CHANGE = 'SEARCH_CHANGE'; +export const SEARCH_CLEAR = 'SEARCH_CLEAR'; +export const SEARCH_SHOW = 'SEARCH_SHOW'; + +export const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST'; +export const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS'; +export const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL'; + +export function changeSearch(value) { + return { + type: SEARCH_CHANGE, + value, + }; +}; + +export function clearSearch() { + return { + type: SEARCH_CLEAR, + }; +}; + +export function submitSearch() { + return (dispatch, getState) => { + const value = getState().getIn(['search', 'value']); + + if (value.length === 0) { + return; + } + + dispatch(fetchSearchRequest()); + + api(getState).get('/api/v2/search', { + params: { + q: value, + resolve: true, + limit: 20, + }, + }).then(response => { + if (response.data.accounts) { + dispatch(importFetchedAccounts(response.data.accounts)); + } + + if (response.data.statuses) { + dispatch(importFetchedStatuses(response.data.statuses)); + } + + dispatch(fetchSearchSuccess(response.data)); + dispatch(fetchRelationships(response.data.accounts.map(item => item.id))); + }).catch(error => { + dispatch(fetchSearchFail(error)); + }); + }; +}; + +export function fetchSearchRequest() { + return { + type: SEARCH_FETCH_REQUEST, + }; +}; + +export function fetchSearchSuccess(results) { + return { + type: SEARCH_FETCH_SUCCESS, + results, + }; +}; + +export function fetchSearchFail(error) { + return { + type: SEARCH_FETCH_FAIL, + error, + }; +}; + +export function showSearch() { + return { + type: SEARCH_SHOW, + }; +}; diff --git a/app/gabsocial/actions/settings.js b/app/gabsocial/actions/settings.js new file mode 100644 index 000000000..0ca8bc126 --- /dev/null +++ b/app/gabsocial/actions/settings.js @@ -0,0 +1,37 @@ +import api from '../api'; +import { debounce } from 'lodash'; +import { showAlertForError } from './alerts'; +import { me } from 'gabsocial/initial_state'; + +export const SETTING_CHANGE = 'SETTING_CHANGE'; +export const SETTING_SAVE = 'SETTING_SAVE'; + +export function changeSetting(path, value) { + return dispatch => { + dispatch({ + type: SETTING_CHANGE, + path, + value, + }); + + dispatch(saveSettings()); + }; +}; + +const debouncedSave = debounce((dispatch, getState) => { + if (!me) return; + + if (getState().getIn(['settings', 'saved'])) { + return; + } + + const data = getState().get('settings').filter((_, path) => path !== 'saved').toJS(); + + api().put('/api/web/settings', { data }) + .then(() => dispatch({ type: SETTING_SAVE })) + .catch(error => dispatch(showAlertForError(error))); +}, 5000, { trailing: true }); + +export function saveSettings() { + return (dispatch, getState) => debouncedSave(dispatch, getState); +}; diff --git a/app/gabsocial/actions/sidebar.js b/app/gabsocial/actions/sidebar.js new file mode 100644 index 000000000..bd6467963 --- /dev/null +++ b/app/gabsocial/actions/sidebar.js @@ -0,0 +1,14 @@ +export const SIDEBAR_OPEN = 'SIDEBAR_OPEN'; +export const SIDEBAR_CLOSE = 'SIDEBAR_CLOSE'; + +export function openSidebar() { + return { + type: SIDEBAR_OPEN, + }; +}; + +export function closeSidebar() { + return { + type: SIDEBAR_CLOSE, + }; +}; diff --git a/app/gabsocial/actions/statuses.js b/app/gabsocial/actions/statuses.js new file mode 100644 index 000000000..69261f41b --- /dev/null +++ b/app/gabsocial/actions/statuses.js @@ -0,0 +1,329 @@ +import api from '../api'; +import openDB from '../storage/db'; +import { evictStatus } from '../storage/modifier'; +import { deleteFromTimelines } from './timelines'; +import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer'; +import { ensureComposeIsVisible } from './compose'; +import { openModal, closeModal } from './modal'; +import { me } from 'gabsocial/initial_state'; + +export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; +export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; +export const STATUS_FETCH_FAIL = 'STATUS_FETCH_FAIL'; + +export const STATUS_DELETE_REQUEST = 'STATUS_DELETE_REQUEST'; +export const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS'; +export const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL'; + +export const CONTEXT_FETCH_REQUEST = 'CONTEXT_FETCH_REQUEST'; +export const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS'; +export const CONTEXT_FETCH_FAIL = 'CONTEXT_FETCH_FAIL'; + +export const STATUS_MUTE_REQUEST = 'STATUS_MUTE_REQUEST'; +export const STATUS_MUTE_SUCCESS = 'STATUS_MUTE_SUCCESS'; +export const STATUS_MUTE_FAIL = 'STATUS_MUTE_FAIL'; + +export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST'; +export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS'; +export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL'; + +export const STATUS_REVEAL = 'STATUS_REVEAL'; +export const STATUS_HIDE = 'STATUS_HIDE'; + +export const REDRAFT = 'REDRAFT'; + +export function fetchStatusRequest(id, skipLoading) { + return { + type: STATUS_FETCH_REQUEST, + id, + skipLoading, + }; +}; + +function getFromDB(dispatch, getState, accountIndex, index, id) { + return new Promise((resolve, reject) => { + const request = index.get(id); + + request.onerror = reject; + + request.onsuccess = () => { + const promises = []; + + if (!request.result) { + reject(); + return; + } + + dispatch(importStatus(request.result)); + + if (getState().getIn(['accounts', request.result.account], null) === null) { + promises.push(new Promise((accountResolve, accountReject) => { + const accountRequest = accountIndex.get(request.result.account); + + accountRequest.onerror = accountReject; + accountRequest.onsuccess = () => { + if (!request.result) { + accountReject(); + return; + } + + dispatch(importAccount(accountRequest.result)); + accountResolve(); + }; + })); + } + + if (request.result.reblog && getState().getIn(['statuses', request.result.reblog], null) === null) { + promises.push(getFromDB(dispatch, getState, accountIndex, index, request.result.reblog)); + } + + resolve(Promise.all(promises)); + }; + }); +} + +export function fetchStatus(id) { + return (dispatch, getState) => { + const skipLoading = getState().getIn(['statuses', id], null) !== null; + + dispatch(fetchContext(id)); + + if (skipLoading) { + return; + } + + dispatch(fetchStatusRequest(id, skipLoading)); + + openDB().then(db => { + const transaction = db.transaction(['accounts', 'statuses'], 'read'); + const accountIndex = transaction.objectStore('accounts').index('id'); + const index = transaction.objectStore('statuses').index('id'); + + return getFromDB(dispatch, getState, accountIndex, index, id).then(() => { + db.close(); + }, error => { + db.close(); + throw error; + }); + }).then(() => { + dispatch(fetchStatusSuccess(skipLoading)); + }, () => api(getState).get(`/api/v1/statuses/${id}`).then(response => { + dispatch(importFetchedStatus(response.data)); + dispatch(fetchStatusSuccess(skipLoading)); + })).catch(error => { + dispatch(fetchStatusFail(id, error, skipLoading)); + }); + }; +}; + +export function fetchStatusSuccess(skipLoading) { + return { + type: STATUS_FETCH_SUCCESS, + skipLoading, + }; +}; + +export function fetchStatusFail(id, error, skipLoading) { + return { + type: STATUS_FETCH_FAIL, + id, + error, + skipLoading, + skipAlert: true, + }; +}; + +export function redraft(status, raw_text) { + return { + type: REDRAFT, + status, + raw_text, + }; +}; + +export function deleteStatus(id, routerHistory, withRedraft = false) { + return (dispatch, getState) => { + if (!me) return; + + let status = getState().getIn(['statuses', id]); + + if (status.get('poll')) { + status = status.set('poll', getState().getIn(['polls', status.get('poll')])); + } + + dispatch(deleteStatusRequest(id)); + + api(getState).delete(`/api/v1/statuses/${id}`).then(response => { + evictStatus(id); + dispatch(deleteStatusSuccess(id)); + dispatch(deleteFromTimelines(id)); + + if (withRedraft) { + dispatch(redraft(status, response.data.text)); + dispatch(openModal('COMPOSE')); + } + }).catch(error => { + dispatch(deleteStatusFail(id, error)); + }); + }; +}; + +export function deleteStatusRequest(id) { + return { + type: STATUS_DELETE_REQUEST, + id: id, + }; +}; + +export function deleteStatusSuccess(id) { + return { + type: STATUS_DELETE_SUCCESS, + id: id, + }; +}; + +export function deleteStatusFail(id, error) { + return { + type: STATUS_DELETE_FAIL, + id: id, + error: error, + }; +}; + +export function fetchContext(id) { + return (dispatch, getState) => { + dispatch(fetchContextRequest(id)); + + api(getState).get(`/api/v1/statuses/${id}/context`).then(response => { + dispatch(importFetchedStatuses(response.data.ancestors.concat(response.data.descendants))); + dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants)); + + }).catch(error => { + if (error.response && error.response.status === 404) { + dispatch(deleteFromTimelines(id)); + } + + dispatch(fetchContextFail(id, error)); + }); + }; +}; + +export function fetchContextRequest(id) { + return { + type: CONTEXT_FETCH_REQUEST, + id, + }; +}; + +export function fetchContextSuccess(id, ancestors, descendants) { + return { + type: CONTEXT_FETCH_SUCCESS, + id, + ancestors, + descendants, + statuses: ancestors.concat(descendants), + }; +}; + +export function fetchContextFail(id, error) { + return { + type: CONTEXT_FETCH_FAIL, + id, + error, + skipAlert: true, + }; +}; + +export function muteStatus(id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(muteStatusRequest(id)); + + api(getState).post(`/api/v1/statuses/${id}/mute`).then(() => { + dispatch(muteStatusSuccess(id)); + }).catch(error => { + dispatch(muteStatusFail(id, error)); + }); + }; +}; + +export function muteStatusRequest(id) { + return { + type: STATUS_MUTE_REQUEST, + id, + }; +}; + +export function muteStatusSuccess(id) { + return { + type: STATUS_MUTE_SUCCESS, + id, + }; +}; + +export function muteStatusFail(id, error) { + return { + type: STATUS_MUTE_FAIL, + id, + error, + }; +}; + +export function unmuteStatus(id) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(unmuteStatusRequest(id)); + + api(getState).post(`/api/v1/statuses/${id}/unmute`).then(() => { + dispatch(unmuteStatusSuccess(id)); + }).catch(error => { + dispatch(unmuteStatusFail(id, error)); + }); + }; +}; + +export function unmuteStatusRequest(id) { + return { + type: STATUS_UNMUTE_REQUEST, + id, + }; +}; + +export function unmuteStatusSuccess(id) { + return { + type: STATUS_UNMUTE_SUCCESS, + id, + }; +}; + +export function unmuteStatusFail(id, error) { + return { + type: STATUS_UNMUTE_FAIL, + id, + error, + }; +}; + +export function hideStatus(ids) { + if (!Array.isArray(ids)) { + ids = [ids]; + } + + return { + type: STATUS_HIDE, + ids, + }; +}; + +export function revealStatus(ids) { + if (!Array.isArray(ids)) { + ids = [ids]; + } + + return { + type: STATUS_REVEAL, + ids, + }; +}; diff --git a/app/gabsocial/actions/store.js b/app/gabsocial/actions/store.js new file mode 100644 index 000000000..34dcafc51 --- /dev/null +++ b/app/gabsocial/actions/store.js @@ -0,0 +1,24 @@ +import { Iterable, fromJS } from 'immutable'; +import { hydrateCompose } from './compose'; +import { importFetchedAccounts } from './importer'; + +export const STORE_HYDRATE = 'STORE_HYDRATE'; +export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY'; + +const convertState = rawState => + fromJS(rawState, (k, v) => + Iterable.isIndexed(v) ? v.toList() : v.toMap()); + +export function hydrateStore(rawState) { + return dispatch => { + const state = convertState(rawState); + + dispatch({ + type: STORE_HYDRATE, + state, + }); + + dispatch(hydrateCompose()); + dispatch(importFetchedAccounts(Object.values(rawState.accounts))); + }; +}; diff --git a/app/gabsocial/actions/streaming.js b/app/gabsocial/actions/streaming.js new file mode 100644 index 000000000..e599805e7 --- /dev/null +++ b/app/gabsocial/actions/streaming.js @@ -0,0 +1,63 @@ +import { connectStream } from '../stream'; +import { + deleteFromTimelines, + expandHomeTimeline, + connectTimeline, + disconnectTimeline, + updateTimelineQueue, +} from './timelines'; +import { updateNotificationsQueue, expandNotifications } from './notifications'; +import { updateConversations } from './conversations'; +import { fetchFilters } from './filters'; +import { getLocale } from '../locales'; + +const { messages } = getLocale(); + +export function connectTimelineStream (timelineId, path, pollingRefresh = null, accept = null) { + + return connectStream (path, pollingRefresh, (dispatch, getState) => { + const locale = getState().getIn(['meta', 'locale']); + + return { + onConnect() { + dispatch(connectTimeline(timelineId)); + }, + + onDisconnect() { + dispatch(disconnectTimeline(timelineId)); + }, + + onReceive (data) { + switch(data.event) { + case 'update': + dispatch(updateTimelineQueue(timelineId, JSON.parse(data.payload), accept)); + break; + case 'delete': + dispatch(deleteFromTimelines(data.payload)); + break; + case 'notification': + dispatch(updateNotificationsQueue(JSON.parse(data.payload), messages, locale, window.location.pathname)); + break; + case 'conversation': + dispatch(updateConversations(JSON.parse(data.payload))); + break; + case 'filters_changed': + dispatch(fetchFilters()); + break; + } + }, + }; + }); +} + +const refreshHomeTimelineAndNotification = (dispatch, done) => { + dispatch(expandHomeTimeline({}, () => dispatch(expandNotifications({}, done)))); +}; + +export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); +export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`); +export const connectPublicStream = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`); +export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept); +export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); +export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`); +export const connectGroupStream = id => connectTimelineStream(`group:${id}`, `group&group=${id}`); diff --git a/app/gabsocial/actions/suggestions.js b/app/gabsocial/actions/suggestions.js new file mode 100644 index 000000000..5394fd81c --- /dev/null +++ b/app/gabsocial/actions/suggestions.js @@ -0,0 +1,55 @@ +import api from '../api'; +import { importFetchedAccounts } from './importer'; +import { me } from 'gabsocial/initial_state'; + +export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST'; +export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS'; +export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL'; + +export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS'; + +export function fetchSuggestions() { + return (dispatch, getState) => { + dispatch(fetchSuggestionsRequest()); + + api(getState).get('/api/v1/suggestions').then(response => { + dispatch(importFetchedAccounts(response.data)); + dispatch(fetchSuggestionsSuccess(response.data)); + }).catch(error => dispatch(fetchSuggestionsFail(error))); + }; +}; + +export function fetchSuggestionsRequest() { + return { + type: SUGGESTIONS_FETCH_REQUEST, + skipLoading: true, + }; +}; + +export function fetchSuggestionsSuccess(accounts) { + return { + type: SUGGESTIONS_FETCH_SUCCESS, + accounts, + skipLoading: true, + }; +}; + +export function fetchSuggestionsFail(error) { + return { + type: SUGGESTIONS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, + }; +}; + +export const dismissSuggestion = accountId => (dispatch, getState) => { + if (!me) return; + + dispatch({ + type: SUGGESTIONS_DISMISS, + id: accountId, + }); + + api(getState).delete(`/api/v1/suggestions/${accountId}`); +}; diff --git a/app/gabsocial/actions/timelines.js b/app/gabsocial/actions/timelines.js new file mode 100644 index 000000000..78f372b23 --- /dev/null +++ b/app/gabsocial/actions/timelines.js @@ -0,0 +1,221 @@ +import { importFetchedStatus, importFetchedStatuses } from './importer'; +import api, { getLinks } from '../api'; +import { Map as ImmutableMap, List as ImmutableList, toJS } from 'immutable'; + +export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; +export const TIMELINE_DELETE = 'TIMELINE_DELETE'; +export const TIMELINE_CLEAR = 'TIMELINE_CLEAR'; +export const TIMELINE_UPDATE_QUEUE = 'TIMELINE_UPDATE_QUEUE'; +export const TIMELINE_DEQUEUE = 'TIMELINE_DEQUEUE'; +export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; + +export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; +export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS'; +export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; + +export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; +export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; + +export const MAX_QUEUED_ITEMS = 40; + +export function updateTimeline(timeline, status, accept) { + return dispatch => { + if (typeof accept === 'function' && !accept(status)) { + return; + } + + dispatch(importFetchedStatus(status)); + + dispatch({ + type: TIMELINE_UPDATE, + timeline, + status, + }); + }; +}; + +export function updateTimelineQueue(timeline, status, accept) { + return dispatch => { + if (typeof accept === 'function' && !accept(status)) { + return; + } + + dispatch({ + type: TIMELINE_UPDATE_QUEUE, + timeline, + status, + }); + } +}; + +export function dequeueTimeline(timeline, expandFunc, optionalExpandArgs) { + return (dispatch, getState) => { + const queuedItems = getState().getIn(['timelines', timeline, 'queuedItems'], ImmutableList()); + const totalQueuedItemsCount = getState().getIn(['timelines', timeline, 'totalQueuedItemsCount'], 0); + + let shouldDispatchDequeue = true; + + if (totalQueuedItemsCount == 0) { + return; + } + else if (totalQueuedItemsCount > 0 && totalQueuedItemsCount <= MAX_QUEUED_ITEMS) { + queuedItems.forEach(status => { + dispatch(updateTimeline(timeline, status.toJS(), null)); + }); + } + else { + if (typeof expandFunc === 'function') { + dispatch(clearTimeline(timeline)); + expandFunc(); + } + else { + if (timeline === 'home') { + dispatch(clearTimeline(timeline)); + dispatch(expandHomeTimeline(optionalExpandArgs)); + } + else if (timeline === 'community') { + dispatch(clearTimeline(timeline)); + dispatch(expandCommunityTimeline(optionalExpandArgs)); + } + else { + shouldDispatchDequeue = false; + } + } + } + + if (!shouldDispatchDequeue) return; + + dispatch({ + type: TIMELINE_DEQUEUE, + timeline, + }); + } +}; + +export function deleteFromTimelines(id) { + return (dispatch, getState) => { + const accountId = getState().getIn(['statuses', id, 'account']); + const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => [status.get('id'), status.get('account')]); + const reblogOf = getState().getIn(['statuses', id, 'reblog'], null); + + dispatch({ + type: TIMELINE_DELETE, + id, + accountId, + references, + reblogOf, + }); + }; +}; + +export function clearTimeline(timeline) { + return (dispatch) => { + dispatch({ type: TIMELINE_CLEAR, timeline }); + }; +}; + +const noOp = () => {}; + +const parseTags = (tags = {}, mode) => { + return (tags[mode] || []).map((tag) => { + return tag.value; + }); +}; + +export function expandTimeline(timelineId, path, params = {}, done = noOp) { + return (dispatch, getState) => { + const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); + const isLoadingMore = !!params.max_id; + + if (timeline.get('isLoading')) { + done(); + return; + } + + if (!params.max_id && !params.pinned && timeline.get('items', ImmutableList()).size > 0) { + params.since_id = timeline.getIn(['items', 0]); + } + + const isLoadingRecent = !!params.since_id; + + dispatch(expandTimelineRequest(timelineId, isLoadingMore)); + + api(getState).get(path, { params }).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); + dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore)); + done(); + }).catch(error => { + dispatch(expandTimelineFail(timelineId, error, isLoadingMore)); + done(); + }); + }; +}; + +export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); +export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done); +export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); +export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); +export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); +export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); +export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); +export const expandGroupTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`group:${id}`, `/api/v1/timelines/group/${id}`, { max_id: maxId }, done); +export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => { + return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { + max_id: maxId, + any: parseTags(tags, 'any'), + all: parseTags(tags, 'all'), + none: parseTags(tags, 'none'), + }, done); +}; + +export function expandTimelineRequest(timeline, isLoadingMore) { + return { + type: TIMELINE_EXPAND_REQUEST, + timeline, + skipLoading: !isLoadingMore, + }; +}; + +export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore) { + return { + type: TIMELINE_EXPAND_SUCCESS, + timeline, + statuses, + next, + partial, + isLoadingRecent, + skipLoading: !isLoadingMore, + }; +}; + +export function expandTimelineFail(timeline, error, isLoadingMore) { + return { + type: TIMELINE_EXPAND_FAIL, + timeline, + error, + skipLoading: !isLoadingMore, + }; +}; + +export function connectTimeline(timeline) { + return { + type: TIMELINE_CONNECT, + timeline, + }; +}; + +export function disconnectTimeline(timeline) { + return { + type: TIMELINE_DISCONNECT, + timeline, + }; +}; + +export function scrollTopTimeline(timeline, top) { + return { + type: TIMELINE_SCROLL_TOP, + timeline, + top, + }; +}; diff --git a/app/gabsocial/actions/trends.js b/app/gabsocial/actions/trends.js new file mode 100644 index 000000000..b23c1c60e --- /dev/null +++ b/app/gabsocial/actions/trends.js @@ -0,0 +1,39 @@ +import api from '../api'; + +export const TRENDS_FETCH_REQUEST = 'TRENDS_FETCH_REQUEST'; +export const TRENDS_FETCH_SUCCESS = 'TRENDS_FETCH_SUCCESS'; +export const TRENDS_FETCH_FAIL = 'TRENDS_FETCH_FAIL'; + +export function fetchTrends() { + return (dispatch, getState) => { + dispatch(fetchTrendsRequest()); + + api(getState).get('/api/v1/trends').then(response => { + dispatch(fetchTrendsSuccess(response.data)); + }).catch(error => dispatch(fetchTrendsFail(error))); + }; +}; + +export function fetchTrendsRequest() { + return { + type: TRENDS_FETCH_REQUEST, + skipLoading: true, + }; +}; + +export function fetchTrendsSuccess(tags) { + return { + type: TRENDS_FETCH_SUCCESS, + tags, + skipLoading: true, + }; +}; + +export function fetchTrendsFail(error) { + return { + type: TRENDS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, + }; +}; diff --git a/app/gabsocial/api.js b/app/gabsocial/api.js new file mode 100644 index 000000000..8898bdeba --- /dev/null +++ b/app/gabsocial/api.js @@ -0,0 +1,40 @@ +'use strict'; + +import axios from 'axios'; +import LinkHeader from 'http-link-header'; +import ready from './ready'; + +export const getLinks = response => { + const value = response.headers.link; + + if (!value) { + return { refs: [] }; + } + + return LinkHeader.parse(value); +}; + +let csrfHeader = {}; + +function setCSRFHeader() { + const csrfToken = document.querySelector('meta[name=csrf-token]'); + if (csrfToken) { + csrfHeader['X-CSRF-Token'] = csrfToken.content; + } +} + +ready(setCSRFHeader); + +export default getState => axios.create({ + headers: Object.assign(csrfHeader, getState ? { + 'Authorization': `Bearer ${getState().getIn(['meta', 'access_token'], '')}`, + } : {}), + + transformResponse: [function (data) { + try { + return JSON.parse(data); + } catch(Exception) { + return data; + } + }], +}); diff --git a/app/gabsocial/base_polyfills.js b/app/gabsocial/base_polyfills.js new file mode 100644 index 000000000..d54ed977c --- /dev/null +++ b/app/gabsocial/base_polyfills.js @@ -0,0 +1,46 @@ +'use strict'; + +import 'intl'; +import 'intl/locale-data/jsonp/en'; +import 'es6-symbol/implement'; +import includes from 'array-includes'; +import assign from 'object-assign'; +import values from 'object.values'; +import isNaN from 'is-nan'; +import { decode as decodeBase64 } from './utils/base64'; + +if (!Array.prototype.includes) { + includes.shim(); +} + +if (!Object.assign) { + Object.assign = assign; +} + +if (!Object.values) { + values.shim(); +} + +if (!Number.isNaN) { + Number.isNaN = isNaN; +} + +if (!HTMLCanvasElement.prototype.toBlob) { + const BASE64_MARKER = ';base64,'; + + Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { + value(callback, type = 'image/png', quality) { + const dataURL = this.toDataURL(type, quality); + let data; + + if (dataURL.indexOf(BASE64_MARKER) >= 0) { + const [, base64] = dataURL.split(BASE64_MARKER); + data = decodeBase64(base64); + } else { + [, data] = dataURL.split(','); + } + + callback(new Blob([data], { type })); + }, + }); +} diff --git a/app/gabsocial/common.js b/app/gabsocial/common.js new file mode 100644 index 000000000..22146c0e0 --- /dev/null +++ b/app/gabsocial/common.js @@ -0,0 +1,14 @@ +'use strict'; + +import Rails from 'rails-ujs'; + +export function start() { + require('font-awesome/css/font-awesome.css'); + require.context('../images/', true); + + try { + Rails.start(); + } catch (e) { + // If called twice + } +}; diff --git a/app/gabsocial/compare_id.js b/app/gabsocial/compare_id.js new file mode 100644 index 000000000..f8c15e327 --- /dev/null +++ b/app/gabsocial/compare_id.js @@ -0,0 +1,12 @@ +'use strict'; + +export default function compareId(id1, id2) { + if (id1 === id2) { + return 0; + } + if (id1.length === id2.length) { + return id1 > id2 ? 1 : -1; + } else { + return id1.length > id2.length ? 1 : -1; + } +} diff --git a/app/gabsocial/components/__tests__/__snapshots__/autosuggest_emoji-test.js.snap b/app/gabsocial/components/__tests__/__snapshots__/autosuggest_emoji-test.js.snap new file mode 100644 index 000000000..1c3727848 --- /dev/null +++ b/app/gabsocial/components/__tests__/__snapshots__/autosuggest_emoji-test.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders emoji with custom url 1`] = ` +

+ foobar + :foobar: +
+`; + +exports[` renders native emoji 1`] = ` +
+ 💙 + :foobar: +
+`; diff --git a/app/gabsocial/components/__tests__/__snapshots__/avatar-test.js.snap b/app/gabsocial/components/__tests__/__snapshots__/avatar-test.js.snap new file mode 100644 index 000000000..37a817e64 --- /dev/null +++ b/app/gabsocial/components/__tests__/__snapshots__/avatar-test.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` Autoplay renders a animated avatar 1`] = ` +
+`; + +exports[` Still renders a still avatar 1`] = ` +
+`; diff --git a/app/gabsocial/components/__tests__/__snapshots__/avatar_overlay-test.js.snap b/app/gabsocial/components/__tests__/__snapshots__/avatar_overlay-test.js.snap new file mode 100644 index 000000000..d59fee42f --- /dev/null +++ b/app/gabsocial/components/__tests__/__snapshots__/avatar_overlay-test.js.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` +
+
+
+`; diff --git a/app/gabsocial/components/__tests__/__snapshots__/button-test.js.snap b/app/gabsocial/components/__tests__/__snapshots__/button-test.js.snap new file mode 100644 index 000000000..5c04e0979 --- /dev/null +++ b/app/gabsocial/components/__tests__/__snapshots__/button-test.js.snap @@ -0,0 +1,108 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` +`; + +exports[` +`; + +exports[` +`; diff --git a/app/gabsocial/components/__tests__/__snapshots__/display_name-test.js.snap b/app/gabsocial/components/__tests__/__snapshots__/display_name-test.js.snap new file mode 100644 index 000000000..3f785b7c3 --- /dev/null +++ b/app/gabsocial/components/__tests__/__snapshots__/display_name-test.js.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders display name + account name 1`] = ` + + + Foo

", + } + } + /> +
+ + @ + +
+`; diff --git a/app/gabsocial/components/__tests__/autosuggest_emoji-test.js b/app/gabsocial/components/__tests__/autosuggest_emoji-test.js new file mode 100644 index 000000000..05616e444 --- /dev/null +++ b/app/gabsocial/components/__tests__/autosuggest_emoji-test.js @@ -0,0 +1,29 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import AutosuggestEmoji from '../autosuggest_emoji'; + +describe('', () => { + it('renders native emoji', () => { + const emoji = { + native: '💙', + colons: ':foobar:', + }; + const component = renderer.create(); + const tree = component.toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it('renders emoji with custom url', () => { + const emoji = { + custom: true, + imageUrl: 'http://example.com/emoji.png', + native: 'foobar', + colons: ':foobar:', + }; + const component = renderer.create(); + const tree = component.toJSON(); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/app/gabsocial/components/__tests__/avatar-test.js b/app/gabsocial/components/__tests__/avatar-test.js new file mode 100644 index 000000000..dd3f7b7d2 --- /dev/null +++ b/app/gabsocial/components/__tests__/avatar-test.js @@ -0,0 +1,36 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { fromJS } from 'immutable'; +import Avatar from '../avatar'; + +describe('', () => { + const account = fromJS({ + username: 'alice', + acct: 'alice', + display_name: 'Alice', + avatar: '/animated/alice.gif', + avatar_static: '/static/alice.jpg', + }); + + const size = 100; + + describe('Autoplay', () => { + it('renders a animated avatar', () => { + const component = renderer.create(); + const tree = component.toJSON(); + + expect(tree).toMatchSnapshot(); + }); + }); + + describe('Still', () => { + it('renders a still avatar', () => { + const component = renderer.create(); + const tree = component.toJSON(); + + expect(tree).toMatchSnapshot(); + }); + }); + + // TODO add autoplay test if possible +}); diff --git a/app/gabsocial/components/__tests__/avatar_overlay-test.js b/app/gabsocial/components/__tests__/avatar_overlay-test.js new file mode 100644 index 000000000..44addea83 --- /dev/null +++ b/app/gabsocial/components/__tests__/avatar_overlay-test.js @@ -0,0 +1,29 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { fromJS } from 'immutable'; +import AvatarOverlay from '../avatar_overlay'; + +describe(' { + const account = fromJS({ + username: 'alice', + acct: 'alice', + display_name: 'Alice', + avatar: '/animated/alice.gif', + avatar_static: '/static/alice.jpg', + }); + + const friend = fromJS({ + username: 'eve', + acct: 'eve@blackhat.lair', + display_name: 'Evelyn', + avatar: '/animated/eve.gif', + avatar_static: '/static/eve.jpg', + }); + + it('renders a overlay avatar', () => { + const component = renderer.create(); + const tree = component.toJSON(); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/app/gabsocial/components/__tests__/button-test.js b/app/gabsocial/components/__tests__/button-test.js new file mode 100644 index 000000000..160cd3cbc --- /dev/null +++ b/app/gabsocial/components/__tests__/button-test.js @@ -0,0 +1,75 @@ +import { shallow } from 'enzyme'; +import React from 'react'; +import renderer from 'react-test-renderer'; +import Button from '../button'; + +describe('); + const tree = component.toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it('renders the props.text instead of children', () => { + const text = 'foo'; + const children =

children

; + const component = renderer.create(); + const tree = component.toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it('renders class="button--block" if props.block given', () => { + const component = renderer.create(