From 88fada4d409ef3d5c85e9c8f4a826ed0a147d0a2 Mon Sep 17 00:00:00 2001 From: jsalyers <jsalyers@syncad.com> Date: Fri, 31 Jul 2020 15:37:10 +0000 Subject: [PATCH] [JES] WIP. Mostly finished condenser changes for decentralized system. Translations for other languages (or at least placeholders) should probably be added, and should come up with suggested accounts to follow for the newbies (cherry picked from commit 5171d6a0d3edccc8b38894ac58054ac5bf52c447) --- src/app/ResolveRoute.js | 9 +- src/app/RootRoute.js | 3 + src/app/components/all.scss | 1 + .../components/cards/UserProfileHeader.jsx | 33 + src/app/components/modules/Header/index.jsx | 2 + src/app/components/modules/Settings.jsx | 57 ++ src/app/components/pages/ListManagement.jsx | 934 ++++++++++++++++++ src/app/components/pages/ListManagement.scss | 222 +++++ src/app/components/pages/UserProfile.scss | 1 + src/app/locales/en.json | 61 ++ src/app/redux/UserProfilesReducer.js | 17 + src/app/redux/UserProfilesSaga.js | 18 + 12 files changed, 1357 insertions(+), 1 deletion(-) create mode 100644 src/app/components/pages/ListManagement.jsx create mode 100644 src/app/components/pages/ListManagement.scss diff --git a/src/app/ResolveRoute.js b/src/app/ResolveRoute.js index 2437c244d..82d2ddc28 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 1c634375e..b5db4591b 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 f2ae5183b..57d31ae66 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 75d7320f3..e542b7279 100644 --- a/src/app/components/cards/UserProfileHeader.jsx +++ b/src/app/components/cards/UserProfileHeader.jsx @@ -127,6 +127,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 a03573494..1fb828bde 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 71cc7838e..e5a52d18b 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', + 'mute_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, + mute_list_description: + values.mute_list_description && + values.mute_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, + mute_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.mute_list_description = mute_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.mute_list_description) + delete metaData.profile.mute_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, + mute_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" + {...mute_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> @@ -989,6 +1043,9 @@ class Settings extends React.Component { </div> </div> )} + <div> + <h4>Blacklisted Users</h4> + </div> </div> ); } diff --git a/src/app/components/pages/ListManagement.jsx b/src/app/components/pages/ListManagement.jsx new file mode 100644 index 000000000..9641a68c8 --- /dev/null +++ b/src/app/components/pages/ListManagement.jsx @@ -0,0 +1,934 @@ +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() { + console.log('entering 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('mute_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 000000000..0b45ec8ee --- /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 7dd75a6d0..81997a640 100644 --- a/src/app/components/pages/UserProfile.scss +++ b/src/app/components/pages/UserProfile.scss @@ -154,6 +154,7 @@ padding: 0px 10px; border-left: 1px solid #CCC; &:first-child {border-left: none;} + &:nth-child(6) {border-left: none;} } } } diff --git a/src/app/locales/en.json b/src/app/locales/en.json index 02fcd07db..d1a044c1c 100644 --- a/src/app/locales/en.json +++ b/src/app/locales/en.json @@ -861,5 +861,66 @@ "resteems": "Resteems", "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 ad07586ec..b053fb033 100644 --- a/src/app/redux/UserProfilesReducer.js +++ b/src/app/redux/UserProfilesReducer.js @@ -2,9 +2,11 @@ import { fromJS } from 'immutable'; // Action constants const ADD_USER_PROFILE = 'user_profile/ADD'; +const ADD_LISTED_ACCOUNTS = 'user_profile/LISTED'; const defaultState = fromJS({ profiles: {}, + listed_accounts: {}, }); export default function reducer(state = defaultState, action) { @@ -21,6 +23,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; } @@ -31,3 +43,8 @@ export const addProfile = payload => ({ type: ADD_USER_PROFILE, payload, }); + +export const addList = payload => ({ + type: ADD_LISTED_ACCOUNTS, + payload, +}); diff --git a/src/app/redux/UserProfilesSaga.js b/src/app/redux/UserProfilesSaga.js index d05d3d266..1ef108b04 100644 --- a/src/app/redux/UserProfilesSaga.js +++ b/src/app/redux/UserProfilesSaga.js @@ -3,11 +3,25 @@ import * as userProfileActions from './UserProfilesReducer'; import { callBridge, getHivePowerForUser } from 'app/utils/steemApi'; const FETCH_PROFILE = 'userProfilesSaga/FETCH_PROFILE'; +const FETCH_LISTS = 'userProfilesSaga/FETCH_LISTS'; export const userProfilesWatches = [ takeLatest(FETCH_PROFILE, fetchUserProfile), + takeLatest(FETCH_LISTS, fetchLists), ]; +export function* fetchLists(action) { + const { observer, follow_type } = action.payload; + const ret = yield call(callBridge, 'get_follow_list', { + observer, + follow_type, + }); + if (!ret) throw new Error('Account not found'); + yield put( + userProfileActions.addList({ username: observer, listed_accounts: ret }) + ); +} + export function* fetchUserProfile(action) { const { account, observer } = action.payload; const ret = yield call(callBridge, 'get_profile', { account, observer }); @@ -28,4 +42,8 @@ export const actions = { type: FETCH_PROFILE, payload, }), + fetchLists: payload => ({ + type: FETCH_LISTS, + payload, + }), }; -- GitLab