From d8e45929835c1e244babca8a9124de920ea9fac4 Mon Sep 17 00:00:00 2001 From: originate Date: Tue, 22 Nov 2016 22:49:38 -0500 Subject: [PATCH 01/90] Display previous title when closing post modal, keep previous title --- app/components/cards/PostsList.jsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/components/cards/PostsList.jsx b/app/components/cards/PostsList.jsx index 067fbb77d4..cd28286b49 100644 --- a/app/components/cards/PostsList.jsx +++ b/app/components/cards/PostsList.jsx @@ -102,11 +102,17 @@ class PostsList extends React.Component { if (!inside_top_bar) { const post_overlay = document.getElementById('post_overlay'); if (post_overlay) post_overlay.removeEventListener('click', this.closeOnOutsideClick); - this.setState({showPost: null}); + window.document.title = this.state.prevTitle; + this.setState({showPost: null, prevTitle: null}); } } } + closeOnButton = () => { + window.document.title = this.state.prevTitle; + this.setState({showPost: null, prevTitle: null}); + } + fetchIfNeeded() { this.scrollListener(); } @@ -150,7 +156,7 @@ class PostsList extends React.Component { onPostClick(post, url) { this.post_url = url; this.props.fetchState(url); - this.setState({showPost: post}); + this.setState({showPost: post, prevTitle: window.document.title}); window.history.pushState({}, '', url); } @@ -178,7 +184,7 @@ class PostsList extends React.Component { - {this.setState({showPost: null})}} /> +
From fb7538db862fbb649c855559bf5030dd91a8b22a Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 23 Nov 2016 15:54:21 -0500 Subject: [PATCH 02/90] remove negative top margin on comment footer, decrease last paragraph bottom margin instead, fix #677 --- app/components/cards/Comment.scss | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/components/cards/Comment.scss b/app/components/cards/Comment.scss index 0156f8fdb8..293a0096c1 100644 --- a/app/components/cards/Comment.scss +++ b/app/components/cards/Comment.scss @@ -1,8 +1,14 @@ .Comment { clear: both; margin-bottom: 2.4rem; - .Markdown p { - margin: 0.1rem 0 0.6rem 0; + .Markdown { + p { + margin: 0.1rem 0 0.6rem 0; + } + + p:last-child { + margin-bottom: 0.2rem; + } } } @@ -90,7 +96,6 @@ .Comment__footer { margin-left: 62px; - margin-top: -0.4rem; color: $dark-gray; a { color: $dark-gray; From 7b95c8b8cb14945862351228c5558926c2f16487 Mon Sep 17 00:00:00 2001 From: originate Date: Thu, 24 Nov 2016 03:16:39 -0500 Subject: [PATCH 03/90] Add padding for avatar on collapsed state for better alignment --- app/components/modules/Header.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/components/modules/Header.scss b/app/components/modules/Header.scss index 539a30b67b..82c3dc4912 100644 --- a/app/components/modules/Header.scss +++ b/app/components/modules/Header.scss @@ -51,6 +51,11 @@ display: flex; text-transform: lowercase; } + @media screen and (max-width: 39.9375em) { + .shrink { + padding: .3rem 1rem; + } + } } ul > li.Header__top-logo > a { From 1d8a482d826ebd39dc38520d3cdd4f70b7693fba Mon Sep 17 00:00:00 2001 From: Sigve Date: Sat, 26 Nov 2016 15:23:28 +0100 Subject: [PATCH 04/90] Update mysql README instructions for Ubuntu 16.x --- README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6098415637..a0ba5a3c8f 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ cp steem-example.json steem-dev.json (note: it's steem.json in production) #### Install mysql server - + OS X : ```bash @@ -55,12 +55,23 @@ sudo apt-get update sudo apt-get install mysql-server ``` +On Ubuntu 16.04+ you may be unable to connect to mysql without root access, if +so update the mysql root user as follows:: + +``` +sudo mysql -u root +DROP USER 'root'@'localhost'; +CREATE USER 'root'@'%' IDENTIFIED BY ''; +GRANT ALL PRIVILEGES ON *.* TO 'root'@'%'; +FLUSH PRIVILEGES; +``` + Now launch mysql client and create steemit_dev database: ```bash mysql -u root > create database steemit_dev; ``` - + Install `sequelize-cli` globally: ```bash From 2779827bf1e3d5c882ccbd2d5cecbbed2e1eb43e Mon Sep 17 00:00:00 2001 From: valzav Date: Sat, 26 Nov 2016 09:04:57 -0700 Subject: [PATCH 05/90] add page_view api call; add PageViewsCounter component --- app/assets/icons/eye.svg | 15 ++++++++ app/components/cards/PostFull.jsx | 16 +++++---- app/components/cards/PostFull.scss | 9 +++++ app/components/elements/Icon.jsx | 1 + app/components/elements/PageViewsCounter.jsx | 37 ++++++++++++++++++++ app/utils/ServerApiClient.js | 9 +++++ server/api/general.js | 17 +++++++++ 7 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 app/assets/icons/eye.svg create mode 100644 app/components/elements/PageViewsCounter.jsx diff --git a/app/assets/icons/eye.svg b/app/assets/icons/eye.svg new file mode 100644 index 0000000000..4b83a783f6 --- /dev/null +++ b/app/assets/icons/eye.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/app/components/cards/PostFull.jsx b/app/components/cards/PostFull.jsx index 08c73e640d..31ac5ff4f2 100644 --- a/app/components/cards/PostFull.jsx +++ b/app/components/cards/PostFull.jsx @@ -21,6 +21,7 @@ import {Long} from 'bytebuffer' import {List} from 'immutable' import {repLog10, parsePayoutAmount} from 'app/utils/ParsersAndFormatters'; import DMCAList from 'app/utils/DMCAList' +import PageViewsCounter from 'app/components/elements/PageViewsCounter'; function TimeAuthorCategory({content, authorRepLog10, showTags}) { return ( @@ -263,17 +264,20 @@ class PostFull extends React.Component {
{!readonly && } + {!readonly && + + {showReplyOption && Reply} + {' '}{showEditOption && !showEdit && Edit} + {' '}{showDeleteOption && !showReply && Delete} + } {content.children} - {!readonly && - - {showReplyOption && Reply} - {' '}{showEditOption && !showEdit && Edit} - {' '}{showDeleteOption && !showReply && Delete} - } + + +
diff --git a/app/components/cards/PostFull.scss b/app/components/cards/PostFull.scss index 4ccbf22024..026f0911e1 100644 --- a/app/components/cards/PostFull.scss +++ b/app/components/cards/PostFull.scss @@ -81,8 +81,17 @@ } .PostFull__responses { + padding-right: 1rem; + //margin-right: 1rem; + //border-right: 1px solid $medium-gray; +} + +.PostFull__views { padding-right: 1rem; margin-right: 1rem; + color: $dark-gray; + font-size: 94%; + font-weight: 600; border-right: 1px solid $medium-gray; } diff --git a/app/components/elements/Icon.jsx b/app/components/elements/Icon.jsx index 4978a89837..37abd94d85 100644 --- a/app/components/elements/Icon.jsx +++ b/app/components/elements/Icon.jsx @@ -32,6 +32,7 @@ const icons = [ 'photo', 'line', 'video', + 'eye', ]; const icons_map = {}; for (const i of icons) icons_map[i] = require(`app/assets/icons/${i}.svg`); diff --git a/app/components/elements/PageViewsCounter.jsx b/app/components/elements/PageViewsCounter.jsx new file mode 100644 index 0000000000..912d2a4c25 --- /dev/null +++ b/app/components/elements/PageViewsCounter.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +// import {connect} from 'react-redux'; +import {recordPageView} from 'app/utils/ServerApiClient'; +import Icon from 'app/components/elements/Icon'; +import pluralize from 'pluralize'; + +export default class PageViewsCounter extends React.Component { + + static propTypes = { + page: React.PropTypes.string + }; + + constructor(props) { + super(props); + this.state = {views: 0}; + } + + shouldComponentUpdate(nextProps, nextState) { + return nextState.views !== this.state.views; + } + + componentDidMount() { + recordPageView(this.props.page).then(views => this.setState({views})); + } + + render() { + const views = this.state.views; + return + {views} + ; + } + +} + +// export default connect(null, dispatch => ({ +// update: (payload) => { dispatch({type: 'UPDATE_NOTIFICOUNTERS', payload})}, +// }))(MarkNotificationRead); diff --git a/app/utils/ServerApiClient.js b/app/utils/ServerApiClient.js index 16e676f8b6..fea198e455 100644 --- a/app/utils/ServerApiClient.js +++ b/app/utils/ServerApiClient.js @@ -49,6 +49,15 @@ export function markNotificationRead(account, fields) { }); } +export function recordPageView(page) { + if (!process.env.BROWSER || window.$STM_ServerBusy) return Promise.resolve(null); + const request = Object.assign({}, request_base, {body: JSON.stringify({csrf: $STM_csrf, page})}); + console.log('-- recordPageView -->', request); + return fetch(`/api/v1/page_view`, request).then(r => r.json()).then(res => { + return res.views; + }); +} + if (process.env.BROWSER) { window.getNotifications = getNotifications; window.markNotificationRead = markNotificationRead; diff --git a/server/api/general.js b/server/api/general.js index 9acecd13f7..19d5f6d7cd 100644 --- a/server/api/general.js +++ b/server/api/general.js @@ -240,6 +240,23 @@ export default function useGeneralApi(app) { console.log('-- /csp_violation -->', this.req.headers['user-agent'], params); this.body = ''; }); + + router.post('/page_view', koaBody, function *() { + const params = this.request.body; + const {csrf, page, ref} = typeof(params) === 'string' ? JSON.parse(params) : params; + console.log('-- /page_view csrf -->', csrf); + if (!checkCSRF(this, csrf)) return; + console.log('-- /page_view -->', this.session.uid, page, ref); + const remote_ip = getRemoteIp(this.req); + try { + const views = yield Tarantool.instance().call('page_view', page, remote_ip, this.session.uid, ref); + this.body = JSON.stringify({views}); + } catch (error) { + console.error('Error in /page_view api call', this.session.uid, error.message); + this.body = JSON.stringify({error: error.message}); + this.status = 500; + } + }); } import {Apis} from 'shared/api_client'; From d21f94667c42edba2350646411d66def48e8abad Mon Sep 17 00:00:00 2001 From: valzav Date: Sun, 27 Nov 2016 20:41:36 -0500 Subject: [PATCH 06/90] add PageViewsCounter to App.jsx so all pages are logged --- app/components/App.jsx | 2 ++ app/components/cards/PostFull.jsx | 2 +- app/components/elements/PageViewsCounter.jsx | 21 ++++++++++---------- app/utils/ServerApiClient.js | 9 ++++++--- server/api/general.js | 3 +-- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/app/components/App.jsx b/app/components/App.jsx index 45f885f7b6..11278c197d 100644 --- a/app/components/App.jsx +++ b/app/components/App.jsx @@ -16,6 +16,7 @@ import Icon from 'app/components/elements/Icon'; import {key_utils} from 'shared/ecc'; import MiniHeader from 'app/components/modules/MiniHeader'; import { translate } from '../Translator.js'; +import PageViewsCounter from 'app/components/elements/PageViewsCounter'; class App extends React.Component { constructor(props) { @@ -243,6 +244,7 @@ class App extends React.Component { + } } diff --git a/app/components/cards/PostFull.jsx b/app/components/cards/PostFull.jsx index 31ac5ff4f2..caaa20f249 100644 --- a/app/components/cards/PostFull.jsx +++ b/app/components/cards/PostFull.jsx @@ -276,7 +276,7 @@ class PostFull extends React.Component { - + diff --git a/app/components/elements/PageViewsCounter.jsx b/app/components/elements/PageViewsCounter.jsx index 912d2a4c25..8eb817ba82 100644 --- a/app/components/elements/PageViewsCounter.jsx +++ b/app/components/elements/PageViewsCounter.jsx @@ -1,5 +1,4 @@ import React from 'react'; -// import {connect} from 'react-redux'; import {recordPageView} from 'app/utils/ServerApiClient'; import Icon from 'app/components/elements/Icon'; import pluralize from 'pluralize'; @@ -7,31 +6,33 @@ import pluralize from 'pluralize'; export default class PageViewsCounter extends React.Component { static propTypes = { - page: React.PropTypes.string + hidden: React.PropTypes.bool + }; + + static defaultProps = { + hidden: true }; constructor(props) { super(props); this.state = {views: 0}; + this.last_page = null; } shouldComponentUpdate(nextProps, nextState) { - return nextState.views !== this.state.views; + return nextState.views !== this.state.views || window.location.pathname !== this.last_page; } - componentDidMount() { - recordPageView(this.props.page).then(views => this.setState({views})); + componentDidUpdate() { + recordPageView(window.location.pathname).then(views => this.setState({views})); + this.last_page = window.location.pathname; } render() { + if (this.props.hidden) return null; const views = this.state.views; return {views} ; } - } - -// export default connect(null, dispatch => ({ -// update: (payload) => { dispatch({type: 'UPDATE_NOTIFICOUNTERS', payload})}, -// }))(MarkNotificationRead); diff --git a/app/utils/ServerApiClient.js b/app/utils/ServerApiClient.js index fea198e455..835cabdb7c 100644 --- a/app/utils/ServerApiClient.js +++ b/app/utils/ServerApiClient.js @@ -49,12 +49,15 @@ export function markNotificationRead(account, fields) { }); } +let last_page, last_views; export function recordPageView(page) { - if (!process.env.BROWSER || window.$STM_ServerBusy) return Promise.resolve(null); + if (page === last_page) return Promise.resolve(last_views); + if (!process.env.BROWSER || window.$STM_ServerBusy) return Promise.resolve(0); const request = Object.assign({}, request_base, {body: JSON.stringify({csrf: $STM_csrf, page})}); - console.log('-- recordPageView -->', request); return fetch(`/api/v1/page_view`, request).then(r => r.json()).then(res => { - return res.views; + last_page = page; + last_views = res.views; + return last_views; }); } diff --git a/server/api/general.js b/server/api/general.js index 19d5f6d7cd..e0552f0afc 100644 --- a/server/api/general.js +++ b/server/api/general.js @@ -244,13 +244,12 @@ export default function useGeneralApi(app) { router.post('/page_view', koaBody, function *() { const params = this.request.body; const {csrf, page, ref} = typeof(params) === 'string' ? JSON.parse(params) : params; - console.log('-- /page_view csrf -->', csrf); if (!checkCSRF(this, csrf)) return; console.log('-- /page_view -->', this.session.uid, page, ref); const remote_ip = getRemoteIp(this.req); try { const views = yield Tarantool.instance().call('page_view', page, remote_ip, this.session.uid, ref); - this.body = JSON.stringify({views}); + this.body = JSON.stringify({views: views[0][0]}); } catch (error) { console.error('Error in /page_view api call', this.session.uid, error.message); this.body = JSON.stringify({error: error.message}); From d7aef315abe47d92d98a023842750b8883ed8f10 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 28 Nov 2016 11:56:56 -0500 Subject: [PATCH 07/90] switch to using onclick, remove menu close event. resolves #722 --- app/components/elements/DropdownMenu.jsx | 9 +++++---- app/components/elements/VerticalMenu.jsx | 6 +----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/app/components/elements/DropdownMenu.jsx b/app/components/elements/DropdownMenu.jsx index 34027f28c9..1a0c017703 100644 --- a/app/components/elements/DropdownMenu.jsx +++ b/app/components/elements/DropdownMenu.jsx @@ -24,7 +24,7 @@ export default class DropdownMenu extends React.Component { } componentWillUnmount() { - document.removeEventListener('mousedown', this.hide); + document.removeEventListener('click', this.hide); } toggle = (e) => { @@ -36,7 +36,7 @@ export default class DropdownMenu extends React.Component { show = (e) => { e.preventDefault(); this.setState({shown: true}); - document.addEventListener('mousedown', this.hide); + document.addEventListener('click', this.hide); }; hide = (e) => { @@ -44,8 +44,9 @@ export default class DropdownMenu extends React.Component { const inside_dropdown = !!findParent(e.target, 'VerticalMenu'); if (inside_dropdown) return; + e.preventDefault() this.setState({shown: false}); - document.removeEventListener('mousedown', this.hide); + document.removeEventListener('click', this.hide); }; navigate = (e) => { @@ -71,7 +72,7 @@ export default class DropdownMenu extends React.Component { {hasDropdown && } - if(hasDropdown) entry = {e.preventDefault()}}>{entry} + if(hasDropdown) entry = {entry} const menu = ; const cls = 'DropdownMenu' + (this.state.shown ? ' show' : '') + (className ? ` ${className}` : '') diff --git a/app/components/elements/VerticalMenu.jsx b/app/components/elements/VerticalMenu.jsx index 4492e647f0..d5e0cc4918 100644 --- a/app/components/elements/VerticalMenu.jsx +++ b/app/components/elements/VerticalMenu.jsx @@ -13,17 +13,13 @@ export default class VerticalMenu extends React.Component { ]), }; - closeMenu = () => { - document.body.click(); - } - render() { const {items, title, className, hideValue} = this.props; return
    {title &&
  • {title}
  • } {items.map(i => { if(i.value === hideValue) return null - return
  • + return
  • {i.link ? {i.icon && }{i.label ? i.label : i.value}   {i.addon} From 498135023bf6079c1f86d5435d657c0503e74d69 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 28 Nov 2016 17:18:39 -0500 Subject: [PATCH 08/90] restore auto-closing of menu, with some exceptions. #722 --- app/components/elements/VerticalMenu.jsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/components/elements/VerticalMenu.jsx b/app/components/elements/VerticalMenu.jsx index d5e0cc4918..992621ecdd 100644 --- a/app/components/elements/VerticalMenu.jsx +++ b/app/components/elements/VerticalMenu.jsx @@ -13,13 +13,21 @@ export default class VerticalMenu extends React.Component { ]), }; + closeMenu = (e) => { + // If this was not a left click, or if CTRL or CMD were held, do not close the menu. + if(e.button !== 0 || e.ctrlKey || e.metaKey) return; + + // Simulate clicking of document body which will close any open menus + document.body.click(); + } + render() { const {items, title, className, hideValue} = this.props; return
      {title &&
    • {title}
    • } {items.map(i => { if(i.value === hideValue) return null - return
    • + return
    • {i.link ? {i.icon && }{i.label ? i.label : i.value}   {i.addon} From 7c5093eb5148918242419aec2b854da6b3e55215 Mon Sep 17 00:00:00 2001 From: valzav Date: Mon, 28 Nov 2016 18:55:43 -0500 Subject: [PATCH 09/90] don't pass ref if it's not external --- app/components/elements/PageViewsCounter.jsx | 6 ++++-- app/utils/ServerApiClient.js | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/components/elements/PageViewsCounter.jsx b/app/components/elements/PageViewsCounter.jsx index 8eb817ba82..e801a3b4ae 100644 --- a/app/components/elements/PageViewsCounter.jsx +++ b/app/components/elements/PageViewsCounter.jsx @@ -24,13 +24,15 @@ export default class PageViewsCounter extends React.Component { } componentDidUpdate() { - recordPageView(window.location.pathname).then(views => this.setState({views})); + let ref = document.referrer || ''; + if (ref.match('://' + window.location.hostname)) ref = ''; + recordPageView(window.location.pathname, ref).then(views => this.setState({views})); this.last_page = window.location.pathname; } render() { - if (this.props.hidden) return null; const views = this.state.views; + if (this.props.hidden || !views) return null; return {views} ; diff --git a/app/utils/ServerApiClient.js b/app/utils/ServerApiClient.js index 835cabdb7c..6e3237e2d2 100644 --- a/app/utils/ServerApiClient.js +++ b/app/utils/ServerApiClient.js @@ -50,10 +50,10 @@ export function markNotificationRead(account, fields) { } let last_page, last_views; -export function recordPageView(page) { +export function recordPageView(page, ref) { if (page === last_page) return Promise.resolve(last_views); if (!process.env.BROWSER || window.$STM_ServerBusy) return Promise.resolve(0); - const request = Object.assign({}, request_base, {body: JSON.stringify({csrf: $STM_csrf, page})}); + 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_views = res.views; From 0fa270e68717cf0d11419bc041ffa121a4da578d Mon Sep 17 00:00:00 2001 From: Sigve Date: Tue, 29 Nov 2016 11:50:31 -0500 Subject: [PATCH 10/90] Add warning for orders above/below market price (15%) fix #436 --- .../modules/ConfirmTransactionForm.jsx | 7 +++-- app/components/pages/Market.jsx | 29 ++++++++++++------- app/redux/Transaction.js | 4 ++- app/redux/TransactionSaga.js | 4 +-- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/app/components/modules/ConfirmTransactionForm.jsx b/app/components/modules/ConfirmTransactionForm.jsx index 5415b8aca6..4f06c603e8 100644 --- a/app/components/modules/ConfirmTransactionForm.jsx +++ b/app/components/modules/ConfirmTransactionForm.jsx @@ -7,7 +7,7 @@ class ConfirmTransactionForm extends Component { static propTypes = { //Steemit onCancel: PropTypes.func, - + warning: PropTypes.string, // redux-form confirm: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), confirmBroadcastOperation: PropTypes.object, @@ -25,13 +25,14 @@ class ConfirmTransactionForm extends Component { } render() { const {onCancel, okClick} = this - const {confirm, confirmBroadcastOperation} = this.props + const {confirm, confirmBroadcastOperation, warning} = this.props const conf = typeof confirm === 'function' ? confirm() : confirm return (

      {typeName(confirmBroadcastOperation)}


      {conf}
      + {warning ?
      {warning}
      : null}
      @@ -52,10 +53,12 @@ export default connect( const confirmBroadcastOperation = state.transaction.get('confirmBroadcastOperation') const confirmErrorCallback = state.transaction.get('confirmErrorCallback') const confirm = state.transaction.get('confirm') + const warning = state.transaction.get('warning') return { confirmBroadcastOperation, confirmErrorCallback, confirm, + warning } }, // mapDispatchToProps diff --git a/app/components/pages/Market.jsx b/app/components/pages/Market.jsx index f303231811..7b5a950035 100644 --- a/app/components/pages/Market.jsx +++ b/app/components/pages/Market.jsx @@ -60,7 +60,8 @@ class Market extends React.Component { const amount_to_sell = parseFloat(ReactDOM.findDOMNode(this.refs.buySteem_total).value) const min_to_receive = parseFloat(ReactDOM.findDOMNode(this.refs.buySteem_amount).value) const price = (amount_to_sell / min_to_receive).toFixed(6) - placeOrder(user, amount_to_sell + " SBD", min_to_receive + " STEEM", "$" + price + "/STEEM", (msg) => { + const {lowest_ask} = this.props.ticker; + placeOrder(user, amount_to_sell + " SBD", min_to_receive + " STEEM", "$" + price + "/STEEM", !!this.state.buy_price_warning, lowest_ask, (msg) => { this.props.notify(msg) this.props.reload(user) }) @@ -72,7 +73,8 @@ class Market extends React.Component { const min_to_receive = parseFloat(ReactDOM.findDOMNode(this.refs.sellSteem_total).value) const amount_to_sell = parseFloat(ReactDOM.findDOMNode(this.refs.sellSteem_amount).value) const price = (min_to_receive / amount_to_sell).toFixed(6) - placeOrder(user, amount_to_sell + " STEEM", min_to_receive + " SBD", "$" + price + "/STEEM", (msg) => { + const {highest_bid} = this.props.ticker; + placeOrder(user, amount_to_sell + " STEEM", min_to_receive + " SBD", "$" + price + "/STEEM", !!this.state.sell_price_warning, highest_bid, (msg) => { this.props.notify(msg) this.props.reload(user) }) @@ -103,9 +105,9 @@ class Market extends React.Component { this.validateSellSteem() } - percentDiff = (a, b) => { - console.log(200 * Math.abs(a - b) / (a + b)) - return 200 * Math.abs(a - b) / (a + b) + percentDiff = (marketPrice, userPrice) => { + marketPrice = parseFloat(marketPrice); + return 100 * (userPrice - marketPrice) / (marketPrice) } validateBuySteem = () => { @@ -113,7 +115,8 @@ class Market extends React.Component { const price = parseFloat(this.refs.buySteem_price.value) const total = parseFloat(this.refs.buySteem_total.value) const valid = (amount > 0 && price > 0 && total > 0) - this.setState({buy_disabled: !valid, buy_price_warning: valid && this.percentDiff(total/amount, price) > 1 }); + const {lowest_ask} = this.props.ticker; + this.setState({buy_disabled: !valid, buy_price_warning: valid && this.percentDiff(lowest_ask, price) > 15 }); } validateSellSteem = () => { @@ -121,7 +124,8 @@ class Market extends React.Component { const price = parseFloat(this.refs.sellSteem_price.value) const total = parseFloat(this.refs.sellSteem_total.value) const valid = (amount > 0 && price > 0 && total > 0) - this.setState({sell_disabled: !valid, sell_price_warning: valid && this.percentDiff(total/amount, price) > 1 }); + const {highest_bid} = this.props.ticker; + this.setState({sell_disabled: !valid, sell_price_warning: valid && this.percentDiff(highest_bid, price) < -15 }); } constructor(props) { @@ -313,7 +317,7 @@ class Market extends React.Component {
      - { const amount = parseFloat(this.refs.buySteem_amount.value) const price = parseFloat(this.refs.buySteem_price.value) @@ -403,7 +407,7 @@ class Market extends React.Component {
      - { const amount = parseFloat(this.refs.sellSteem_amount.value) const price = parseFloat(this.refs.sellSteem_price.value) @@ -551,7 +555,7 @@ module.exports = { //successCallback })) }, - placeOrder: (owner, amount_to_sell, min_to_receive, effectivePrice, successCallback, fill_or_kill = false, expiration = DEFAULT_EXPIRE) => { + placeOrder: (owner, amount_to_sell, min_to_receive, effectivePrice, priceWarning, marketPrice, successCallback, fill_or_kill = false, expiration = DEFAULT_EXPIRE) => { // create_order jsc 12345 "1.000 SBD" "100.000 STEEM" true 1467122240 false // Padd amounts to 3 decimal places @@ -560,17 +564,20 @@ module.exports = { min_to_receive = min_to_receive.replace(min_to_receive.split(' ')[0], String(parseFloat(min_to_receive).toFixed(3))) - const confirmStr = /STEEM$/.test(amount_to_sell) ? + const isSell = /STEEM$/.test(amount_to_sell); + const confirmStr = isSell ? `Sell ${amount_to_sell} for at least ${min_to_receive} (${effectivePrice})` : `Buy at least ${min_to_receive} for ${amount_to_sell} (${effectivePrice})` const successMessage = `Order placed: ${confirmStr}` const confirm = confirmStr + '?' + const warning = priceWarning ? "This price is well " + (isSell ? "below" : "above") + " the current market price of $" + parseFloat(marketPrice).toFixed(4) + "/STEEM, are you sure?" : null; const orderid = Math.floor(Date.now() / 1000) dispatch(transaction.actions.broadcastOperation({ type: 'limit_order_create', operation: {owner, amount_to_sell, min_to_receive, fill_or_kill, expiration, orderid}, //, //__config: {successMessage}}, confirm, + warning, successCallback: () => {successCallback(successMessage);} })) } diff --git a/app/redux/Transaction.js b/app/redux/Transaction.js index e4d625b0bd..34d8cfb972 100644 --- a/app/redux/Transaction.js +++ b/app/redux/Transaction.js @@ -6,7 +6,7 @@ export default createModule({ initialState: fromJS({ operations: [], status: { key: '', error: false, busy: false, }, - errors: null, + errors: null }), transformations: [ { @@ -14,11 +14,13 @@ export default createModule({ reducer: (state, {payload}) => { const operation = fromJS(payload.operation) const confirm = payload.confirm + const warning = payload.warning return state.merge({ show_confirm_modal: true, confirmBroadcastOperation: operation, confirmErrorCallback: payload.errorCallback, confirm, + warning }) } }, diff --git a/app/redux/TransactionSaga.js b/app/redux/TransactionSaga.js index 3c4bdc9c64..c77d56f33d 100644 --- a/app/redux/TransactionSaga.js +++ b/app/redux/TransactionSaga.js @@ -93,12 +93,12 @@ function* error_account_witness_vote({operation: {account, witness, approve}}) { /** Keys, username, and password are not needed for the initial call. This will check the login and may trigger an action to prompt for the password / key. */ function* broadcastOperation({payload: - {type, operation, confirm, keys, username, password, successCallback, errorCallback} + {type, operation, confirm, warning, keys, username, password, successCallback, errorCallback} }) { const operationParam = {type, operation, keys, username, password, successCallback, errorCallback} const conf = typeof confirm === 'function' ? confirm() : confirm if(conf) { - yield put(tr.actions.confirmOperation({confirm, operation: operationParam, errorCallback})) + yield put(tr.actions.confirmOperation({confirm, warning, operation: operationParam, errorCallback})) return } const payload = {operations: [[type, operation]], keys, username, successCallback, errorCallback} From ac1c1d903c81db8eceae725a4c0cfc6dab8b4b11 Mon Sep 17 00:00:00 2001 From: Sigve Date: Tue, 29 Nov 2016 12:11:47 -0500 Subject: [PATCH 11/90] Fix yAxis labels #563 --- app/components/elements/DepthChart.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/components/elements/DepthChart.jsx b/app/components/elements/DepthChart.jsx index 69c2c5a65b..a9e2f67e68 100644 --- a/app/components/elements/DepthChart.jsx +++ b/app/components/elements/DepthChart.jsx @@ -6,6 +6,7 @@ const ReactHighcharts = require("react-highcharts/dist/ReactHighstock"); // multiply the x values by a constant factor and divide by this factor for // display purposes (tooltip, x-axis) const power = 100; +const precision = 1000; function orderEqual(a, b) { return ( @@ -161,7 +162,7 @@ function generateDepthChart(bidsArray, asksArray) { labels: { align: "left", formatter: function () { - let value = this.value / power; + let value = this.value / precision; return "$" + (value > 10e6 ? (value / 10e6).toFixed(2) + "M" : value > 10000 ? (value / 10e3).toFixed(2) + "k" : value); From 27bfd544e6ccffa1ddf637e079354a3b798a5f43 Mon Sep 17 00:00:00 2001 From: Sigve Date: Tue, 29 Nov 2016 12:28:37 -0500 Subject: [PATCH 12/90] Set current market prices as default, fix #434 --- app/components/pages/Market.jsx | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/app/components/pages/Market.jsx b/app/components/pages/Market.jsx index 7b5a950035..41cdac52f2 100644 --- a/app/components/pages/Market.jsx +++ b/app/components/pages/Market.jsx @@ -21,6 +21,24 @@ class Market extends React.Component { user: React.PropTypes.string, }; + constructor(props) { + super(props); + this.state = { + buy_disabled: true, + sell_disabled: true, + buy_price_warning: false, + sell_price_warning: false, + }; + } + + componentWillReceiveProps(np) { + if (!this.props.ticker && np.ticker) { + const {lowest_ask, highest_bid} = np.ticker; + if (this.refs.buySteem_price) this.refs.buySteem_price.value = parseFloat(lowest_ask).toFixed(6); + if (this.refs.sellSteem_price) this.refs.sellSteem_price.value = parseFloat(highest_bid).toFixed(6); + } + } + shouldComponentUpdate = (nextProps, nextState) => { if( this.props.user !== nextProps.user && nextProps.user) { this.props.reload(nextProps.user) @@ -128,17 +146,6 @@ class Market extends React.Component { this.setState({sell_disabled: !valid, sell_price_warning: valid && this.percentDiff(highest_bid, price) < -15 }); } - constructor(props) { - super(props); - this.state = { - buy_disabled: true, - sell_disabled: true, - buy_price_warning: false, - sell_price_warning: false, - }; - } - - render() { const {sellSteem, buySteem, cancelOrderClick, setFormPrice, validateBuySteem, validateSellSteem} = this From ee37c1a5be8d97d344b84354d18614289fec126a Mon Sep 17 00:00:00 2001 From: James Calfee Date: Tue, 29 Nov 2016 13:07:45 -0600 Subject: [PATCH 13/90] Story embedded support for direct viemo urls like: https://vimeo.com/192097629 (close #673) --- app/components/cards/MarkdownViewer.jsx | 33 +++++++++++++++++-------- shared/HtmlReady.js | 24 +++++++++++++++++- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/app/components/cards/MarkdownViewer.jsx b/app/components/cards/MarkdownViewer.jsx index 2c6718690a..8a288bcb55 100644 --- a/app/components/cards/MarkdownViewer.jsx +++ b/app/components/cards/MarkdownViewer.jsx @@ -96,19 +96,32 @@ class MarkdownViewer extends Component { // In addition to inserting the youtube compoennt, this allows react to compare separately preventing excessive re-rendering. let idx = 0 const sections = [] - // HtmlReady inserts ~~~ youtube:${id} ~~~ - for(let section of cleanText.split('~~~ youtube:')) { - if(/^[A-Za-z0-9\_\-]+ ~~~/.test(section)) { - const youTubeId = section.split(' ')[0] - section = section.substring(youTubeId.length + ' ~~~'.length) + + // HtmlReady inserts ~~~ embed:${id} youtube ~~~ + for(let section of cleanText.split('~~~ embed:')) { + const match = section.match(/^([A-Za-z0-9\_\-]+) (youtube|vimeo) ~~~/) + if(match && match.length >= 3) { + const id = match[1] + const type = match[2] const w = large ? 640 : 480, h = large ? 360 : 270 - sections.push( - - ) + if(type === 'youtube') { + sections.push( + + ) + } else if(type === 'vimeo') { + const url = `https://player.vimeo.com/video/${id}` + sections.push( + + ) + } else { + console.error('MarkdownViewer unknown embed type', type); + } + section = section.substring(`${id} ${type} ~~~`.length) + if(section === '') continue } - if(section === '') continue sections.push(
      ) } diff --git a/shared/HtmlReady.js b/shared/HtmlReady.js index baa5756827..87a2c9ff9a 100644 --- a/shared/HtmlReady.js +++ b/shared/HtmlReady.js @@ -166,6 +166,7 @@ function linkifyNode(child, state) {try{ const {mutate} = state if(!child.data) return if(embedYouTubeNode(child, state.links, state.images)) return + if(embedVimeoNode(child, state.links, state.images)) return const data = XMLSerializer.serializeToString(child) const content = linkify(data, state.mutate, state.hashtags, state.usertags, state.images, state.links) @@ -229,12 +230,33 @@ function embedYouTubeNode(child, links, images) {try{ } if(!id) return false - const v = DOMParser.parseFromString(`~~~ youtube:${id} ~~~`) + const v = DOMParser.parseFromString(`~~~ embed:${id} youtube ~~~`) child.parentNode.replaceChild(v, child) if(links) links.add(url) if(images) images.add('https://img.youtube.com/vi/' + id + '/0.jpg') return true +} catch(error) {console.log(error); return false}} + +function embedVimeoNode(child, links, /*images*/) {try{ + if(!child.data) return false + const data = child.data + let id + { + const m = data.match(linksRe.vimeoId) + id = m && m.length >= 2 ? m[1] : null + } + if(!id) return false; + + const url = `https://player.vimeo.com/video/${id}` + const v = DOMParser.parseFromString(`~~~ embed:${id} vimeo ~~~`) + child.parentNode.replaceChild(v, child) + if(links) links.add(url) + + // Preview image requires a callback.. http://stackoverflow.com/questions/1361149/get-img-thumbnails-from-vimeo + // if(images) images.add('https://.../vi/' + id + '/0.jpg') + + return true } catch(error) {console.log(error); return false}} function ipfsPrefix(url) { From 8993749c6c3204c154340a9eb699678dc6054a5b Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 29 Nov 2016 14:58:26 -0500 Subject: [PATCH 14/90] consolidate close modal logic --- app/components/cards/PostsList.jsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/components/cards/PostsList.jsx b/app/components/cards/PostsList.jsx index cd28286b49..7014851e69 100644 --- a/app/components/cards/PostsList.jsx +++ b/app/components/cards/PostsList.jsx @@ -102,13 +102,12 @@ class PostsList extends React.Component { if (!inside_top_bar) { const post_overlay = document.getElementById('post_overlay'); if (post_overlay) post_overlay.removeEventListener('click', this.closeOnOutsideClick); - window.document.title = this.state.prevTitle; - this.setState({showPost: null, prevTitle: null}); + this.closePostModal(); } } } - closeOnButton = () => { + closePostModal = () => { window.document.title = this.state.prevTitle; this.setState({showPost: null, prevTitle: null}); } @@ -184,7 +183,7 @@ class PostsList extends React.Component { - +
      From e451013e4001e3d3643d404888fc123cd52f8f81 Mon Sep 17 00:00:00 2001 From: originate Date: Tue, 29 Nov 2016 15:37:49 -0500 Subject: [PATCH 15/90] Remove fastclick for JS dropdown conflicts, shrinkwrap fix --- app/components/App.jsx | 1 - npm-shrinkwrap.json | 5 ----- package.json | 1 - 3 files changed, 7 deletions(-) diff --git a/app/components/App.jsx b/app/components/App.jsx index 45f885f7b6..0042d04941 100644 --- a/app/components/App.jsx +++ b/app/components/App.jsx @@ -31,7 +31,6 @@ class App extends React.Component { } componentDidMount() { - require('fastclick').attach(document.body); // setTimeout(() => this.setState({showCallout: false}), 15000); } diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index a74e51b7a6..0a05f3434a 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1755,11 +1755,6 @@ "from": "fast-levenshtein@>=2.0.4 <2.1.0", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.5.tgz" }, - "fastclick": { - "version": "1.0.6", - "from": "fastclick@>=1.0.6 <2.0.0", - "resolved": "https://registry.npmjs.org/fastclick/-/fastclick-1.0.6.tgz" - }, "fastparse": { "version": "1.1.1", "from": "fastparse@>=1.1.1 <2.0.0", diff --git a/package.json b/package.json index 8b34c3ae00..dd4ee45872 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "ecurve": "^1.0.2", "estraverse-fb": "^1.3.1", "extract-text-webpack-plugin": "^1.0.1", - "fastclick": "^1.0.6", "file-loader": "^0.8.5", "foundation-sites": "6.2.1", "git-rev-sync": "^1.6.0", From 767f446c70e2b8a6af0fabb7a730a5a2ea0a7ee8 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 29 Nov 2016 16:21:36 -0500 Subject: [PATCH 16/90] add videoWrapper to vimeo iframes --- app/components/cards/MarkdownViewer.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/components/cards/MarkdownViewer.jsx b/app/components/cards/MarkdownViewer.jsx index 8a288bcb55..884bb796d9 100644 --- a/app/components/cards/MarkdownViewer.jsx +++ b/app/components/cards/MarkdownViewer.jsx @@ -113,8 +113,10 @@ class MarkdownViewer extends Component { } else if(type === 'vimeo') { const url = `https://player.vimeo.com/video/${id}` sections.push( - +
      + +
      ) } else { console.error('MarkdownViewer unknown embed type', type); From ede3ce30ab5271afaf05a156c4cc415166bfca61 Mon Sep 17 00:00:00 2001 From: valzav Date: Tue, 29 Nov 2016 18:04:21 -0500 Subject: [PATCH 17/90] store views counter in mysql not in tarantool --- db/migrations/20161129170500-create-page.js | 24 +++++++++++++++++++++ db/models/page.js | 13 +++++++++++ 2 files changed, 37 insertions(+) create mode 100644 db/migrations/20161129170500-create-page.js create mode 100644 db/models/page.js diff --git a/db/migrations/20161129170500-create-page.js b/db/migrations/20161129170500-create-page.js new file mode 100644 index 0000000000..d7d9da791c --- /dev/null +++ b/db/migrations/20161129170500-create-page.js @@ -0,0 +1,24 @@ +'use strict'; +module.exports = { + up: function (queryInterface, Sequelize) { + return queryInterface.createTable('pages', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + permlink: {type: Sequelize.STRING(256)}, + views: {type: Sequelize.INTEGER}, + created_at: { + allowNull: false, + type: Sequelize.DATE + } + }).then(function () { + queryInterface.addIndex('pages', ['permlink']); + }); + }, + down: function (queryInterface, Sequelize) { + return queryInterface.dropTable('pages'); + } +}; diff --git a/db/models/page.js b/db/models/page.js new file mode 100644 index 0000000000..dfe6fb3594 --- /dev/null +++ b/db/models/page.js @@ -0,0 +1,13 @@ +module.exports = function (sequelize, DataTypes) { + var Page = sequelize.define('Page', { + permlink: DataTypes.STRING(256), + views: DataTypes.INTEGER, + }, { + tableName: 'pages', + createdAt: 'created_at', + updatedAt: false, + timestamps : true, + underscored : true + }); + return Page; +}; From e6168643e8a8c132eb48c0736188c2e671fc2bba Mon Sep 17 00:00:00 2001 From: Sigve Date: Tue, 29 Nov 2016 19:10:49 -0500 Subject: [PATCH 18/90] Don't trigger loading state for certain api methods #729 --- app/redux/AppReducer.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/redux/AppReducer.js b/app/redux/AppReducer.js index a9b4136c65..1a34f6c020 100644 --- a/app/redux/AppReducer.js +++ b/app/redux/AppReducer.js @@ -38,8 +38,17 @@ export default function reducer(state = defaultState, action) { let res = state; if (action.type === 'RPC_REQUEST_STATUS') { const request_id = action.payload.id + ''; + // console.log(new Date().getTime(), "RPC_REQUEST_STATUS:", action.payload.method); if (action.payload.event === 'BEGIN') { - res = state.mergeDeep({loading: true, requests: {[request_id]: Date.now()}}); + const noLoadingMethods = [ + "get_dynamic_global_properties", + "get_api_by_name", + "get_followers" + ]; + res = state.mergeDeep({ + loading: noLoadingMethods.indexOf(action.payload.method) !== -1 ? false : true, + requests: {[request_id]: Date.now()} + }); } if (action.payload.event === 'END' || action.payload.event === 'ERROR') { res = res.deleteIn(['requests', request_id]); From fb8aa6b2ee442e7525bc61dbde520f483b3b38c8 Mon Sep 17 00:00:00 2001 From: Sigve Date: Tue, 29 Nov 2016 19:23:27 -0500 Subject: [PATCH 19/90] Reduce PostsIndex and PostsList render updates logic fix #729 --- app/components/cards/PostsList.jsx | 44 +++++++++++++++++++--------- app/components/pages/PostsIndex.jsx | 11 ++++--- app/components/pages/UserProfile.jsx | 7 +++-- 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/app/components/cards/PostsList.jsx b/app/components/cards/PostsList.jsx index 067fbb77d4..276d3a0311 100644 --- a/app/components/cards/PostsList.jsx +++ b/app/components/cards/PostsList.jsx @@ -8,6 +8,7 @@ import Callout from 'app/components/elements/Callout'; import CloseButton from 'react-foundation-components/lib/global/close-button'; import {findParent} from 'app/utils/DomUtils'; import Icon from 'app/components/elements/Icon'; +import Immutable from "immutable"; function topPosition(domElt) { if (!domElt) { @@ -19,7 +20,8 @@ function topPosition(domElt) { class PostsList extends React.Component { static propTypes = { - posts: PropTypes.array.isRequired, + posts: PropTypes.object.isRequired, + postsInfo: PropTypes.object.isRequired, loading: PropTypes.bool.isRequired, category: PropTypes.string, loadMore: PropTypes.func, @@ -47,7 +49,20 @@ class PostsList extends React.Component { this.onPostClick = this.onPostClick.bind(this); this.onBackButton = this.onBackButton.bind(this); this.closeOnOutsideClick = this.closeOnOutsideClick.bind(this); - this.shouldComponentUpdate = shouldComponentUpdate(this, 'PostsList') + // this.shouldComponentUpdate = shouldComponentUpdate(this, 'PostsList') + } + + shouldComponentUpdate(np, ns) { + // console.log("np.postsInfo !== this.props.postsInfo", !Immutable.is(np.postsInfo, this.props.postsInfo)); + return ( + !Immutable.is(np.postsInfo, this.props.postsInfo) || + np.loadMore !== this.props.loadMore || + np.showSpam !== this.props.showSpam || + np.loading !== this.props.loading || + np.category !== this.props.category || + ns.showNegativeComments !== this.state.showNegativeComments || + ns.showPost !== this.state.showPost + ); } componentDidMount() { @@ -124,11 +139,11 @@ class PostsList extends React.Component { (document.documentElement || document.body.parentNode || document.body).scrollTop; if (topPosition(el) + el.offsetHeight - scrollTop - window.innerHeight < 10) { const {loadMore, posts, category} = this.props; - if (loadMore && posts && posts.length > 0) loadMore(posts[posts.length - 1], category); + if (loadMore && posts && posts.size) loadMore(posts.last(), category); } // Detect if we're in mobile mode (renders larger preview imgs) - var mq = window.matchMedia('screen and (max-width: 39.9375em)'); + const mq = window.matchMedia('screen and (max-width: 39.9375em)'); if(mq.matches) { this.setState({thumbSize: 'mobile'}) } else { @@ -155,21 +170,21 @@ class PostsList extends React.Component { } render() { - const {posts, loading, category, emptyText} = this.props; - const {comments} = this.props + const {posts, loading, category, emptyText, postsInfo} = this.props; const {account} = this.props const {thumbSize, showPost} = this.state - if (!loading && !posts.length && emptyText) { + + if (!loading && (posts && !posts.size) && emptyText) { return {emptyText}; } - const renderSummary = items => items.map(({item, ignore, netVoteSign, authorRepLog10}) =>
    • - + const renderSummary = items => items.map(item =>
    • +
    • ) return (
        - {renderSummary(comments)} + {renderSummary(postsInfo)}
      {loading &&
      } {showPost &&
      @@ -196,7 +211,7 @@ import {connect} from 'react-redux' export default connect( (state, props) => { const {posts, showSpam} = props; - const comments = [] + let postsInfo = Immutable.List(); const pathname = state.app.get('location').pathname; posts.forEach(item => { const content = state.global.get('content').get(item); @@ -211,9 +226,10 @@ export default connect( const ignore = username ? state.global.getIn(key, List()).contains('ignore') : false const {hide, netVoteSign, authorRepLog10} = content.get('stats').toJS() if(!(ignore || hide) || showSpam) // rephide - comments.push({item, ignore, netVoteSign, authorRepLog10}) + postsInfo = postsInfo.push(Immutable.fromJS({item, ignore, netVoteSign, authorRepLog10})) }) - return {...props, comments, pathname}; + + return {...props, postsInfo, pathname}; }, dispatch => ({ fetchState: (pathname) => { diff --git a/app/components/pages/PostsIndex.jsx b/app/components/pages/PostsIndex.jsx index 4453a496b3..1d159431ff 100644 --- a/app/components/pages/PostsIndex.jsx +++ b/app/components/pages/PostsIndex.jsx @@ -9,11 +9,13 @@ import {isFetchingOrRecentlyUpdated} from 'app/utils/StateFunctions'; import {Link} from 'react-router'; import MarkNotificationRead from 'app/components/elements/MarkNotificationRead'; import { translate } from 'app/Translator'; +import Immutable from "immutable"; class PostsIndex extends React.Component { static propTypes = { discussions: PropTypes.object, + accounts: PropTypes.object, status: PropTypes.object, routeParams: PropTypes.object, requestData: PropTypes.func, @@ -69,7 +71,7 @@ class PostsIndex extends React.Component { const account_name = order.slice(1); order = 'by_feed'; topics_order = 'trending'; - posts = this.props.global.getIn(['accounts', account_name, 'feed']); + posts = this.props.accounts.getIn([account_name, 'feed']); const isMyAccount = this.props.current_user && this.props.current_user.get('username') === account_name; if (isMyAccount) { emptyText =
      @@ -100,8 +102,9 @@ class PostsIndex extends React.Component {
      {markNotificationRead} - Date: Wed, 30 Nov 2016 09:58:55 -0600 Subject: [PATCH 20/90] Updated comment --- app/components/cards/MarkdownViewer.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/cards/MarkdownViewer.jsx b/app/components/cards/MarkdownViewer.jsx index 884bb796d9..7275f0becc 100644 --- a/app/components/cards/MarkdownViewer.jsx +++ b/app/components/cards/MarkdownViewer.jsx @@ -97,7 +97,7 @@ class MarkdownViewer extends Component { let idx = 0 const sections = [] - // HtmlReady inserts ~~~ embed:${id} youtube ~~~ + // HtmlReady inserts ~~~ embed:${id} type ~~~ for(let section of cleanText.split('~~~ embed:')) { const match = section.match(/^([A-Za-z0-9\_\-]+) (youtube|vimeo) ~~~/) if(match && match.length >= 3) { From a2953fa408c1b17855bad4e9c7a76d3b4ca63171 Mon Sep 17 00:00:00 2001 From: James Calfee Date: Wed, 30 Nov 2016 10:38:04 -0600 Subject: [PATCH 21/90] Add allowDangerousHTML boolean for use in HelpContent #732. --- app/components/cards/MarkdownViewer.jsx | 6 +++++- app/components/elements/HelpContent.jsx | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/components/cards/MarkdownViewer.jsx b/app/components/cards/MarkdownViewer.jsx index 2c6718690a..173fe71d96 100644 --- a/app/components/cards/MarkdownViewer.jsx +++ b/app/components/cards/MarkdownViewer.jsx @@ -28,11 +28,13 @@ class MarkdownViewer extends Component { jsonMetadata: React.PropTypes.object, highQualityPost: React.PropTypes.bool, noImage: React.PropTypes.bool, + allowDangerousHTML: React.PropTypes.bool, } static defaultProps = { className: '', large: false, + allowDangerousHTML: false, } constructor() { @@ -81,7 +83,9 @@ class MarkdownViewer extends Component { // Complete removal of javascript and other dangerous tags.. // The must remain as close as possible to dangerouslySetInnerHTML let cleanText = renderedText - if (this.props.className !== 'HelpContent') { + if (this.props.allowDangerousHTML === true) { + console.log('WARN\tMarkdownViewer rendering unsanitized content') + } else { cleanText = sanitize(renderedText, sanitizeConfig({large, highQualityPost, noImage: noImage && allowNoImage})) } diff --git a/app/components/elements/HelpContent.jsx b/app/components/elements/HelpContent.jsx index b2d6728b5e..21cd0db33a 100644 --- a/app/components/elements/HelpContent.jsx +++ b/app/components/elements/HelpContent.jsx @@ -110,6 +110,6 @@ export default class HelpContent extends React.Component { value = value.replace(//gi, (match, name) => { return renderToString(); }); - return ; + return ; } } From 8e876aed69a0064f3130aefdf30962b337fa86da Mon Sep 17 00:00:00 2001 From: valzav Date: Wed, 30 Nov 2016 11:51:52 -0500 Subject: [PATCH 22/90] store views counter in mysql not in tarantool --- server/api/general.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/server/api/general.js b/server/api/general.js index e0552f0afc..789d6e6667 100644 --- a/server/api/general.js +++ b/server/api/general.js @@ -7,7 +7,7 @@ import recordWebEvent from 'server/record_web_event'; import {esc, escAttrs} from 'db/models'; import {emailRegex, getRemoteIp, rateLimitReq, checkCSRF} from 'server/utils'; import coBody from 'co-body'; -import Tarantool from 'db/tarantool'; +// import Tarantool from 'db/tarantool'; export default function useGeneralApi(app) { const router = koa_router({prefix: '/api/v1'}); @@ -245,11 +245,21 @@ export default function useGeneralApi(app) { const params = this.request.body; const {csrf, page, ref} = typeof(params) === 'string' ? JSON.parse(params) : params; if (!checkCSRF(this, csrf)) return; - console.log('-- /page_view -->', this.session.uid, page, ref); + console.log('-- /page_view -->', this.session.uid, page); const remote_ip = getRemoteIp(this.req); try { - const views = yield Tarantool.instance().call('page_view', page, remote_ip, this.session.uid, ref); - this.body = JSON.stringify({views: views[0][0]}); + let views = 1; + // const views = yield Tarantool.instance().call('page_view', page, remote_ip, this.session.uid, ref); + const page_model = yield models.Page.findOne( + {attributes: ['id', 'views'], where: {permlink: esc(page)}} + ); + if (page_model) { + views = page_model.views + 1; + yield yield models.Page.update({views}, {where: {id: page_model.id}}); + } else { + yield models.Page.create(escAttrs({permlink: page, views})); + } + this.body = JSON.stringify({views}); } catch (error) { console.error('Error in /page_view api call', this.session.uid, error.message); this.body = JSON.stringify({error: error.message}); From 6920261915f6c38c6308afc1057d8f376c396549 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 30 Nov 2016 13:05:06 -0500 Subject: [PATCH 23/90] instead of grayed out price, show light yellow background as warning --- app/components/pages/Market.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/pages/Market.scss b/app/components/pages/Market.scss index e3ddf0a974..3d872381d8 100644 --- a/app/components/pages/Market.scss +++ b/app/components/pages/Market.scss @@ -76,7 +76,7 @@ input.sell-color:hover { } input.price_warning { - color: rgba(0,0,0,0.25); + background: rgba(255, 153, 0, 0.13); } } From 27c3bf12c9338139166df13c2043bc7ca63fedd1 Mon Sep 17 00:00:00 2001 From: Sigve Date: Wed, 30 Nov 2016 13:33:06 -0500 Subject: [PATCH 24/90] Fix jumping header on mobile scroll in PostsList view --- app/components/cards/PostsList.scss | 32 +++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/app/components/cards/PostsList.scss b/app/components/cards/PostsList.scss index b8c713f723..f918224614 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-x: hidden; - overflow-y: scroll; + background-color: $white; // padding: 0 .9rem; - -webkit-overflow-scrolling: touch; } + + .PostsList__post_top_overlay { position: fixed; top: 0; @@ -19,8 +19,7 @@ width: 100%; z-index: 310; height: 2.5rem; - overflow-x: hidden; - overflow-y: scroll; + overflow: hidden; border-bottom: 1px solid $light-gray; } @@ -48,10 +47,11 @@ } .PostsList__post_container { - position: relative; - background-color: $white; - margin: 1rem auto; - padding: 2rem 0.9rem 0 0.9rem; + overflow: hidden; + position: relative; + background-color: $white; + margin: 1rem auto; + padding: 2rem 0.9rem 0 0.9rem; .PostFull { background-color: $white; } @@ -85,7 +85,21 @@ 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; } From 1b935676851cb1b81a99447cc437d0188b1ff6c3 Mon Sep 17 00:00:00 2001 From: valzav Date: Wed, 30 Nov 2016 13:51:13 -0500 Subject: [PATCH 25/90] make page.permlink index unique --- db/migrations/20161129170500-create-page.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrations/20161129170500-create-page.js b/db/migrations/20161129170500-create-page.js index d7d9da791c..d5fcde139c 100644 --- a/db/migrations/20161129170500-create-page.js +++ b/db/migrations/20161129170500-create-page.js @@ -15,7 +15,7 @@ module.exports = { type: Sequelize.DATE } }).then(function () { - queryInterface.addIndex('pages', ['permlink']); + queryInterface.addIndex('pages', ['permlink'], {indicesType: 'UNIQUE'}); }); }, down: function (queryInterface, Sequelize) { From 6104e12e6697d82c45d3cdfb80fba8b913e72b23 Mon Sep 17 00:00:00 2001 From: Sigve Date: Wed, 30 Nov 2016 15:51:40 -0500 Subject: [PATCH 26/90] Refactor Post/PostFull to remove global object, add shouldComponentUpdate for less render cycles --- app/components/cards/Comment.jsx | 60 +++++++++++++++++------------- app/components/cards/PostFull.jsx | 10 ++--- app/components/cards/PostsList.jsx | 1 - app/components/pages/Post.jsx | 47 ++++++++++++++++------- 4 files changed, 72 insertions(+), 46 deletions(-) diff --git a/app/components/cards/Comment.jsx b/app/components/cards/Comment.jsx index 2bfbc758b2..48fa4c4cd9 100644 --- a/app/components/cards/Comment.jsx +++ b/app/components/cards/Comment.jsx @@ -9,13 +9,12 @@ import { connect } from 'react-redux'; import { Link } from 'react-router'; import user from 'app/redux/User'; import TimeAgoWrapper from 'app/components/elements/TimeAgoWrapper'; -import Icon from 'app/components/elements/Icon'; import Userpic from 'app/components/elements/Userpic'; import transaction from 'app/redux/Transaction' import {List} from 'immutable' import { translate } from 'app/Translator'; -export function sortComments( g, comments, sort_order ) { +export function sortComments( cont, comments, sort_order ) { function netNegative(a) { return a.get("net_rshares") < 0; @@ -24,8 +23,8 @@ export function sortComments( g, comments, sort_order ) { let sort_orders = { /** sort replies by active */ active: (a,b) => { - let acontent = g.get('content').get(a); - let bcontent = g.get('content').get(b); + let acontent = cont.get(a); + let bcontent = cont.get(b); if (netNegative(acontent)) { return 1; } else if (netNegative(bcontent)) { @@ -36,8 +35,8 @@ export function sortComments( g, comments, sort_order ) { return bactive - aactive; }, update: (a,b) => { - let acontent = g.get('content').get(a); - let bcontent = g.get('content').get(b); + let acontent = cont.get(a); + let bcontent = cont.get(b); if (netNegative(acontent)) { return 1; } else if (netNegative(bcontent)) { @@ -48,8 +47,9 @@ export function sortComments( g, comments, sort_order ) { return bactive.getTime() - aactive.getTime(); }, new: (a,b) => { - let acontent = g.get('content').get(a); - let bcontent = g.get('content').get(b); + let acontent = cont.get(a); + console.log("acontent", acontent); + let bcontent = cont.get(b); if (netNegative(acontent)) { return 1; } else if (netNegative(bcontent)) { @@ -60,8 +60,8 @@ export function sortComments( g, comments, sort_order ) { return bactive - aactive; }, trending: (a,b) => { - let acontent = g.get('content').get(a); - let bcontent = g.get('content').get(b); + let acontent = cont.get(a); + let bcontent = cont.get(b); if (netNegative(acontent)) { return 1; } else if (netNegative(bcontent)) { @@ -83,7 +83,7 @@ class CommentImpl extends React.Component { static propTypes = { // html props - global: React.PropTypes.object.isRequired, + cont: React.PropTypes.object.isRequired, content: React.PropTypes.string.isRequired, sort_order: React.PropTypes.oneOf(['active', 'updated', 'new', 'trending']).isRequired, root: React.PropTypes.bool, @@ -122,8 +122,8 @@ class CommentImpl extends React.Component { } this.saveOnShow = (type) => { if(process.env.BROWSER) { - const g = this.props.global; - const content = g.get('content').get(this.props.content) + const {cont} = this.props; + const content = cont.get(this.props.content) const formId = content.get('author') + '/' + content.get('permlink') if(type) localStorage.setItem('showEditor-' + formId, JSON.stringify({type}, null, 0)) @@ -137,7 +137,7 @@ class CommentImpl extends React.Component { this.saveOnShow = this.saveOnShow.bind(this) this.onDeletePost = () => { const {props: {deletePost}} = this - const content = this.props.global.get('content').get(this.props.content); + const content = this.props.cont.get(this.props.content); deletePost(content.get('author'), content.get('permlink')) } this.toggleCollapsed = this.toggleCollapsed.bind(this); @@ -170,8 +170,7 @@ class CommentImpl extends React.Component { * it hides the comment body (but not the header) until the "reveal comment" link is clicked. */ _checkHide(props) { - const g = props.global; - const content = g.get('content').get(props.content); + const content = props.cont.get(props.content); if (content) { const hide = content.getIn(['stats', 'hide']) if(hide) { @@ -191,8 +190,8 @@ class CommentImpl extends React.Component { } initEditor(props) { if(this.state.PostReplyEditor) return - const g = props.global; - const content = g.get('content').get(props.content); + const {cont} = this.props; + const content = cont.get(props.content); if (!content) return const post = content.get('author') + '/' + content.get('permlink') const PostReplyEditor = ReplyEditor(post + '-reply') @@ -213,8 +212,8 @@ class CommentImpl extends React.Component { this.setState({PostReplyEditor, PostEditEditor}) } render() { - let g = this.props.global; - const dis = g.get('content').get(this.props.content); + const {cont} = this.props; + const dis = cont.get(this.props.content); if (!dis) { return
      {translate('loading')}...
      } @@ -269,11 +268,20 @@ class CommentImpl extends React.Component { if(!this.state.collapsed) { replies = comment.replies; - sortComments( g, replies, this.props.sort_order ); + sortComments( cont, replies, this.props.sort_order ); // When a comment has hidden replies and is collapsed, the reply count is off //console.log("replies:", replies.length, "num_visible:", replies.filter( reply => !g.get('content').get(reply).getIn(['stats', 'hide'])).length) - replies = replies.map((reply, idx) => ); + replies = replies.map((reply, idx) => ( + ) + ); } const commentClasses = ['hentry'] @@ -349,10 +357,10 @@ class CommentImpl extends React.Component { const Comment = connect( // mapStateToProps (state, ownProps) => { - const {global, content} = ownProps + const {content, cont} = ownProps let {depth} = ownProps if(depth == null) depth = 1 - const c = global.getIn(['content', content]) + const c = cont.get(content); let comment_link = null let rc = ownProps.rootComment if(c) { @@ -369,7 +377,7 @@ const Comment = connect( anchor_link: '#@' + content, // Using a hash here is not standard but intentional; see issue #124 for details rootComment: rc, username, - ignore, + ignore } }, diff --git a/app/components/cards/PostFull.jsx b/app/components/cards/PostFull.jsx index 08c73e640d..55ad20c83a 100644 --- a/app/components/cards/PostFull.jsx +++ b/app/components/cards/PostFull.jsx @@ -37,7 +37,7 @@ class PostFull extends React.Component { static propTypes = { // html props /* Show extra options (component is being viewed alone) */ - global: React.PropTypes.object.isRequired, + cont: React.PropTypes.object.isRequired, post: React.PropTypes.string.isRequired, // connector props @@ -65,7 +65,7 @@ class PostFull extends React.Component { } this.onDeletePost = () => { const {props: {deletePost}} = this - const content = this.props.global.get('content').get(this.props.post); + const content = this.props.cont.get(this.props.post); deletePost(content.get('author'), content.get('permlink')) } } @@ -93,7 +93,7 @@ class PostFull extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - const names = 'global, post, username'.split(', ') + const names = 'cont, post, username'.split(', ') return names.findIndex(name => this.props[name] !== nextProps[name]) !== -1 || this.state !== nextState } @@ -129,7 +129,7 @@ class PostFull extends React.Component { } showPromotePost = () => { - const post_content = this.props.global.get('content').get(this.props.post); + const post_content = this.props.cont.get(this.props.post); if (!post_content) return const author = post_content.get('author') const permlink = post_content.get('permlink') @@ -139,7 +139,7 @@ class PostFull extends React.Component { render() { const {props: {username, post}, state: {PostFullReplyEditor, PostFullEditEditor, formId, showReply, showEdit}, onShowReply, onShowEdit, onDeletePost} = this - const post_content = this.props.global.get('content').get(this.props.post); + const post_content = this.props.cont.get(this.props.post); if (!post_content) return null; const p = extractContent(immutableAccessor, post_content); const content = post_content.toJS(); diff --git a/app/components/cards/PostsList.jsx b/app/components/cards/PostsList.jsx index 276d3a0311..f22916ce15 100644 --- a/app/components/cards/PostsList.jsx +++ b/app/components/cards/PostsList.jsx @@ -2,7 +2,6 @@ import React, {PropTypes} from 'react'; import PostSummary from 'app/components/cards/PostSummary'; import Post from 'app/components/pages/Post'; import LoadingIndicator from 'app/components/elements/LoadingIndicator'; -import shouldComponentUpdate from 'app/utils/shouldComponentUpdate'; import debounce from 'lodash.debounce'; import Callout from 'app/components/elements/Callout'; import CloseButton from 'react-foundation-components/lib/global/close-button'; diff --git a/app/components/pages/Post.jsx b/app/components/pages/Post.jsx index 8d8c937f2f..4604649505 100644 --- a/app/components/pages/Post.jsx +++ b/app/components/pages/Post.jsx @@ -3,22 +3,20 @@ import React from 'react'; import Comment from 'app/components/cards/Comment'; import PostFull from 'app/components/cards/PostFull'; import {connect} from 'react-redux'; -import { Link } from 'react-router'; import {sortComments} from 'app/components/cards/Comment'; -import DropdownMenu from 'app/components/elements/DropdownMenu'; -import user from 'app/redux/User' // import { Link } from 'react-router'; import FoundationDropdownMenu from 'app/components/elements/FoundationDropdownMenu'; import SvgImage from 'app/components/elements/SvgImage'; import {List} from 'immutable' import { translate } from 'app/Translator'; import { localizedCurrency } from 'app/components/elements/LocalizedCurrency'; +import shouldComponentUpdate from 'app/utils/shouldComponentUpdate'; class Post extends React.Component { static propTypes = { - global: React.PropTypes.object.isRequired, + content: React.PropTypes.object.isRequired, post: React.PropTypes.string, routeParams: React.PropTypes.object, location: React.PropTypes.object, @@ -33,7 +31,9 @@ class Post extends React.Component { this.showSignUp = () => { window.location = '/enter_email'; } + this.shouldComponentUpdate = shouldComponentUpdate(this, 'Post') } + componentDidMount() { if (window.location.hash.indexOf('comments') !== -1) { const comments_el = document.getElementById('comments'); @@ -57,15 +57,14 @@ class Post extends React.Component { render() { const {showSignUp} = this - const {current_user, following, signup_bonus} = this.props + const {current_user, following, signup_bonus, content} = this.props const {showNegativeComments, commentHidden, showAnyway} = this.state - let g = this.props.global; let post = this.props.post; if (!post) { const route_params = this.props.routeParams; post = route_params.username + '/' + route_params.slug; } - const dis = g.get('content').get(post); + const dis = content.get(post); if (!dis) return null; @@ -92,9 +91,9 @@ class Post extends React.Component { if( this.props.location && this.props.location.query.sort ) sort_order = this.props.location.query.sort; - sortComments( g, replies, sort_order ); + sortComments( content, replies, sort_order ); const keep = a => { - const c = g.getIn(['content', a]) + const c = content.get(a); const hide = c.getIn(['stats', 'hide']) let ignore = false if(following) { @@ -103,15 +102,35 @@ class Post extends React.Component { return !hide && !ignore } const positiveComments = replies.filter(a => keep(a)) - .map(reply => ); + .map(reply => ( + ) + ); // Not the complete hidding logic, just move to the bottom, the rest hide in-place const negativeReplies = replies.filter(a => !keep(a)); const stuffHidden = negativeReplies.length > 0 || commentHidden const negativeComments = - negativeReplies.map(reply => ); + negativeReplies.map(reply => ( + ) + ); const negativeGroup = !stuffHidden ? null : (
      @@ -145,7 +164,7 @@ class Post extends React.Component {
      - +
      {!current_user &&
      @@ -187,7 +206,7 @@ export default connect(state => { following = state.global.getIn(key, List()) } return { - global: state.global, + content: state.global.get('content'), signup_bonus: state.offchain.get('signup_bonus'), current_user, following, From 3e13c43ef39fb12ce633772352d497d68514600d Mon Sep 17 00:00:00 2001 From: James Calfee Date: Wed, 30 Nov 2016 15:01:01 -0600 Subject: [PATCH 27/90] Adjust password data-entry error wording. Only show format error on Login. Expand to check WIFs in addition to Steemit passwords. (close #725) --- app/components/modules/LoginForm.jsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/components/modules/LoginForm.jsx b/app/components/modules/LoginForm.jsx index 2bb7b8fe83..a4c0cf8aaf 100644 --- a/app/components/modules/LoginForm.jsx +++ b/app/components/modules/LoginForm.jsx @@ -155,10 +155,9 @@ class LoginForm extends Component {
      ; } } - const standardPassword = checkPasswordChecksum(password.value) - let password_info = null - if (standardPassword !== undefined && !standardPassword) - password_info = 'This password was probably typed or copied incorrectly. A password generated by Steemit should not contain 0 (zero), O (capital o), I (capital i) and l (lower case L) characters.' + const password_info = checkPasswordChecksum(password.value) === false ? + 'This password or private key was entered incorrectly. There is probably a handwriting or data-entry error. Hint: A password or private key generated by Steemit will never contain 0 (zero), O (capital o), I (capital i) and l (lower case L) characters.' : + null const form = (
      { @@ -178,7 +177,7 @@ class LoginForm extends Component {
      {error &&
      {error} 
      } - {password_info &&
      {password_info} 
      } + {error && password_info &&
      {password_info} 
      }
      {loginBroadcastOperation &&
      This operation requires your {authType} key (or use your master password).
      @@ -226,11 +225,15 @@ function urlAccountName() { } function checkPasswordChecksum(password) { - if(!/^P.{45,}/.test(password)) {// 52 is the length + // A Steemit generated password is a WIF prefixed with a P .. + // It is possible to login directly with a WIF + const wif = /^P/.test(password) ? password.substring(1) : password + + if(!/^5[HJK].{45,}/i.test(wif)) {// 51 is the wif length // not even close return undefined } - const wif = password.substring(1) + return PrivateKey.isWif(wif) } From bbb96cdf8494227d735fcc7031a4a95b4e26531a Mon Sep 17 00:00:00 2001 From: Sigve Date: Wed, 30 Nov 2016 19:52:31 -0500 Subject: [PATCH 28/90] Add follower count to FollowSaga state update, add get_following to noLoadingMethods --- app/redux/AppReducer.js | 3 ++- app/redux/FollowSaga.js | 16 +++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/redux/AppReducer.js b/app/redux/AppReducer.js index 1a34f6c020..f9658473fc 100644 --- a/app/redux/AppReducer.js +++ b/app/redux/AppReducer.js @@ -43,7 +43,8 @@ export default function reducer(state = defaultState, action) { const noLoadingMethods = [ "get_dynamic_global_properties", "get_api_by_name", - "get_followers" + "get_followers", + "get_following" ]; res = state.mergeDeep({ loading: noLoadingMethods.indexOf(action.payload.method) !== -1 ? false : true, diff --git a/app/redux/FollowSaga.js b/app/redux/FollowSaga.js index 6422453c6a..3f9392a33a 100644 --- a/app/redux/FollowSaga.js +++ b/app/redux/FollowSaga.js @@ -4,15 +4,14 @@ import {Apis} from 'shared/api_client'; import {List} from 'immutable' // Test limit with 2 (not 1, infinate looping) -export function* loadFollows(method, follower, type, start = '', limit = 100) { - const res = fromJS(yield Apis.follow(method, follower, start, type, limit)) +export function* loadFollows(method, account, type, start = '', limit = 100) { + const res = fromJS(yield Apis.follow(method, account, start, type, limit)) // console.log('res.toJS()', res.toJS()) let cnt = 0 let lastFollowing = null const key = method === "get_following" ? "following" : "follower"; - yield put({type: 'global/UPDATE', payload: { - key: ['follow', method, follower], + key: ['follow', method, account], notSet: Map(), updater: m => { m = m.update('result', Map(), m2 => { @@ -25,14 +24,17 @@ export function* loadFollows(method, follower, type, start = '', limit = 100) { }) return m2 }) - return m.merge({[type]: {loading: true, error: null}}) + const count = m.get('result').filter(a => { + return a.get(0) === "blog"; + }).size; + return m.merge({count, [type]: {loading: true, error: null}}) } }}) if(cnt === limit) { - yield call(loadFollows, method, follower, type, lastFollowing) + yield call(loadFollows, method, account, type, lastFollowing) } else { yield put({type: 'global/UPDATE', payload: { - key: ['follow', method, follower], + key: ['follow', method, account], updater: m => m.merge({[type]: {loading: false, error: null}}) }}) } From 272e24fe4993d7cc9e5f3292ec21896e1affff55 Mon Sep 17 00:00:00 2001 From: Sigve Date: Wed, 30 Nov 2016 19:53:16 -0500 Subject: [PATCH 29/90] Add shouldComponentUpdate logic to UserWallet and UserProfile --- app/components/modules/UserWallet.jsx | 55 ++++++++++++++++----------- app/components/pages/UserProfile.jsx | 48 ++++++++++++++--------- 2 files changed, 61 insertions(+), 42 deletions(-) diff --git a/app/components/modules/UserWallet.jsx b/app/components/modules/UserWallet.jsx index 630a73cbf7..36b750d3fc 100644 --- a/app/components/modules/UserWallet.jsx +++ b/app/components/modules/UserWallet.jsx @@ -13,6 +13,7 @@ import {steemTip, powerTip, valueTip, savingsTip} from 'app/utils/Tips' import {numberWithCommas, vestingSteem} from 'app/utils/StateFunctions' import FoundationDropdownMenu from 'app/components/elements/FoundationDropdownMenu' import WalletSubMenu from 'app/components/elements/WalletSubMenu' +import shouldComponentUpdate from 'app/utils/shouldComponentUpdate'; class UserWallet extends React.Component { constructor() { @@ -28,18 +29,21 @@ class UserWallet extends React.Component { this.setState({showDeposit: !this.state.showDeposit, depositType: 'VESTS'}) } // this.onShowDeposit = this.onShowDeposit.bind(this) + this.shouldComponentUpdate = shouldComponentUpdate(this, 'UserWallet'); } render() { - const {state: {showDeposit, depositType, toggleDivestError}, onShowDeposit, onShowDepositSteem, onShowDepositPower} = this - const {convertToSteem, price_per_steem, savings_withdraws} = this.props - let account = this.props.account; - let current_user = this.props.current_user; - let gprops = this.props.global.getIn( ['props'] ).toJS(); + const {state: {showDeposit, depositType, toggleDivestError}, + onShowDeposit, onShowDepositSteem, onShowDepositPower} = this + const {convertToSteem, price_per_steem, savings_withdraws, account, + current_user} = this.props + const gprops = this.props.gprops.toJS(); - let vesting_steemf = vestingSteem(account, gprops); + if (!account) return null; + + let vesting_steemf = vestingSteem(account.toJS(), gprops); let vesting_steem = vesting_steemf.toFixed(3); - let isMyAccount = current_user && current_user.get('username') === account.name; + let isMyAccount = current_user && current_user.get('username') === account.get('name'); const disabledWarning = false; // isMyAccount = false; // false to hide wallet transactions @@ -47,17 +51,18 @@ class UserWallet extends React.Component { const showTransfer = (asset, transferType, e) => { e.preventDefault(); this.props.showTransfer({ - to: (isMyAccount ? null : account.name), + to: (isMyAccount ? null : account.get('name')), asset, transferType }); }; - const {savings_balance, savings_sbd_balance} = account + const savings_balance = account.get('savings_balance'); + const savings_sbd_balance = account.get('savings_sbd_balance'); const powerDown = (cancel, e) => { e.preventDefault() - const {name} = account - const vesting_shares = cancel ? '0.000000 VESTS' : account.vesting_shares + const name = account.get('name'); + const vesting_shares = cancel ? '0.000000 VESTS' : account.get('vesting_shares') this.setState({toggleDivestError: null}) const errorCallback = e2 => {this.setState({toggleDivestError: e2.toString()})} const successCallback = () => {this.setState({toggleDivestError: null})} @@ -77,11 +82,11 @@ class UserWallet extends React.Component { }) } - const balance_steem = parseFloat(account.balance.split(' ')[0]); + const balance_steem = parseFloat(account.get('balance').split(' ')[0]); const saving_balance_steem = parseFloat(savings_balance.split(' ')[0]); const total_steem = (vesting_steemf + balance_steem + saving_balance_steem + savings_pending).toFixed(3); - const divesting = parseFloat(account.vesting_withdraw_rate.split(' ')[0]) > 0.000000; - const sbd_balance = parseFloat(account.sbd_balance) + const divesting = parseFloat(account.get('vesting_withdraw_rate').split(' ')[0]) > 0.000000; + const sbd_balance = parseFloat(account.get('sbd_balance')) const sbd_balance_savings = parseFloat(savings_sbd_balance.split(' ')[0]); const total_sbd = sbd_balance + sbd_balance_savings + savings_sbd_pending @@ -98,16 +103,18 @@ class UserWallet extends React.Component { /// transfer log let idx = 0 - const transfer_log = account.transfer_history.map(item => { - const data = item[1].op[1] + const transfer_log = account.get('transfer_history').map(item => { + const data = item.getIn([1, 'op', 1]); + const type = item.getIn([1, 'op', 0]); + // Filter out rewards - if (item[1].op[0] === "curation_reward" || item[1].op[0] === "author_reward") { + if (type === "curation_reward" || type === "author_reward") { return null; } if(data.sbd_payout === '0.000 SBD' && data.vesting_payout === '0.000000 VESTS') return null - return ; + return ; }).filter(el => !!el); transfer_log.reverse(); @@ -134,7 +141,7 @@ class UserWallet extends React.Component { { value: 'Buy or Sell', link: '/market' }, { value: 'Convert to STEEM', link: '#', onClick: convertToSteem }, ] - const isWithdrawScheduled = new Date(account.next_vesting_withdrawal + 'Z').getTime() > Date.now() + const isWithdrawScheduled = new Date(account.get('next_vesting_withdrawal') + 'Z').getTime() > Date.now() const depositReveal = showDeposit &&
      @@ -162,7 +169,7 @@ class UserWallet extends React.Component { return (
      - {isMyAccount ? :

      BALANCES


      } + {isMyAccount ? :

      BALANCES


      }
      {isMyAccount && } @@ -228,7 +235,7 @@ class UserWallet extends React.Component {
      - {isWithdrawScheduled && The next power down is scheduled to happen  . } + {isWithdrawScheduled && The next power down is scheduled to happen  . } {/*toggleDivestError &&
      {toggleDivestError}
      */}
      @@ -275,12 +282,14 @@ export default connect( price_per_steem = parseFloat(base.split(' ')[0]) } const savings_withdraws = state.user.get('savings_withdraws') - const sbd_interest = state.global.get('props').get('sbd_interest_rate') + const gprops = state.global.get('props'); + const sbd_interest = gprops.get('sbd_interest_rate') return { ...ownProps, price_per_steem, savings_withdraws, - sbd_interest + sbd_interest, + gprops } }, // mapDispatchToProps diff --git a/app/components/pages/UserProfile.jsx b/app/components/pages/UserProfile.jsx index e60a56af0b..561015e45a 100644 --- a/app/components/pages/UserProfile.jsx +++ b/app/components/pages/UserProfile.jsx @@ -37,6 +37,19 @@ export default class UserProfile extends React.Component { this.loadMore = this.loadMore.bind(this); } + shouldComponentUpdate(np) { + return ( + np.current_user !== this.props.current_user || + np.accounts !== this.props.accounts || + np.wifShown !== this.props.wifShown || + np.global_status !== this.props.global_status || + np.follow !== this.props.follow || + np.loading !== this.props.loading || + np.location.pathname !== this.props.location.pathname || + np.routeParams.accountname !== this.props.routeParams.accountname + ) + } + componentWillUnmount() { this.props.clearTransferDefaults() } @@ -54,14 +67,14 @@ export default class UserProfile extends React.Component { default: console.log('unhandled category:', category); } - if (isFetchingOrRecentlyUpdated(this.props.global.get('status'), order, category)) return; + if (isFetchingOrRecentlyUpdated(this.props.global_status, order, category)) return; const [author, permlink] = last_post.split('/'); this.props.requestData({author, permlink, order, category, accountname}); } render() { const { - props: {current_user, wifShown}, + props: {current_user, wifShown, global_status, follow}, onPrint } = this; let { accountname, section } = this.props.routeParams; @@ -76,7 +89,7 @@ export default class UserProfile extends React.Component { // const isMyAccount = current_user ? current_user.get('username') === accountname : false; let account - let accountImm = this.props.global.getIn(['accounts', accountname]); + let accountImm = this.props.accounts.get(accountname); if( accountImm ) { account = accountImm.toJS(); } @@ -85,16 +98,13 @@ export default class UserProfile extends React.Component { } let followerCount, followingCount; - const followers = this.props.global.getIn( ['follow', 'get_followers', accountname] ); - const following = this.props.global.getIn( ['follow', 'get_following', accountname] ); - + const followers = follow ? follow.getIn( ['get_followers', accountname] ) : null; + const following = follow ? follow.getIn( ['get_following', accountname] ) : null; if(followers && followers.has('result') && followers.has('blog')) { const status_followers = followers.get('blog') const followers_loaded = status_followers.get('loading') === false && status_followers.get('error') == null if (followers_loaded) { - followerCount = followers.get('result').filter(a => { - return a.get(0) === "blog"; - }).size; + followerCount = followers.get('count'); } } @@ -102,9 +112,7 @@ export default class UserProfile extends React.Component { const status_following = following.get('blog') const following_loaded = status_following.get('loading') === false && status_following.get('error') == null if (following_loaded) { - followingCount = following.get('result').filter(a => { - return a.get(0) === "blog"; - }).size; + followingCount = following.get('count'); } } @@ -114,7 +122,7 @@ export default class UserProfile extends React.Component { const name = account.name; let tab_content = null; - const global_status = this.props.global.get('status'); + // const global_status = this.props.global.get('status'); const status = global_status ? global_status.getIn([section, 'by_author']) : null; const fetching = (status && status.fetching) || this.props.loading; @@ -129,8 +137,8 @@ export default class UserProfile extends React.Component { if( section === 'transfers' ) { walletClass = 'active' tab_content =
      - @@ -139,14 +147,14 @@ export default class UserProfile extends React.Component { } else if( section === 'curation-rewards' ) { rewardsClass = "active"; - tab_content = } else if( section === 'author-rewards' ) { rewardsClass = "active"; - tab_content = @@ -370,11 +378,13 @@ module.exports = { // const current_account = current_user && state.global.getIn(['accounts', current_user.get('username')]) return { discussions: state.global.get('discussion_idx'), - global: state.global, current_user, // current_account, wifShown, - loading: state.app.get('loading') + loading: state.app.get('loading'), + global_status: state.global.get('status'), + accounts: state.global.get('accounts'), + follow: state.global.get('follow') }; }, dispatch => ({ From 185d967089385d77b999c24829c97837c1533b6e Mon Sep 17 00:00:00 2001 From: Sigve Date: Thu, 1 Dec 2016 10:35:18 -0500 Subject: [PATCH 30/90] Improve noLoadingRequests methods handling --- app/redux/AppReducer.js | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/app/redux/AppReducer.js b/app/redux/AppReducer.js index f9658473fc..ce3c8f1149 100644 --- a/app/redux/AppReducer.js +++ b/app/redux/AppReducer.js @@ -7,6 +7,7 @@ const defaultState = Map({ error: '', location: {}, notifications: null, + noLoadingRequests: 0, notificounters: Map({ total: 0, feed: 0, @@ -38,23 +39,29 @@ export default function reducer(state = defaultState, action) { let res = state; if (action.type === 'RPC_REQUEST_STATUS') { const request_id = action.payload.id + ''; - // console.log(new Date().getTime(), "RPC_REQUEST_STATUS:", action.payload.method); + const noLoadingMethods = [ + 'get_dynamic_global_properties', + 'get_api_by_name', + 'get_followers', + 'get_following' + ]; + const noLoadMethod = noLoadingMethods.indexOf(action.payload.method) !== -1; if (action.payload.event === 'BEGIN') { - const noLoadingMethods = [ - "get_dynamic_global_properties", - "get_api_by_name", - "get_followers", - "get_following" - ]; res = state.mergeDeep({ - loading: noLoadingMethods.indexOf(action.payload.method) !== -1 ? false : true, - requests: {[request_id]: Date.now()} + loading: noLoadMethod ? false : true, + requests: {[request_id]: Date.now()}, + noLoadingRequests: state.get('noLoadingRequests') + (noLoadMethod ? 1 : 0) }); } if (action.payload.event === 'END' || action.payload.event === 'ERROR') { + const noLoadCount = state.get('noLoadingRequests') - (noLoadMethod ? 1 : 0); res = res.deleteIn(['requests', request_id]); - const loading = res.get('requests').size > 0; - res = res.set('loading', loading); + // console.log("RPC_REQUEST END:", action.payload.method, res.get('requests').size, "noLoadCount", noLoadCount); + const loading = (res.get('requests').size - noLoadCount) > 0; + res = res.mergeDeep({ + loading, + noLoadingRequests: noLoadCount + }); } } if (action.type === 'ADD_NOTIFICATION') { From b3fb08d627c3a558a0858af96f348e6535fef724 Mon Sep 17 00:00:00 2001 From: Sigve Date: Thu, 1 Dec 2016 10:36:01 -0500 Subject: [PATCH 31/90] Improve UserProfile shouldComponentUpdate logic --- app/components/pages/UserProfile.jsx | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/app/components/pages/UserProfile.jsx b/app/components/pages/UserProfile.jsx index 561015e45a..390ef4a5c3 100644 --- a/app/components/pages/UserProfile.jsx +++ b/app/components/pages/UserProfile.jsx @@ -38,12 +38,27 @@ export default class UserProfile extends React.Component { } shouldComponentUpdate(np) { + const {follow} = this.props; + let followersLoading = false, npFollowersLoading = false; + let followingLoading = false, npFollowingLoading = false; + + const account = np.routeParams.accountname.toLowerCase(); + if (follow) { + followersLoading = follow.getIn(['get_followers', account, 'blog', 'loading'], false); + followingLoading = follow.getIn(['get_following', account, 'blog', 'loading'], false); + } + if (np.follow) { + npFollowersLoading = np.follow.getIn(['get_followers', account, 'blog', 'loading'], false); + npFollowingLoading = np.follow.getIn(['get_following', account, 'blog', 'loading'], false); + } + return ( np.current_user !== this.props.current_user || - np.accounts !== this.props.accounts || + np.accounts.get(account) !== this.props.accounts.get(account) || np.wifShown !== this.props.wifShown || np.global_status !== this.props.global_status || - np.follow !== this.props.follow || + ((npFollowersLoading !== followersLoading) && !npFollowersLoading) || + ((npFollowingLoading !== followingLoading) && !npFollowingLoading) || np.loading !== this.props.loading || np.location.pathname !== this.props.location.pathname || np.routeParams.accountname !== this.props.routeParams.accountname @@ -219,7 +234,8 @@ export default class UserProfile extends React.Component { tab_content = (
      ); } } - else if( (section === 'recent-replies') && account.recent_replies ) { + else if( (section === 'recent-replies')) { + if (account.recent_replies) { tab_content =
      {isMyAccount && }
      ; + } else { + tab_content = (
      ); + } } else if( section === 'permissions' && isMyAccount ) { walletClass = 'active' From 31c46a36bc253f2b5616cd013b1dc48d20eb9ba7 Mon Sep 17 00:00:00 2001 From: Sigve Date: Thu, 1 Dec 2016 11:49:02 -0500 Subject: [PATCH 32/90] Add current open orders to UserWallet balances for logged in user, fix #51 --- app/components/modules/UserWallet.jsx | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/app/components/modules/UserWallet.jsx b/app/components/modules/UserWallet.jsx index 630a73cbf7..aefc07df3f 100644 --- a/app/components/modules/UserWallet.jsx +++ b/app/components/modules/UserWallet.jsx @@ -1,6 +1,7 @@ /* eslint react/prop-types: 0 */ import React from 'react'; import {connect} from 'react-redux' +import {Link} from 'react-router' import g from 'app/redux/GlobalReducer' import SavingsWithdrawHistory from 'app/components/elements/SavingsWithdrawHistory'; import TransferHistoryRow from 'app/components/cards/TransferHistoryRow'; @@ -13,6 +14,10 @@ import {steemTip, powerTip, valueTip, savingsTip} from 'app/utils/Tips' import {numberWithCommas, vestingSteem} from 'app/utils/StateFunctions' import FoundationDropdownMenu from 'app/components/elements/FoundationDropdownMenu' import WalletSubMenu from 'app/components/elements/WalletSubMenu' +import Tooltip from 'app/components/elements/Tooltip' +import { translate } from 'app/Translator'; + +const assetPrecision = 1000; class UserWallet extends React.Component { constructor() { @@ -31,7 +36,7 @@ class UserWallet extends React.Component { } render() { const {state: {showDeposit, depositType, toggleDivestError}, onShowDeposit, onShowDepositSteem, onShowDepositPower} = this - const {convertToSteem, price_per_steem, savings_withdraws} = this.props + const {convertToSteem, price_per_steem, savings_withdraws, open_orders} = this.props let account = this.props.account; let current_user = this.props.current_user; let gprops = this.props.global.getIn( ['props'] ).toJS(); @@ -84,6 +89,18 @@ class UserWallet extends React.Component { const sbd_balance = parseFloat(account.sbd_balance) const sbd_balance_savings = parseFloat(savings_sbd_balance.split(' ')[0]); const total_sbd = sbd_balance + sbd_balance_savings + savings_sbd_pending + const sbdOrders = (!open_orders || !isMyAccount) ? 0 : open_orders.reduce((o, order) => { + if (order.sell_price.base.indexOf("SBD") !== -1) { + o += order.for_sale; + } + return o; + }, 0) / assetPrecision; + const steemOrders = (!open_orders || !isMyAccount) ? 0 : open_orders.reduce((o, order) => { + if (order.sell_price.base.indexOf("STEEM") !== -1) { + o += order.for_sale; + } + return o; + }, 0) / assetPrecision; // set displayed estimated value let total_value = '$' + numberWithCommas( @@ -143,8 +160,10 @@ class UserWallet extends React.Component {
      const steem_balance_str = numberWithCommas(balance_steem.toFixed(3)) // formatDecimal(balance_steem, 3) + const steem_orders_balance_str = numberWithCommas(steemOrders.toFixed(3)) const power_balance_str = numberWithCommas(vesting_steem) // formatDecimal(vesting_steem, 3) const sbd_balance_str = numberWithCommas('$' + sbd_balance.toFixed(3)) // formatDecimal(account.sbd_balance, 3) + const sbd_orders_balance_str = numberWithCommas('$' + sbdOrders.toFixed(3)) const savings_balance_str = numberWithCommas(saving_balance_steem.toFixed(3) + ' STEEM') const savings_sbd_balance_str = numberWithCommas('$' + sbd_balance_savings.toFixed(3)) @@ -176,6 +195,7 @@ class UserWallet extends React.Component { {isMyAccount ? : steem_balance_str + ' STEEM'} + {steemOrders ?
      ({steem_orders_balance_str} STEEM)
      : null}
      @@ -196,6 +216,7 @@ class UserWallet extends React.Component { {isMyAccount ? : sbd_balance_str} + {sbdOrders ?
      ({sbd_orders_balance_str})
      : null}
      @@ -278,6 +299,7 @@ export default connect( const sbd_interest = state.global.get('props').get('sbd_interest_rate') return { ...ownProps, + open_orders: state.market.get('open_orders'), price_per_steem, savings_withdraws, sbd_interest From 5927eb80ea12536c1580cacee2d12c33532a08b4 Mon Sep 17 00:00:00 2001 From: Mike Cifani Date: Thu, 1 Dec 2016 12:22:57 -0500 Subject: [PATCH 33/90] 411 cant scroll share (#739) * New compressed share menu, fix share bug, new element * Styling update for share icons --- app/components/all.scss | 1 + app/components/cards/PostFull.jsx | 3 ++- app/components/elements/ShareMenu.jsx | 33 ++++++++++++++++++++++++++ app/components/elements/ShareMenu.scss | 20 ++++++++++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 app/components/elements/ShareMenu.jsx create mode 100644 app/components/elements/ShareMenu.scss diff --git a/app/components/all.scss b/app/components/all.scss index 6db96bf39f..02afe4d16d 100644 --- a/app/components/all.scss +++ b/app/components/all.scss @@ -27,6 +27,7 @@ @import "./elements/Reblog"; @import "./elements/YoutubePreview"; @import "./elements/SignupProgressBar"; +@import "./elements/ShareMenu"; // modules @import "./modules/Header"; diff --git a/app/components/cards/PostFull.jsx b/app/components/cards/PostFull.jsx index 08c73e640d..704e1afc30 100644 --- a/app/components/cards/PostFull.jsx +++ b/app/components/cards/PostFull.jsx @@ -21,6 +21,7 @@ import {Long} from 'bytebuffer' import {List} from 'immutable' import {repLog10, parsePayoutAmount} from 'app/utils/ParsersAndFormatters'; import DMCAList from 'app/utils/DMCAList' +import ShareMenu from 'app/components/elements/ShareMenu'; function TimeAuthorCategory({content, authorRepLog10, showTags}) { return ( @@ -274,7 +275,7 @@ class PostFull extends React.Component { {' '}{showEditOption && !showEdit && Edit} {' '}{showDeleteOption && !showReply && Delete} } - +
      diff --git a/app/components/elements/ShareMenu.jsx b/app/components/elements/ShareMenu.jsx new file mode 100644 index 0000000000..799c33df8d --- /dev/null +++ b/app/components/elements/ShareMenu.jsx @@ -0,0 +1,33 @@ +import React from 'react' +import { Link } from 'react-router' +import Icon from 'app/components/elements/Icon.jsx'; + +export default class ShareMenu extends React.Component { + + static propTypes = { + items: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, + title: React.PropTypes.string + }; + + render(){ + const title = this.props.title; + const items = this.props.menu; + return +
        + {title &&
      • {title}
      • } + {items.map(i => { + return
      • + {i.link ? + {i.icon && }{} +   {i.addon} + : + + {i.icon && }{i.label ? i.label : i.value} + + } +
      • + })} +
      +
      + } +} diff --git a/app/components/elements/ShareMenu.scss b/app/components/elements/ShareMenu.scss new file mode 100644 index 0000000000..40b7983990 --- /dev/null +++ b/app/components/elements/ShareMenu.scss @@ -0,0 +1,20 @@ +.shareMenu { + display: inline-block; + vertical-align: middle; +} + +.shareItems { + list-style: none; + display: inline; + margin-left: 0.01em; + li { + float: left; + padding-left: 5px; + } + li > a:hover { + color: #ffffff; + } + li > a:link { + text-decoration: none; + } +} From b0e25076fe9c4a738c9505145f0a7e7ec98a9fc8 Mon Sep 17 00:00:00 2001 From: Sigve Date: Thu, 1 Dec 2016 13:13:45 -0500 Subject: [PATCH 34/90] Also update global account state on MarketSaga get_accounts, fixes #437 --- app/components/pages/Market.jsx | 2 +- app/components/pages/Witnesses.jsx | 28 ++++++++++++++-------------- app/redux/MarketSaga.js | 18 +++++++++++------- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/app/components/pages/Market.jsx b/app/components/pages/Market.jsx index 41cdac52f2..0cf12ee685 100644 --- a/app/components/pages/Market.jsx +++ b/app/components/pages/Market.jsx @@ -211,7 +211,7 @@ class Market extends React.Component { }, {}) } - let account = this.props.account + let account = this.props.account ? this.props.account.toJS() : null; let open_orders = this.props.open_orders; let orderbook = aggOrders(normalizeOrders(this.props.orderbook)); diff --git a/app/components/pages/Witnesses.jsx b/app/components/pages/Witnesses.jsx index 3c910da090..75719b6652 100644 --- a/app/components/pages/Witnesses.jsx +++ b/app/components/pages/Witnesses.jsx @@ -5,7 +5,7 @@ import links from 'app/utils/Links' import Icon from 'app/components/elements/Icon'; import transaction from 'app/redux/Transaction' import ByteBuffer from 'bytebuffer' -import {Set} from 'immutable' +import {Set, is} from 'immutable' import { translate } from 'app/Translator'; const Long = ByteBuffer.Long @@ -16,7 +16,7 @@ class Witnesses extends React.Component { // HTML properties // Redux connect properties - global: object.isRequired, + witnesses: object.isRequired, accountWitnessVote: func.isRequired, username: string, witness_votes: object, @@ -36,20 +36,20 @@ class Witnesses extends React.Component { } } + shouldComponentUpdate(np, ns) { + return ( + !is(np.witness_votes, this.props.witness_votes) || + np.witnesses !== this.props.witnesses || + np.username !== this.props.username || + ns.customUsername !== this.state.customUsername + ); + } + render() { - const {props: {global, witness_votes}, state: {customUsername}, accountWitnessVote, onWitnessChange} = this - const sorted_witnesses = global.getIn(['witnesses']) + const {props: {witness_votes}, state: {customUsername}, accountWitnessVote, onWitnessChange} = this + const sorted_witnesses = this.props.witnesses .sort((a, b) => Long.fromString(String(b.get('votes'))).subtract(Long.fromString(String(a.get('votes'))).toString())); - const header = -
      -
      - -
      -
      - -
      -
      const up = ; let witness_vote_count = 30 let rank = 1 @@ -164,7 +164,7 @@ module.exports = { const current_account = current_user && state.global.getIn(['accounts', username]) const witness_votes = current_account && Set(current_account.get('witness_votes')) return { - global: state.global, + witnesses: state.global.get('witnesses'), username, witness_votes, }; diff --git a/app/redux/MarketSaga.js b/app/redux/MarketSaga.js index a7178d4472..576c6c52b0 100644 --- a/app/redux/MarketSaga.js +++ b/app/redux/MarketSaga.js @@ -1,9 +1,10 @@ -import {takeLatest, takeEvery} from 'redux-saga'; -import {call, put, select} from 'redux-saga/effects'; +import {takeLatest} from 'redux-saga'; +import {call, put} from 'redux-saga/effects'; import Apis from 'shared/api_client/ApiInstances'; import MarketReducer from './MarketReducer'; -import constants from './constants'; -import {fromJS, Map} from 'immutable' +import g from 'app/redux/GlobalReducer' +// import constants from './constants'; +import {fromJS} from 'immutable' export const marketWatches = [watchLocationChange, watchUserLogin, watchMarketUpdate]; @@ -68,9 +69,12 @@ export function* fetchOpenOrders(set_user_action) { const state = yield call([db_api, db_api.exec], 'get_open_orders', [username]); yield put(MarketReducer.actions.receiveOpenOrders(state)); - const [account] = yield call(Apis.db_api, 'get_accounts', [username]) - yield put(MarketReducer.actions.receiveAccount({ account })) - + let [account] = yield call(Apis.db_api, 'get_accounts', [username]) + if(account) { + account = fromJS(account) + yield put(MarketReducer.actions.receiveAccount({ account })) + yield put(g.actions.receiveAccount({ account })) + } } catch (error) { console.error('~~ Saga fetchOpenOrders error ~~>', error); yield put({type: 'global/STEEM_API_ERROR', error: error.message}); From 418076cd2365bd22bc196e8c01b3d68dc02be3f6 Mon Sep 17 00:00:00 2001 From: Sigve Date: Thu, 1 Dec 2016 13:20:15 -0500 Subject: [PATCH 35/90] Remove console.log, add thumbSize to shouldComponentUpdate --- app/components/cards/Comment.jsx | 1 - app/components/cards/PostsList.jsx | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/cards/Comment.jsx b/app/components/cards/Comment.jsx index 48fa4c4cd9..7985c77313 100644 --- a/app/components/cards/Comment.jsx +++ b/app/components/cards/Comment.jsx @@ -48,7 +48,6 @@ export function sortComments( cont, comments, sort_order ) { }, new: (a,b) => { let acontent = cont.get(a); - console.log("acontent", acontent); let bcontent = cont.get(b); if (netNegative(acontent)) { return 1; diff --git a/app/components/cards/PostsList.jsx b/app/components/cards/PostsList.jsx index f22916ce15..0d2f08ac2a 100644 --- a/app/components/cards/PostsList.jsx +++ b/app/components/cards/PostsList.jsx @@ -60,7 +60,8 @@ class PostsList extends React.Component { np.loading !== this.props.loading || np.category !== this.props.category || ns.showNegativeComments !== this.state.showNegativeComments || - ns.showPost !== this.state.showPost + ns.showPost !== this.state.showPost || + ns.thumbSize !== this.state.thumbSize ); } From a5a5da5ef5025a5404a80be1d17151f268de5b84 Mon Sep 17 00:00:00 2001 From: Sigve Date: Thu, 1 Dec 2016 13:27:06 -0500 Subject: [PATCH 36/90] Ensure 'result' is not undefined in FollowSaga --- app/redux/FollowSaga.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/redux/FollowSaga.js b/app/redux/FollowSaga.js index 3f9392a33a..88ef6b1f2a 100644 --- a/app/redux/FollowSaga.js +++ b/app/redux/FollowSaga.js @@ -24,9 +24,9 @@ export function* loadFollows(method, account, type, start = '', limit = 100) { }) return m2 }) - const count = m.get('result').filter(a => { + const count = m.get('result') ? m.get('result').filter(a => { return a.get(0) === "blog"; - }).size; + }).size : 0; return m.merge({count, [type]: {loading: true, error: null}}) } }}) From 51851239096a37099cd527afdf3c53d5be28611a Mon Sep 17 00:00:00 2001 From: originate Date: Thu, 1 Dec 2016 14:47:36 -0500 Subject: [PATCH 37/90] Handled for malformed case on top categories --- server/server.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/server.js b/server/server.js index 9fe3079e11..abc1dc6f9c 100644 --- a/server/server.js +++ b/server/server.js @@ -54,6 +54,14 @@ app.use(function *(next) { return; } } + // normalize top category filtering from cased params + if (this.method === 'GET' && /(hot|created|trending|active)/ig.test(this.url)) { + const p = this.originalUrl.toLowerCase(); + if(p !== this.originalUrl) { + this.redirect(p); + return; + } + } // start registration process if user get to create_account page and has no id in session yet if(this.url === '/create_account' && !this.session.user) { this.status = 302; From 041f2feb794224224bcff862a2964f9e9e771d47 Mon Sep 17 00:00:00 2001 From: Sigve Date: Thu, 1 Dec 2016 14:52:16 -0500 Subject: [PATCH 38/90] Use 'posts' for update logic instead of creating an immutable set in PostsList connect --- app/components/cards/PostsList.jsx | 41 +++++++++++++++++++----------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/app/components/cards/PostsList.jsx b/app/components/cards/PostsList.jsx index 0d2f08ac2a..7c460354bc 100644 --- a/app/components/cards/PostsList.jsx +++ b/app/components/cards/PostsList.jsx @@ -52,9 +52,8 @@ class PostsList extends React.Component { } shouldComponentUpdate(np, ns) { - // console.log("np.postsInfo !== this.props.postsInfo", !Immutable.is(np.postsInfo, this.props.postsInfo)); return ( - !Immutable.is(np.postsInfo, this.props.postsInfo) || + np.posts !== this.props.posts || np.loadMore !== this.props.loadMore || np.showSpam !== this.props.showSpam || np.loading !== this.props.loading || @@ -170,6 +169,7 @@ class PostsList extends React.Component { } render() { + console.log('PostsList render') const {posts, loading, category, emptyText, postsInfo} = this.props; const {account} = this.props const {thumbSize, showPost} = this.state @@ -177,10 +177,19 @@ class PostsList extends React.Component { if (!loading && (posts && !posts.size) && emptyText) { return {emptyText}; } - const renderSummary = items => items.map(item =>
    • - + const renderSummary = items => items.map(item =>
    • +
    • ) + return (
        @@ -211,22 +220,24 @@ import {connect} from 'react-redux' export default connect( (state, props) => { const {posts, showSpam} = props; - let postsInfo = Immutable.List(); + const postsInfo = []; const pathname = state.app.get('location').pathname; + + const current = state.user.get('current') + const username = current ? current.get('username') : null + const content = state.global.get('content'); + posts.forEach(item => { - const content = state.global.get('content').get(item); - if(!content) { - console.error('PostsList --> Missing content key', item) + const cont = content.get(item); + if(!cont) { + console.error('PostsList --> Missing cont key', item) return } - // let total_payout = 0; - const current = state.user.get('current') - const username = current ? current.get('username') : null - const key = ['follow', 'get_following', username, 'result', content.get('author')] + const key = ['follow', 'get_following', username, 'result', cont.get('author')] const ignore = username ? state.global.getIn(key, List()).contains('ignore') : false - const {hide, netVoteSign, authorRepLog10} = content.get('stats').toJS() + const {hide, netVoteSign, authorRepLog10} = cont.get('stats').toJS() if(!(ignore || hide) || showSpam) // rephide - postsInfo = postsInfo.push(Immutable.fromJS({item, ignore, netVoteSign, authorRepLog10})) + postsInfo.push({item, ignore, netVoteSign, authorRepLog10}) }) return {...props, postsInfo, pathname}; From 815c189f3b00dd5ab7046b500612ddf0027ae3ac Mon Sep 17 00:00:00 2001 From: James Calfee Date: Thu, 1 Dec 2016 14:28:38 -0600 Subject: [PATCH 39/90] Adjust follow lists to only appear after loaded has finished. --- app/redux/FollowSaga.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/redux/FollowSaga.js b/app/redux/FollowSaga.js index 88ef6b1f2a..240e819168 100644 --- a/app/redux/FollowSaga.js +++ b/app/redux/FollowSaga.js @@ -14,7 +14,7 @@ export function* loadFollows(method, account, type, start = '', limit = 100) { key: ['follow', method, account], notSet: Map(), updater: m => { - m = m.update('result', Map(), m2 => { + m = m.update('result_loading', Map(), m2 => { res.forEach(value => { cnt++ let what = value.get('what') @@ -24,10 +24,7 @@ export function* loadFollows(method, account, type, start = '', limit = 100) { }) return m2 }) - const count = m.get('result') ? m.get('result').filter(a => { - return a.get(0) === "blog"; - }).size : 0; - return m.merge({count, [type]: {loading: true, error: null}}) + return m.merge({[type]: {loading: true, error: null}}) } }}) if(cnt === limit) { @@ -35,7 +32,13 @@ export function* loadFollows(method, account, type, start = '', limit = 100) { } else { yield put({type: 'global/UPDATE', payload: { key: ['follow', method, account], - updater: m => m.merge({[type]: {loading: false, error: null}}) + updater: m => { + const result = m.get('result_loading') + const count = !result ? 0 : result.filter(a => a.get(0) === "blog").size + m = m.delete('result_loading') + m = m.set('result', result) + return m.merge({count, [type]: {loading: false, error: null}}) + } }}) } } From 5477055889531c20f8abdc8d7e272b2655ccfcf4 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 1 Dec 2016 15:40:50 -0500 Subject: [PATCH 40/90] do not set comment_options on edit. fix #735 --- app/components/elements/ReplyEditor.jsx | 40 ++++++++++++++----------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/app/components/elements/ReplyEditor.jsx b/app/components/elements/ReplyEditor.jsx index 35ede76298..fd36875528 100644 --- a/app/components/elements/ReplyEditor.jsx +++ b/app/components/elements/ReplyEditor.jsx @@ -437,17 +437,20 @@ export default formId => reduxForm( // const post = state.global.getIn(['content', author + '/' + permlink]) const username = state.user.getIn(['current', 'username']) + const isEdit = /edit/.test(type) + const isNew = /^submit_/.test(type) + // Wire up the current and parent props for either an Edit or a Submit (new post) //'submit_story', 'submit_comment', 'edit' const linkProps = - /^submit_/.test(type) ? { // submit new + isNew ? { // submit new parent_author: author, parent_permlink: permlink, author: username, // permlink, assigned in TransactionSaga } : // edit existing - /^edit$/.test(type) ? {author, permlink, parent_author, parent_permlink} + isEdit ? {author, permlink, parent_author, parent_permlink} : null if (!linkProps) throw new Error('Unknown type: ' + type) @@ -480,7 +483,7 @@ export default formId => reduxForm( if(rootTag) allCategories = allCategories.add(rootTag) // merge - const meta = /edit/.test(type) ? jsonMetadata : {} + const meta = isEdit ? jsonMetadata : {} if(allCategories.size) meta.tags = allCategories.toJS(); else delete meta.tags if(rtags.usertags.size) meta.users = rtags.usertags; else delete meta.users if(rtags.images.size) meta.image = rtags.images; else delete meta.image @@ -500,28 +503,31 @@ export default formId => reduxForm( } if(meta.tags.length > 5) { - const includingCategory = /edit/.test(type) ? ` (including the category '${rootCategory}')` : '' + const includingCategory = isEdit ? ` (including the category '${rootCategory}')` : '' errorCallback(`You have ${meta.tags.length} tags total${includingCategory}. Please use only 5 in your post and category line.`) return } // loadingCallback starts the loading indicator loadingCallback() - const originalBody = /edit/.test(type) ? originalPost.body : null + const originalBody = isEdit ? originalPost.body : null const __config = {originalBody, autoVote} - switch(payoutType) { - case '0%': // decline payout - __config.comment_options = { - max_accepted_payout: '0.000 SBD', - } - break; - case '100%': // 100% steem power payout - __config.comment_options = { - percent_steem_dollars: 0, // 10000 === 100% (of 50%) - } - break; - default: // 50% steem power, 50% sd+steem + // Avoid changing payout option during edits #735 + if(!isEdit) { + switch(payoutType) { + case '0%': // decline payout + __config.comment_options = { + max_accepted_payout: '0.000 SBD', + } + break; + case '100%': // 100% steem power payout + __config.comment_options = { + percent_steem_dollars: 0, // 10000 === 100% (of 50%) + } + break; + default: // 50% steem power, 50% sd+steem + } } const operation = { From 6950ec6e9311ce291e6f6cb0644a20a2e40704ec Mon Sep 17 00:00:00 2001 From: Sigve Date: Thu, 1 Dec 2016 15:44:34 -0500 Subject: [PATCH 41/90] Export cmp method from shouldComponentUpdate --- app/utils/shouldComponentUpdate.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/utils/shouldComponentUpdate.js b/app/utils/shouldComponentUpdate.js index c514c731db..74a82d671b 100644 --- a/app/utils/shouldComponentUpdate.js +++ b/app/utils/shouldComponentUpdate.js @@ -13,14 +13,14 @@ export default function (instance, name) { return (nextProps, nextState) => { const upd = mixin(nextProps, nextState) if (upd && process.env.BROWSER && window.steemDebug_shouldComponentUpdate) { - cmp(name, 'props', instance.props, nextProps) - cmp(name, 'state', instance.state, nextState) + cmp(name, instance.props, nextProps) + cmp(name, instance.state, nextState) } return upd } } -function cmp(name, type, a, b) { +export function cmp(name, a, b) { const aKeys = new Set(a && Object.keys(a)) const bKeys = new Set(b && Object.keys(b)) const ab = new Set([...aKeys, ...aKeys]) From 398573fcec77b3dbe026fea33192389f1ce40343 Mon Sep 17 00:00:00 2001 From: Sigve Date: Thu, 1 Dec 2016 15:47:06 -0500 Subject: [PATCH 42/90] Refactor PostsList connect method to move array creation inside render and add follow loading status --- app/components/cards/PostsList.jsx | 56 ++++++++++++++++-------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/app/components/cards/PostsList.jsx b/app/components/cards/PostsList.jsx index 7c460354bc..9576ab55ff 100644 --- a/app/components/cards/PostsList.jsx +++ b/app/components/cards/PostsList.jsx @@ -7,7 +7,7 @@ import Callout from 'app/components/elements/Callout'; import CloseButton from 'react-foundation-components/lib/global/close-button'; import {findParent} from 'app/utils/DomUtils'; import Icon from 'app/components/elements/Icon'; -import Immutable from "immutable"; +import {List} from "immutable"; function topPosition(domElt) { if (!domElt) { @@ -20,7 +20,6 @@ class PostsList extends React.Component { static propTypes = { posts: PropTypes.object.isRequired, - postsInfo: PropTypes.object.isRequired, loading: PropTypes.bool.isRequired, category: PropTypes.string, loadMore: PropTypes.func, @@ -51,9 +50,14 @@ class PostsList extends React.Component { // this.shouldComponentUpdate = shouldComponentUpdate(this, 'PostsList') } + componentDidMount() { + this.attachScrollListener(); + } + shouldComponentUpdate(np, ns) { return ( np.posts !== this.props.posts || + np.ignoreLoading !== this.props.ignoreLoading || np.loadMore !== this.props.loadMore || np.showSpam !== this.props.showSpam || np.loading !== this.props.loading || @@ -64,10 +68,6 @@ class PostsList extends React.Component { ); } - componentDidMount() { - this.attachScrollListener(); - } - componentWillUnmount() { this.detachScrollListener(); window.removeEventListener('popstate', this.onBackButton); @@ -169,14 +169,29 @@ class PostsList extends React.Component { } render() { - console.log('PostsList render') - const {posts, loading, category, emptyText, postsInfo} = this.props; - const {account} = this.props + const {posts, showSpam, loading, category, emptyText, content, + username, get_following, account} = this.props; const {thumbSize, showPost} = this.state if (!loading && (posts && !posts.size) && emptyText) { return {emptyText}; } + + const postsInfo = []; + + posts.forEach(item => { + const cont = content.get(item); + if(!cont) { + console.error('PostsList --> Missing cont key', item) + return + } + const key = ['result', cont.get('author')] + const ignore = get_following ? get_following.getIn(key, List()).contains('ignore') : false + const {hide, netVoteSign, authorRepLog10} = cont.get('stats').toJS() + if(!(ignore || hide) || showSpam) // rephide + postsInfo.push({item, ignore, netVoteSign, authorRepLog10}) + }); + const renderSummary = items => items.map(item =>
      • { - const {posts, showSpam} = props; - const postsInfo = []; const pathname = state.app.get('location').pathname; const current = state.user.get('current') const username = current ? current.get('username') : null const content = state.global.get('content'); - - posts.forEach(item => { - const cont = content.get(item); - if(!cont) { - console.error('PostsList --> Missing cont key', item) - return - } - const key = ['follow', 'get_following', username, 'result', cont.get('author')] - const ignore = username ? state.global.getIn(key, List()).contains('ignore') : false - const {hide, netVoteSign, authorRepLog10} = cont.get('stats').toJS() - if(!(ignore || hide) || showSpam) // rephide - postsInfo.push({item, ignore, netVoteSign, authorRepLog10}) - }) - - return {...props, postsInfo, pathname}; + const get_following = state.global.getIn(['follow', 'get_following', username]); + const ignoreLoading = !get_following ? false : get_following.getIn(['ignore', 'loading']); + // console.log("get_following:", get_following ? get_following.toJS() : null); + return {...props, content, ignoreLoading, get_following, pathname}; }, dispatch => ({ fetchState: (pathname) => { From 4288ed2cd158c667dbda0cdf74022dd296d8ee7f Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 28 Nov 2016 16:30:28 -0500 Subject: [PATCH 43/90] add new profile fields --- app/components/modules/Settings.jsx | 133 ++++++++++++++++++++-------- app/locales/en.js | 6 +- app/locales/ru.js | 6 +- 3 files changed, 104 insertions(+), 41 deletions(-) diff --git a/app/components/modules/Settings.jsx b/app/components/modules/Settings.jsx index 8bab7be48c..a1ad4810cd 100644 --- a/app/components/modules/Settings.jsx +++ b/app/components/modules/Settings.jsx @@ -7,43 +7,49 @@ import store from 'store'; import transaction from 'app/redux/Transaction' import o2j from 'shared/clash/object2json' import Userpic from 'app/components/elements/Userpic'; +import {reduxForm} from 'redux-form' +import {cleanReduxInput} from 'app/utils/ReduxForms' class Settings extends React.Component { state = { errorMessage: '', - succesMessage: '', - userImage: this.props.userImage || '', - changed: false + successMessage: '', } - handleCurrencyChange(event) { store.set('currency', event.target.value) } - - handleLanguageChange = (event) => { - const language = event.target.value - store.set('language', language) - this.props.changeLanguage(language) - } - - handleUrlChange = event => { - this.setState({userImage: event.target.value, changed: true}) - } - - handleUserImageSubmit = event => { + handleSubmit = event => { event.preventDefault() - this.setState({loading: true}) - const {account, updateAccount} = this.props let {metaData} = this.props - if (!metaData) metaData = {} - if (metaData == '{created_at: \'GENESIS\'}') metaData = {created_at: "GENESIS"} if(!metaData.profile) metaData.profile = {} - metaData.profile.profile_image = this.state.userImage - metaData = JSON.stringify(metaData); + delete metaData.user_image; // old field... cleanup + + const {profile_image, name, about, location, website} = this.props.fields + // Update relevant fields + metaData.profile.profile_image = profile_image.value + metaData.profile.name = name.value + metaData.profile.about = about.value + metaData.profile.location = location.value + metaData.profile.website = website.value + + // Remove empty keys + if(!metaData.profile.profile_image) delete metaData.profile.profile_image; + if(!metaData.profile.name) delete metaData.profile.name; + if(!metaData.profile.about) delete metaData.profile.about; + if(!metaData.profile.location) delete metaData.profile.location; + if(!metaData.profile.website) delete metaData.profile.website; + + // TODO: Update language & currency + //store.set('language', language) + //this.props.changeLanguage(language) + //store.set('currency', event.target.value) + + const {account, updateAccount} = this.props + this.setState({loading: true}) updateAccount({ - json_metadata: metaData, + json_metadata: JSON.stringify(metaData), account: account.name, memo_key: account.memo_key, errorCallback: (e) => { @@ -66,17 +72,24 @@ class Settings extends React.Component { loading: false, changed: false, errorMessage: '', - succesMessage: translate('saved') + '!', + successMessage: translate('saved') + '!', }) - // remove succesMessage after a while - setTimeout(() => this.setState({succesMessage: ''}), 2000) + // remove successMessage after a while + setTimeout(() => this.setState({successMessage: ''}), 2000) } }) } render() { const {state, props} = this + + const {invalid} = this.props + const disabled = !props.isOwnAccount || state.loading || invalid + + const {profile_image, name, about, location, website} = this.props.fields + return
        + {/*
        */} +
        - - - -
        -
        } } -export default connect( +export default reduxForm( + { + form: 'accountSettings', + fields: ['profile_image', 'name', 'about', 'location', 'website'] + }, // mapStateToProps (state, ownProps) => { const {accountname} = ownProps.routeParams @@ -134,13 +178,24 @@ export default connect( const current_user = state.user.get('current') const username = current_user ? current_user.get('username') : '' const metaData = account ? o2j.ifStringParseJSON(account.json_metadata) : {} - const userImage = metaData && metaData.profile ? metaData.profile.profile_image : '' + const profile_image = metaData && metaData.profile ? metaData.profile.profile_image : '' + + const validate = values => ({ + profile_image: values.profile_image && !/^https?:\/\//.test(values.profile_image) ? 'Invalid URL' : null, + name: values.name && values.name.length > 20 ? 'Name is too long' : null, + about: values.about && values.about.length > 160 ? 'About is too long' : null, + location: values.location && values.location.length > 30 ? 'Location is too long' : null, + website: values.website && values.website.length > 100 ? 'Website URL is too long' : null, + }) + return { account, metaData, - userImage, + profile_image, isOwnAccount: username == accountname, + validate, + initialValues: {profile_image}, ...ownProps } }, diff --git a/app/locales/en.js b/app/locales/en.js index 6bfaadd67a..0c4b4d3576 100644 --- a/app/locales/en.js +++ b/app/locales/en.js @@ -546,7 +546,11 @@ const en = { by_verifying_you_agree_with: 'By verifying your account you agree to the', by_verifying_you_agree_with_privacy_policy: 'Privacy Policy', by_verifying_you_agree_with_privacy_policy_of_website_APP_URL: 'of ' + APP_URL, - add_image_url: 'Profile picture url', + profile_image_url: 'Profile picture url', + profile_name: 'Display Name', + profile_about: 'About', + profile_location: 'Location', + profile_website: 'Website', saved: 'Saved', server_returned_error: 'server returned error', } diff --git a/app/locales/ru.js b/app/locales/ru.js index 370af809a4..b2c70e0d87 100644 --- a/app/locales/ru.js +++ b/app/locales/ru.js @@ -563,7 +563,11 @@ const ru = { few {# неподтвержденныe транзакции} many {# неподтвержденных транзакций} }`, - add_image_url: 'Добавьте url вашего изображения', + profile_image_url: 'Добавьте url вашего изображения', + profile_name: 'Display Name', + profile_about: 'About', + profile_location: 'Location', + profile_website: 'Website', saved: 'Сохранено', server_returned_error: 'ошибка сервера', } From c8989154e715e5938e89f2162acf02da48eedf5b Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 29 Nov 2016 14:09:24 -0500 Subject: [PATCH 44/90] fix comment css and increase userpic resolution --- app/components/cards/Comment.scss | 2 +- app/components/elements/Userpic.jsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/components/cards/Comment.scss b/app/components/cards/Comment.scss index 293a0096c1..9aad097677 100644 --- a/app/components/cards/Comment.scss +++ b/app/components/cards/Comment.scss @@ -75,7 +75,7 @@ visibility: hidden; //width: 1rem; float: right; - a { + > a { color: $dark-gray; letter-spacing: 0.1rem; padding: 0 0.5rem; diff --git a/app/components/elements/Userpic.jsx b/app/components/elements/Userpic.jsx index fec64da05b..feb9185bc4 100644 --- a/app/components/elements/Userpic.jsx +++ b/app/components/elements/Userpic.jsx @@ -20,7 +20,8 @@ class Userpic extends Component { } catch (e) {} if (url && /^(https?:)\/\//.test(url)) { - url = $STM_Config.img_proxy_prefix + '48x48/' + url; + const size = width && width > 48 ? '320x320' : '72x72' + url = $STM_Config.img_proxy_prefix + size + '/' + url; } else { if(hideIfDefault) { return null; From 8f6d12dd6c59ce98a77073d140bd2236ed927c11 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 29 Nov 2016 14:22:33 -0500 Subject: [PATCH 45/90] Add profile info to Author dropdown --- app/components/all.scss | 1 + app/components/elements/Author.jsx | 36 +++++++++++++++++++---- app/components/elements/Author.scss | 28 ++++++++++++++++++ app/utils/NormalizeProfile.js | 45 +++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 app/components/elements/Author.scss create mode 100644 app/utils/NormalizeProfile.js diff --git a/app/components/all.scss b/app/components/all.scss index 02afe4d16d..f3127ad2a2 100644 --- a/app/components/all.scss +++ b/app/components/all.scss @@ -28,6 +28,7 @@ @import "./elements/YoutubePreview"; @import "./elements/SignupProgressBar"; @import "./elements/ShareMenu"; +@import "./elements/Author"; // modules @import "./modules/Header"; diff --git a/app/components/elements/Author.jsx b/app/components/elements/Author.jsx index 189bfb5f64..4815bef8f7 100644 --- a/app/components/elements/Author.jsx +++ b/app/components/elements/Author.jsx @@ -7,6 +7,9 @@ import Icon from 'app/components/elements/Icon'; import { Link } from 'react-router'; import {authorNameAndRep} from 'app/utils/ComponentFormatters'; import Reputation from 'app/components/elements/Reputation'; +import Userpic from 'app/components/elements/Userpic'; +import { translate } from 'app/Translator'; +import normalizeProfile from 'app/utils/NormalizeProfile'; const {string, bool, number} = React.PropTypes @@ -27,24 +30,43 @@ class Author extends React.Component { const {username} = this.props // redux const author_link = - if(!username || !(follow || mute)) + if(!username || !(follow || mute) || username === author) return author_link + const {name, about} = normalizeProfile(this.props.account.toJS()) + const dropdown =
        - Profile   - + + + + + + {name} + + + @{author} + +
        + +
        + +
        + {about} +
        + + return ( @@ -65,9 +87,11 @@ export default connect( (state, ownProps) => { const current = state.user.get('current') const username = current && current.get('username') + const account = state.global.getIn(['accounts', ownProps.author]); return { ...ownProps, username, + account, } }, // dispatch => ({ diff --git a/app/components/elements/Author.scss b/app/components/elements/Author.scss new file mode 100644 index 0000000000..f4fcbe0eac --- /dev/null +++ b/app/components/elements/Author.scss @@ -0,0 +1,28 @@ +.Author__dropdown { + width: 290px; + min-height: 108px; + + .Userpic { + margin-right: 1rem; + float: left; + } + + .Author__name { + text-decoration: none; + display: block; + font-size: 110%; + color: #444; + font-weight: 600; + line-height: 1; + } + + .Author__username { + text-decoration: none; + font-size: 90%; + color: #666; + } + + .Author__bio { + font-size: 90%; + } +} diff --git a/app/utils/NormalizeProfile.js b/app/utils/NormalizeProfile.js new file mode 100644 index 0000000000..79a9210b27 --- /dev/null +++ b/app/utils/NormalizeProfile.js @@ -0,0 +1,45 @@ +function truncate(str, len) { + if(str && str.length > len) { + return str.substring(0, len - 1) + '...' + } + return str +} + +/** + * Enforce profile data length & format standards. + */ +export default function normalizeProfile(account) { + + if(! account) return {} + + // Parse + let profile = {}; + if(account.json_metadata) { + try { + const md = JSON.parse(account.json_metadata); + if(md.profile) { + profile = md.profile; + } + } catch (e) { + console.error('Invalid json metadata string', account.json_metadata, 'in account', account.name); + } + } + + // Read & normalize + let {name, about, location, website, profile_image} = profile + + name = truncate(name, 20) + about = truncate(about, 160) + location = truncate(location, 30) + + if(website && website.length > 100) website = null; + if(profile_image && !/^https?:\/\//.test(profile_image)) profile_image = null; + + return { + name, + about, + location, + website, + profile_image, + }; +} From d2a7facba335c66dcbe42f5b248e80d137848f92 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 30 Nov 2016 11:48:06 -0500 Subject: [PATCH 46/90] new icons --- app/assets/icons/calendar.svg | 4 ++++ app/assets/icons/location.svg | 4 ++++ app/components/elements/Icon.jsx | 2 ++ 3 files changed, 10 insertions(+) create mode 100644 app/assets/icons/calendar.svg create mode 100644 app/assets/icons/location.svg diff --git a/app/assets/icons/calendar.svg b/app/assets/icons/calendar.svg new file mode 100644 index 0000000000..60ec762d45 --- /dev/null +++ b/app/assets/icons/calendar.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/location.svg b/app/assets/icons/location.svg new file mode 100644 index 0000000000..d7844b23b6 --- /dev/null +++ b/app/assets/icons/location.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/components/elements/Icon.jsx b/app/components/elements/Icon.jsx index 4978a89837..e34c6e43f7 100644 --- a/app/components/elements/Icon.jsx +++ b/app/components/elements/Icon.jsx @@ -32,6 +32,8 @@ const icons = [ 'photo', 'line', 'video', + 'location', + 'calendar', ]; const icons_map = {}; for (const i of icons) icons_map[i] = require(`app/assets/icons/${i}.svg`); From 29435028f5a956dd40de413086f3208aa934565d Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 30 Nov 2016 11:50:08 -0500 Subject: [PATCH 47/90] add fields to UserProfile --- app/components/elements/DateJoinWrapper.jsx | 2 +- app/components/modules/Settings.jsx | 5 +-- app/components/pages/UserProfile.jsx | 31 +++++++++----- app/components/pages/UserProfile.scss | 45 +++++++++++++++------ 4 files changed, 56 insertions(+), 27 deletions(-) diff --git a/app/components/elements/DateJoinWrapper.jsx b/app/components/elements/DateJoinWrapper.jsx index cc84e15496..a7179a9319 100644 --- a/app/components/elements/DateJoinWrapper.jsx +++ b/app/components/elements/DateJoinWrapper.jsx @@ -9,7 +9,7 @@ export default class DateJoinWrapper extends React.Component { let joinMonth = monthNames[date.getMonth()]; let joinYear = date.getFullYear(); return ( -

        Joined {joinMonth}, {joinYear}

        + Joined {joinMonth} {joinYear} ) } } diff --git a/app/components/modules/Settings.jsx b/app/components/modules/Settings.jsx index a1ad4810cd..b7e456cd09 100644 --- a/app/components/modules/Settings.jsx +++ b/app/components/modules/Settings.jsx @@ -178,7 +178,7 @@ export default reduxForm( const current_user = state.user.get('current') const username = current_user ? current_user.get('username') : '' const metaData = account ? o2j.ifStringParseJSON(account.json_metadata) : {} - const profile_image = metaData && metaData.profile ? metaData.profile.profile_image : '' + const profile = metaData && metaData.profile ? metaData.profile : {} const validate = values => ({ profile_image: values.profile_image && !/^https?:\/\//.test(values.profile_image) ? 'Invalid URL' : null, @@ -192,10 +192,9 @@ export default reduxForm( return { account, metaData, - profile_image, isOwnAccount: username == accountname, validate, - initialValues: {profile_image}, + initialValues: profile, ...ownProps } }, diff --git a/app/components/pages/UserProfile.jsx b/app/components/pages/UserProfile.jsx index d5304d9c47..77937122e3 100644 --- a/app/components/pages/UserProfile.jsx +++ b/app/components/pages/UserProfile.jsx @@ -27,6 +27,7 @@ import DateJoinWrapper from 'app/components/elements/DateJoinWrapper'; import { translate } from 'app/Translator'; import WalletSubMenu from 'app/components/elements/WalletSubMenu'; import Userpic from 'app/components/elements/Userpic'; +import normalizeProfile from 'app/utils/NormalizeProfile'; export default class UserProfile extends React.Component { constructor() { @@ -110,7 +111,6 @@ export default class UserProfile extends React.Component { const rep = repLog10(account.reputation); const isMyAccount = username === account.name - const name = account.name; let tab_content = null; const global_status = this.props.global.get('status'); @@ -179,7 +179,7 @@ export default class UserProfile extends React.Component { if( account.posts || account.comments ) { tab_content = Read The Beginner's Guide
        Read The Steemit Welcome Guide
      : -
      {translate('user_hasnt_started_bloggin_yet', {name})}
      ; +
      {translate('user_hasnt_started_bloggin_yet', {name: accountname})}
      ; tab_content = - {isMyAccount && } + {isMyAccount && }
      ; } else if( section === 'permissions' && isMyAccount ) { @@ -316,6 +316,8 @@ export default class UserProfile extends React.Component {
      ; + const {name, location, about, website} = normalizeProfile(account); + return (
      @@ -327,13 +329,17 @@ export default class UserProfile extends React.Component {
      -

      + +

      - {account.name}{' '} - ({rep}) -

      + {name || account.name}{' '} + + ({rep}) + +
      + {about &&

      {about}

      }
      {followerCount ? translate('follower_count', {followerCount}) : translate('followers')} @@ -342,8 +348,12 @@ export default class UserProfile extends React.Component { {translate('post_count', {postCount: account.post_count || 0})} {followingCount ? translate('followed_count', {followingCount}) : translate('following')}
      +

      + {location && {location}} + {website && {website.replace(/^https?:\/\//, '')}} + +

      -
      @@ -367,6 +377,7 @@ module.exports = { const wifShown = state.global.get('UserKeys_wifShown') const current_user = state.user.get('current') // const current_account = current_user && state.global.getIn(['accounts', current_user.get('username')]) + return { discussions: state.global.get('discussion_idx'), global: state.global, diff --git a/app/components/pages/UserProfile.scss b/app/components/pages/UserProfile.scss index 70925cdee8..e865367786 100644 --- a/app/components/pages/UserProfile.scss +++ b/app/components/pages/UserProfile.scss @@ -65,14 +65,26 @@ background: #23579d; /* for older browsers */ background: linear-gradient(to bottom, #1a4072 0%, #23579d 100%); - height: 155px; + min-height: 155px; } - h2 { + h3 { padding-top: 20px; - .Userpic { - margin-right: 1rem; - vertical-align: middle; - } + font-weight: 600; + } + + .Icon { + margin-left: 0.5rem; + svg {fill: #def;} + } + + .Userpic { + margin-right: 1rem; + vertical-align: middle; + } + + .UserProfile__rep { + font-size: 80%; + font-weight: 200; } .UserProfile__buttons { @@ -87,22 +99,29 @@ } } + .UserProfile__bio { + margin: -0.4rem auto 0.5rem; + font-size: 95%; + max-width: 420px; + } + .UserProfile__info { + font-size: 90%; + } + .UserProfile__stats { margin-bottom: 5px; padding-bottom: 5px; + font-size: 90%; a { @include hoverUnderline; vertical-align: middle; } - span { + > span { padding: 0px 10px; - } - - span:nth-child(2) { - border-left: 1px solid grey; - border-right: 1px solid grey; + border-left: 1px solid #CCC; + &:first-child {border-left: none;} } .NotifiCounter { @@ -129,7 +148,7 @@ padding-right: 0; } - .UserProfile__banner h2 .Userpic { + .UserProfile__banner .Userpic { width: 36px !important; height: 36px !important; } From 443005350c6083cd8e2c2b8ab7744397145e373a Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 30 Nov 2016 16:00:55 -0500 Subject: [PATCH 48/90] bugfixes --- app/components/elements/Author.jsx | 2 +- app/components/pages/UserProfile.jsx | 3 ++- app/components/pages/Witnesses.jsx | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/components/elements/Author.jsx b/app/components/elements/Author.jsx index 4815bef8f7..01b80c721e 100644 --- a/app/components/elements/Author.jsx +++ b/app/components/elements/Author.jsx @@ -36,7 +36,7 @@ class Author extends React.Component { if(!username || !(follow || mute) || username === author) return author_link - const {name, about} = normalizeProfile(this.props.account.toJS()) + const {name, about} = this.props.account ? normalizeProfile(this.props.account.toJS()) : {} const dropdown =
      diff --git a/app/components/pages/UserProfile.jsx b/app/components/pages/UserProfile.jsx index 77937122e3..c65afb7be0 100644 --- a/app/components/pages/UserProfile.jsx +++ b/app/components/pages/UserProfile.jsx @@ -317,6 +317,7 @@ export default class UserProfile extends React.Component {
      ; const {name, location, about, website} = normalizeProfile(account); + const website_label = website ? website.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '') : null return (
      @@ -350,7 +351,7 @@ export default class UserProfile extends React.Component {

      {location && {location}} - {website && {website.replace(/^https?:\/\//, '')}} + {website && {website_label}}

      diff --git a/app/components/pages/Witnesses.jsx b/app/components/pages/Witnesses.jsx index 3c910da090..de0d86e789 100644 --- a/app/components/pages/Witnesses.jsx +++ b/app/components/pages/Witnesses.jsx @@ -62,7 +62,7 @@ class Witnesses extends React.Component { let witness_thread = "" if(thread) { if(links.remote.test(thread)) { - witness_thread = {translate('witness_thread')}  + witness_thread = {translate('witness_thread')}  } else { witness_thread = {translate('witness_thread')} } From 616f7dedb25f13cdf0b48aa8b941a67e1287e400 Mon Sep 17 00:00:00 2001 From: James Calfee Date: Thu, 1 Dec 2016 08:22:56 -0600 Subject: [PATCH 49/90] Convert settings page to use local react forms library. #118 --- app/components/modules/Settings.jsx | 71 +++++++++++++++-------------- app/components/modules/Transfer.jsx | 1 - app/utils/ReactForm.js | 2 +- 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/app/components/modules/Settings.jsx b/app/components/modules/Settings.jsx index b7e456cd09..ece8800cc1 100644 --- a/app/components/modules/Settings.jsx +++ b/app/components/modules/Settings.jsx @@ -7,25 +7,45 @@ import store from 'store'; import transaction from 'app/redux/Transaction' import o2j from 'shared/clash/object2json' import Userpic from 'app/components/elements/Userpic'; -import {reduxForm} from 'redux-form' -import {cleanReduxInput} from 'app/utils/ReduxForms' +import reactForm from 'app/utils/ReactForm' class Settings extends React.Component { + constructor(props) { + super() + this.initForm(props) + } + state = { errorMessage: '', successMessage: '', } - handleSubmit = event => { - event.preventDefault() + initForm(props) { + reactForm({ + instance: this, + name: 'accountSettings', + fields: ['profile_image', 'name', 'about', 'location', 'website'], + initialValues: props.profile, + validation: values => ({ + profile_image: values.profile_image && !/^https?:\/\//.test(values.profile_image) ? 'Invalid URL' : null, + name: values.name && values.name.length > 20 ? 'Name is too long' : null, + about: values.about && values.about.length > 160 ? 'About is too long' : null, + location: values.location && values.location.length > 30 ? 'Location is too long' : null, + website: values.website && values.website.length > 100 ? 'Website URL is too long' : null, + }) + }) + this.handleSubmitForm = + this.state.accountSettings.handleSubmit((data, event) => this.handleSubmit(data, event)) + } + handleSubmit = () => { let {metaData} = this.props if (!metaData) metaData = {} if(!metaData.profile) metaData.profile = {} delete metaData.user_image; // old field... cleanup - const {profile_image, name, about, location, website} = this.props.fields + const {profile_image, name, about, location, website} = this.state // Update relevant fields metaData.profile.profile_image = profile_image.value @@ -83,10 +103,10 @@ class Settings extends React.Component { render() { const {state, props} = this - const {invalid} = this.props - const disabled = !props.isOwnAccount || state.loading || invalid + const {submitting, valid} = this.state.accountSettings + const disabled = !props.isOwnAccount || state.loading || submitting || !valid - const {profile_image, name, about, location, website} = this.props.fields + const {profile_image, name, about, location, website} = this.state return
      @@ -118,36 +138,35 @@ class Settings extends React.Component {
      */} -
      -
      + -
      {profile_image.touched && profile_image.error}
      +
      {profile_image.blur && profile_image.touched && profile_image.error}
      {name.touched && name.error}
      {about.touched && about.error}
      {location.touched && location.error}
      {website.touched && website.error}
      @@ -166,35 +185,21 @@ class Settings extends React.Component { } } -export default reduxForm( - { - form: 'accountSettings', - fields: ['profile_image', 'name', 'about', 'location', 'website'] - }, +export default connect( // mapStateToProps (state, ownProps) => { - const {accountname} = ownProps.routeParams + const {accountname} = ownProps.routeParams const account = state.global.getIn(['accounts', accountname]).toJS() const current_user = state.user.get('current') const username = current_user ? current_user.get('username') : '' const metaData = account ? o2j.ifStringParseJSON(account.json_metadata) : {} const profile = metaData && metaData.profile ? metaData.profile : {} - const validate = values => ({ - profile_image: values.profile_image && !/^https?:\/\//.test(values.profile_image) ? 'Invalid URL' : null, - name: values.name && values.name.length > 20 ? 'Name is too long' : null, - about: values.about && values.about.length > 160 ? 'About is too long' : null, - location: values.location && values.location.length > 30 ? 'Location is too long' : null, - website: values.website && values.website.length > 100 ? 'Website URL is too long' : null, - }) - - return { account, metaData, isOwnAccount: username == accountname, - validate, - initialValues: profile, + profile, ...ownProps } }, diff --git a/app/components/modules/Transfer.jsx b/app/components/modules/Transfer.jsx index fc8a8ee33d..862aba6e40 100644 --- a/app/components/modules/Transfer.jsx +++ b/app/components/modules/Transfer.jsx @@ -127,7 +127,6 @@ class TransferForm extends Component { const isMemoPrivate = memo && /^#/.test(memo.value) const form = ( { - // bind redux-form to react-redux this.setState({loading: true}) dispatchSubmit({...data, errorCallback: this.errorCallback, currentUser, toVesting, transferType}) })} diff --git a/app/utils/ReactForm.js b/app/utils/ReactForm.js index cdcff9deb6..ff802e440c 100644 --- a/app/utils/ReactForm.js +++ b/app/utils/ReactForm.js @@ -27,7 +27,7 @@ export default function reactForm({name, instance, fields, initialValues, valida instance.setState( {[name]: fs}, () => { - const ret = fn(data) || {} + const ret = fn(data, e) || {} for(const fieldName of Object.keys(ret)) { const error = ret[fieldName] if(!error) continue From 1722c254b63405d6affeff90616cf4dc0d515d49 Mon Sep 17 00:00:00 2001 From: James Calfee Date: Thu, 1 Dec 2016 08:33:09 -0600 Subject: [PATCH 50/90] Add redux-form deprecated comments. #118 --- app/components/elements/ChangePassword.jsx | 2 +- app/components/elements/ConvertToSteem.jsx | 2 +- app/components/elements/KeyEdit.js | 2 +- app/components/elements/ReplyEditor.jsx | 2 +- app/components/modules/BlocktradesDeposit.jsx | 2 +- app/redux/RootReducer.js | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/components/elements/ChangePassword.jsx b/app/components/elements/ChangePassword.jsx index abf96353ba..c74f738fd3 100644 --- a/app/components/elements/ChangePassword.jsx +++ b/app/components/elements/ChangePassword.jsx @@ -210,7 +210,7 @@ const keyValidate = (values) => ({ confirmSaved: ! values.confirmSaved ? translate('required') : null, }) -import {reduxForm} from 'redux-form' +import {reduxForm} from 'redux-form' // @deprecated, instead use: app/utils/ReactForm.js export default reduxForm( { form: 'changePassword', fields: ['password', 'confirmPassword', 'confirmCheck', 'confirmSaved', 'twofa'] }, // mapStateToProps diff --git a/app/components/elements/ConvertToSteem.jsx b/app/components/elements/ConvertToSteem.jsx index bdb63695b8..d63e418453 100644 --- a/app/components/elements/ConvertToSteem.jsx +++ b/app/components/elements/ConvertToSteem.jsx @@ -1,7 +1,7 @@ /* eslint react/prop-types: 0 */ import React from 'react' import ReactDOM from 'react-dom'; -import {reduxForm} from 'redux-form'; +import {reduxForm} from 'redux-form'; // @deprecated, instead use: app/utils/ReactForm.js import transaction from 'app/redux/Transaction' import shouldComponentUpdate from 'app/utils/shouldComponentUpdate' import TransactionError from 'app/components/elements/TransactionError' diff --git a/app/components/elements/KeyEdit.js b/app/components/elements/KeyEdit.js index d5a662bd21..95ac06511c 100644 --- a/app/components/elements/KeyEdit.js +++ b/app/components/elements/KeyEdit.js @@ -1,6 +1,6 @@ import React, {PropTypes, Component} from 'react' import LoadingIndicator from 'app/components/elements/LoadingIndicator' -import {reduxForm} from 'redux-form' +import {reduxForm} from 'redux-form' // @deprecated, instead use: app/utils/ReactForm.js import {PrivateKey} from 'shared/ecc' import {cleanReduxInput} from 'app/utils/ReduxForms' import { translate } from 'app/Translator'; diff --git a/app/components/elements/ReplyEditor.jsx b/app/components/elements/ReplyEditor.jsx index 35ede76298..1319784061 100644 --- a/app/components/elements/ReplyEditor.jsx +++ b/app/components/elements/ReplyEditor.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import {reduxForm} from 'redux-form' +import {reduxForm} from 'redux-form' // @deprecated, instead use: app/utils/ReactForm.js import transaction from 'app/redux/Transaction'; import MarkdownViewer from 'app/components/cards/MarkdownViewer' import CategorySelector from 'app/components/cards/CategorySelector' diff --git a/app/components/modules/BlocktradesDeposit.jsx b/app/components/modules/BlocktradesDeposit.jsx index 877f02d7a3..071bfd4a22 100644 --- a/app/components/modules/BlocktradesDeposit.jsx +++ b/app/components/modules/BlocktradesDeposit.jsx @@ -1,6 +1,6 @@ import React from 'react'; import {Map} from 'immutable' -import {reduxForm} from 'redux-form' +import {reduxForm} from 'redux-form' // @deprecated, instead use: app/utils/ReactForm.js import TimeAgoWrapper from 'app/components/elements/TimeAgoWrapper' import Icon from 'app/components/elements/Icon' import DropdownMenu from 'app/components/elements/DropdownMenu' diff --git a/app/redux/RootReducer.js b/app/redux/RootReducer.js index 4e35654f1e..80068c8edf 100644 --- a/app/redux/RootReducer.js +++ b/app/redux/RootReducer.js @@ -9,7 +9,7 @@ import user from './User'; // import auth from './AuthSaga'; import transaction from './Transaction'; import offchain from './Offchain'; -import {reducer as formReducer} from 'redux-form'; +import {reducer as formReducer} from 'redux-form'; // @deprecated, instead use: app/utils/ReactForm.js import {contentStats} from 'app/utils/StateFunctions' function initReducer(reducer, type) { From f2be4e4cda4f9d8391cfbbdcca7fa4854d3d9033 Mon Sep 17 00:00:00 2001 From: James Calfee Date: Thu, 1 Dec 2016 08:54:17 -0600 Subject: [PATCH 51/90] Add touch boolean set when any field in a react form is dirty. --- app/utils/ReactForm.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/utils/ReactForm.js b/app/utils/ReactForm.js index ff802e440c..e50b44a564 100644 --- a/app/utils/ReactForm.js +++ b/app/utils/ReactForm.js @@ -15,10 +15,10 @@ export default function reactForm({name, instance, fields, initialValues, valida const formState = instance.state = instance.state || {} formState[name] = { - // validate: () => isValid(instance, fields, validation), + // validate: () => setFormState(instance, fields, validation), handleSubmit: (fn) => (e) => { e.preventDefault() - const valid = isValid(name, instance, fields, validation) + const {valid} = setFormState(name, instance, fields, validation) if(!valid) return const data = getData(fields, instance.state) let formValid = true @@ -104,7 +104,7 @@ export default function reactForm({name, instance, fields, initialValues, valida instance.setState( {[fieldName]: v}, - () => {isValid(name, instance, fields, validation)} + () => {setFormState(name, instance, fields, validation)} ) } @@ -117,8 +117,9 @@ export default function reactForm({name, instance, fields, initialValues, valida } } -function isValid(name, instance, fields, validation) { +function setFormState(name, instance, fields, validation) { let formValid = true + let formTouched = false const v = validation(getData(fields, instance.state)) for(const field of fields) { const fieldName = n(field) @@ -126,13 +127,15 @@ function isValid(name, instance, fields, validation) { const error = validate ? validate : null const value = {...(instance.state[fieldName] || {})} value.error = error + formTouched = formTouched || value.touched if(error) formValid = false instance.setState({[fieldName]: value}) } const fs = {...(instance.state[name] || {})} fs.valid = formValid + fs.touched = formTouched instance.setState({[name]: fs}) - return formValid + return fs } function getData(fields, state) { From dee8809c44a32266c6e2a148fb47bf00e4f9deb3 Mon Sep 17 00:00:00 2001 From: James Calfee Date: Thu, 1 Dec 2016 08:54:38 -0600 Subject: [PATCH 52/90] Disable update button unless form is dirty. #118 --- app/components/modules/Settings.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/modules/Settings.jsx b/app/components/modules/Settings.jsx index ece8800cc1..fdd51b19c8 100644 --- a/app/components/modules/Settings.jsx +++ b/app/components/modules/Settings.jsx @@ -103,8 +103,8 @@ class Settings extends React.Component { render() { const {state, props} = this - const {submitting, valid} = this.state.accountSettings - const disabled = !props.isOwnAccount || state.loading || submitting || !valid + const {submitting, valid, touched} = this.state.accountSettings + const disabled = !props.isOwnAccount || state.loading || submitting || !valid || !touched const {profile_image, name, about, location, website} = this.state From 851ab55209379aeb08616e0ab4015a49b0d324ae Mon Sep 17 00:00:00 2001 From: James Calfee Date: Thu, 1 Dec 2016 10:36:01 -0600 Subject: [PATCH 53/90] Add react form updateInitialValues callback for async submissions. --- app/components/modules/LoginForm.jsx | 2 +- app/components/modules/Transfer.jsx | 2 +- app/utils/ReactForm.js | 45 +++++++++++++++++++--------- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/app/components/modules/LoginForm.jsx b/app/components/modules/LoginForm.jsx index a4c0cf8aaf..5b055f416f 100644 --- a/app/components/modules/LoginForm.jsx +++ b/app/components/modules/LoginForm.jsx @@ -160,7 +160,7 @@ class LoginForm extends Component { null const form = ( - { + { // bind redux-form to react-redux console.log('Login\tdispatchSubmit'); return dispatchSubmit(data, loginBroadcastOperation, afterLoginRedirectToWelcome) diff --git a/app/components/modules/Transfer.jsx b/app/components/modules/Transfer.jsx index 862aba6e40..5923eff85e 100644 --- a/app/components/modules/Transfer.jsx +++ b/app/components/modules/Transfer.jsx @@ -126,7 +126,7 @@ class TransferForm extends Component { const {submitting, valid, handleSubmit} = this.state.transfer const isMemoPrivate = memo && /^#/.test(memo.value) const form = ( - { + { this.setState({loading: true}) dispatchSubmit({...data, errorCallback: this.errorCallback, currentUser, toVesting, transferType}) })} diff --git a/app/utils/ReactForm.js b/app/utils/ReactForm.js index e50b44a564..d47ee3a431 100644 --- a/app/utils/ReactForm.js +++ b/app/utils/ReactForm.js @@ -16,18 +16,25 @@ export default function reactForm({name, instance, fields, initialValues, valida const formState = instance.state = instance.state || {} formState[name] = { // validate: () => setFormState(instance, fields, validation), - handleSubmit: (fn) => (e) => { - e.preventDefault() + handleSubmit: submitCallback => event => { + event.preventDefault() const {valid} = setFormState(name, instance, fields, validation) if(!valid) return const data = getData(fields, instance.state) let formValid = true const fs = instance.state[name] || {} fs.submitting = true + + // User can call this function upon successful submission + const updateInitialValues = () => { + setInitialValuesFromForm(name, instance, fields, initialValues) + formState[name].resetForm() + } + instance.setState( {[name]: fs}, () => { - const ret = fn(data, e) || {} + const ret = submitCallback({data, event, updateInitialValues}) || {} for(const fieldName of Object.keys(ret)) { const error = ret[fieldName] if(!error) continue @@ -73,22 +80,24 @@ export default function reactForm({name, instance, fields, initialValues, valida // Caution: fs.props is expanded , so only add valid props for the component fs.props = {name: fieldName} - const initialValue = initialValues[fieldName] - - if(fieldType === 'checked') { - fs.value = toString(initialValue) - fs.props.checked = toBoolean(initialValue) - } else if(fieldType === 'selected') { - fs.props.selected = toString(initialValue) - fs.value = fs.props.selected - } else { - fs.props.value = toString(initialValue) - fs.value = fs.props.value + { + const initialValue = initialValues[fieldName] + if(fieldType === 'checked') { + fs.value = toString(initialValue) + fs.props.checked = toBoolean(initialValue) + } else if(fieldType === 'selected') { + fs.props.selected = toString(initialValue) + fs.value = fs.props.selected + } else { + fs.props.value = toString(initialValue) + fs.value = fs.props.value + } } fs.props.onChange = e => { const value = e && e.target ? e.target.value : e // API may pass value directly const v = {...(instance.state[fieldName] || {})} + const initialValue = initialValues[fieldName] if(fieldType === 'checked') { v.touched = toString(value) !== toString(initialValue) @@ -138,6 +147,14 @@ function setFormState(name, instance, fields, validation) { return fs } +function setInitialValuesFromForm(name, instance, fields, initialValues) { + const data = getData(fields, instance.state) + for(const field of fields) { + const fieldName = n(field) + initialValues[fieldName] = data[fieldName] + } +} + function getData(fields, state) { const data = {} for(const field of fields) { From 72f178cf1f670f233bc198e8d059eba650ec1adc Mon Sep 17 00:00:00 2001 From: James Calfee Date: Thu, 1 Dec 2016 10:36:36 -0600 Subject: [PATCH 54/90] Call updateInitialValues on form after successful profile update. #118 --- app/components/modules/Settings.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/components/modules/Settings.jsx b/app/components/modules/Settings.jsx index fdd51b19c8..e4cada21c2 100644 --- a/app/components/modules/Settings.jsx +++ b/app/components/modules/Settings.jsx @@ -36,10 +36,10 @@ class Settings extends React.Component { }) }) this.handleSubmitForm = - this.state.accountSettings.handleSubmit((data, event) => this.handleSubmit(data, event)) + this.state.accountSettings.handleSubmit(args => this.handleSubmit(args)) } - handleSubmit = () => { + handleSubmit = ({updateInitialValues}) => { let {metaData} = this.props if (!metaData) metaData = {} if(!metaData.profile) metaData.profile = {} @@ -96,6 +96,7 @@ class Settings extends React.Component { }) // remove successMessage after a while setTimeout(() => this.setState({successMessage: ''}), 2000) + updateInitialValues() } }) } From 8901545e5013602231ee881d0c8dacf3520af8a6 Mon Sep 17 00:00:00 2001 From: James Calfee Date: Thu, 1 Dec 2016 10:54:01 -0600 Subject: [PATCH 55/90] Add settings loading indicator and autocomplete off. #118 --- app/components/modules/Settings.jsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/components/modules/Settings.jsx b/app/components/modules/Settings.jsx index e4cada21c2..207bbccd3b 100644 --- a/app/components/modules/Settings.jsx +++ b/app/components/modules/Settings.jsx @@ -6,6 +6,7 @@ import {ALLOWED_CURRENCIES} from 'config/client_config' import store from 'store'; import transaction from 'app/redux/Transaction' import o2j from 'shared/clash/object2json' +import LoadingIndicator from 'app/components/elements/LoadingIndicator' import Userpic from 'app/components/elements/Userpic'; import reactForm from 'app/utils/ReactForm' @@ -143,36 +144,37 @@ class Settings extends React.Component {
      {profile_image.blur && profile_image.touched && profile_image.error}
      {name.touched && name.error}
      {about.touched && about.error}
      {location.touched && location.error}
      {website.touched && website.error}

      - + {state.loading &&
      } + {!state.loading && } {' '}{ state.errorMessage ? {state.errorMessage} From 9fba9662f92b3e78f9ae8d16f0ae5e10d836c1b0 Mon Sep 17 00:00:00 2001 From: James Calfee Date: Thu, 1 Dec 2016 10:55:36 -0600 Subject: [PATCH 56/90] Longer timout on success message. #118 --- app/components/modules/Settings.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/modules/Settings.jsx b/app/components/modules/Settings.jsx index 207bbccd3b..10fdc830de 100644 --- a/app/components/modules/Settings.jsx +++ b/app/components/modules/Settings.jsx @@ -96,7 +96,7 @@ class Settings extends React.Component { successMessage: translate('saved') + '!', }) // remove successMessage after a while - setTimeout(() => this.setState({successMessage: ''}), 2000) + setTimeout(() => this.setState({successMessage: ''}), 4000) updateInitialValues() } }) From 40384950e12ea20c053d7a4b799fcca665608e33 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 1 Dec 2016 12:15:05 -0500 Subject: [PATCH 57/90] css spacing --- app/components/pages/UserProfile.scss | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/components/pages/UserProfile.scss b/app/components/pages/UserProfile.scss index e865367786..a8a499a9f2 100644 --- a/app/components/pages/UserProfile.scss +++ b/app/components/pages/UserProfile.scss @@ -73,12 +73,12 @@ } .Icon { - margin-left: 0.5rem; + margin-left: 1rem; svg {fill: #def;} } .Userpic { - margin-right: 1rem; + margin-right: 0.75rem; vertical-align: middle; } @@ -103,6 +103,7 @@ margin: -0.4rem auto 0.5rem; font-size: 95%; max-width: 420px; + line-height: 1.4; } .UserProfile__info { font-size: 90%; From 988fadb47f9a449a3c0f915e333791bbd5af7bbe Mon Sep 17 00:00:00 2001 From: valzav Date: Thu, 1 Dec 2016 16:29:33 -0500 Subject: [PATCH 58/90] don't log page_view db queries --- server/api/general.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/api/general.js b/server/api/general.js index 789d6e6667..bedcf59448 100644 --- a/server/api/general.js +++ b/server/api/general.js @@ -251,13 +251,13 @@ export default function useGeneralApi(app) { let views = 1; // const views = yield Tarantool.instance().call('page_view', page, remote_ip, this.session.uid, ref); const page_model = yield models.Page.findOne( - {attributes: ['id', 'views'], where: {permlink: esc(page)}} + {attributes: ['id', 'views'], where: {permlink: esc(page)}, logging: false} ); if (page_model) { views = page_model.views + 1; - yield yield models.Page.update({views}, {where: {id: page_model.id}}); + yield yield models.Page.update({views}, {where: {id: page_model.id}, logging: false}); } else { - yield models.Page.create(escAttrs({permlink: page, views})); + yield models.Page.create(escAttrs({permlink: page, views}), {logging: false}); } this.body = JSON.stringify({views}); } catch (error) { From c2467dcee56bf9caff91e407adb4154e96df8505 Mon Sep 17 00:00:00 2001 From: valzav Date: Thu, 1 Dec 2016 17:15:33 -0500 Subject: [PATCH 59/90] add mixpanel integration --- package.json | 1 + server/api/general.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/package.json b/package.json index dd4ee45872..f8cc32a34f 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "lodash.debounce": "^4.0.7", "medium-editor-insert-plugin": "^2.3.2", "minimist": "^1.2.0", + "mixpanel": "^0.5.0", "mysql": "^2.10.2", "net": "^1.0.2", "newrelic": "^1.33.0", diff --git a/server/api/general.js b/server/api/general.js index bedcf59448..48fa6c315c 100644 --- a/server/api/general.js +++ b/server/api/general.js @@ -7,8 +7,12 @@ import recordWebEvent from 'server/record_web_event'; import {esc, escAttrs} from 'db/models'; import {emailRegex, getRemoteIp, rateLimitReq, checkCSRF} from 'server/utils'; import coBody from 'co-body'; +import Mixpanel from 'mixpanel'; // import Tarantool from 'db/tarantool'; +const mixpanel = config.mixpanel ? Mixpanel.init(config.mixpanel) : null; + + export default function useGeneralApi(app) { const router = koa_router({prefix: '/api/v1'}); app.use(router.routes()); @@ -144,6 +148,13 @@ export default function useGeneralApi(app) { })).catch(error => { console.error('!!! Can\'t create account model in /accounts api', this.session.uid, error); }); + if (mixpanel) { + mixpanel.track('Signup', { + distinct_id: this.session.uid, + ip: remote_ip + }); + mixpanel.people.set(this.session.uid, {ip: remote_ip}); + } } catch (error) { console.error('Error in /accounts api call', this.session.uid, error.toString()); this.body = JSON.stringify({error: error.message}); @@ -193,6 +204,11 @@ export default function useGeneralApi(app) { ); if (db_account) this.session.user = db_account.user_id; this.body = JSON.stringify({status: 'ok'}); + const remote_ip = getRemoteIp(this.req); + if (mixpanel) { + mixpanel.people.set(this.session.uid, {ip: remote_ip, $ip: remote_ip}); + mixpanel.people.increment(this.session.uid, 'Visits', 1); + } } catch (error) { console.error('Error in /login_account api call', this.session.uid, error.message); this.body = JSON.stringify({error: error.message}); @@ -260,6 +276,22 @@ export default function useGeneralApi(app) { yield models.Page.create(escAttrs({permlink: page, views}), {logging: false}); } this.body = JSON.stringify({views}); + if (mixpanel) { + let referring_domain = ''; + if (ref) { + const matches = ref.match(/^https?\:\/\/([^\/?#]+)(?:[\/?#]|$)/i); + referring_domain = matches && matches[1]; + } + mixpanel.track('PageView', { + distinct_id: this.session.uid, + Page: page, + ip: remote_ip, + $referrer: ref, + $referring_domain: referring_domain + }); + mixpanel.people.set_once(this.session.uid, '$referrer', ref); + mixpanel.people.set_once(this.session.uid, 'FirstPage', page); + } } catch (error) { console.error('Error in /page_view api call', this.session.uid, error.message); this.body = JSON.stringify({error: error.message}); From f088b90c419827f163a962d519ae724d428a9c7d Mon Sep 17 00:00:00 2001 From: Sigve Date: Thu, 1 Dec 2016 17:17:53 -0500 Subject: [PATCH 60/90] Fix FollowSaga setting result to undefined --- app/redux/FollowSaga.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/redux/FollowSaga.js b/app/redux/FollowSaga.js index 240e819168..4688c94445 100644 --- a/app/redux/FollowSaga.js +++ b/app/redux/FollowSaga.js @@ -33,7 +33,7 @@ export function* loadFollows(method, account, type, start = '', limit = 100) { yield put({type: 'global/UPDATE', payload: { key: ['follow', method, account], updater: m => { - const result = m.get('result_loading') + const result = m.get('result_loading', m.get('result')) const count = !result ? 0 : result.filter(a => a.get(0) === "blog").size m = m.delete('result_loading') m = m.set('result', result) From 33f962a94e0edc6cb45672be72bd286f26a9f65e Mon Sep 17 00:00:00 2001 From: Sigve Date: Thu, 1 Dec 2016 17:19:54 -0500 Subject: [PATCH 61/90] Refactor PostsList to use pureRenderMixin, fix props passed by UserProfile --- app/components/cards/PostsList.jsx | 63 ++++++++++++++-------------- app/components/pages/UserProfile.jsx | 6 +-- 2 files changed, 34 insertions(+), 35 deletions(-) diff --git a/app/components/cards/PostsList.jsx b/app/components/cards/PostsList.jsx index 9576ab55ff..a9268e74ef 100644 --- a/app/components/cards/PostsList.jsx +++ b/app/components/cards/PostsList.jsx @@ -8,6 +8,7 @@ import CloseButton from 'react-foundation-components/lib/global/close-button'; import {findParent} from 'app/utils/DomUtils'; import Icon from 'app/components/elements/Icon'; import {List} from "immutable"; +import shouldComponentUpdate from 'app/utils/shouldComponentUpdate'; function topPosition(domElt) { if (!domElt) { @@ -47,37 +48,28 @@ class PostsList extends React.Component { this.onPostClick = this.onPostClick.bind(this); this.onBackButton = this.onBackButton.bind(this); this.closeOnOutsideClick = this.closeOnOutsideClick.bind(this); - // this.shouldComponentUpdate = shouldComponentUpdate(this, 'PostsList') + this.shouldComponentUpdate = shouldComponentUpdate(this, 'PostsList') } componentDidMount() { this.attachScrollListener(); } - shouldComponentUpdate(np, ns) { - return ( - np.posts !== this.props.posts || - np.ignoreLoading !== this.props.ignoreLoading || - np.loadMore !== this.props.loadMore || - np.showSpam !== this.props.showSpam || - np.loading !== this.props.loading || - np.category !== this.props.category || - ns.showNegativeComments !== this.state.showNegativeComments || - ns.showPost !== this.state.showPost || - ns.thumbSize !== this.state.thumbSize - ); - } - - componentWillUnmount() { - this.detachScrollListener(); - window.removeEventListener('popstate', this.onBackButton); - window.removeEventListener('keydown', this.onBackButton); - const post_overlay = document.getElementById('post_overlay'); - if (post_overlay) post_overlay.removeEventListener('click', this.closeOnOutsideClick); - document.getElementsByTagName('body')[0].className = ""; - } - - componentWillUpdate(nextProps) { + // shouldComponentUpdate(np, ns) { + // return ( + // np.posts !== this.props.posts || + // np.ignoreLoading !== this.props.ignoreLoading || + // np.loadMore !== this.props.loadMore || + // np.showSpam !== this.props.showSpam || + // np.loading !== this.props.loading || + // np.category !== this.props.category || + // ns.showNegativeComments !== this.state.showNegativeComments || + // ns.showPost !== this.state.showPost || + // ns.thumbSize !== this.state.thumbSize + // ); + // } + + componentWillUpdate() { const location = `${window.location.pathname}${window.location.search}${window.location.hash}`; if (this.state.showPost && (location !== this.post_url)) { this.setState({showPost: null}); @@ -102,6 +94,15 @@ class PostsList extends React.Component { } } + componentWillUnmount() { + this.detachScrollListener(); + window.removeEventListener('popstate', this.onBackButton); + window.removeEventListener('keydown', this.onBackButton); + const post_overlay = document.getElementById('post_overlay'); + if (post_overlay) post_overlay.removeEventListener('click', this.closeOnOutsideClick); + document.getElementsByTagName('body')[0].className = ""; + } + onBackButton(e) { if (e.keyCode && e.keyCode !== 27) return; window.removeEventListener('popstate', this.onBackButton); @@ -170,7 +171,7 @@ class PostsList extends React.Component { render() { const {posts, showSpam, loading, category, emptyText, content, - username, get_following, account} = this.props; + follow, account} = this.props; const {thumbSize, showPost} = this.state if (!loading && (posts && !posts.size) && emptyText) { @@ -185,8 +186,8 @@ class PostsList extends React.Component { console.error('PostsList --> Missing cont key', item) return } - const key = ['result', cont.get('author')] - const ignore = get_following ? get_following.getIn(key, List()).contains('ignore') : false + const key = [cont.get('author')] + const ignore = follow ? follow.getIn(key, List()).contains('ignore') : false const {hide, netVoteSign, authorRepLog10} = cont.get('stats').toJS() if(!(ignore || hide) || showSpam) // rephide postsInfo.push({item, ignore, netVoteSign, authorRepLog10}) @@ -239,10 +240,8 @@ export default connect( const current = state.user.get('current') const username = current ? current.get('username') : null const content = state.global.get('content'); - const get_following = state.global.getIn(['follow', 'get_following', username]); - const ignoreLoading = !get_following ? false : get_following.getIn(['ignore', 'loading']); - // console.log("get_following:", get_following ? get_following.toJS() : null); - return {...props, content, ignoreLoading, get_following, pathname}; + const follow = state.global.getIn(['follow', 'follow', username, 'result']); + return {...props, content, follow, pathname}; }, dispatch => ({ fetchState: (pathname) => { diff --git a/app/components/pages/UserProfile.jsx b/app/components/pages/UserProfile.jsx index 390ef4a5c3..8f9fa67394 100644 --- a/app/components/pages/UserProfile.jsx +++ b/app/components/pages/UserProfile.jsx @@ -204,7 +204,7 @@ export default class UserProfile extends React.Component { { tab_content = Date: Thu, 1 Dec 2016 17:51:18 -0500 Subject: [PATCH 62/90] Move EmptyText out of PostsList --- app/components/cards/PostsList.jsx | 29 +---------- app/components/pages/PostsIndex.jsx | 21 ++++---- app/components/pages/UserProfile.jsx | 73 ++++++++++++++++++---------- 3 files changed, 60 insertions(+), 63 deletions(-) diff --git a/app/components/cards/PostsList.jsx b/app/components/cards/PostsList.jsx index a9268e74ef..20587c00a5 100644 --- a/app/components/cards/PostsList.jsx +++ b/app/components/cards/PostsList.jsx @@ -24,10 +24,6 @@ class PostsList extends React.Component { loading: PropTypes.bool.isRequired, category: PropTypes.string, loadMore: PropTypes.func, - emptyText: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.node, - ]), showSpam: PropTypes.bool, fetchState: PropTypes.func.isRequired, pathname: PropTypes.string, @@ -55,20 +51,6 @@ class PostsList extends React.Component { this.attachScrollListener(); } - // shouldComponentUpdate(np, ns) { - // return ( - // np.posts !== this.props.posts || - // np.ignoreLoading !== this.props.ignoreLoading || - // np.loadMore !== this.props.loadMore || - // np.showSpam !== this.props.showSpam || - // np.loading !== this.props.loading || - // np.category !== this.props.category || - // ns.showNegativeComments !== this.state.showNegativeComments || - // ns.showPost !== this.state.showPost || - // ns.thumbSize !== this.state.thumbSize - // ); - // } - componentWillUpdate() { const location = `${window.location.pathname}${window.location.search}${window.location.hash}`; if (this.state.showPost && (location !== this.post_url)) { @@ -170,16 +152,10 @@ class PostsList extends React.Component { } render() { - const {posts, showSpam, loading, category, emptyText, content, + const {posts, showSpam, loading, category, content, follow, account} = this.props; const {thumbSize, showPost} = this.state - - if (!loading && (posts && !posts.size) && emptyText) { - return {emptyText}; - } - const postsInfo = []; - posts.forEach(item => { const cont = content.get(item); if(!cont) { @@ -236,12 +212,11 @@ import {connect} from 'react-redux' 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 content = state.global.get('content'); const follow = state.global.getIn(['follow', 'follow', username, 'result']); - return {...props, content, follow, pathname}; + return {...props, username, content, follow, pathname}; }, dispatch => ({ fetchState: (pathname) => { diff --git a/app/components/pages/PostsIndex.jsx b/app/components/pages/PostsIndex.jsx index 1d159431ff..3e410c64f0 100644 --- a/app/components/pages/PostsIndex.jsx +++ b/app/components/pages/PostsIndex.jsx @@ -82,12 +82,12 @@ class PostsIndex extends React.Component {
      ; markNotificationRead = } else { - emptyText = translate('user_hasnt_followed_anything_yet', {name: account_name}); + emptyText =
      {translate('user_hasnt_followed_anything_yet', {name: account_name})}
      ; } } else { posts = this.getPosts(order, category); if (posts !== null && posts.size === 0) { - emptyText = `No ` + topics_order + (category ? ` #` + category : '') + ` posts found`; + emptyText =
      {`No ` + topics_order + (category ? ` #` + category : '') + ` posts found`}
      ; } } @@ -102,14 +102,15 @@ class PostsIndex extends React.Component { {markNotificationRead} - + {(!fetching && (posts && !posts.size)) ? emptyText : + }
      diff --git a/app/components/pages/UserProfile.jsx b/app/components/pages/UserProfile.jsx index 8f9fa67394..7738d6f078 100644 --- a/app/components/pages/UserProfile.jsx +++ b/app/components/pages/UserProfile.jsx @@ -27,7 +27,6 @@ import DateJoinWrapper from 'app/components/elements/DateJoinWrapper'; import { translate } from 'app/Translator'; import WalletSubMenu from 'app/components/elements/WalletSubMenu'; import Userpic from 'app/components/elements/Userpic'; -import Immutable from "immutable"; export default class UserProfile extends React.Component { constructor() { @@ -202,19 +201,27 @@ export default class UserProfile extends React.Component { // -- see also GlobalReducer.js if( account.posts || account.comments ) { - tab_content = ; + let posts = accountImm.get('posts') || accountImm.get('comments'); + if (!fetching && (posts && !posts.size)) { + tab_content =
      {translate('user_hasnt_made_any_posts_yet', {name})}
      ; + } else { + tab_content = ( + + ); + } } else { tab_content = (
      ); } } else if(!section || section === 'blog') { if (account.blog) { + let posts = accountImm.get('blog'); const emptyText = isMyAccount ?
      Looks like you haven't posted anything yet.

      Submit a Story
      @@ -222,30 +229,44 @@ export default class UserProfile extends React.Component { Read The Steemit Welcome Guide
      :
      {translate('user_hasnt_started_bloggin_yet', {name})}
      ; - tab_content = ; + + if (!fetching && (posts && !posts.size)) { + tab_content = emptyText; + } else { + tab_content = ( + + ); + } } else { tab_content = (
      ); } } else if( (section === 'recent-replies')) { if (account.recent_replies) { - tab_content =
      - - {isMyAccount && } -
      ; + let posts = accountImm.get('recent_replies'); + if (!fetching && (posts && !posts.size)) { + tab_content =
      {translate('user_hasnt_had_any_replies_yet', {name}) + '.'}
      ; + } else { + tab_content = ( +
      + + {isMyAccount && } +
      + ); + } } else { tab_content = (
      ); } From 5261d00aea621bd8d1cfbb27a2c1ae53a362d5ea Mon Sep 17 00:00:00 2001 From: valzav Date: Thu, 1 Dec 2016 18:19:17 -0500 Subject: [PATCH 63/90] track chain events --- app/redux/FetchDataSaga.js | 7 ------- app/redux/TransactionSaga.js | 3 +++ app/utils/ServerApiClient.js | 6 +++++- server/api/general.js | 8 ++++++-- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/app/redux/FetchDataSaga.js b/app/redux/FetchDataSaga.js index e1bd30ca9d..aa2ab22fd6 100644 --- a/app/redux/FetchDataSaga.js +++ b/app/redux/FetchDataSaga.js @@ -26,13 +26,6 @@ export function* fetchState(location_change_action) { const server_location = yield select(state => state.offchain.get('server_location')); if (pathname === server_location) return; - // virtual pageview - const {ga} = window - if(ga) { - ga('set', 'page', pathname); - ga('send', 'pageview'); - } - let url = `${pathname}`; if (url === '/') url = 'trending'; // Replace /curation-rewards and /author-rewards with /transfers for UserProfile diff --git a/app/redux/TransactionSaga.js b/app/redux/TransactionSaga.js index c77d56f33d..ef08bfae28 100644 --- a/app/redux/TransactionSaga.js +++ b/app/redux/TransactionSaga.js @@ -13,6 +13,7 @@ import user from 'app/redux/User' import tr from 'app/redux/Transaction' import getSlug from 'speakingurl' import {DEBT_TICKER} from 'config/client_config' +import {serverApiRecordEvent} from 'app/utils/ServerApiClient' const {transaction} = ops @@ -117,6 +118,8 @@ function* broadcastOperation({payload: } } yield call(broadcast, {payload}) + const eventType = type.replace(/^([a-z])/, g => g.toUpperCase()).replace(/_([a-z])/g, g => g[1].toUpperCase()); + serverApiRecordEvent(eventType, '') } catch(error) { console.error('TransactionSage', error) if(errorCallback) errorCallback(error.toString()) diff --git a/app/utils/ServerApiClient.js b/app/utils/ServerApiClient.js index 6e3237e2d2..00bcce46ba 100644 --- a/app/utils/ServerApiClient.js +++ b/app/utils/ServerApiClient.js @@ -25,7 +25,7 @@ export function serverApiLogout() { let last_call; export function serverApiRecordEvent(type, val) { if (!process.env.BROWSER || window.$STM_ServerBusy) return; - if (last_call && (new Date() - last_call < 60000)) return; + if (last_call && (new Date() - last_call < 5000)) return; last_call = new Date(); const value = val && val.stack ? `${val.toString()} | ${val.stack}` : val; const request = Object.assign({}, request_base, {body: JSON.stringify({csrf: $STM_csrf, type, value})}); @@ -52,6 +52,10 @@ export function markNotificationRead(account, fields) { let last_page, last_views; export function recordPageView(page, ref) { if (page === last_page) return Promise.resolve(last_views); + 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 => { diff --git a/server/api/general.js b/server/api/general.js index 48fa6c315c..b66f0ee47f 100644 --- a/server/api/general.js +++ b/server/api/general.js @@ -240,9 +240,13 @@ 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}); + } else { + const str_value = typeof value === 'string' ? value : JSON.stringify(value); + recordWebEvent(this, type, str_value); + } this.body = JSON.stringify({status: 'ok'}); - recordWebEvent(this, type, str_value); } catch (error) { console.error('Error in /record_event api call', error.message); this.body = JSON.stringify({error: error.message}); From f948e97ae8eb064e576da803797ceb144ba43175 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 2 Dec 2016 09:54:47 -0500 Subject: [PATCH 64/90] only fetch followers/following on account page, not on post pages --- app/redux/FetchDataSaga.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/redux/FetchDataSaga.js b/app/redux/FetchDataSaga.js index e1bd30ca9d..5f8619d4b6 100644 --- a/app/redux/FetchDataSaga.js +++ b/app/redux/FetchDataSaga.js @@ -14,7 +14,7 @@ export function* watchDataRequests() { export function* fetchState(location_change_action) { const {pathname} = location_change_action.payload; - const m = pathname.match(/@([a-z0-9\.-]+)/) + const m = pathname.match(/^\/@([a-z0-9\.-]+)/) if(m && m.length === 2) { const username = m[1] const hasFollows = yield select(state => state.global.hasIn(['follow', 'get_followers', username])) From 8d2b8d103cc6bb74318ed8926fc50dd40b37b6e5 Mon Sep 17 00:00:00 2001 From: Sigve Date: Fri, 2 Dec 2016 10:23:58 -0500 Subject: [PATCH 65/90] Rename noLoad variables for clarity --- app/redux/AppReducer.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/redux/AppReducer.js b/app/redux/AppReducer.js index ce3c8f1149..74e036a8b7 100644 --- a/app/redux/AppReducer.js +++ b/app/redux/AppReducer.js @@ -7,7 +7,7 @@ const defaultState = Map({ error: '', location: {}, notifications: null, - noLoadingRequests: 0, + ignoredLoadingRequestCount: 0, notificounters: Map({ total: 0, feed: 0, @@ -39,28 +39,28 @@ export default function reducer(state = defaultState, action) { let res = state; if (action.type === 'RPC_REQUEST_STATUS') { const request_id = action.payload.id + ''; - const noLoadingMethods = [ + const loadingBlacklist = [ 'get_dynamic_global_properties', 'get_api_by_name', 'get_followers', 'get_following' ]; - const noLoadMethod = noLoadingMethods.indexOf(action.payload.method) !== -1; + const loadingIgnored = loadingBlacklist.indexOf(action.payload.method) !== -1; if (action.payload.event === 'BEGIN') { res = state.mergeDeep({ - loading: noLoadMethod ? false : true, + loading: loadingIgnored ? false : true, requests: {[request_id]: Date.now()}, - noLoadingRequests: state.get('noLoadingRequests') + (noLoadMethod ? 1 : 0) + ignoredLoadingRequestCount: state.get('ignoredLoadingRequestCount') + (loadingIgnored ? 1 : 0) }); } if (action.payload.event === 'END' || action.payload.event === 'ERROR') { - const noLoadCount = state.get('noLoadingRequests') - (noLoadMethod ? 1 : 0); + const ignoredLoadingRequestCount = state.get('ignoredLoadingRequestCount') - (loadingIgnored ? 1 : 0); res = res.deleteIn(['requests', request_id]); - // console.log("RPC_REQUEST END:", action.payload.method, res.get('requests').size, "noLoadCount", noLoadCount); - const loading = (res.get('requests').size - noLoadCount) > 0; + // console.log("RPC_REQUEST END:", action.payload.method, res.get('requests').size, "ignoredLoadingRequestCount", ignoredLoadingRequestCount); + const loading = (res.get('requests').size - ignoredLoadingRequestCount) > 0; res = res.mergeDeep({ loading, - noLoadingRequests: noLoadCount + ignoredLoadingRequestCount }); } } From f7ecd0adeff65a43a02abafa2066975e462ff03e Mon Sep 17 00:00:00 2001 From: Sigve Date: Fri, 2 Dec 2016 10:37:37 -0500 Subject: [PATCH 66/90] Restore emptyText wrapper --- app/components/cards/PostsList.jsx | 1 - app/components/pages/PostsIndex.jsx | 3 ++- app/components/pages/UserProfile.jsx | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/components/cards/PostsList.jsx b/app/components/cards/PostsList.jsx index 20587c00a5..de244ac8b1 100644 --- a/app/components/cards/PostsList.jsx +++ b/app/components/cards/PostsList.jsx @@ -3,7 +3,6 @@ import PostSummary from 'app/components/cards/PostSummary'; import Post from 'app/components/pages/Post'; import LoadingIndicator from 'app/components/elements/LoadingIndicator'; import debounce from 'lodash.debounce'; -import Callout from 'app/components/elements/Callout'; import CloseButton from 'react-foundation-components/lib/global/close-button'; import {findParent} from 'app/utils/DomUtils'; import Icon from 'app/components/elements/Icon'; diff --git a/app/components/pages/PostsIndex.jsx b/app/components/pages/PostsIndex.jsx index 3e410c64f0..a9d7ad31fe 100644 --- a/app/components/pages/PostsIndex.jsx +++ b/app/components/pages/PostsIndex.jsx @@ -10,6 +10,7 @@ import {Link} from 'react-router'; import MarkNotificationRead from 'app/components/elements/MarkNotificationRead'; import { translate } from 'app/Translator'; import Immutable from "immutable"; +import Callout from 'app/components/elements/Callout'; class PostsIndex extends React.Component { @@ -102,7 +103,7 @@ class PostsIndex extends React.Component {
      {markNotificationRead} - {(!fetching && (posts && !posts.size)) ? emptyText : + {(!fetching && (posts && !posts.size)) ? {emptyText} : {translate('user_hasnt_made_any_posts_yet', {name})}; + tab_content = {translate('user_hasnt_made_any_posts_yet', {name})}; } else { tab_content = ( Read The Beginner's Guide
      Read The Steemit Welcome Guide : -
      {translate('user_hasnt_started_bloggin_yet', {name})}
      ; + translate('user_hasnt_started_bloggin_yet', {name}); if (!fetching && (posts && !posts.size)) { - tab_content = emptyText; + tab_content = {emptyText}; } else { tab_content = ( {translate('user_hasnt_had_any_replies_yet', {name}) + '.'}; + tab_content = {translate('user_hasnt_had_any_replies_yet', {name}) + '.'}; } else { tab_content = (
      From add3d830be331699c159142f6c38374419f8f30c Mon Sep 17 00:00:00 2001 From: James Calfee Date: Fri, 2 Dec 2016 09:48:46 -0600 Subject: [PATCH 67/90] Revert "Fix FollowSaga setting result to undefined" This reverts commit f088b90c419827f163a962d519ae724d428a9c7d. --- app/redux/FollowSaga.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/redux/FollowSaga.js b/app/redux/FollowSaga.js index 4688c94445..240e819168 100644 --- a/app/redux/FollowSaga.js +++ b/app/redux/FollowSaga.js @@ -33,7 +33,7 @@ export function* loadFollows(method, account, type, start = '', limit = 100) { yield put({type: 'global/UPDATE', payload: { key: ['follow', method, account], updater: m => { - const result = m.get('result_loading', m.get('result')) + const result = m.get('result_loading') const count = !result ? 0 : result.filter(a => a.get(0) === "blog").size m = m.delete('result_loading') m = m.set('result', result) From 0aa805ca92ef0f0add9e2f1ebfa7942c6f789a5b Mon Sep 17 00:00:00 2001 From: James Calfee Date: Fri, 2 Dec 2016 09:52:15 -0600 Subject: [PATCH 68/90] Moving to another branch... Revert "Adjust follow lists to only appear after loaded has finished." This reverts commit 815c189f3b00dd5ab7046b500612ddf0027ae3ac. --- app/redux/FollowSaga.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/app/redux/FollowSaga.js b/app/redux/FollowSaga.js index 240e819168..88ef6b1f2a 100644 --- a/app/redux/FollowSaga.js +++ b/app/redux/FollowSaga.js @@ -14,7 +14,7 @@ export function* loadFollows(method, account, type, start = '', limit = 100) { key: ['follow', method, account], notSet: Map(), updater: m => { - m = m.update('result_loading', Map(), m2 => { + m = m.update('result', Map(), m2 => { res.forEach(value => { cnt++ let what = value.get('what') @@ -24,7 +24,10 @@ export function* loadFollows(method, account, type, start = '', limit = 100) { }) return m2 }) - return m.merge({[type]: {loading: true, error: null}}) + const count = m.get('result') ? m.get('result').filter(a => { + return a.get(0) === "blog"; + }).size : 0; + return m.merge({count, [type]: {loading: true, error: null}}) } }}) if(cnt === limit) { @@ -32,13 +35,7 @@ export function* loadFollows(method, account, type, start = '', limit = 100) { } else { yield put({type: 'global/UPDATE', payload: { key: ['follow', method, account], - updater: m => { - const result = m.get('result_loading') - const count = !result ? 0 : result.filter(a => a.get(0) === "blog").size - m = m.delete('result_loading') - m = m.set('result', result) - return m.merge({count, [type]: {loading: false, error: null}}) - } + updater: m => m.merge({[type]: {loading: false, error: null}}) }}) } } From 11f6016672aa2f000bd468c6d3f811f7c6982ceb Mon Sep 17 00:00:00 2001 From: James Calfee Date: Fri, 2 Dec 2016 10:20:00 -0600 Subject: [PATCH 69/90] Adjust type testing in ReplyEditor to better match its paramter type: React.PropTypes.oneOf(['submit_story', 'submit_comment', 'edit']) #735 --- app/components/elements/ReplyEditor.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/elements/ReplyEditor.jsx b/app/components/elements/ReplyEditor.jsx index fd36875528..ebf7323412 100644 --- a/app/components/elements/ReplyEditor.jsx +++ b/app/components/elements/ReplyEditor.jsx @@ -390,7 +390,7 @@ export default formId => reduxForm( const fields = ['body', 'autoVote'] const {type, parent_author, jsonMetadata} = ownProps const isStory = /submit_story/.test(type) || ( - /edit/.test(type) && parent_author === '' + type === 'edit' && parent_author === '' ) if (isStory) fields.push('title') if (isStory) fields.push('category') @@ -437,7 +437,7 @@ export default formId => reduxForm( // const post = state.global.getIn(['content', author + '/' + permlink]) const username = state.user.getIn(['current', 'username']) - const isEdit = /edit/.test(type) + const isEdit = type === 'edit' const isNew = /^submit_/.test(type) // Wire up the current and parent props for either an Edit or a Submit (new post) From d71d389471f23b112b155567ad9c6d618a757d66 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 2 Dec 2016 11:50:54 -0500 Subject: [PATCH 70/90] indent --- app/components/cards/PostFull.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/components/cards/PostFull.jsx b/app/components/cards/PostFull.jsx index 1783d8591c..97ba29f635 100644 --- a/app/components/cards/PostFull.jsx +++ b/app/components/cards/PostFull.jsx @@ -266,10 +266,10 @@ class PostFull extends React.Component {
      {!readonly && } {!readonly && - - {showReplyOption && Reply} - {' '}{showEditOption && !showEdit && Edit} - {' '}{showDeleteOption && !showReply && Delete} + + {showReplyOption && Reply} + {' '}{showEditOption && !showEdit && Edit} + {' '}{showDeleteOption && !showReply && Delete} } From 058a7de642a5e2cf2289fcf92cb432c51bc60389 Mon Sep 17 00:00:00 2001 From: James Calfee Date: Fri, 2 Dec 2016 10:52:33 -0600 Subject: [PATCH 71/90] Lowercase category in url. --- server/server.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/server/server.js b/server/server.js index abc1dc6f9c..800e7ed7d4 100644 --- a/server/server.js +++ b/server/server.js @@ -55,10 +55,12 @@ app.use(function *(next) { } } // normalize top category filtering from cased params - if (this.method === 'GET' && /(hot|created|trending|active)/ig.test(this.url)) { - const p = this.originalUrl.toLowerCase(); - if(p !== this.originalUrl) { - this.redirect(p); + 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('/')); return; } } From 66b4e42b5cec59f3402e0e432b8bbdebe81768ab Mon Sep 17 00:00:00 2001 From: valzav Date: Fri, 2 Dec 2016 11:53:35 -0500 Subject: [PATCH 72/90] store page view in tarantool db as well for the comparison reasons --- server/api/general.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/api/general.js b/server/api/general.js index b66f0ee47f..d2a0b5b528 100644 --- a/server/api/general.js +++ b/server/api/general.js @@ -242,6 +242,7 @@ export default function useGeneralApi(app) { console.log('-- /record_event -->', this.session.uid, type, value); if (type.match(/^[A-Z]/)) { mixpanel.track(type, {distinct_id: this.session.uid}); + mixpanel.people.increment(this.session.uid, type, 1); } else { const str_value = typeof value === 'string' ? value : JSON.stringify(value); recordWebEvent(this, type, str_value); @@ -269,7 +270,9 @@ export default function useGeneralApi(app) { const remote_ip = getRemoteIp(this.req); try { let views = 1; - // const views = yield Tarantool.instance().call('page_view', page, remote_ip, this.session.uid, ref); + if (config.tarantool) { + yield Tarantool.instance().call('page_view', page, remote_ip, this.session.uid, ref); + } const page_model = yield models.Page.findOne( {attributes: ['id', 'views'], where: {permlink: esc(page)}, logging: false} ); @@ -293,8 +296,9 @@ export default function useGeneralApi(app) { $referrer: ref, $referring_domain: referring_domain }); - mixpanel.people.set_once(this.session.uid, '$referrer', ref); + if (ref) mixpanel.people.set_once(this.session.uid, '$referrer', ref); mixpanel.people.set_once(this.session.uid, 'FirstPage', page); + mixpanel.people.increment(this.session.uid, 'PageView', 1); } } catch (error) { console.error('Error in /page_view api call', this.session.uid, error.message); From 4e5bf793c5af589451c333ae3de83c61c5dd2112 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 2 Dec 2016 11:55:42 -0500 Subject: [PATCH 73/90] add plus sign to orders balance for clarity --- app/components/modules/UserWallet.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/modules/UserWallet.jsx b/app/components/modules/UserWallet.jsx index aefc07df3f..1941ae79c7 100644 --- a/app/components/modules/UserWallet.jsx +++ b/app/components/modules/UserWallet.jsx @@ -195,7 +195,7 @@ class UserWallet extends React.Component { {isMyAccount ? : steem_balance_str + ' STEEM'} - {steemOrders ?
      ({steem_orders_balance_str} STEEM)
      : null} + {steemOrders ?
      (+{steem_orders_balance_str} STEEM)
      : null}
      @@ -216,7 +216,7 @@ class UserWallet extends React.Component { {isMyAccount ? : sbd_balance_str} - {sbdOrders ?
      ({sbd_orders_balance_str})
      : null} + {sbdOrders ?
      (+{sbd_orders_balance_str})
      : null}
      From 6cc126d1d4dff9c3dff3078ca6673636ae5057dc Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 2 Dec 2016 11:57:15 -0500 Subject: [PATCH 74/90] add todo note --- app/redux/MarketSaga.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/redux/MarketSaga.js b/app/redux/MarketSaga.js index 576c6c52b0..2abc14e9e5 100644 --- a/app/redux/MarketSaga.js +++ b/app/redux/MarketSaga.js @@ -73,7 +73,7 @@ export function* fetchOpenOrders(set_user_action) { if(account) { account = fromJS(account) yield put(MarketReducer.actions.receiveAccount({ account })) - yield put(g.actions.receiveAccount({ account })) + yield put(g.actions.receiveAccount({ account })) // TODO: move out of MarketSaga. See notes in #741 } } catch (error) { console.error('~~ Saga fetchOpenOrders error ~~>', error); From 6f79ec8cfb5fc050ddc11eb13168484a500501bb Mon Sep 17 00:00:00 2001 From: Sigve Date: Fri, 2 Dec 2016 12:15:49 -0500 Subject: [PATCH 75/90] FollowSaga refactor: separate ignore/blog results #745 --- app/components/cards/Comment.jsx | 2 +- app/components/cards/PostsList.jsx | 10 ++++++---- app/components/elements/Follow.jsx | 6 +++--- app/components/pages/Post.jsx | 14 +++++++------- app/components/pages/UserProfile.jsx | 14 ++++++-------- app/redux/FollowSaga.js | 15 +++++++++------ 6 files changed, 32 insertions(+), 29 deletions(-) diff --git a/app/components/cards/Comment.jsx b/app/components/cards/Comment.jsx index 7985c77313..27e4d39133 100644 --- a/app/components/cards/Comment.jsx +++ b/app/components/cards/Comment.jsx @@ -368,7 +368,7 @@ const Comment = connect( } const current = state.user.get('current') const username = current ? current.get('username') : null - const key = ['follow', 'get_following', username, 'result', c.get('author')] + const key = ['follow', 'get_following', username, 'ignore', 'result', c.get('author')] const ignore = username ? state.global.getIn(key, List()).contains('ignore') : false return { ...ownProps, diff --git a/app/components/cards/PostsList.jsx b/app/components/cards/PostsList.jsx index 98bc8219a6..a527bdd3d6 100644 --- a/app/components/cards/PostsList.jsx +++ b/app/components/cards/PostsList.jsx @@ -157,7 +157,7 @@ class PostsList extends React.Component { render() { const {posts, showSpam, loading, category, content, - follow, account} = this.props; + ignoredAccounts, account} = this.props; const {thumbSize, showPost} = this.state const postsInfo = []; posts.forEach(item => { @@ -167,7 +167,8 @@ class PostsList extends React.Component { return } const key = [cont.get('author')] - const ignore = follow ? follow.getIn(key, List()).contains('ignore') : false + const ignore = ignoredAccounts ? ignoredAccounts.getIn(key, List()).contains('ignore') : false + // console.log('ignoredAccounts:', ignoredAccounts ? ignoredAccounts.toJS() : ignoredAccounts, 'key', key, 'ignore', ignore) const {hide, netVoteSign, authorRepLog10} = cont.get('stats').toJS() if(!(ignore || hide) || showSpam) // rephide postsInfo.push({item, ignore, netVoteSign, authorRepLog10}) @@ -219,8 +220,9 @@ export default connect( const current = state.user.get('current') const username = current ? current.get('username') : null const content = state.global.get('content'); - const follow = state.global.getIn(['follow', 'follow', username, 'result']); - return {...props, username, content, follow, pathname}; + // console.log(username, state.global.getIn(['follow']) ? state.global.getIn(['follow']).toJS() : null); + const ignoredAccounts = state.global.getIn(['follow', 'get_following', username, 'ignore', 'result']); + return {...props, username, content, ignoredAccounts, pathname}; }, dispatch => ({ fetchState: (pathname) => { diff --git a/app/components/elements/Follow.jsx b/app/components/elements/Follow.jsx index 7aa1ebf37a..6e1dfaaca6 100644 --- a/app/components/elements/Follow.jsx +++ b/app/components/elements/Follow.jsx @@ -85,18 +85,18 @@ module.exports = connect( } const f = state.global.getIn(['follow', 'get_following', follower], emptyMap) const loading = f.getIn(['blog', 'loading'], false) || f.getIn(['ignore', 'loading'], false) - const existingFollows = Set(f.getIn(['result', following], emptySet))// Convert List to Set + const existingFollows = Set(f.getIn(['blog', 'result', following], emptySet))// Convert List to Set return { follower, existingFollows, - loading, + loading }; }, dispatch => ({ follow: (follower, following, what) => { const json = ['follow', {follower, following, what: what.toJS()}] dispatch(g.actions.update({ - key: ['follow', 'get_following', follower, 'result', following], + key: ['follow', 'get_following', follower, 'blog', 'result', following], notSet: Set(), updater: () => what })) diff --git a/app/components/pages/Post.jsx b/app/components/pages/Post.jsx index 4604649505..6b8fc11b4a 100644 --- a/app/components/pages/Post.jsx +++ b/app/components/pages/Post.jsx @@ -57,7 +57,7 @@ class Post extends React.Component { render() { const {showSignUp} = this - const {current_user, following, signup_bonus, content} = this.props + const {current_user, ignoredAccounts, signup_bonus, content} = this.props const {showNegativeComments, commentHidden, showAnyway} = this.state let post = this.props.post; if (!post) { @@ -96,8 +96,8 @@ class Post extends React.Component { const c = content.get(a); const hide = c.getIn(['stats', 'hide']) let ignore = false - if(following) { - ignore = following.get(c.get('author'), List()).contains('ignore') + if(ignoredAccounts) { + ignore = ignoredAccounts.get(c.get('author'), List()).contains('ignore') } return !hide && !ignore } @@ -200,16 +200,16 @@ class Post extends React.Component { export default connect(state => { const current_user = state.user.get('current') - let following + let ignoredAccounts if(current_user) { - const key = ['follow', 'get_following', current_user.get('username'), 'result'] - following = state.global.getIn(key, List()) + const key = ['follow', 'get_following', current_user.get('username'), 'ignore', 'result'] + ignoredAccounts = state.global.getIn(key, List()) } return { content: state.global.get('content'), signup_bonus: state.offchain.get('signup_bonus'), current_user, - following, + ignoredAccounts } } )(Post); diff --git a/app/components/pages/UserProfile.jsx b/app/components/pages/UserProfile.jsx index 89f58ff37d..f6a868e3d9 100644 --- a/app/components/pages/UserProfile.jsx +++ b/app/components/pages/UserProfile.jsx @@ -114,19 +114,17 @@ export default class UserProfile extends React.Component { } let followerCount, followingCount; - const followers = follow ? follow.getIn( ['get_followers', accountname] ) : null; - const following = follow ? follow.getIn( ['get_following', accountname] ) : null; - if(followers && followers.has('result') && followers.has('blog')) { - const status_followers = followers.get('blog') - const followers_loaded = status_followers.get('loading') === false && status_followers.get('error') == null + const followers = follow ? follow.getIn( ['get_followers', accountname, 'blog'] ) : null; + const following = follow ? follow.getIn( ['get_following', accountname, 'blog'] ) : null; + if(followers && followers.has('result')) { + const followers_loaded = followers.get('loading') === false && followers.get('error') == null if (followers_loaded) { followerCount = followers.get('count'); } } - if (following && following.has('result') && following.has('blog')) { - const status_following = following.get('blog') - const following_loaded = status_following.get('loading') === false && status_following.get('error') == null + if (following && following.has('result')) { + const following_loaded = following.get('loading') === false && following.get('error') == null if (following_loaded) { followingCount = following.get('count'); } diff --git a/app/redux/FollowSaga.js b/app/redux/FollowSaga.js index 88ef6b1f2a..9bfc567234 100644 --- a/app/redux/FollowSaga.js +++ b/app/redux/FollowSaga.js @@ -14,7 +14,7 @@ export function* loadFollows(method, account, type, start = '', limit = 100) { key: ['follow', method, account], notSet: Map(), updater: m => { - m = m.update('result', Map(), m2 => { + m = m.updateIn([type, 'result'], Map(), m2 => { res.forEach(value => { cnt++ let what = value.get('what') @@ -24,10 +24,7 @@ export function* loadFollows(method, account, type, start = '', limit = 100) { }) return m2 }) - const count = m.get('result') ? m.get('result').filter(a => { - return a.get(0) === "blog"; - }).size : 0; - return m.merge({count, [type]: {loading: true, error: null}}) + return m.mergeDeep({[type]: {loading: true, error: null}}) } }}) if(cnt === limit) { @@ -35,7 +32,13 @@ export function* loadFollows(method, account, type, start = '', limit = 100) { } else { yield put({type: 'global/UPDATE', payload: { key: ['follow', method, account], - updater: m => m.merge({[type]: {loading: false, error: null}}) + updater: m => { + const count = m.getIn([type, 'result']) ? m.getIn([type, 'result']).filter(a => { + return a.get(0) === type; + }).size : 0; + m = m.mergeDeep({[type]: {count, loading: false, error: null}}) + return m; + } }}) } } From 4311128a395a4f5313f48c451b70271e38af7f19 Mon Sep 17 00:00:00 2001 From: Sigve Date: Fri, 2 Dec 2016 12:38:59 -0500 Subject: [PATCH 76/90] Revert "FollowSaga refactor: separate ignore/blog results #745" This reverts commit 6f79ec8cfb5fc050ddc11eb13168484a500501bb. --- app/components/cards/Comment.jsx | 2 +- app/components/cards/PostsList.jsx | 10 ++++------ app/components/elements/Follow.jsx | 6 +++--- app/components/pages/Post.jsx | 14 +++++++------- app/components/pages/UserProfile.jsx | 14 ++++++++------ app/redux/FollowSaga.js | 15 ++++++--------- 6 files changed, 29 insertions(+), 32 deletions(-) diff --git a/app/components/cards/Comment.jsx b/app/components/cards/Comment.jsx index 27e4d39133..7985c77313 100644 --- a/app/components/cards/Comment.jsx +++ b/app/components/cards/Comment.jsx @@ -368,7 +368,7 @@ const Comment = connect( } const current = state.user.get('current') const username = current ? current.get('username') : null - const key = ['follow', 'get_following', username, 'ignore', 'result', c.get('author')] + const key = ['follow', 'get_following', username, 'result', c.get('author')] const ignore = username ? state.global.getIn(key, List()).contains('ignore') : false return { ...ownProps, diff --git a/app/components/cards/PostsList.jsx b/app/components/cards/PostsList.jsx index a527bdd3d6..98bc8219a6 100644 --- a/app/components/cards/PostsList.jsx +++ b/app/components/cards/PostsList.jsx @@ -157,7 +157,7 @@ class PostsList extends React.Component { render() { const {posts, showSpam, loading, category, content, - ignoredAccounts, account} = this.props; + follow, account} = this.props; const {thumbSize, showPost} = this.state const postsInfo = []; posts.forEach(item => { @@ -167,8 +167,7 @@ class PostsList extends React.Component { return } const key = [cont.get('author')] - const ignore = ignoredAccounts ? ignoredAccounts.getIn(key, List()).contains('ignore') : false - // console.log('ignoredAccounts:', ignoredAccounts ? ignoredAccounts.toJS() : ignoredAccounts, 'key', key, 'ignore', ignore) + const ignore = follow ? follow.getIn(key, List()).contains('ignore') : false const {hide, netVoteSign, authorRepLog10} = cont.get('stats').toJS() if(!(ignore || hide) || showSpam) // rephide postsInfo.push({item, ignore, netVoteSign, authorRepLog10}) @@ -220,9 +219,8 @@ export default connect( const current = state.user.get('current') const username = current ? current.get('username') : null const content = state.global.get('content'); - // console.log(username, state.global.getIn(['follow']) ? state.global.getIn(['follow']).toJS() : null); - const ignoredAccounts = state.global.getIn(['follow', 'get_following', username, 'ignore', 'result']); - return {...props, username, content, ignoredAccounts, pathname}; + const follow = state.global.getIn(['follow', 'follow', username, 'result']); + return {...props, username, content, follow, pathname}; }, dispatch => ({ fetchState: (pathname) => { diff --git a/app/components/elements/Follow.jsx b/app/components/elements/Follow.jsx index 6e1dfaaca6..7aa1ebf37a 100644 --- a/app/components/elements/Follow.jsx +++ b/app/components/elements/Follow.jsx @@ -85,18 +85,18 @@ module.exports = connect( } const f = state.global.getIn(['follow', 'get_following', follower], emptyMap) const loading = f.getIn(['blog', 'loading'], false) || f.getIn(['ignore', 'loading'], false) - const existingFollows = Set(f.getIn(['blog', 'result', following], emptySet))// Convert List to Set + const existingFollows = Set(f.getIn(['result', following], emptySet))// Convert List to Set return { follower, existingFollows, - loading + loading, }; }, dispatch => ({ follow: (follower, following, what) => { const json = ['follow', {follower, following, what: what.toJS()}] dispatch(g.actions.update({ - key: ['follow', 'get_following', follower, 'blog', 'result', following], + key: ['follow', 'get_following', follower, 'result', following], notSet: Set(), updater: () => what })) diff --git a/app/components/pages/Post.jsx b/app/components/pages/Post.jsx index 6b8fc11b4a..4604649505 100644 --- a/app/components/pages/Post.jsx +++ b/app/components/pages/Post.jsx @@ -57,7 +57,7 @@ class Post extends React.Component { render() { const {showSignUp} = this - const {current_user, ignoredAccounts, signup_bonus, content} = this.props + const {current_user, following, signup_bonus, content} = this.props const {showNegativeComments, commentHidden, showAnyway} = this.state let post = this.props.post; if (!post) { @@ -96,8 +96,8 @@ class Post extends React.Component { const c = content.get(a); const hide = c.getIn(['stats', 'hide']) let ignore = false - if(ignoredAccounts) { - ignore = ignoredAccounts.get(c.get('author'), List()).contains('ignore') + if(following) { + ignore = following.get(c.get('author'), List()).contains('ignore') } return !hide && !ignore } @@ -200,16 +200,16 @@ class Post extends React.Component { export default connect(state => { const current_user = state.user.get('current') - let ignoredAccounts + let following if(current_user) { - const key = ['follow', 'get_following', current_user.get('username'), 'ignore', 'result'] - ignoredAccounts = state.global.getIn(key, List()) + const key = ['follow', 'get_following', current_user.get('username'), 'result'] + following = state.global.getIn(key, List()) } return { content: state.global.get('content'), signup_bonus: state.offchain.get('signup_bonus'), current_user, - ignoredAccounts + following, } } )(Post); diff --git a/app/components/pages/UserProfile.jsx b/app/components/pages/UserProfile.jsx index f6a868e3d9..89f58ff37d 100644 --- a/app/components/pages/UserProfile.jsx +++ b/app/components/pages/UserProfile.jsx @@ -114,17 +114,19 @@ export default class UserProfile extends React.Component { } let followerCount, followingCount; - const followers = follow ? follow.getIn( ['get_followers', accountname, 'blog'] ) : null; - const following = follow ? follow.getIn( ['get_following', accountname, 'blog'] ) : null; - if(followers && followers.has('result')) { - const followers_loaded = followers.get('loading') === false && followers.get('error') == null + const followers = follow ? follow.getIn( ['get_followers', accountname] ) : null; + const following = follow ? follow.getIn( ['get_following', accountname] ) : null; + if(followers && followers.has('result') && followers.has('blog')) { + const status_followers = followers.get('blog') + const followers_loaded = status_followers.get('loading') === false && status_followers.get('error') == null if (followers_loaded) { followerCount = followers.get('count'); } } - if (following && following.has('result')) { - const following_loaded = following.get('loading') === false && following.get('error') == null + if (following && following.has('result') && following.has('blog')) { + const status_following = following.get('blog') + const following_loaded = status_following.get('loading') === false && status_following.get('error') == null if (following_loaded) { followingCount = following.get('count'); } diff --git a/app/redux/FollowSaga.js b/app/redux/FollowSaga.js index 9bfc567234..88ef6b1f2a 100644 --- a/app/redux/FollowSaga.js +++ b/app/redux/FollowSaga.js @@ -14,7 +14,7 @@ export function* loadFollows(method, account, type, start = '', limit = 100) { key: ['follow', method, account], notSet: Map(), updater: m => { - m = m.updateIn([type, 'result'], Map(), m2 => { + m = m.update('result', Map(), m2 => { res.forEach(value => { cnt++ let what = value.get('what') @@ -24,7 +24,10 @@ export function* loadFollows(method, account, type, start = '', limit = 100) { }) return m2 }) - return m.mergeDeep({[type]: {loading: true, error: null}}) + const count = m.get('result') ? m.get('result').filter(a => { + return a.get(0) === "blog"; + }).size : 0; + return m.merge({count, [type]: {loading: true, error: null}}) } }}) if(cnt === limit) { @@ -32,13 +35,7 @@ export function* loadFollows(method, account, type, start = '', limit = 100) { } else { yield put({type: 'global/UPDATE', payload: { key: ['follow', method, account], - updater: m => { - const count = m.getIn([type, 'result']) ? m.getIn([type, 'result']).filter(a => { - return a.get(0) === type; - }).size : 0; - m = m.mergeDeep({[type]: {count, loading: false, error: null}}) - return m; - } + updater: m => m.merge({[type]: {loading: false, error: null}}) }}) } } From 35e3fc9894d6bb5d544e037508b453d90c29d367 Mon Sep 17 00:00:00 2001 From: Sigve Date: Fri, 2 Dec 2016 12:40:15 -0500 Subject: [PATCH 77/90] Fix 'follow' typo --- app/components/cards/PostsList.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/cards/PostsList.jsx b/app/components/cards/PostsList.jsx index 98bc8219a6..9741b91a4d 100644 --- a/app/components/cards/PostsList.jsx +++ b/app/components/cards/PostsList.jsx @@ -219,7 +219,7 @@ export default connect( const current = state.user.get('current') const username = current ? current.get('username') : null const content = state.global.get('content'); - const follow = state.global.getIn(['follow', 'follow', username, 'result']); + const follow = state.global.getIn(['follow', 'get_following', username, 'result']); return {...props, username, content, follow, pathname}; }, dispatch => ({ From cccf820be5ce68640c3f8ec7cdc742099f485cb5 Mon Sep 17 00:00:00 2001 From: Tim Fesenko Date: Fri, 2 Dec 2016 13:01:19 -0500 Subject: [PATCH 78/90] full power badge (#748) * full sp reward icon concept #261 * add tooltip and icon alignment --- app/components/cards/PostSummary.jsx | 6 +++++- app/components/cards/PostSummary.scss | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/components/cards/PostSummary.jsx b/app/components/cards/PostSummary.jsx index 66a13abc98..fe6f061daa 100644 --- a/app/components/cards/PostSummary.jsx +++ b/app/components/cards/PostSummary.jsx @@ -79,6 +79,7 @@ class PostSummary extends React.Component { let title_text = p.title; let comments_link; let is_comment = false; + let full_power = content.get('percent_steem_dollars') === 0; if( content.get( 'parent_author') !== "" ) { title_text = "Re: " + content.get('root_title'); @@ -94,7 +95,10 @@ class PostSummary extends React.Component { navigate(e, onClick, post, title_link_url)}>{desc}
      ; let content_title =

      - navigate(e, onClick, post, title_link_url)}>{title_text} + navigate(e, onClick, post, title_link_url)}> + {title_text} + {full_power && } +

      ; // author and category diff --git a/app/components/cards/PostSummary.scss b/app/components/cards/PostSummary.scss index ad515049ae..e4a0edbdf0 100644 --- a/app/components/cards/PostSummary.scss +++ b/app/components/cards/PostSummary.scss @@ -52,6 +52,14 @@ ul.PostsList__summaries { > a:visited { color: #777; } + .Icon { + margin: 0 0.25rem; + svg { + width: 0.85rem; + height: 0.85rem; + vertical-align: 5%; + } + } } } .PostSummary__collapse { From e0c5a86461e07cd708d19f26367fa10ee1beca9a Mon Sep 17 00:00:00 2001 From: Tim Fesenko Date: Fri, 2 Dec 2016 13:01:57 -0500 Subject: [PATCH 79/90] do not show dropdown for comments with 0 votes (#747) --- app/components/elements/Voting.jsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/components/elements/Voting.jsx b/app/components/elements/Voting.jsx index 533ba2a718..37720b6fc5 100644 --- a/app/components/elements/Voting.jsx +++ b/app/components/elements/Voting.jsx @@ -111,7 +111,7 @@ class Voting extends React.Component { } render() { - const {myVote, active_votes, showList, voting, flag, vesting_shares} = this.props; + const {myVote, active_votes, showList, voting, flag, vesting_shares, is_comment} = this.props; const {username} = this.props; const {votingUp, votingDown, showWeight, weight} = this.state; // console.log('-- Voting.render -->', myVote, votingUp, votingDown); @@ -161,7 +161,9 @@ class Voting extends React.Component { const up = ; const classUp = 'Voting__button Voting__button-up' + (myVote > 0 ? ' Voting__button--upvoted' : '') + (votingUpActive ? ' votingUp' : ''); - const cashout_active = pending_payout > 0 || (cashout_time && cashout_time.indexOf('1969') !== 0 && cashout_time.indexOf('1970') !== 0) + // TODO: clean up the date logic after shared-db upgrade + // There is an "active cashout" if: (a) there is a pending payout, OR (b) there is a valid cashout_time AND (it's a top level post OR a comment with at least 1 vote) + const cashout_active = pending_payout > 0 || (cashout_time && cashout_time.indexOf('1969') !== 0 && cashout_time.indexOf('1970') !== 0 && (active_votes.size > 0 || !is_comment)) const payoutItems = []; if(cashout_active) { @@ -170,7 +172,7 @@ class Voting extends React.Component { if(promoted > 0) { payoutItems.push({value: 'Promotion Cost $' + formatDecimal(promoted).join('')}); } - const hide_cashout_532 = cashout_time.indexOf('1969') === 0 // tmpfix for #532 + const hide_cashout_532 = cashout_time.indexOf('1969') === 0 // tmpfix for #532. TODO: remove after shared-db if (cashout_active && !hide_cashout_532) { payoutItems.push({value: }); } From 85ae34ef908b8d8d730a539aafab08779679540a Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 2 Dec 2016 14:22:20 -0500 Subject: [PATCH 80/90] add power up icon to PostFull heading --- app/components/cards/PostFull.jsx | 6 +++++- app/components/cards/PostFull.scss | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/components/cards/PostFull.jsx b/app/components/cards/PostFull.jsx index 6a37da3e79..cabaaa4740 100644 --- a/app/components/cards/PostFull.jsx +++ b/app/components/cards/PostFull.jsx @@ -204,8 +204,12 @@ class PostFull extends React.Component { const pending_payout = parsePayoutAmount(content.pending_payout_value); const total_payout = parsePayoutAmount(content.total_payout_value); const high_quality_post = pending_payout + total_payout > 10.0; + const full_power = post_content.get('percent_steem_dollars') === 0; - let post_header =

      {content.title}

      + let post_header =

      + {content.title} + {full_power && } +

      if(content.depth > 0) { let parent_link = `/${content.category}/@${content.parent_author}/${content.parent_permlink}`; let direct_parent_link diff --git a/app/components/cards/PostFull.scss b/app/components/cards/PostFull.scss index 026f0911e1..9378a1a6f6 100644 --- a/app/components/cards/PostFull.scss +++ b/app/components/cards/PostFull.scss @@ -24,6 +24,10 @@ > h1 { overflow: hidden; font: 700 200% "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Arial, sans-serif; + .Icon { + margin: 0 0 0 0.5rem; + vertical-align: -30%; + } } a { color: $dark-gray; From 961913ba59539d728b735c37ef82c99db8a0be10 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 2 Dec 2016 14:31:15 -0500 Subject: [PATCH 81/90] add hover effect for share icons --- app/components/cards/PostFull.scss | 4 ++-- app/components/elements/ShareMenu.scss | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/components/cards/PostFull.scss b/app/components/cards/PostFull.scss index 9378a1a6f6..e886823d60 100644 --- a/app/components/cards/PostFull.scss +++ b/app/components/cards/PostFull.scss @@ -38,7 +38,7 @@ border-right: none; .Icon.clock { top: 5px; - path { + svg { fill: $dark-gray; } } @@ -54,7 +54,7 @@ clear: right; line-height: 2rem; font-size: 94%; - svg path, svg polygon { + svg { fill: $dark-gray; } a, .FoundationDropdownMenu__label { diff --git a/app/components/elements/ShareMenu.scss b/app/components/elements/ShareMenu.scss index 40b7983990..31ba48f7eb 100644 --- a/app/components/elements/ShareMenu.scss +++ b/app/components/elements/ShareMenu.scss @@ -13,6 +13,7 @@ } li > a:hover { color: #ffffff; + svg {fill: #1A5099;} } li > a:link { text-decoration: none; From e4939749d2d257dd4ec9f6bfba9590c25f805f70 Mon Sep 17 00:00:00 2001 From: originate Date: Fri, 2 Dec 2016 14:31:40 -0500 Subject: [PATCH 82/90] Add media state for follow/mute for gracefully handling collasped positioning --- app/components/pages/UserProfile.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/components/pages/UserProfile.jsx b/app/components/pages/UserProfile.jsx index c65afb7be0..f971431aba 100644 --- a/app/components/pages/UserProfile.jsx +++ b/app/components/pages/UserProfile.jsx @@ -326,7 +326,7 @@ export default class UserProfile extends React.Component {
      -
      +
      @@ -355,6 +355,9 @@ export default class UserProfile extends React.Component {

      +
      + +
      From fd7e7db5e28d6a3cc2ce73628e1944d39054f688 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 2 Dec 2016 14:34:22 -0500 Subject: [PATCH 83/90] use higher resolution youtube preview images --- app/components/elements/YoutubePreview.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/elements/YoutubePreview.jsx b/app/components/elements/YoutubePreview.jsx index b51d3ec367..6de7220f91 100644 --- a/app/components/elements/YoutubePreview.jsx +++ b/app/components/elements/YoutubePreview.jsx @@ -37,7 +37,7 @@ export default class YoutubePreview extends React.Component { // mqdefault.jpg (medium quality version, 320px × 180px) // hqdefault.jpg (high quality version, 480px × 360px // sddefault.jpg (standard definition version, 640px × 480px) - const thumbnail = width <= 320 ? 'mqdefault.jpg' : width <= 480 ? 'hqdefault.jpg' : '0.jpg' + const thumbnail = width <= 320 ? 'mqdefault.jpg' : width <= 480 ? 'hqdefault.jpg' : 'maxresdefault.jpg' const previewLink = `https://img.youtube.com/vi/${youTubeId}/${thumbnail}` return (
      From 068cfe04c9ae4c65f6e2f07c1efebc5ed2feed4d Mon Sep 17 00:00:00 2001 From: Sigve Date: Fri, 2 Dec 2016 14:34:42 -0500 Subject: [PATCH 84/90] Sort transfer log by date fix #750 --- app/components/modules/UserWallet.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/components/modules/UserWallet.jsx b/app/components/modules/UserWallet.jsx index 55a5af24b5..dde009ce00 100644 --- a/app/components/modules/UserWallet.jsx +++ b/app/components/modules/UserWallet.jsx @@ -120,7 +120,11 @@ class UserWallet extends React.Component { /// transfer log let idx = 0 - const transfer_log = account.get('transfer_history').map(item => { + const transfer_log = account.get('transfer_history') + .sort((a, b) => { + return new Date(b.getIn([1, 'timestamp'])) - new Date(a.getIn([1, 'timestamp'])) + }) + .map(item => { const data = item.getIn([1, 'op', 1]); const type = item.getIn([1, 'op', 0]); From d91372094226476c3e7f575d4f8f8505f9ae38f4 Mon Sep 17 00:00:00 2001 From: originate Date: Fri, 2 Dec 2016 14:49:17 -0500 Subject: [PATCH 85/90] Display better aligned mobile follow actions for small only, updated css --- app/components/pages/UserProfile.jsx | 2 +- app/components/pages/UserProfile.scss | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/components/pages/UserProfile.jsx b/app/components/pages/UserProfile.jsx index f971431aba..dab66abd13 100644 --- a/app/components/pages/UserProfile.jsx +++ b/app/components/pages/UserProfile.jsx @@ -355,7 +355,7 @@ export default class UserProfile extends React.Component {

      -
      +
      diff --git a/app/components/pages/UserProfile.scss b/app/components/pages/UserProfile.scss index a8a499a9f2..9ae57730a8 100644 --- a/app/components/pages/UserProfile.scss +++ b/app/components/pages/UserProfile.scss @@ -162,6 +162,15 @@ } } + .UserProfile__banner .UserProfile__buttons_mobile { + position: inherit; + margin-bottom: .5rem; + .button { + background-color: $white; + color: $black; + } + } + .UserWallet__balance { > div:last-of-type { text-align: left; From 3db50d8a6e6e550e9a89e4fdebf5421bd27e9a9d Mon Sep 17 00:00:00 2001 From: Sigve Date: Fri, 2 Dec 2016 15:04:08 -0500 Subject: [PATCH 86/90] Check if onBackButton event has keyCode with 'in' fix #613 --- app/components/cards/PostsList.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/cards/PostsList.jsx b/app/components/cards/PostsList.jsx index 9741b91a4d..f47a956a45 100644 --- a/app/components/cards/PostsList.jsx +++ b/app/components/cards/PostsList.jsx @@ -85,7 +85,7 @@ class PostsList extends React.Component { } onBackButton(e) { - if (e.keyCode && e.keyCode !== 27) return; + if ('keyCode' in e && e.keyCode !== 27) return; window.removeEventListener('popstate', this.onBackButton); window.removeEventListener('keydown', this.onBackButton); this.setState({showPost: null}); From 3e82641aa6dc8c2b1b613d183ff577cbdc355bf8 Mon Sep 17 00:00:00 2001 From: valzav Date: Fri, 2 Dec 2016 15:19:32 -0500 Subject: [PATCH 87/90] count only unique page views; fix PageViewsCounter issue --- app/components/elements/PageViewsCounter.jsx | 16 ++++++++++++---- app/components/elements/ShareMenu.jsx | 2 +- server/api/general.js | 9 +++++---- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/app/components/elements/PageViewsCounter.jsx b/app/components/elements/PageViewsCounter.jsx index e801a3b4ae..ff3dbbd9be 100644 --- a/app/components/elements/PageViewsCounter.jsx +++ b/app/components/elements/PageViewsCounter.jsx @@ -19,15 +19,23 @@ export default class PageViewsCounter extends React.Component { this.last_page = null; } + pageView() { + let ref = document.referrer || ''; + if (ref.match('://' + window.location.hostname)) ref = ''; + recordPageView(window.location.pathname, ref).then(views => this.setState({views})); + this.last_page = window.location.pathname; + } + + componentDidMount() { + this.pageView(); + } + shouldComponentUpdate(nextProps, nextState) { return nextState.views !== this.state.views || window.location.pathname !== this.last_page; } componentDidUpdate() { - let ref = document.referrer || ''; - if (ref.match('://' + window.location.hostname)) ref = ''; - recordPageView(window.location.pathname, ref).then(views => this.setState({views})); - this.last_page = window.location.pathname; + this.pageView(); } render() { diff --git a/app/components/elements/ShareMenu.jsx b/app/components/elements/ShareMenu.jsx index 799c33df8d..6375b98fe6 100644 --- a/app/components/elements/ShareMenu.jsx +++ b/app/components/elements/ShareMenu.jsx @@ -5,7 +5,7 @@ import Icon from 'app/components/elements/Icon.jsx'; export default class ShareMenu extends React.Component { static propTypes = { - items: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, + menu: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, title: React.PropTypes.string }; diff --git a/server/api/general.js b/server/api/general.js index d2a0b5b528..247a263ad4 100644 --- a/server/api/general.js +++ b/server/api/general.js @@ -8,7 +8,7 @@ import {esc, escAttrs} from 'db/models'; import {emailRegex, getRemoteIp, rateLimitReq, checkCSRF} from 'server/utils'; import coBody from 'co-body'; import Mixpanel from 'mixpanel'; -// import Tarantool from 'db/tarantool'; +import Tarantool from 'db/tarantool'; const mixpanel = config.mixpanel ? Mixpanel.init(config.mixpanel) : null; @@ -269,14 +269,15 @@ export default function useGeneralApi(app) { console.log('-- /page_view -->', this.session.uid, page); const remote_ip = getRemoteIp(this.req); try { - let views = 1; + let views = 1, unique = true; if (config.tarantool) { - yield Tarantool.instance().call('page_view', page, remote_ip, this.session.uid, ref); + const res = yield Tarantool.instance().call('page_view', page, remote_ip, this.session.uid, ref); + unique = res[0][0]; } const page_model = yield models.Page.findOne( {attributes: ['id', 'views'], where: {permlink: esc(page)}, logging: false} ); - if (page_model) { + if (page_model && unique) { views = page_model.views + 1; yield yield models.Page.update({views}, {where: {id: page_model.id}, logging: false}); } else { From 705c216ad906e09988aa902547cb59d20d54b3fb Mon Sep 17 00:00:00 2001 From: valzav Date: Fri, 2 Dec 2016 15:50:28 -0500 Subject: [PATCH 88/90] fix /page_view issue --- server/api/general.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/server/api/general.js b/server/api/general.js index 247a263ad4..e8b15b572c 100644 --- a/server/api/general.js +++ b/server/api/general.js @@ -200,7 +200,7 @@ export default function useGeneralApi(app) { try { this.session.a = account; const db_account = yield models.Account.findOne( - {attributes: ['user_id'], where: {name: esc(account)}} + {attributes: ['user_id'], where: {name: esc(account)}, logging: false} ); if (db_account) this.session.user = db_account.user_id; this.body = JSON.stringify({status: 'ok'}); @@ -277,11 +277,13 @@ export default function useGeneralApi(app) { const page_model = yield models.Page.findOne( {attributes: ['id', 'views'], where: {permlink: esc(page)}, logging: false} ); - if (page_model && unique) { - views = page_model.views + 1; - yield yield models.Page.update({views}, {where: {id: page_model.id}, logging: false}); - } else { - yield models.Page.create(escAttrs({permlink: page, views}), {logging: false}); + if (unique) { + if (page_model) { + views = page_model.views + 1; + yield yield models.Page.update({views}, {where: {id: page_model.id}, logging: false}); + } else { + yield models.Page.create(escAttrs({permlink: page, views}), {logging: false}); + } } this.body = JSON.stringify({views}); if (mixpanel) { From 34f4d2466feade0e44c4d7b2b2f06563839908ce Mon Sep 17 00:00:00 2001 From: Sigve Date: Fri, 2 Dec 2016 16:18:20 -0500 Subject: [PATCH 89/90] Fix transfer_log reverse call, remove sorting, #751 --- app/components/modules/UserWallet.jsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/components/modules/UserWallet.jsx b/app/components/modules/UserWallet.jsx index dde009ce00..6b2ea98395 100644 --- a/app/components/modules/UserWallet.jsx +++ b/app/components/modules/UserWallet.jsx @@ -121,9 +121,6 @@ class UserWallet extends React.Component { /// transfer log let idx = 0 const transfer_log = account.get('transfer_history') - .sort((a, b) => { - return new Date(b.getIn([1, 'timestamp'])) - new Date(a.getIn([1, 'timestamp'])) - }) .map(item => { const data = item.getIn([1, 'op', 1]); const type = item.getIn([1, 'op', 0]); @@ -136,8 +133,7 @@ class UserWallet extends React.Component { if(data.sbd_payout === '0.000 SBD' && data.vesting_payout === '0.000000 VESTS') return null return ; - }).filter(el => !!el); - transfer_log.reverse(); + }).filter(el => !!el).reverse(); let steem_menu = [ { value: 'Transfer', link: '#', onClick: showTransfer.bind( this, 'STEEM', 'Transfer to Account' ) }, From 921502b4a63855cffd0eb353fb6ea947a8a6d85b Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 2 Dec 2016 16:27:24 -0500 Subject: [PATCH 90/90] Release notes 0.1.161202 --- release-notes.txt | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/release-notes.txt b/release-notes.txt index a772c8b00c..ac2acd34ac 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -1,3 +1,35 @@ +--------------------------------------------------------------------- +0.1.161202 +--------------------------------------------------------------------- + +New features +-------- + - views counter #744 + - profile customization #737 + - full power badge #748 + - add current open orders to wallet balances #740 + +Bug fixes +-------- + - various market bug fixes and price warning #728 + - performance tweaks: minimize rendering and API calls #738 + - fix witness votes not appearing for logged in user #741 + - add support for vimeo auto embed #731 + - fix obscure bug which causes certain keys to trigger back event #754 + - fix follow mute button alignment for mobile display #753 + - do not show dropdown for comments with 0 votes #747 + - fix bug preventing declined payout post from being edited #743 + - handle malformed categories in url #742 + - fix share menu scrolling behavior #739 + - adjust password data-entry error wording #736 + - clarify dangerous-html flag usage #733 + - remove fastclick for JS dropdown conflicts #727 + - allow links to open in new tab without closing menu #726 + - add padding for avatar on collapsed state #717 + - display previous title when closing post modal #709 + - remove negative top margin on comment footer #714 + + --------------------------------------------------------------------- 0.1.161123 ---------------------------------------------------------------------