diff --git a/config/default.json b/config/default.json index 60fd32c071e80091581370a7f2180df1d14ea330..9631c0e8473106b8f818ea15de6052bfa6ba0eea 100644 --- a/config/default.json +++ b/config/default.json @@ -39,7 +39,8 @@ "steemd_connection_server": "https://api.hive.blog", "steemd_use_appbase": false, "chain_id": "0000000000000000000000000000000000000000000000000000000000000000", - "alternative_api_endpoints": "https://api.hive.blog https://anyx.io", + "alternative_api_endpoints": "https://api.hive.blog https://anyx.io https://api.hivekings.com", + "default_observer":"hive.blog", "failover_threshold": 3, "address_prefix": "STM", "conveyor_posting_wif": false, diff --git a/src/app/ResolveRoute.js b/src/app/ResolveRoute.js index 2437c244d1d1d5dd1d40345f9376a131192a8890..82d2ddc2826fef6542012905dd513536f46352b6 100644 --- a/src/app/ResolveRoute.js +++ b/src/app/ResolveRoute.js @@ -13,7 +13,11 @@ const reg = pattern => { ) .replace('<tag>', '([\\w\\W\\d-]{1,32})') .replace('<permlink>', '([\\w\\d-]+)') - .replace('/', '\\/'); + .replace('/', '\\/') + .replace( + '<list_type>', + '(blacklisted|muted|followed_blacklists|followed_muted_lists)' + ); return new RegExp('^\\/' + pattern + '$'); }; @@ -27,6 +31,7 @@ export const routeRegex = { PostJson: reg('<tag>/<account>/<permlink>(\\.json)'), UserJson: reg('<account>(\\.json)'), Search: reg('search'), + ListManagement: reg('<account>/lists/<list_type>'), }; export default function resolveRoute(path) { @@ -88,6 +93,8 @@ export default function resolveRoute(path) { // /search, /search?q=searchTerm&s=searchOrder match = path.match(routeRegex.Search); if (match) return { page: 'SearchIndex' }; + match = path.match(routeRegex.ListManagement); + if (match) return { page: 'ListManagement', params: match.slice(2) }; //, params: match.slice(1) }; // ----------- diff --git a/src/app/RootRoute.js b/src/app/RootRoute.js index 1c634375e64aac674b7c9a13531101ac531bb98a..b5db4591b96626ca0b52ff4da36f66ee7b6fa89a 100644 --- a/src/app/RootRoute.js +++ b/src/app/RootRoute.js @@ -98,6 +98,9 @@ export default { //cb(null, [require('app/components/pages/PostsIndex')]); cb(null, [PostsIndex]); //}); + } else if (route.page === 'ListManagement') { + require('app/components/pages/ListManagement'); + cb(null, [require('app/components/pages/ListManagement')]); } else { //require.ensure([], (require) => { cb(process.env.BROWSER ? null : Error(404), [ diff --git a/src/app/components/all.scss b/src/app/components/all.scss index f2ae5183bed9d545a44f2dbfc2166790169d397b..57d31ae66f0080b3c74795558e59e52abb2c5d2e 100644 --- a/src/app/components/all.scss +++ b/src/app/components/all.scss @@ -65,3 +65,4 @@ @import "./pages/CommunitiesIndex"; @import "./pages/CommunityRoles"; @import "./pages/Rewards"; +@import "./pages/ListManagement"; diff --git a/src/app/components/cards/UserProfileHeader.jsx b/src/app/components/cards/UserProfileHeader.jsx index b9352b81c57c0c612dab60b6e44426a49e5081d4..a586ecd3ba3db6f577e191dba6def9ddd9f515da 100644 --- a/src/app/components/cards/UserProfileHeader.jsx +++ b/src/app/components/cards/UserProfileHeader.jsx @@ -146,6 +146,39 @@ class UserProfileHeader extends React.Component { )} </span> )} + + <span> + <br /> + <Link to={`/@${accountname}/lists/blacklisted`}> + Blacklisted Users + </Link> + </span> + + <span> + <Link to={`/@${accountname}/lists/muted`}> + Muted Users + </Link> + </span> + + <span> + <Link + to={`/@${ + accountname + }/lists/followed_blacklists`} + > + Followed Blacklists + </Link> + </span> + + <span> + <Link + to={`/@${ + accountname + }/lists/followed_muted_lists`} + > + Followed Muted Lists + </Link> + </span> </div> <p className="UserProfile__info"> diff --git a/src/app/components/modules/Header/index.jsx b/src/app/components/modules/Header/index.jsx index 3dbcea49a332487167eb54072138eea9d30dad8c..8e97a3c6b06072c668aeb8ec9ea2b3456a429ff3 100644 --- a/src/app/components/modules/Header/index.jsx +++ b/src/app/components/modules/Header/index.jsx @@ -217,6 +217,8 @@ class Header extends React.Component { username: user_title, }); } + } else if (route.page === 'ListManagement') { + page_title = 'Manage Lists'; } else { page_name = ''; //page_title = route.page.replace( /([a-z])([A-Z])/g, '$1 $2' ).toLowerCase(); } diff --git a/src/app/components/modules/Settings.jsx b/src/app/components/modules/Settings.jsx index 71cc7838ec59192ae2fad80bc71b8c6742b1d131..9f8948606deda10ab7ca020230de5ad6a1764235 100644 --- a/src/app/components/modules/Settings.jsx +++ b/src/app/components/modules/Settings.jsx @@ -91,6 +91,8 @@ class Settings extends React.Component { 'witness_owner', 'witness_description', 'account_is_witness', + 'blacklist_description', + 'muted_list_description', ], initialValues: props.profile, validation: values => { @@ -135,6 +137,16 @@ class Settings extends React.Component { values.witness_description.length > 512 ? tt('settings_jsx.witness_description_is_too_long') : null, + blacklist_description: + values.blacklist_description && + values.blacklist_description.length > 256 + ? 'description is too long' + : null, + muted_list_description: + values.muted_list_description && + values.muted_list_description.length > 256 + ? 'description is too long' + : null, }; }, }); @@ -207,6 +219,8 @@ class Settings extends React.Component { website, witness_owner, witness_description, + blacklist_description, + muted_list_description, } = this.state; // Update relevant fields @@ -218,6 +232,8 @@ class Settings extends React.Component { metaData.profile.website = website.value; metaData.profile.witness_owner = witness_owner.value; metaData.profile.witness_description = witness_description.value; + metaData.profile.blacklist_description = blacklist_description.value; + metaData.profile.muted_list_description = muted_list_description.value; metaData.profile.version = 2; // signal upgrade to posting_json_metadata // Remove empty keys @@ -232,6 +248,10 @@ class Settings extends React.Component { delete metaData.profile.witness_owner; if (!metaData.profile.witness_description) delete metaData.profile.witness_description; + if (!metaData.profile.blacklist_description) + delete metaData.profile.blacklist_description; + if (!metaData.profile.muted_list_description) + delete metaData.profile.muted_list_description; const { account, updateAccount } = this.props; this.setState({ loading: true }); @@ -503,6 +523,8 @@ class Settings extends React.Component { witness_description, progress, account_is_witness, + blacklist_description, + muted_list_description, } = this.state; const endpoint_options = this.generateAPIEndpointOptions(); @@ -660,6 +682,38 @@ class Settings extends React.Component { website.error} </div> </div> + <div className="form__field column small-12 medium-6 large-4"> + <label> + Blacklist Description + <input + type="text" + maxLength="256" + autoComplete="off" + {...blacklist_description.props} + /> + </label> + <div className="error"> + {website.blur && + website.touched && + website.error} + </div> + </div> + <div className="form__field column small-12 medium-6 large-4"> + <label> + Mute List Description + <input + type="text" + maxLength="256" + autoComplete="off" + {...muted_list_description.props} + /> + </label> + <div className="error"> + {website.blur && + website.touched && + website.error} + </div> + </div> {account_is_witness.value && ( <div className="form__field column small-12 medium-6 large-4"> <label> diff --git a/src/app/components/pages/ListManagement.jsx b/src/app/components/pages/ListManagement.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3ca990139465b5e01a68e259e747f432e5b4b764 --- /dev/null +++ b/src/app/components/pages/ListManagement.jsx @@ -0,0 +1,933 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import UserProfileHeader from 'app/components/cards/UserProfileHeader'; +import { actions as UserProfilesSagaActions } from 'app/redux/UserProfilesSaga'; +import * as hive_api from '@hiveio/hive-js'; +import * as transactionActions from 'app/redux/TransactionReducer'; +import debounce from 'lodash.debounce'; +import { Link } from 'react-router'; +import LoadingIndicator from 'app/components/elements/LoadingIndicator'; +import tt from 'counterpart'; +import { callBridge } from 'app/utils/steemApi'; +import 'app/components/cards/Comment'; + +class ListManagement extends React.Component { + static propTypes = { + username: PropTypes.string, + accountname: PropTypes.string, + list_type: PropTypes.string, + profile: PropTypes.object, + }; + + constructor(props) { + super(props); + this.state = { + all_listed_accounts: [], + account_filter: '', + unmatched_accounts: [], + validated_accounts: [], + is_busy: false, + start_index: 0, + entries_per_page: 10, + error_message: '', + updates_are_pending: false, + first_time_user: false, + expand_welcome_panel: false, + }; + this.handle_filter_change = this.handle_filter_change.bind(this); + this.validate_accounts_to_add = this.validate_accounts_to_add.bind( + this + ); + this.broadcastFollowOperation = this.broadcastFollowOperation.bind( + this + ); + this.handle_page_select = this.handle_page_select.bind(this); + this.get_accounts_from_api = this.get_accounts_from_api.bind(this); + this.is_user_following_any_lists = this.is_user_following_any_lists.bind( + this + ); + this.toggle_help = this.toggle_help.bind(this); + this.dismiss_intro = this.dismiss_intro.bind(this); + } + + componentDidMount() { + this.get_accounts_from_api(); + this.timer = setInterval(() => { + this.get_accounts_from_api(); + }, 5000); + this.is_user_following_any_lists(); + } + + componentWillMount() { + const { + profile, + accountname, + fetchProfile, + username, + list_type, + } = this.props; + if (!profile) fetchProfile(accountname, username); + } + + componentWillUnmount() { + clearInterval(this.timer); + } + + componentDidUpdate(prevProps) { + if ( + prevProps.accountname !== this.props.accountname || + prevProps.username !== this.props.username + ) { + if (!this.props.profile) + fetchProfile(this.props.accountname, this.props.username); + } + + if (prevProps.list_type !== this.props.list_type) { + this.get_accounts_from_api(); + this.setState({ account_filter: '', start_index: 0 }); + this.searchbox.value = ''; + } + } + + is_user_following_any_lists() { + const { username } = this.props; + callBridge('does_user_follow_any_lists', { observer: username }) + .then( + async result => { + this.setState({ first_time_user: !result }); + }, + async error => { + console.log( + 'bridge_api.does_user_follow_any_lists returned an error: ', + error.toString() + ); + } + ) + .catch(error => { + console.log( + 'bridge_api.does_user_follow_any_lists returned an error: ', + error.toString() + ); + }); + } + + get_accounts_from_api() { + const { accountname, list_type } = this.props; + let sort_type = ''; + switch (list_type) { + case 'blacklisted': + sort_type = 'blacklisted'; + break; + case 'followed_blacklists': + sort_type = 'follow_blacklist'; + break; + case 'muted': + sort_type = 'muted'; + break; + case 'followed_muted_lists': + sort_type = 'follow_muted'; + break; + default: + sort_type = ''; + return; + } + + callBridge('get_follow_list', { + observer: accountname, + follow_type: sort_type, + }) + .then( + async result => { + if ( + this.state.updates_are_pending && + result.length != this.state.all_listed_accounts.length + ) { + //updates came through, so set pending to false + this.setState({ updates_are_pending: false }); + } + this.setState({ all_listed_accounts: result }); + }, + async error => { + console.log('callBridge returned an error: ', error); + this.setState({ error: error.toString() }); + } + ) + .catch(error => { + console.log('callBridge returned an error: ', error); + this.setState({ error: error.toString() }); + }); + } + + get_filtered_accounts() { + let matches = []; + let loaded_accounts = this.state.all_listed_accounts; + if (!loaded_accounts || loaded_accounts.length == 0) return []; + for (var account of loaded_accounts) { + if (account.name.includes(this.state.account_filter)) + matches.push(account); + } + return matches; + } + + get_accounts_to_display() { + let accounts = []; + if (this.state.account_filter === '') + accounts = this.state.all_listed_accounts; + else accounts = this.get_filtered_accounts(); + + if (!accounts || accounts.length == 0) return []; + + let result = []; + let end_index = Math.min( + accounts.length, + this.state.start_index + this.state.entries_per_page + ); + for (var i = this.state.start_index; i < end_index; i++) { + result.push(accounts[i]); + } + + return result; + } + + get_list_length() { + if (!this.state.all_listed_accounts) return 0; + if (this.state.account_filter === '') + return this.state.all_listed_accounts.length; + else return this.get_filtered_accounts().length; + } + + handle_page_select(direction) { + let new_index = this.state.start_index; + switch (direction) { + case 'next': + new_index = + this.state.start_index + this.state.entries_per_page; + break; + case 'previous': + new_index = + this.state.start_index - this.state.entries_per_page; + break; + case 'first': + new_index = 0; + break; + case 'last': + new_index = + (Math.ceil( + this.get_list_length() / this.state.entries_per_page + ) - + 1) * + this.state.entries_per_page; + break; + default: + new_index = 0; + break; + } + + if (new_index > this.get_list_length()) return; + else if (new_index < 0) new_index = 0; + + this.setState({ start_index: new_index }); + } + + validate_accounts_to_add = debounce(text => { + if (text === '') { + this.setState({ unmatched_accounts: [], validated_accounts: [] }); + } + this.setState({ unmatched_accounts: [] }); + let accounts = []; + let validated_accounts = []; + let all_users = text.split(','); + for (var user of all_users) { + if (user !== '') accounts.push(user); + } + + hive_api.api.getAccounts(accounts, (error, result) => { + let bad_accounts = []; + let result_string = JSON.stringify(result); + for (var check_user of all_users) { + if (check_user === '') continue; + if (!result_string.includes(check_user)) + bad_accounts.push(check_user); + else validated_accounts.push(check_user); + + if (error) { + console.log( + 'hiveio/hive-js returned an error when fetching accounts: ', + error + ); + } + } + + this.setState({ + validated_accounts: validated_accounts, + unmatched_accounts: bad_accounts, + }); + }); + }, 300); + + toggle_help() { + let expand_welcome_panel = this.state.expand_welcome_panel; + this.setState({ expand_welcome_panel: !expand_welcome_panel }); + } + + dismiss_intro() { + //Subscribes them to blacklist and mute list of the null account + //Null account has no lists, but this is a way for us to determine + //if the user has been here before or not + this.setState({ expand_welcome_panel: false, first_time_user: false }); + this.handle_reset_list(true); + } + + broadcastFollowOperation() { + if (this.state.is_busy) { + console.log(tt('list_management_jsx.busy')); + return; + } + let what = ''; + switch (this.props.list_type) { + case 'blacklisted': + what = 'blacklist'; + break; + case 'followed_blacklists': + what = 'follow_blacklist'; + break; + case 'muted': + what = 'ignore'; + break; + case 'followed_muted_lists': + what = 'follow_muted'; + break; + default: + return; + } + + let follower = this.props.username; + let following = this.state.validated_accounts; + this.setState({ is_busy: true }); + this.props.updateList(follower, following, what, () => { + this.multiadd.value = ''; + this.setState({ + is_busy: false, + validated_accounts: [], + unmatched_accounts: [], + updates_are_pending: true, + }); + }); + } + + generate_table_rows() { + let show_button = + this.props.username === this.props.accountname ? 'inline' : 'none'; + let listed_accounts = this.get_accounts_to_display(); + let items = []; + let button_text = ''; + let include_blacklist_description = false; + let include_mute_list_description = false; + switch (this.props.list_type) { + case 'blacklisted': + button_text = tt('list_management_jsx.button_unblacklist'); + break; + case 'muted': + button_text = tt('list_management_jsx.button_unmute'); + break; + case 'followed_blacklists': + button_text = tt( + 'list_management_jsx.button_unfollow_blacklist' + ); + include_blacklist_description = true; + break; + case 'followed_muted_lists': + button_text = tt( + 'list_management_jsx.button_unfollow_muted_list' + ); + include_mute_list_description = true; + break; + default: + button_text = '???'; + break; + } + if (this.state.is_busy) + button_text = tt('list_management_jsx.button_busy'); + + if (listed_accounts.length == 0) { + let item = ( + <tr key={'empty_tr'}> + <td colSpan="2" style={{ width: '75%' }}> + <center> + <b> + {this.state.account_filter === '' + ? tt('list_management_jsx.empty_list') + : tt( + 'list_management_jsx.no_results_found' + )} + </b> + </center> + </td> + </tr> + ); + items.push(item); + return items; + } + + for (var account of listed_accounts) { + let item = ( + <tr key={account.name + 'tr'}> + <td style={{ width: '75%' }}> + <Link to={'/@' + account.name}> + <strong>{account.name}</strong> + </Link> + {include_blacklist_description && ( + <div style={{ display: 'inline' }}> + {account.blacklist_description == + null + ? '' + : account.blacklist_description} + </div> + )} + {include_mute_list_description && ( + <div style={{ display: 'inline' }}> + {account.mute_list_description == + null + ? '' + : account.mute_list_description} + </div> + )} + </td> + <td> + <span + style={{ display: show_button }} + className="button slim hollow secondary" + onClick={this.handle_unlist.bind( + this, + account.name + )} + > + {button_text} + </span> + </td> + </tr> + ); + items.push(item); + } + + return items; + } + + handle_filter_change(event) { + this.setState({ account_filter: event.target.value, start_index: 0 }); + } + + handle_reset_list(reset_all) { + if (this.state.is_busy) { + console.log(tt('list_management_jsx.busy')); + return; + } + + let what = ''; + switch (this.props.list_type) { + case 'blacklisted': + what = 'reset_blacklist'; + break; + case 'followed_blacklists': + what = 'reset_follow_blacklist'; + break; + case 'muted': + what = 'reset_mute_list'; + break; + case 'followed_muted_lists': + what = 'reset_follow_muted_list'; + break; + default: + return; + } + + if (reset_all) what = 'reset_all_lists'; + + let follower = this.props.username; + let following = 'all'; //there is an 'all' account, but it appears unused so i'm stealing their identity for this + this.setState({ is_busy: true }); + this.props.updateList(follower, following, what, () => { + this.setState({ is_busy: false, updates_are_pending: false }); + this.get_accounts_from_api(); + }); + } + + handle_unlist(account) { + if (this.state.is_busy) { + console.log(tt('list_management_jsx.busy')); + return; + } + let what = ''; + switch (this.props.list_type) { + case 'blacklisted': + what = 'unblacklist'; + break; + case 'followed_blacklists': + what = 'unfollow_blacklist'; + break; + case 'muted': + what = ''; + break; + case 'followed_muted_lists': + what = 'unfollow_muted'; + break; + default: + return; + } + + let follower = this.props.username; + let following = account; + this.setState({ is_busy: true }); + this.props.updateList(follower, following, what, () => { + this.setState({ is_busy: false, updates_are_pending: true }); + this.get_accounts_from_api(); + }); + } + + render() { + if (!this.props.profile) + return ( + <center> + <LoadingIndicator type="circle" /> + </center> + ); + + let blacklist_description = this.props.profile + .get('metadata') + .get('profile') + .get('blacklist_description'); + if (!blacklist_description) + blacklist_description = + "User hasn't added a description to their blacklist yet"; + let mute_list_description = this.props.profile + .get('metadata') + .get('profile') + .get('muted_list_description'); + if (!mute_list_description) + mute_list_description = + "User hasn't added a description to their mute list yet"; + + let list_rows = this.generate_table_rows(); + let list_length = this.get_list_length(); + + let viewing_own_page = this.props.username === this.props.accountname; + + let header_text = '', + add_to_text = '', + button_text = '', + description_text = '', + reset_button_text = ''; + let reset_all_button_text = tt('list_management_jsx.reset_all_lists'); + switch (this.props.list_type) { + case 'blacklisted': + header_text = + tt('list_management_jsx.blacklisted_header') + + this.props.accountname; + add_to_text = tt('list_management_jsx.add_to_blacklist'); + button_text = tt('list_management_jsx.button_blacklist'); + description_text = 'List Description: '; + reset_button_text = tt('list_management_jsx.reset_blacklist'); + break; + case 'muted': + header_text = + tt('list_management_jsx.muted_header') + + this.props.accountname; + add_to_text = tt('list_management_jsx.add_to_muted_list'); + button_text = tt('list_management_jsx.button_mute'); + description_text = 'List Description: '; + reset_button_text = tt('list_management_jsx.reset_muted_list'); + break; + case 'followed_blacklists': + header_text = tt( + 'list_management_jsx.followed_blacklists_header' + ); + add_to_text = tt('list_management_jsx.follow_blacklists'); + button_text = tt( + 'list_management_jsx.button_follow_blacklists' + ); + reset_button_text = tt( + 'list_management_jsx.reset_followed_blacklists' + ); + break; + case 'followed_muted_lists': + header_text = tt( + 'list_management_jsx.followed_muted_lists_header' + ); + add_to_text = tt('list_management_jsx.follow_muted_lists'); + button_text = tt( + 'list_management_jsx.button_follow_muted_lists' + ); + reset_button_text = tt( + 'list_management_jsx.reset_followed_muted_list' + ); + break; + } + + if (this.state.is_busy) + button_text = tt('list_management_jsx.button_busy'); + + let current_page_number = + Math.floor(this.state.start_index / this.state.entries_per_page) + + 1; + let total_pages = Math.max( + Math.ceil(list_length / this.state.entries_per_page), + 1 + ); + + return ( + <div> + <div className="UserProfile"> + <UserProfileHeader + current_user={this.props.username} + accountname={this.props.accountname} + profile={this.props.profile} + /> + </div> + <p /> + + <center> + <span onClick={this.toggle_help}> + {' '} + <h5> + <u>{tt('list_management_jsx.what_is_this')}</u>{' '} + {!this.state.first_time_user && + !this.state.expand_welcome_panel} + </h5> + </span> + </center> + + {((this.state.first_time_user && viewing_own_page) || + this.state.expand_welcome_panel) && ( + <div> + <center> + <table style={{ width: '35%', border: '2' }}> + <thead /> + + <tbody> + <tr> + <td> + <center> + {this.state.first_time_user && + viewing_own_page && ( + <h5> + {tt( + 'list_management_jsx.welcome_header' + )} + </h5> + )} + </center> + </td> + </tr> + + <tr> + <td> + <center> + {tt( + 'list_management_jsx.info1' + )}{' '} + {tt( + 'list_management_jsx.info2' + )}{' '} + <Link + to={ + '/@' + + this.props.username + + '/settings' + } + > + {tt( + 'list_management_jsx.info3' + )} + </Link> + {tt( + 'list_management_jsx.info4' + )}{' '} + {tt( + 'list_management_jsx.info5' + )} + <p /> + {tt( + 'list_management_jsx.info6' + )} + </center> + </td> + </tr> + </tbody> + </table> + {viewing_own_page && + this.state.first_time_user && ( + <div> + {tt('list_management_jsx.info7')} <br /> + <span + className="button slim hollow secondary" + onClick={e => { + this.dismiss_intro(); + }} + > + {tt( + 'list_management_jsx.acknowledge' + )} + </span> + </div> + )} + </center> + </div> + )} + <p /> + <div> + <center> + <h4>{header_text}</h4> + {(this.props.list_type === 'blacklisted' || + this.props.list_type === 'muted') && ( + <h6> + {tt( + 'list_management_jsx.list_description_placement' + )}{' '} + {this.props.list_type === 'blacklisted' + ? blacklist_description + : mute_list_description} + </h6> + )} + <p /> + <table style={{ width: '35%' }}> + <thead /> + <tbody> + <tr> + <td style={{ textAlign: 'right' }}> + <right> + {tt( + 'list_management_jsx.search_for_user' + )} + </right> + </td> + <td> + <input + type="text" + ref={el => (this.searchbox = el)} + style={{ width: '350px' }} + onChange={this.handle_filter_change} + /> + </td> + </tr> + </tbody> + </table> + <p /> + + <table style={{ width: '35%' }}> + <thead /> + + <tbody>{list_rows}</tbody> + </table> + + {list_length > this.state.entries_per_page && ( + <span + className="button slim hollow secondary" + value="first" + onClick={e => { + this.handle_page_select('first'); + }} + > + {tt('list_management_jsx.first')} + </span> + )} + {list_length > this.state.entries_per_page && ( + <span + className="button slim hollow secondary" + value="prev" + onClick={e => { + this.handle_page_select('previous'); + }} + > + {tt('list_management_jsx.previous')} + </span> + )} + {list_length > this.state.entries_per_page && ( + <span + className="button slim hollow secondary" + value="next" + onClick={e => { + this.handle_page_select('next'); + }} + > + {tt('list_management_jsx.next')} + </span> + )} + {list_length > this.state.entries_per_page && ( + <span + className="button slim hollow secondary" + value="last" + onClick={e => { + this.handle_page_select('last'); + }} + > + {tt('list_management_jsx.last')} + </span> + )} + {list_length > this.state.entries_per_page && ( + <div> + {tt('list_management_jsx.page_count', { + current: current_page_number, + total: total_pages, + })} + </div> + )} + + {this.props.username === this.props.accountname && ( + <div> + <p /> + <p /> + <h4> + {tt( + 'list_management_jsx.add_users_to_list' + )} + </h4> + <table style={{ width: '35%' }}> + <thead /> + <tbody> + <tr> + <td + style={{ + width: '50%', + textAlign: 'right', + }} + > + {add_to_text}{' '} + {tt( + 'list_management_jsx.multi_add_notes' + )} + </td> + <td style={{ width: '25%' }}> + <input + type="text" + name="multiadd" + ref={el => + (this.multiadd = el) + } + onChange={e => { + this.validate_accounts_to_add( + e.target.value + ); + }} + /> + </td> + + <td style={{ width: '25%' }}> + {this.state.validated_accounts + .length > 0 && + this.state + .unmatched_accounts + .length == 0 && ( + <span + className="button slim hollow secondary" + onClick={ + this + .broadcastFollowOperation + } + > + {button_text} + </span> + )} + </td> + </tr> + </tbody> + </table> + <p /> + <h4> + {tt('list_management_jsx.reset_header')} + </h4> + <span + className="button slim hollow secondary" + onClick={this.handle_reset_list.bind( + this, + false + )} + > + {this.state.is_busy + ? tt('list_management_jsx.button_busy') + : reset_button_text} + </span> + <span + className="button slim hollow secondary" + onClick={this.handle_reset_list.bind( + this, + true + )} + > + {this.state.is_busy + ? tt('list_management_jsx.button_busy') + : reset_all_button_text} + </span> + </div> + )} + + {this.state.unmatched_accounts.length > 0 && ( + <div style={{ color: 'red' }}> + <b> + {tt('list_management_jsx.unknown_accounts')}{' '} + {this.state.unmatched_accounts.join(', ')} + </b> + </div> + )} + {this.state.updates_are_pending && ( + <div style={{ color: 'red' }}> + <b> + {tt( + 'list_management_jsx.updates_are_pending' + )} + </b> + </div> + )} + </center> + </div> + </div> + ); + } +} + +module.exports = { + path: '@:accountname/lists/:list_type', + component: connect( + (state, ownProps) => { + const username = state.user.getIn(['current', 'username']); + const list_type = ownProps.routeParams.list_type; + const accountname = ownProps.routeParams.accountname.toLowerCase(); + const profile = state.userProfiles.getIn(['profiles', accountname]); + return { + username, + list_type, + accountname, + profile, + }; + }, + dispatch => ({ + fetchProfile: (account, observer) => + dispatch( + UserProfilesSagaActions.fetchProfile({ account, observer }) + ), + updateList: (follower, following, type, callback) => { + const what = type ? [type] : []; + const json = ['follow', { follower, following, what }]; + console.log('about to dispatch broadcastOperation!'); + dispatch( + transactionActions.broadcastOperation({ + type: 'custom_json', + operation: { + id: 'follow', + required_posting_auths: [follower], + json: JSON.stringify(json), + }, + successCallback: callback, + errorCallback: callback, + }) + ); + }, + fetchListedAccounts: (observer, list_type) => { + dispatch( + UserProfilesSagaActions.fetchLists({ observer, list_type }) + ); + }, + //TODO: add sagas for fetching various listed users + }) + )(ListManagement), +}; diff --git a/src/app/components/pages/ListManagement.scss b/src/app/components/pages/ListManagement.scss new file mode 100644 index 0000000000000000000000000000000000000000..0b45ec8ee372cd98e58256cfa01ab8cbf3907a82 --- /dev/null +++ b/src/app/components/pages/ListManagement.scss @@ -0,0 +1,222 @@ +.UserProfile { + margin-top: -1.5rem; + + .articles { + @include MQ(M) { + margin: 0 auto; + } + } +} + +.UserProfile__blacklists { + .account_warn {font-size: 0.8em;} + .VerticalMenu { + font-size: 1rem; + text-shadow: none; + @include themify($themes) { + background-color: themed('backgroundColor'); + color: themed('textColorPrimary'); + } + } +} + +.UserProfile__postmenu { + margin: -0.5rem 0 1rem; + padding-bottom: 0.5rem; + @include themify($themes) { + border-bottom: themed('border'); + } + div {display: inline-block; padding: 0 0.5rem; margin: 0 0.5rem;} +} + +.UserProfile__tab_content { + margin-top: 1.5rem; +} + +.UserProfile__top-nav { +background-color: $color-blue-dark; +padding: 0; +.menu { + background-color: transparent; +} +.menu > li > a { + transition: all 200ms ease-in; + transform: translate3d( 0, 0, 0); + padding-left: 0.7rem; + padding-right: 0.7rem; + background-color: transparent; + color: $color-white; + &:hover, &:focus { + background-color: $color-hive-black; + } + &.active { + @include themify($themes) { + background-color: themed('backgroundColor'); + color: themed('textColorPrimary'); + } + z-index: 2; + font-weight: bold; + } +} + +div.UserProfile__top-menu { + max-width: 71.42857rem; + margin-left: auto; + margin-right: auto; + display: flex; + flex-flow: row wrap; + width: 100%; + // Override default svg vertical alignment + .Icon > svg, .Icon span.icon { + vertical-align: middle!important; + } +} +} + +.UserProfile__section-title { +margin-bottom: 1.5rem; +padding-bottom: 0.5rem; +border-bottom: 1px solid #EEE; +} + +.UserProfile__banner { + text-align: center; + color: $white; + a { + color: $white; + } + > div.column { + background: $color-background-less-dark; + background-size: cover; + background-repeat: no-repeat; + background-position: 50% 50%; + text-shadow: 1px 1px 2px black; + .button {text-shadow: none;} + + min-height: 155px; + } + h1 { + padding-top: 20px; + font-weight: 600; + font-size: 1.84524rem!important; + @media screen and (max-width: 39.9375em) { + font-size: 1.13095rem!important; + } + } + .Icon { + margin-left: 1rem; + svg {fill: #def;} + } + + .Userpic { + margin-right: 0.75rem; + vertical-align: middle; + } + + .UserProfile__rep { + font-size: 80%; + font-weight: 200; + } + + .UserProfile__buttons { + position: absolute; + top: 15px; + right: 5px; + + label.button { + color: black; + border-radius: 3px; + background-color: white; + } + } + + .UserProfile__bio { + margin: -0.4rem auto 0.5rem; + font-size: 95%; + max-width: 420px; + line-height: 1.4; + } + .UserProfile__info { + font-size: 90%; + } + + .UserProfile__stats { + margin-bottom: 5px; + padding-bottom: 5px; + font-size: 90%; + + a { + @include hoverUnderline; + vertical-align: middle; + } + + > span { + padding: 0px 10px; + border-left: 1px solid #CCC; + &:first-child {border-left: none;} + &:nth-child(6) {border-left: none;} + } + } +} + +@media screen and (max-width: 39.9375em) { + + div.UserProfile__top-nav .menu li>a { + padding: 8px; + } + + .UserProfile__top-menu > div.columns { + padding-left: 0; + padding-right: 0; + } + + .UserProfile__banner .Userpic { + width: 36px !important; + height: 36px !important; + } + + .UserProfile__banner .UserProfile__buttons { + text-align: right; + + label.button { + display: block; + } + } + + .UserProfile__banner .UserProfile__buttons_mobile { + position: inherit; + margin-bottom: .5rem; + .button { + background-color: $white; + color: $black; + } + } +} + +// Temporary fix to prevent alternate User Profile pages outside the blog from taking the narrow layout. + +.UserProfile { +.articles { + margin-bottom: 4em; + &__h1 { + text-transform: none; + @include MQ(M) { + @include font-size(20px); + } + } +} +&__tab_content.layout-block.settings, &__tab_content.layout-block.curation-rewards, &__tab_content.layout-block.author-rewards { + .articles { + padding: 1.5em 1.5em; + max-width: 1056px; + @include MQ(XL) { + min-width: 1050px; + } + } +} +.settings .articles__layout-selector, .curation-rewards .articles__layout-selector, .author-rewards .articles__layout-selector { + display: none; + } +} + + diff --git a/src/app/components/pages/UserProfile.scss b/src/app/components/pages/UserProfile.scss index 732040bb7357a9c0599b2e5bfe8e804a7f837aaa..e6c9865e2cd13d522345689b99f7cb006fb8a9e3 100644 --- a/src/app/components/pages/UserProfile.scss +++ b/src/app/components/pages/UserProfile.scss @@ -157,6 +157,7 @@ padding: 0px 10px; border-left: 1px solid #CCC; &:first-child {border-left: none;} + &:nth-child(5) {border-left: none; border-right: 1px solid #CCC} } } } diff --git a/src/app/locales/en.json b/src/app/locales/en.json index 00195177a8881411e4211acb692691af4dcf2332..e0315b66c2bc06099067c14835733d400e1fbbd0 100644 --- a/src/app/locales/en.json +++ b/src/app/locales/en.json @@ -869,8 +869,69 @@ "all": "All", "replies": "Replies", "follows": "Follows", - "resteems": "Resteems", + "resteems": "Reblogs", "upvotes": "Upvotes", "mentions": "Mentions" + }, + "list_management_jsx": { + "busy": + "currently waiting for a broadcast operation to finish, try again soon", + "button_unblacklist": "Unblacklist", + "button_unmute": "Unmute", + "button_unfollow_blacklist": "Unfollow Blacklist", + "button_unfollow_muted_list": "Unfollow Muted List", + "button_busy": "Busy, Please Wait", + "blacklisted_header": "Accounts Blacklisted By ", + "muted_header": "Accounts Muted By ", + "followed_blacklists_header": "Followed Blacklists", + "followed_muted_lists_header": "Followed Muted LIsts", + "add_to_blacklist": "Add Accounts To Blacklist ", + "add_to_muted_list": "Add Accounts to Muted List", + "follow_blacklists": "Follow Blacklists Of These Accounts ", + "follow_muted_lists": "Follow Muted Lists Of These Accounts ", + "button_blacklist": "Blacklist Accounts", + "button_mute": "Mute Users", + "button_follow_blacklists": "Follow Blacklists", + "button_follow_muted_lists": "Follow Muted Lists", + "unknown_accounts": "Unable to find account(s): ", + "page_count": "Viewing Page %(current)s of %(total)s", + "search_for_user": "Search List For Account:", + "add_users_to_list": "Add Account(s) To List", + "multi_add_notes": "(single account or comma separated list)", + "updates_are_pending": + "Your list will reflect the updates shortly. You do not have to remain on this page for them to take effect.", + "list_description_field": + "Enter a description for your list (how you choose people, if they can contact you for removal, etc)", + "save_description": "Save Description", + "reset_blacklist": "Reset Blacklist", + "reset_muted_list": "Reset Muted List", + "reset_followed_blacklists": "Reset Followed Blacklists", + "reset_followed_muted_list": "Reset Followed Muted Lists", + "reset_all_lists": "Reset All Follows/Lists", + "reset_header": "Reset Options", + "empty_list": "There are no users on this list yet", + "welcome_header": "It looks like you might be new here!", + "welcome_body": + "This is the new decentralized list feature. You can create your own black list and mute list and give them a description", + "list_description_placement": "List Description:", + "no_results_found": "No search results found", + "acknowledge": "Acknowledge", + "what_is_this": "What Is This?", + "info1": + "This is the new decentralized list system. From here you can manage your own mute list or blacklist, as well as subscribe to the mute lists and blacklists of other users. ", + "info2": "There are some new fields on the ", + "info3": "Settings ", + "info4": + "page where you can set a description of how you choose who you've added to your lists and if there are any actions that account can take to get removed from them. ", + "info5": + "You can see the descriptions of the lists of other accounts by browsing directly to their personal blacklist or mute list page. These links can be found on their profile page below the follower information. ", + "info6": + "To get started, we recommend that you follow the blacklists/mute lists of these accounts: ", + "info7": + "Click the button below to dismiss this dialog. This will automatically subscribe you to the mute list and blacklist of the null account.", + "first": "First", + "previous": "Previous", + "next": "Next", + "last": "Last" } } diff --git a/src/app/redux/UserProfilesReducer.js b/src/app/redux/UserProfilesReducer.js index 74d092c760ce81e8848933dd494e2b97d618803e..b232864fa9423b4a101e7da3c34a542cc19c01c5 100644 --- a/src/app/redux/UserProfilesReducer.js +++ b/src/app/redux/UserProfilesReducer.js @@ -80,6 +80,16 @@ export default function reducer(state = defaultState, action) { return state; } + case ADD_LISTED_ACCOUNTS: { + if (payload) { + return state.setIn( + ['listedAccounts', payload.username], + fromJS(payload.listed_accounts) + ); + } + return state; + } + default: return state; } diff --git a/src/app/utils/steemApi.js b/src/app/utils/steemApi.js index 0c144a4b39e5aba0f1ab2a31dc7309cced51f74b..f3dcafbed35a497ca69cc92dc270692d1f49e823 100644 --- a/src/app/utils/steemApi.js +++ b/src/app/utils/steemApi.js @@ -26,6 +26,9 @@ export async function callBridge(method, params) { delete params.observer; } + if (params.observer === null || params.observer === undefined) + params.observer = $STM_Config.default_observer; + return new Promise(function(resolve, reject) { api.call('bridge.' + method, params, function(err, data) { if (err) { diff --git a/src/server/index.js b/src/server/index.js index 0312fc594ab53eef1b322320d1b5695b99bfa970..40da400d9f1f806c38bec2fa92d3921021524f18 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -37,6 +37,7 @@ global.$STM_Config = { alternative_api_endpoints: alternativeApiEndpoints, referral: config.get('referral'), rebranded_api: true, + default_observer: config.get('default_observer'), }; const WebpackIsomorphicTools = require('webpack-isomorphic-tools');