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