diff --git a/app/ResolveRoute.js b/app/ResolveRoute.js index 1dc89d98ba..cce2d3025b 100644 --- a/app/ResolveRoute.js +++ b/app/ResolveRoute.js @@ -2,7 +2,10 @@ export const routeRegex = { PostsIndex: /^\/(@[\w\.\d-]+)\/feed\/?$/, UserProfile1: /^\/(@[\w\.\d-]+)\/?$/, UserProfile2: /^\/(@[\w\.\d-]+)\/(blog|posts|comments|recommended|transfers|curation-rewards|author-rewards|permissions|created|recent-replies|feed|password|followed|followers|settings)\/?$/, - PostNoCategory: /^\/(@[\w\.\d-]+)\/([\w\d-]+)/ + UserEndPoints: /^(blog|posts|comments|recommended|transfers|curation-rewards|author-rewards|permissions|created|recent-replies|feed|password|followed|followers|settings)$/, + CategoryFilters: /^\/(hot|created|trending|active|promoted)\/?$/ig, + PostNoCategory: /^\/(@[\w\.\d-]+)\/([\w\d-]+)/, + UserJson: /^\/(@[\w\.\d-]+)(\/json)$/ } export default function resolveRoute(path) diff --git a/app/components/cards/Comment.jsx b/app/components/cards/Comment.jsx index e6cd3097c4..6e0ff39a19 100644 --- a/app/components/cards/Comment.jsx +++ b/app/components/cards/Comment.jsx @@ -22,30 +22,13 @@ export function sortComments( cont, comments, sort_order ) { let sort_orders = { /** sort replies by active */ - active: (a,b) => { + votes: (a,b) => { let acontent = cont.get(a); let bcontent = cont.get(b); - if (netNegative(acontent)) { - return 1; - } else if (netNegative(bcontent)) { - return -1; - } - let aactive = Date.parse( acontent.get('active') ); - let bactive = Date.parse( bcontent.get('active') ); + let aactive = acontent.get('active_votes').size; + let bactive = bcontent.get('active_votes').size; return bactive - aactive; }, - update: (a,b) => { - let acontent = cont.get(a); - let bcontent = cont.get(b); - if (netNegative(acontent)) { - return 1; - } else if (netNegative(bcontent)) { - return -1; - } - let aactive = Date.parse( acontent.get('last_update') ); - let bactive = Date.parse( bcontent.get('last_update') ); - return bactive.getTime() - aactive.getTime(); - }, new: (a,b) => { let acontent = cont.get(a); let bcontent = cont.get(b); @@ -84,7 +67,7 @@ class CommentImpl extends React.Component { // html props cont: React.PropTypes.object.isRequired, content: React.PropTypes.string.isRequired, - sort_order: React.PropTypes.oneOf(['active', 'updated', 'new', 'trending']).isRequired, + sort_order: React.PropTypes.oneOf(['votes', 'new', 'trending']).isRequired, root: React.PropTypes.bool, showNegativeComments: React.PropTypes.bool, onHide: React.PropTypes.func, diff --git a/app/components/cards/Comment.scss b/app/components/cards/Comment.scss index 4574d9d67f..460007aa84 100644 --- a/app/components/cards/Comment.scss +++ b/app/components/cards/Comment.scss @@ -83,14 +83,6 @@ } } -.Comment > div > .Comment__header > .Comment__header_collapse > .Voting { - visibility: hidden; -} - -.Comment:hover > div > .Comment__header > .Comment__header_collapse > .Voting { - visibility: visible; -} - .Comment__body { margin-left: 62px; max-width: 50rem; diff --git a/app/components/cards/PostFull.jsx b/app/components/cards/PostFull.jsx index fbfd28fc66..a62949939d 100644 --- a/app/components/cards/PostFull.jsx +++ b/app/components/cards/PostFull.jsx @@ -4,8 +4,6 @@ import TimeAgoWrapper from 'app/components/elements/TimeAgoWrapper'; import pluralize from 'pluralize'; import Icon from 'app/components/elements/Icon'; import { connect } from 'react-redux'; -// import FormattedAsset from 'app/components/elements/FormattedAsset'; -// import Userpic from 'app/components/elements/Userpic'; import user from 'app/redux/User'; import transaction from 'app/redux/Transaction' import Voting from 'app/components/elements/Voting'; @@ -23,6 +21,7 @@ import {repLog10, parsePayoutAmount} from 'app/utils/ParsersAndFormatters'; import DMCAList from 'app/utils/DMCAList' import PageViewsCounter from 'app/components/elements/PageViewsCounter'; import ShareMenu from 'app/components/elements/ShareMenu'; +import {serverApiRecordEvent} from 'app/utils/ServerApiClient'; function TimeAuthorCategory({content, authorRepLog10, showTags}) { return ( @@ -101,6 +100,7 @@ class PostFull extends React.Component { } fbShare(e) { + serverApiRecordEvent('FbShare', this.share_params.link); e.preventDefault(); window.FB.ui({ method: 'share', @@ -109,6 +109,7 @@ class PostFull extends React.Component { } twitterShare(e) { + serverApiRecordEvent('TwitterShare', this.share_params.link); e.preventDefault(); const winWidth = 640; const winHeight = 320; @@ -120,6 +121,7 @@ class PostFull extends React.Component { } linkedInShare(e) { + serverApiRecordEvent('LinkedInShare', this.share_params.link); e.preventDefault(); const winWidth = 720; const winHeight = 480; @@ -174,6 +176,7 @@ class PostFull extends React.Component { net_rshares.compare(Long.ZERO) <= 0 this.share_params = { + link, url: 'https://steemit.com' + link, title: title + ' — Steemit', desc: p.desc @@ -260,14 +263,14 @@ class PostFull extends React.Component { } - {showPromote && } + {showPromote && }
-
+
{!readonly && } {!readonly && diff --git a/app/components/cards/PostFull.scss b/app/components/cards/PostFull.scss index 1a5c269893..8b6b4663c2 100644 --- a/app/components/cards/PostFull.scss +++ b/app/components/cards/PostFull.scss @@ -118,6 +118,10 @@ } } +.Promote__button { + margin-right: 0px!important; +} + /* Small only */ @media screen and (max-width: 39.9375em) { .Post { @@ -129,7 +133,7 @@ -ms-flex: 0 0 100%; flex: 0 0 100%; } - .PostFull__footer > .right-sub-menu { + .PostFull__footer > .RightShare__Menu { text-align: left; } } diff --git a/app/components/cards/PostSummary.jsx b/app/components/cards/PostSummary.jsx index fe6f061daa..9d16f29825 100644 --- a/app/components/cards/PostSummary.jsx +++ b/app/components/cards/PostSummary.jsx @@ -40,13 +40,28 @@ class PostSummary extends React.Component { netVoteSign: React.PropTypes.number, currentCategory: React.PropTypes.string, thumbSize: React.PropTypes.string, + nsfwPref: React.PropTypes.string, onClick: React.PropTypes.func }; - shouldComponentUpdate(props) { + constructor() { + super(); + this.state = {revealNsfw: false} + this.onRevealNsfw = this.onRevealNsfw.bind(this) + } + + shouldComponentUpdate(props, state) { return props.thumbSize !== this.props.thumbSize || props.pending_payout !== this.props.pending_payout || - props.total_payout !== this.props.total_payout; + props.total_payout !== this.props.total_payout || + props.username !== this.props.username || + props.nsfwPref !== this.props.nsfwPref || + state.revealNsfw !== this.state.revealNsfw; + } + + onRevealNsfw(e) { + e.preventDefault(); + this.setState({revealNsfw: true}) } render() { @@ -70,7 +85,7 @@ class PostSummary extends React.Component {
} - const {gray, pictures, authorRepLog10, hasFlag} = content.get('stats', Map()).toJS() + const {gray, pictures, authorRepLog10, hasFlag, isNsfw} = content.get('stats', Map()).toJS() const p = extractContent(immutableAccessor, content); let desc = p.desc if(p.image_link)// image link is already shown in the preview @@ -96,6 +111,7 @@ class PostSummary extends React.Component {
; let content_title =

navigate(e, onClick, post, title_link_url)}> + {isNsfw && nsfw} {title_text} {full_power && } @@ -108,10 +124,26 @@ class PostSummary extends React.Component { {} in - if( !(currentCategory && currentCategory.match( /nsfw/ )) ) { - if (currentCategory !== '-' && currentCategory !== p.category && p.category.match(/nsfw/) ) { - return null; - } + const {nsfwPref, username} = this.props + const {revealNsfw} = this.state + + if(isNsfw) { + if(nsfwPref === 'hide') { + // user wishes to hide these posts entirely + return null; + } else if(!revealNsfw && nsfwPref !== 'show') { + // warn the user, unless they have chosen to reveal this post or have their preference set to "show always" + return ( +
+
+ This post is nsfw. + You can reveal it or{' '} + {username ? adjust your display preferences. + : create an account to save your preferences.} +
+
+ ) + } } let thumb = null; @@ -139,7 +171,6 @@ class PostSummary extends React.Component {

{author_category} -
{thumb}
@@ -171,7 +202,10 @@ export default connect( pending_payout = content.get('pending_payout_value'); total_payout = content.get('total_payout_value'); } - return {post, content, pending_payout, total_payout}; + return { + post, content, pending_payout, total_payout, + username: state.user.getIn(['current', 'username']) || state.offchain.get('account') + }; }, (dispatch) => ({ diff --git a/app/components/cards/PostSummary.scss b/app/components/cards/PostSummary.scss index e4a0edbdf0..dc3b569c70 100644 --- a/app/components/cards/PostSummary.scss +++ b/app/components/cards/PostSummary.scss @@ -8,6 +8,24 @@ ul.PostsList__summaries { background-color: #fff; clear: left; @include clearfix; + + .PostSummary__nsfw-warning { + border: 1px solid $medium-gray; + border-radius: 0.5rem; + padding: 1.5rem 2rem; + min-height: 80px; + } + + .nsfw-flag { + color: #C00; + border: 1px solid #C00; + font-size: 75%; + border-radius: 3px; + font-weight: normal; + font-family: Arial; + margin: 0 0.1rem; + padding: 0 0.2rem; + } } .PostSummary__image { @@ -61,6 +79,9 @@ ul.PostsList__summaries { } } } + .nsfw-flag { + margin-right: 0.25rem; + } } .PostSummary__collapse { visibility: hidden; diff --git a/app/components/cards/PostsList.jsx b/app/components/cards/PostsList.jsx index aab3144b7f..8ad5664864 100644 --- a/app/components/cards/PostsList.jsx +++ b/app/components/cards/PostsList.jsx @@ -36,6 +36,7 @@ class PostsList extends React.Component { this.state = { thumbSize: 'desktop', showNegativeComments: false, + nsfwPref: 'warn', showPost: null } this.scrollListener = this.scrollListener.bind(this); @@ -45,6 +46,18 @@ class PostsList extends React.Component { this.shouldComponentUpdate = shouldComponentUpdate(this, 'PostsList') } + componentWillMount() { + this.readNsfwPref() + } + + readNsfwPref() { + if(!process.env.BROWSER) return + const {username} = this.props + const key = 'nsfwPref' + (username ? '-' + username : '') + const nsfwPref = localStorage.getItem(key) || 'warn' + this.setState({nsfwPref}) + } + componentDidMount() { this.attachScrollListener(); } @@ -54,6 +67,7 @@ class PostsList extends React.Component { if (this.state.showPost && (location !== this.post_url)) { this.setState({showPost: null}); } + this.readNsfwPref(); } componentDidUpdate(prevProps, prevState) { @@ -158,7 +172,7 @@ class PostsList extends React.Component { render() { const {posts, showSpam, loading, category, content, ignore_result, account} = this.props; - const {thumbSize, showPost} = this.state + const {thumbSize, showPost, nsfwPref} = this.state const postsInfo = []; posts.forEach(item => { const cont = content.get(item); @@ -183,6 +197,7 @@ class PostsList extends React.Component { netVoteSign={item.netVoteSign} authorRepLog10={item.authorRepLog10} onClick={this.onPostClick} + nsfwPref={nsfwPref} /> ) @@ -217,7 +232,7 @@ export default connect( (state, props) => { const pathname = state.app.get('location').pathname; const current = state.user.get('current') - const username = current ? current.get('username') : null + const username = current ? current.get('username') : state.offchain.get('account') const content = state.global.get('content'); const ignore_result = state.global.getIn(['follow', 'get_following', username, 'ignore_result']); return {...props, username, content, ignore_result, pathname}; diff --git a/app/components/cards/PostsList.scss b/app/components/cards/PostsList.scss index 76d01ff53c..b8c713f723 100644 --- a/app/components/cards/PostsList.scss +++ b/app/components/cards/PostsList.scss @@ -5,13 +5,13 @@ width: 100%; height: 100%; z-index: 300; - overflow: scroll; + overflow-x: hidden; + overflow-y: scroll; background-color: $white; // padding: 0 .9rem; + -webkit-overflow-scrolling: touch; } - - .PostsList__post_top_overlay { position: fixed; top: 0; @@ -19,7 +19,8 @@ width: 100%; z-index: 310; height: 2.5rem; - overflow: hidden; + overflow-x: hidden; + overflow-y: scroll; border-bottom: 1px solid $light-gray; } @@ -47,10 +48,10 @@ } .PostsList__post_container { - position: relative; - background-color: $white; - margin: 1rem auto; - padding: 2rem 0.9rem 0 0.9rem; + position: relative; + background-color: $white; + margin: 1rem auto; + padding: 2rem 0.9rem 0 0.9rem; .PostFull { background-color: $white; } @@ -84,21 +85,7 @@ body.with-post-overlay { } } -@media screen and (max-width: 66rem) { - .PostsList__post_container { - overflow-y: auto; - -webkit-overflow-scrolling: touch; - height: 100%; - } -} - @media screen and (min-width: 67rem) { - - .PostsList__post_overlay { - overflow-y: auto; - -webkit-overflow-scrolling: touch; - } - .PostsList__post_container { width: 62rem; } diff --git a/app/components/elements/ShareMenu.scss b/app/components/elements/ShareMenu.scss index e764347c40..0d52598e82 100644 --- a/app/components/elements/ShareMenu.scss +++ b/app/components/elements/ShareMenu.scss @@ -20,3 +20,7 @@ text-decoration: none; } } + +.RightShare__Menu { + padding-right: 0.6rem!important; +} diff --git a/app/components/elements/TagList.jsx b/app/components/elements/TagList.jsx index 786bb4b30e..935070c740 100644 --- a/app/components/elements/TagList.jsx +++ b/app/components/elements/TagList.jsx @@ -8,7 +8,7 @@ export default ({post, horizontal, single}) => { if (single) return {post.category}; - let json = post.json_metadata; + const json = post.json_metadata; let tags = [] try { @@ -18,6 +18,9 @@ export default ({post, horizontal, single}) => { tags = json && JSON.parse(json).tags || []; } if(typeof tags == 'string') tags = tags.split(' '); + if(!Array.isArray(tags)) { + tags = []; + } } catch(e) { tags = [] } @@ -29,14 +32,12 @@ export default ({post, horizontal, single}) => { tags = tags.filter( (value, index, self) => value && (self.indexOf(value) === index) ) if (horizontal) { // show it as a dropdown in Preview - const list = tags.map( (tag,idx) => {tag} ) + const list = tags.map( (tag, idx) => {tag} ) return
{list}
; - } else { - if(tags.length == 1) { - return {tags[0]} - } else { - const list = tags.map(function (tag) {return {value: tag, link: `/${sort_order}/${tag}`}}); - return ; - } } + if(tags.length == 1) { + return {tags[0]} + } + const list = tags.map(tag => {return {value: tag, link: `/${sort_order}/${tag}`}}); + return ; } diff --git a/app/components/modules/Header.jsx b/app/components/modules/Header.jsx index 091e89aa44..7451d9e842 100644 --- a/app/components/modules/Header.jsx +++ b/app/components/modules/Header.jsx @@ -196,7 +196,7 @@ class Header extends React.Component {
  • steemitbeta
  • {(topic_link || user_name || page_name) &&
  • |
  • } {topic_link &&
  • {topic_link}
  • } - {user_name &&
  • {user_name}
  • } + {user_name &&
  • @{user_name}
  • } {page_name &&
  • {page_name}
  • } {(topic_link || user_name || page_name) && sort_order &&
  • |
  • } {selected_sort_order && } diff --git a/app/components/modules/Settings.jsx b/app/components/modules/Settings.jsx index 356e8c9602..2747d34aea 100644 --- a/app/components/modules/Settings.jsx +++ b/app/components/modules/Settings.jsx @@ -16,6 +16,7 @@ class Settings extends React.Component { constructor(props) { super() this.initForm(props) + this.onNsfwPrefChange = this.onNsfwPrefChange.bind(this) } state = { @@ -41,6 +42,19 @@ class Settings extends React.Component { this.state.accountSettings.handleSubmit(args => this.handleSubmit(args)) } + componentWillMount() { + const {accountname} = this.props + const nsfwPref = (process.env.BROWSER ? localStorage.getItem('nsfwPref-' + accountname) : null) || 'warn' + this.setState({nsfwPref}) + } + + onNsfwPrefChange(e) { + const nsfwPref = e.currentTarget.value; + const {accountname} = this.props; + localStorage.setItem('nsfwPref-'+accountname, nsfwPref) + this.setState({nsfwPref}) + } + handleSubmit = ({updateInitialValues}) => { let {metaData} = this.props if (!metaData) metaData = {} @@ -147,6 +161,7 @@ class Settings extends React.Component {
    */}
    +

    Profile

    + + {isOwnAccount && +
    +
    +

    +

    Content Preferences

    +
    + Not safe for work (NSFW) +
    + +
    +
    } {ignores && ignores.size > 0 &&
    @@ -213,6 +244,7 @@ export default connect( return { account, metaData, + accountname, isOwnAccount: username == accountname, profile, follow: state.global.get('follow'), diff --git a/app/components/pages/Post.jsx b/app/components/pages/Post.jsx index 3a4e0fd7aa..ae8f56c801 100644 --- a/app/components/pages/Post.jsx +++ b/app/components/pages/Post.jsx @@ -143,8 +143,8 @@ class Post extends React.Component { ); - let sort_orders = [ 'trending', 'active', 'new', 'updated' ]; - let sort_labels = [ translate('trending'), translate('active'), translate('new'), translate('updated') ]; + let sort_orders = [ 'trending', 'votes', 'new']; + let sort_labels = [ translate('trending'), translate('votes'), translate('new') ]; let sort_menu = []; let selflink = `/${dis.get('category')}/@${post}`; diff --git a/app/components/pages/PostsIndex.jsx b/app/components/pages/PostsIndex.jsx index 90235bc3ef..5ec09ed814 100644 --- a/app/components/pages/PostsIndex.jsx +++ b/app/components/pages/PostsIndex.jsx @@ -21,7 +21,7 @@ class PostsIndex extends React.Component { routeParams: PropTypes.object, requestData: PropTypes.func, loading: PropTypes.bool, - current_user: PropTypes.object + username: PropTypes.string }; static defaultProps = { @@ -73,7 +73,7 @@ class PostsIndex extends React.Component { order = 'by_feed'; topics_order = 'trending'; posts = this.props.accounts.getIn([account_name, 'feed']); - const isMyAccount = this.props.current_user && this.props.current_user.get('username') === account_name; + const isMyAccount = this.props.username === account_name; if (isMyAccount) { emptyText =
    Looks like you haven't followed anything yet.

    @@ -131,7 +131,7 @@ module.exports = { status: state.global.get('status'), loading: state.app.get('loading'), accounts: state.global.get('accounts'), - current_user: state.user.get('current') + username: state.user.getIn(['current', 'username']) || state.offchain.get('account'), }; }, (dispatch) => { diff --git a/app/components/pages/UserProfile.jsx b/app/components/pages/UserProfile.jsx index ffcb41bda1..cb8c0737a5 100644 --- a/app/components/pages/UserProfile.jsx +++ b/app/components/pages/UserProfile.jsx @@ -40,6 +40,8 @@ export default class UserProfile extends React.Component { shouldComponentUpdate(np) { const {follow} = this.props; + const {follow_count} = this.props; + let followersLoading = false, npFollowersLoading = false; let followingLoading = false, npFollowingLoading = false; @@ -62,7 +64,8 @@ export default class UserProfile extends React.Component { ((npFollowingLoading !== followingLoading) && !npFollowingLoading) || np.loading !== this.props.loading || np.location.pathname !== this.props.location.pathname || - np.routeParams.accountname !== this.props.routeParams.accountname + np.routeParams.accountname !== this.props.routeParams.accountname || + np.follow_count !== this.props.follow_count ) } @@ -118,11 +121,22 @@ export default class UserProfile extends React.Component { } else { return
    {translate('unknown_account')}
    } - const followers = follow && follow.getIn(['get_followers', accountname]); const following = follow && follow.getIn(['get_following', accountname]); - const followerCount = followers && followers.get('blog_count') - const followingCount = following && following.get('blog_count') + + // instantiate following items + let totalCounts = this.props.follow_count; + let followerCount = ""; + let followingCount = ""; + + if (totalCounts && accountname) { + totalCounts = totalCounts.get(accountname); + if (totalCounts) { + totalCounts = totalCounts.toJS(); + followerCount = totalCounts.follower_count; + followingCount = totalCounts.following_count; + } + } const rep = repLog10(account.reputation); @@ -382,11 +396,11 @@ export default class UserProfile extends React.Component { {about &&

    {about}

    }
    - {followerCount ? translate('follower_count', {followerCount}) : translate('followers')} + {translate('follower_count', {followerCount})} {isMyAccount && } {translate('post_count', {postCount: account.post_count || 0})} - {followingCount ? translate('followed_count', {followingCount}) : translate('following')} + {translate('followed_count', {followingCount})}

    {location && {location}} @@ -429,7 +443,8 @@ module.exports = { loading: state.app.get('loading'), global_status: state.global.get('status'), accounts: state.global.get('accounts'), - follow: state.global.get('follow') + follow: state.global.get('follow'), + follow_count: state.global.get('follow_count') }; }, dispatch => ({ diff --git a/app/locales/en.js b/app/locales/en.js index 81303b32e6..5f0bfb4bc9 100644 --- a/app/locales/en.js +++ b/app/locales/en.js @@ -207,7 +207,7 @@ const en = { i_understand_dont_show_again: "I understand, don't show me again", ok: 'Ok', convert_to_LIQUID_TOKEN: 'Convert to ' + LIQUID_TOKEN, - DEBT_TOKEN_will_be_unavailable: 'This action will take place 3.5 days from now and can not be canceled. These ' + DEBT_TOKEN + ' will immediatly become unavailable', + DEBT_TOKEN_will_be_unavailable: 'This action will take place 3.5 days from now and can not be canceled. These ' + DEBT_TOKEN + ' will immediately become unavailable', amount: 'Amount', convert: 'Convert', invalid_amount: 'Invalid amount', diff --git a/app/locales/es.js b/app/locales/es.js index 923402e2b2..69c6f77674 100644 --- a/app/locales/es.js +++ b/app/locales/es.js @@ -188,7 +188,7 @@ const es = { i_understand_dont_show_again: "I understand, don't show me again", ok: 'Ok', convert_to_steem: 'Convertir a Steem', - steem_dollars_will_be_unavailable: 'This action will take place 3.5 days from now and can not be canceled. These Steem Dollars will immediatly become unavailable', + steem_dollars_will_be_unavailable: 'This action will take place 3.5 days from now and can not be canceled. These Steem Dollars will immediately become unavailable', amount: 'Cantidad', steem_dollars: 'STEEM DOLLARS', convert: 'Convertir', diff --git a/app/locales/es_AR.js b/app/locales/es_AR.js index e765cace64..0d95799887 100644 --- a/app/locales/es_AR.js +++ b/app/locales/es_AR.js @@ -188,7 +188,7 @@ const es_AR = { i_understand_dont_show_again: "I understand, don't show me again", ok: 'Ok', convert_to_steem: 'Convertir a Steem', - steem_dollars_will_be_unavailable: 'This action will take place 3.5 days from now and can not be canceled. These Steem Dollars will immediatly become unavailable', + steem_dollars_will_be_unavailable: 'This action will take place 3.5 days from now and can not be canceled. These Steem Dollars will immediately become unavailable', amount: 'Cantidad', steem_dollars: 'STEEM DOLLARS', convert: 'Convertir', diff --git a/app/redux/FetchDataSaga.js b/app/redux/FetchDataSaga.js index 8fb80af62d..d23537af60 100644 --- a/app/redux/FetchDataSaga.js +++ b/app/redux/FetchDataSaga.js @@ -1,6 +1,6 @@ import {takeLatest, takeEvery} from 'redux-saga'; import {call, put, select, fork} from 'redux-saga/effects'; -import {loadFollows} from 'app/redux/FollowSaga'; +import {loadFollows, fetchFollowCount} from 'app/redux/FollowSaga'; import {getContent} from 'app/redux/SagaShared'; import Apis from 'shared/api_client/ApiInstances'; import GlobalReducer from './GlobalReducer'; @@ -21,16 +21,23 @@ export function* getContentCaller(action) { yield getContent(action.payload); } +let is_initial_state = true; export function* fetchState(location_change_action) { const {pathname} = location_change_action.payload; const m = pathname.match(/^\/@([a-z0-9\.-]+)/) if(m && m.length === 2) { const username = m[1] + yield fork(fetchFollowCount, username) yield fork(loadFollows, "get_followers", username, 'blog') yield fork(loadFollows, "get_following", username, 'blog') } + + // `ignore_fetch` case should only trigger on initial page load. No need to call + // fetchState immediately after loading fresh state from the server. Details: #593 const server_location = yield select(state => state.offchain.get('server_location')); - if (pathname === server_location) return; + const ignore_fetch = (pathname === server_location && is_initial_state) + is_initial_state = false; + if(ignore_fetch) return; let url = `${pathname}`; if (url === '/') url = 'trending'; diff --git a/app/redux/FollowSaga.js b/app/redux/FollowSaga.js index fc5bc52131..899a82f4c0 100644 --- a/app/redux/FollowSaga.js +++ b/app/redux/FollowSaga.js @@ -6,6 +6,16 @@ import {Apis} from 'shared/api_client'; This loadFollows both 'blog' and 'ignore' */ +//fetch for follow/following count +export function* fetchFollowCount(account) { + const counts = yield call(Apis.follow, 'get_follow_count', account) + yield put({type: 'global/UPDATE', payload: { + key: ['follow_count', account], + updater: m => m.mergeDeep({'follower_count': counts.follower_count, + 'following_count': counts.following_count}) + }}) +} + // Test limit with 2 (not 1, infinate looping) export function* loadFollows(method, account, type, force = false) { if(yield select(state => state.global.getIn(['follow', method, account, type + '_loading']))) { diff --git a/app/redux/TransactionSaga.js b/app/redux/TransactionSaga.js index 9cf73ea227..b10e7e6db9 100644 --- a/app/redux/TransactionSaga.js +++ b/app/redux/TransactionSaga.js @@ -156,7 +156,8 @@ function* broadcastOperation({payload: yield call(broadcast, {payload}) let eventType = type.replace(/^([a-z])/, g => g.toUpperCase()).replace(/_([a-z])/g, g => g[1].toUpperCase()); if (eventType === 'Comment' && !operation.parent_author) eventType = 'Post'; - serverApiRecordEvent(eventType, '') + const page = eventType === 'Vote' ? `@${operation.author}/${operation.permlink}` : ''; + serverApiRecordEvent(eventType, page); } catch(error) { console.error('TransactionSage', error) if(errorCallback) errorCallback(error.toString()) diff --git a/app/utils/ExtractContent.js b/app/utils/ExtractContent.js index 5d8e8cec89..d85c318f08 100644 --- a/app/utils/ExtractContent.js +++ b/app/utils/ExtractContent.js @@ -37,20 +37,19 @@ export default function extractContent(get, content) { if (category) link = `/${category}${link}`; const body = get(content, 'body'); let jsonMetadata = {} + let image_link try { jsonMetadata = JSON.parse(json_metadata) + // First, attempt to find an image url in the json metadata + if(jsonMetadata) { + if(jsonMetadata.image && Array.isArray(jsonMetadata.image)) { + [image_link] = jsonMetadata.image + } + } } catch(error) { // console.error('Invalid json metadata string', json_metadata, 'in post', author, permlink); } - // First, attempt to find an image url in the json metadata - let image_link - if(jsonMetadata) { - if(jsonMetadata.image) { - [image_link] = jsonMetadata.image - } - } - // If nothing found in json metadata, parse body and check images/links if(!image_link) { let rtags diff --git a/app/utils/NormalizeProfile.js b/app/utils/NormalizeProfile.js index c7c414ab7c..b3f48f5998 100644 --- a/app/utils/NormalizeProfile.js +++ b/app/utils/NormalizeProfile.js @@ -23,6 +23,10 @@ export default function normalizeProfile(account) { if(md.profile) { profile = md.profile; } + if(!(typeof profile == 'object')) { + console.error('Expecting object in account.json_metadata.profile:', profile); + profile = {}; + } } catch (e) { console.error('Invalid json metadata string', account.json_metadata, 'in account', account.name); } diff --git a/app/utils/ServerApiClient.js b/app/utils/ServerApiClient.js index 2e97749250..3ba01bf7b2 100644 --- a/app/utils/ServerApiClient.js +++ b/app/utils/ServerApiClient.js @@ -49,20 +49,21 @@ export function markNotificationRead(account, fields) { }); } -let last_page, last_views; +let last_page, last_views, last_page_promise; export function recordPageView(page, ref) { - if (page === last_page) return Promise.resolve(last_views); + if (last_page_promise && page === last_page) return last_page_promise; if (window.ga) { // virtual pageview window.ga('set', 'page', page); window.ga('send', 'pageview'); } if (!process.env.BROWSER || window.$STM_ServerBusy) return Promise.resolve(0); const request = Object.assign({}, request_base, {body: JSON.stringify({csrf: $STM_csrf, page, ref})}); - return fetch(`/api/v1/page_view`, request).then(r => r.json()).then(res => { - last_page = page; + last_page_promise = fetch(`/api/v1/page_view`, request).then(r => r.json()).then(res => { last_views = res.views; return last_views; }); + last_page = page; + return last_page_promise; } if (process.env.BROWSER) { diff --git a/app/utils/StateFunctions.js b/app/utils/StateFunctions.js index b55f8a55f3..923a97e6f4 100644 --- a/app/utils/StateFunctions.js +++ b/app/utils/StateFunctions.js @@ -76,5 +76,21 @@ export function contentStats(content) { const hide = authorRepLog10 < 0 && !hasPendingPayout && !hasReplies // rephide const pictures = !gray - return {hide, gray, pictures, netVoteSign, hasPendingPayout, authorRepLog10, hasReplies, hasFlag} + // Combine tags+category to check nsfw status + const json = content.get('json_metadata') + let tags = [] + try { + tags = json && JSON.parse(json).tags || []; + if(typeof tags == 'string') { + tags = [tags]; + } if(!Array.isArray(tags)) { + tags = []; + } + } catch(e) { + tags = [] + } + tags.push(content.get('category')) + const isNsfw = tags.filter(tag => tag.match(/^nsfw$/i)).length > 0; + + return {hide, gray, pictures, netVoteSign, hasPendingPayout, authorRepLog10, hasReplies, hasFlag, isNsfw} } diff --git a/server/api/general.js b/server/api/general.js index 7c37eda49c..e74d7720af 100644 --- a/server/api/general.js +++ b/server/api/general.js @@ -272,11 +272,11 @@ export default function useGeneralApi(app) { const {csrf, type, value} = typeof(params) === 'string' ? JSON.parse(params) : params; if (!checkCSRF(this, csrf)) return; console.log('-- /record_event -->', this.session.uid, type, value); + const str_value = typeof value === 'string' ? value : JSON.stringify(value); if (type.match(/^[A-Z]/)) { - mixpanel.track(type, {distinct_id: this.session.uid}); + mixpanel.track(type, {distinct_id: this.session.uid, Page: str_value}); mixpanel.people.increment(this.session.uid, type, 1); } else { - const str_value = typeof value === 'string' ? value : JSON.stringify(value); recordWebEvent(this, type, str_value); } this.body = JSON.stringify({status: 'ok'}); diff --git a/server/json/user_json.jsx b/server/json/user_json.jsx new file mode 100644 index 0000000000..3c23f5ffae --- /dev/null +++ b/server/json/user_json.jsx @@ -0,0 +1,30 @@ +import koa_router from 'koa-router'; +import React from 'react'; +import {routeRegex} from "app/ResolveRoute"; +import Apis from 'shared/api_client/ApiInstances' + +export default function useUserJson(app) { + const router = koa_router(); + app.use(router.routes()); + + router.get(routeRegex.UserJson, function *() { + // validate and build user details in JSON + const segments = this.url.split('/'); + const user_name = segments[1].replace('@',''); + let user = ""; + let status = ""; + + const [chainAccount] = yield Apis.db_api('get_accounts', [user_name]); + + if(chainAccount) { + user = chainAccount; + status = "200"; + } else { + user = "No account found"; + status = "404"; + } + // return response and status code + this.body = {user, status}; + }); + +} diff --git a/server/server.js b/server/server.js index 941c1cdcd2..8c6b177f5d 100644 --- a/server/server.js +++ b/server/server.js @@ -15,6 +15,7 @@ import useAccountRecoveryApi from './api/account_recovery'; import useNotificationsApi from './api/notifications'; import useEnterAndConfirmEmailPages from './server_pages/enter_confirm_email'; import useEnterAndConfirmMobilePages from './server_pages/enter_confirm_mobile'; +import useUserJson from './json/user_json'; import isBot from 'koa-isbot'; import session from '@steem/crypto-session'; import csrf from 'koa-csrf'; @@ -22,7 +23,8 @@ import flash from 'koa-flash'; import minimist from 'minimist'; import Grant from 'grant-koa'; import config from '../config'; -import secureRandom from 'secure-random' +import {routeRegex} from 'app/ResolveRoute'; +import secureRandom from 'secure-random'; const grant = new Grant(config.grant); // import uploadImage from 'server/upload-image' //medium-editor @@ -49,20 +51,20 @@ app.use(function *(next) { return; } // normalize user name url from cased params - if (this.method === 'GET' && /^\/(@[\w\.\d-]+)\/?$/.test(this.url)) { + if (this.method === 'GET' && (routeRegex.UserProfile1.test(this.url) || routeRegex.PostNoCategory.test(this.url))) { const p = this.originalUrl.toLowerCase(); if(p !== this.originalUrl) { + this.status = 301; this.redirect(p); return; } } // normalize top category filtering from cased params - if (this.method === 'GET' && /^\/(hot|created|trending|active)\//.test(this.url)) { - const segments = this.url.split('/') - const category = segments[2] - if(category !== category.toLowerCase()) { - segments[2] = category.toLowerCase() - this.redirect(segments.join('/')); + if (this.method === 'GET' && routeRegex.CategoryFilters.test(this.url)) { + const p = this.originalUrl.toLowerCase(); + if(p !== this.originalUrl) { + this.status = 301; + this.redirect(p); return; } } @@ -125,6 +127,8 @@ app.use(function* (next) { useRedirects(app); useEnterAndConfirmEmailPages(app); useEnterAndConfirmMobilePages(app); +useUserJson(app); + if (env === 'production') { app.use(helmet.contentSecurityPolicy(config.helmet)); diff --git a/server/server_pages/enter_confirm_mobile.jsx b/server/server_pages/enter_confirm_mobile.jsx index 62a1a0da60..58778d4807 100644 --- a/server/server_pages/enter_confirm_mobile.jsx +++ b/server/server_pages/enter_confirm_mobile.jsx @@ -19,13 +19,20 @@ const assets_file = process.env.NODE_ENV === 'production' ? 'tmp/webpack-stats-p const assets = Object.assign({}, require(assets_file), {script: []}); function *confirmMobileHandler() { + if (!checkCSRF(this, this.request.body.csrf)) return; const confirmation_code = this.params && this.params.code ? this.params.code : this.request.body.code; console.log('-- /confirm_mobile -->', this.session.uid, this.session.user, confirmation_code); - const mid = yield models.Identity.findOne( + let mid = yield models.Identity.findOne( {attributes: ['id', 'user_id', 'verified', 'updated_at', 'phone'], where: {user_id: this.session.user, confirmation_code, provider: 'phone'}, order: 'id DESC'} ); if (!mid) { + mid = yield models.Identity.findOne( + {attributes: ['id'], where: {user_id: this.session.user, provider: 'phone'}, order: 'id DESC'} + ); + if (mid) { + yield mid.destroy({force: true}); + } this.flash = {error: 'Wrong confirmation code.'}; this.redirect('/enter_mobile'); return; @@ -207,6 +214,7 @@ export default function useEnterAndConfirmMobilePages(app) {

    +