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