From 15b25b039881f29720ee13343f571b17c1590511 Mon Sep 17 00:00:00 2001 From: NejcZdovc Date: Wed, 8 Mar 2017 20:09:53 +0100 Subject: [PATCH] Payments features for 1.0 Resolves #7347 Auditors: @bsclifton @bradleyrichter @mrose17 Test plan: --- app/extensions/brave/img/ledger/icon_pin.svg | 14 ++ .../brave/img/ledger/icon_remove.svg | 8 + .../locales/en-US/preferences.properties | 1 + .../preferences/payment/ledgerTable.js | 159 ++++++++++++++---- docs/state.md | 1 + js/components/sortableTable.js | 153 ++++++++++++----- js/state/syncUtil.js | 3 +- 7 files changed, 256 insertions(+), 83 deletions(-) create mode 100644 app/extensions/brave/img/ledger/icon_pin.svg create mode 100644 app/extensions/brave/img/ledger/icon_remove.svg diff --git a/app/extensions/brave/img/ledger/icon_pin.svg b/app/extensions/brave/img/ledger/icon_pin.svg new file mode 100644 index 00000000000..d79fdc493f0 --- /dev/null +++ b/app/extensions/brave/img/ledger/icon_pin.svg @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/extensions/brave/img/ledger/icon_remove.svg b/app/extensions/brave/img/ledger/icon_remove.svg new file mode 100644 index 00000000000..95dc2238cb1 --- /dev/null +++ b/app/extensions/brave/img/ledger/icon_remove.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/app/extensions/brave/locales/en-US/preferences.properties b/app/extensions/brave/locales/en-US/preferences.properties index 2ad893c0595..f05779a0c05 100644 --- a/app/extensions/brave/locales/en-US/preferences.properties +++ b/app/extensions/brave/locales/en-US/preferences.properties @@ -148,6 +148,7 @@ site=Site views=Views timeSpent=Time Spent include=Include +actions=Actions percentage=% remove=Remove bravery=Bravery diff --git a/app/renderer/components/preferences/payment/ledgerTable.js b/app/renderer/components/preferences/payment/ledgerTable.js index 122a8cef12b..2afb528dc36 100644 --- a/app/renderer/components/preferences/payment/ledgerTable.js +++ b/app/renderer/components/preferences/payment/ledgerTable.js @@ -13,6 +13,8 @@ const SortableTable = require('../../../../../js/components/sortableTable') const globalStyles = require('../../styles/global') const verifiedGreenIcon = require('../../../../extensions/brave/img/ledger/verified_green_icon.svg') const verifiedWhiteIcon = require('../../../../extensions/brave/img/ledger/verified_white_icon.svg') +const removeIcon = require('../../../../extensions/brave/img/ledger/icon_remove.svg') +const pinIcon = require('../../../../extensions/brave/img/ledger/icon_pin.svg') // other const settings = require('../../../../../js/constants/settings') @@ -73,19 +75,31 @@ class LedgerTable extends ImmutableComponent { return true } + isPinned (synopsis) { + const hostSettings = this.props.siteSettings.get(this.getHostPattern(synopsis)) + if (hostSettings) { + return !!hostSettings.get('ledgerPinned') + } + return false + } + banSite (hostPattern) { aboutActions.changeSiteSetting(hostPattern, 'ledgerPaymentsShown', false) } + togglePinSite (hostPattern, pinned) { + aboutActions.changeSiteSetting(hostPattern, 'ledgerPinned', pinned) + } + get columnClassNames () { return [ - css(styles.tableTd), - css(styles.tableTd, styles.alignRight), - css(styles.tableTd), - css(styles.tableTd), - css(styles.tableTd, styles.alignRight), - css(styles.tableTd, styles.alignRight), - css(styles.tableTd, styles.alignRight) + css(styles.tableTd, styles.alignRight, styles.verifiedTd), // verified + css(styles.tableTd, styles.alignRight), // sites + css(styles.tableTd), // include + css(styles.tableTd, styles.alignRight), // views + css(styles.tableTd, styles.alignRight), // time spent + css(styles.tableTd, styles.alignRight), // percentage + css(styles.tableTd, styles.alignLeft) // actions ] } @@ -93,10 +107,11 @@ class LedgerTable extends ImmutableComponent { if (!synopsis || !synopsis.get || !this.shouldShow(synopsis)) { return [] } + const faviconURL = synopsis.get('faviconURL') - const rank = synopsis.get('rank') const views = synopsis.get('views') const verified = synopsis.get('verified') + const pinned = this.isPinned(synopsis) const duration = synopsis.get('duration') const publisherURL = synopsis.get('publisherURL') const percentage = synopsis.get('percentage') @@ -105,18 +120,11 @@ class LedgerTable extends ImmutableComponent { return [ { - html:
- -
, + html: verified && this.getVerifiedIcon(synopsis), value: '' }, - rank, { html:
- { - verified && this.getVerifiedIcon(synopsis) - } { faviconURL @@ -142,7 +150,18 @@ class LedgerTable extends ImmutableComponent { html: this.getFormattedTime(synopsis), value: duration }, - percentage + percentage, + { + html: + + + , + value: '' + } ] } @@ -150,6 +169,17 @@ class LedgerTable extends ImmutableComponent { if (!this.synopsis || !this.synopsis.size) { return null } + + const allRows = this.synopsis.filter(synopsis => { + return !getSetting(settings.HIDE_EXCLUDED_SITES, this.props.settings) || this.enabledForSite(synopsis) + }) + const pinnedRows = allRows.filter(synopsis => { + return this.isPinned(synopsis) + }) + const unPinnedRows = allRows.filter(synopsis => { + return !this.isPinned(synopsis) + }) + return
@@ -164,36 +194,50 @@ class LedgerTable extends ImmutableComponent {
this.enabledForSite(item) - ? css(styles.tableTr) - : css(styles.tableTr, styles.paymentsDisabled) - ).toJS()} + rowClassNames={[ + pinnedRows.map(item => this.enabledForSite(item) + ? css(styles.tableTr) + : css(styles.tableTr, styles.paymentsDisabled) + ).toJS(), + unPinnedRows.map(item => this.enabledForSite(item) + ? css(styles.tableTr) + : css(styles.tableTr, styles.paymentsDisabled) + ).toJS() + ]} + bodyClassNames={[css(styles.pinnedBody), '']} onContextMenu={aboutActions.contextMenu} contextMenuName='synopsis' - rowObjects={this.synopsis.map(entry => { - return { - hostPattern: this.getHostPattern(entry), - location: entry.get('publisherURL') - } - }).toJS()} - rows={this.synopsis.filter(synopsis => { - return !getSetting(settings.HIDE_EXCLUDED_SITES, this.props.settings) || this.enabledForSite(synopsis) - }).map((synopsis) => this.getRow(synopsis)).toJS()} + rowObjects={[ + pinnedRows.map(entry => { + return { + hostPattern: this.getHostPattern(entry), + location: entry.get('publisherURL') + } + }).toJS(), + unPinnedRows.map(item => this.enabledForSite(item) + ? css(styles.tableTr) + : css(styles.tableTr, styles.paymentsDisabled) + ).toJS() + ]} + rows={[ + pinnedRows.map((synopsis) => this.getRow(synopsis)).toJS(), + unPinnedRows.map((synopsis) => this.getRow(synopsis)).toJS() + ]} />
} } const verifiedBadge = (icon) => ({ - position: 'absolute', height: '20px', width: '20px', - left: '-10px', - top: '3px', + marginRight: '-10px', + display: 'block', background: `url(${icon}) center no-repeat` }) @@ -213,7 +257,8 @@ const styles = StyleSheet.create({ tableClass: { width: '100%', - textAlign: 'left' + textAlign: 'left', + borderCollapse: 'collapse' }, tableTh: { @@ -234,6 +279,15 @@ const styles = StyleSheet.create({ padding: '0 15px' }, + verifiedTd: { + padding: '0 0 0 15px' + }, + + pinnedBody: { + borderBottom: `1px solid ${globalStyles.color.braveOrange}`, + borderCollapse: 'collapse' + }, + siteData: { display: 'flex', flex: '1', @@ -278,8 +332,41 @@ const styles = StyleSheet.create({ textAlign: 'right' }, + alignLeft: { + textAlign: 'left' + }, + paymentsDisabled: { opacity: 0.6 + }, + + mainIcon: { + backgroundColor: globalStyles.color.gray, + width: '15px', + height: '16px', + display: 'inline-block', + marginRight: '10px', + marginTop: '6px', + + ':hover': { + backgroundColor: globalStyles.color.buttonColor + } + }, + + pinIcon: { + '-webkit-mask-image': `url(${pinIcon})` + }, + + pinnedIcon: { + backgroundColor: globalStyles.color.braveOrange, + + ':hover': { + backgroundColor: globalStyles.color.braveDarkOrange + } + }, + + removeIcon: { + '-webkit-mask-image': `url(${removeIcon})` } }) diff --git a/docs/state.md b/docs/state.md index 2c924b40542..e830202a1f1 100644 --- a/docs/state.md +++ b/docs/state.md @@ -244,6 +244,7 @@ AppStore httpsEverywhere: boolean, ledgerPayments: boolean, // false if site should not be paid by the ledger. Defaults to true. ledgerPaymentsShown: boolean, // false if site should not be paid by the ledger and should not be shown in the UI. Defaults to true. + ledgerPinned: boolean, // false if site should not be pinned. Defaults to false mediaPermission: boolean, midiSysexPermission: boolean, notificationsPermission: boolean, diff --git a/js/components/sortableTable.js b/js/components/sortableTable.js index 401c696c514..cdddd586297 100644 --- a/js/components/sortableTable.js +++ b/js/components/sortableTable.js @@ -25,8 +25,9 @@ class SortableTable extends React.Component { this.state = { selection: Immutable.Set() } + this.counter = 0 } - componentDidMount (event) { + componentDidMount () { return tableSort(this.table) } /** @@ -220,20 +221,43 @@ class SortableTable extends React.Component { return this.props.columnClassNames && this.props.columnClassNames.length === this.props.headings.length } - get hasRowClassNames () { - return this.props.rowClassNames && - this.props.rowClassNames.length === this.props.rows.length + get hasBodyClassNames () { + return this.props.bodyClassNames && + this.props.bodyClassNames.length === this.props.rows.length } get hasContextMenu () { return typeof this.props.onContextMenu === 'function' && typeof this.props.contextMenuName === 'string' } + get isMultiDimensioned () { + return this.props.rows && + Array.isArray(this.props.rows[0]) && + (Array.isArray(this.props.rows[0][0]) || this.props.rows[0].length === 0) + } + get entryMultiDimension () { + for (let i = 0; i < this.props.rows.length; i++) { + if (this.props.rows[i].length > 0) { + return this.props.rows[i] + } + } + + return undefined + } get sortingDisabled () { if (typeof this.props.sortingDisabled === 'boolean') { return this.props.sortingDisabled } return false } + hasRowClassNames (bodyIndex) { + if (this.isMultiDimensioned) { + return this.props.rowClassNames && + this.props.rowClassNames[bodyIndex].length === this.props.rows[bodyIndex].length + } + + return this.props.rowClassNames && + this.props.rowClassNames.length === this.props.rows.length + } getTableAttributes () { const tableAttributes = {} if (this.props.multiSelect && this.multipleItemsSelected) { @@ -244,18 +268,31 @@ class SortableTable extends React.Component { } return tableAttributes } - getRowAttributes (row, index) { + getRowAttributes (row, index, bodyIndex) { const rowAttributes = {} + let handlerInput // Object bound to this row. Not passed to multi-select handlers. - const handlerInput = this.props.rowObjects && + if (this.isMultiDimensioned) { + // Object bound to this row. Not passed to multi-select handlers. + handlerInput = this.props.rowObjects[bodyIndex] && + (this.props.rowObjects[bodyIndex].size > 0 || this.props.rowObjects[bodyIndex].length > 0) + ? (typeof this.props.rowObjects[bodyIndex].toJS === 'function' + ? this.props.rowObjects[bodyIndex].get(index).toJS() + : (typeof this.props.rowObjects[bodyIndex][index].toJS === 'function' + ? this.props.rowObjects[bodyIndex][index].toJS() + : this.props.rowObjects[bodyIndex][index])) + : row + } else { + handlerInput = this.props.rowObjects && (this.props.rowObjects.size > 0 || this.props.rowObjects.length > 0) - ? (typeof this.props.rowObjects.toJS === 'function' - ? this.props.rowObjects.get(index).toJS() - : (typeof this.props.rowObjects[index].toJS === 'function' - ? this.props.rowObjects[index].toJS() - : this.props.rowObjects[index])) - : row + ? (typeof this.props.rowObjects.toJS === 'function' + ? this.props.rowObjects.get(index).toJS() + : (typeof this.props.rowObjects[index].toJS === 'function' + ? this.props.rowObjects[index].toJS() + : this.props.rowObjects[index])) + : row + } // Allow parent control to optionally specify context const thisArg = this.props.thisArg || this @@ -310,6 +347,61 @@ class SortableTable extends React.Component { return rowAttributes } + generateTableRows (rows, bodyIndex) { + return rows.map((row, i) => { + const entry = row.map((item, j) => { + const value = typeof item === 'object' ? item.value : item + const html = typeof item === 'object' ? item.html : item + const cell = typeof item === 'object' ? item.cell : item + return + { + cell || (value === true ? '✕' : html) + } + + }) + const currentIndex = this.counter + this.counter ++ + const rowAttributes = row.length + ? this.getRowAttributes(row, i, bodyIndex) + : null + + const classes = [] + if (rowAttributes) classes.push(rowAttributes.className) + if (this.hasRowClassNames(bodyIndex)) { + if (this.isMultiDimensioned) { + classes.push(this.props.rowClassNames[bodyIndex][i]) + } else { + classes.push(this.props.rowClassNames[i]) + } + } + if (this.stateOwner.state.selection.includes(this.getGlobalIndex(currentIndex))) classes.push('selected') + if (this.sortingDisabled) classes.push('no-sort') + + return row.length + ? {entry} + : null + }) + } + generateTableBody () { + this.counter = 0 + + if (this.isMultiDimensioned) { + return this.props.rows.map((rows, i) => { + const content = this.generateTableRows(rows, i) + return (content.length > 0) + ? + {content} + + : null + }) + } else { + return {this.generateTableRows(this.props.rows)} + } + } render () { if (!this.props.headings || !this.props.rows) { return false @@ -325,8 +417,9 @@ class SortableTable extends React.Component { {this.props.headings.map((heading, j) => { + const firstRow = this.isMultiDimensioned ? this.entryMultiDimension[0] : this.props.rows[0] const firstEntry = this.props.rows.length > 0 - ? this.props.rows[0][j] + ? firstRow[j] : undefined let dataType = typeof firstEntry if (dataType === 'object' && firstEntry.value) { @@ -353,39 +446,7 @@ class SortableTable extends React.Component { })} - - { - this.props.rows.map((row, i) => { - const entry = row.map((item, j) => { - const value = typeof item === 'object' ? item.value : item - const html = typeof item === 'object' ? item.html : item - const cell = typeof item === 'object' ? item.cell : item - return - { - cell || (value === true ? '✕' : html) - } - - }) - const rowAttributes = row.length - ? this.getRowAttributes(row, i) - : null - - const classes = [] - if (rowAttributes) classes.push(rowAttributes.className) - if (this.hasRowClassNames) classes.push(this.props.rowClassNames[i]) - if (this.stateOwner.state.selection.includes(this.getGlobalIndex(i))) classes.push('selected') - if (this.sortingDisabled) classes.push('no-sort') - - return row.length - ? {entry} - : null - }) - } - + {this.generateTableBody()} } } diff --git a/js/state/syncUtil.js b/js/state/syncUtil.js index da19b7e8c8c..0d16336d637 100644 --- a/js/state/syncUtil.js +++ b/js/state/syncUtil.js @@ -38,7 +38,8 @@ const siteSettingDefaults = { httpsEverywhere: true, fingerprintingProtection: false, ledgerPayments: true, - ledgerPaymentsShown: true + ledgerPaymentsShown: true, + ledgerPinned: false } // Whitelist of valid browser-laptop site fields. In browser-laptop, site