diff --git a/src/renderer/component/expandable/index.js b/src/renderer/component/expandable/index.js new file mode 100644 index 000000000..6dc708a6f --- /dev/null +++ b/src/renderer/component/expandable/index.js @@ -0,0 +1,7 @@ +import { connect } from 'react-redux'; +import Expandable from './view'; + +export default connect( + null, + null +)(Expandable); diff --git a/src/renderer/component/expandable/view.jsx b/src/renderer/component/expandable/view.jsx new file mode 100644 index 000000000..2bbd6636b --- /dev/null +++ b/src/renderer/component/expandable/view.jsx @@ -0,0 +1,57 @@ +// @flow +import React, { PureComponent, Node } from 'react'; +import classnames from 'classnames'; +import Button from 'component/button'; + +// Note: +// When we use this in other parts of the app, we will probably need to +// add props for collapsed height + +type Props = { + children: Node | Array, +}; + +type State = { + expanded: boolean, +}; + +export default class Expandable extends PureComponent { + constructor() { + super(); + + this.state = { + expanded: false, + }; + + (this: any).handleClick = this.handleClick.bind(this); + } + + handleClick() { + this.setState({ + expanded: !this.state.expanded, + }); + } + + render() { + const { children } = this.props; + const { expanded } = this.state; + + return ( +
+
+ {children} +
+
+ ); + } +} diff --git a/src/renderer/component/fileDetails/index.js b/src/renderer/component/fileDetails/index.js index fb92ce86f..819c505f0 100644 --- a/src/renderer/component/fileDetails/index.js +++ b/src/renderer/component/fileDetails/index.js @@ -4,8 +4,12 @@ import { makeSelectContentTypeForUri, makeSelectMetadataForUri, makeSelectFileInfoForUri, + doNotify, } from 'lbry-redux'; +import { selectUser } from 'lbryinc'; import { doOpenFileInFolder } from 'redux/actions/file'; +import { selectHasClickedComment } from 'redux/selectors/app'; +import { doClickCommentButton } from 'redux/actions/app'; import FileDetails from './view'; const select = (state, props) => ({ @@ -13,10 +17,14 @@ const select = (state, props) => ({ contentType: makeSelectContentTypeForUri(props.uri)(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state), metadata: makeSelectMetadataForUri(props.uri)(state), + hasClickedComment: selectHasClickedComment(state), + user: selectUser(state), }); const perform = dispatch => ({ openFolder: path => dispatch(doOpenFileInFolder(path)), + showSnackBar: message => dispatch(doNotify({ message, displayType: ['snackbar'] })), + clickCommentButton: () => dispatch(doClickCommentButton()), }); export default connect( diff --git a/src/renderer/component/fileDetails/view.jsx b/src/renderer/component/fileDetails/view.jsx index f36cad539..637eef139 100644 --- a/src/renderer/component/fileDetails/view.jsx +++ b/src/renderer/component/fileDetails/view.jsx @@ -1,77 +1,128 @@ // @flow -import * as React from 'react'; +import type { Claim, Metadata } from 'types/claim'; +import type { FileInfo } from 'types/file_info'; +import React, { Fragment, PureComponent } from 'react'; +import { Lbryio } from 'lbryinc'; import MarkdownPreview from 'component/common/markdown-preview'; import Button from 'component/button'; import path from 'path'; -import type { Claim } from 'types/claim'; +import Expandable from 'component/expandable'; type Props = { claim: Claim, - fileInfo: { - download_path: string, - }, - metadata: { - description: string, - language: string, - license: string, - }, + fileInfo: FileInfo, + metadata: Metadata, openFolder: string => void, contentType: string, + clickCommentButton: () => void, + showSnackBar: string => void, + hasClickedComment: boolean, + user: ?any, }; -const FileDetails = (props: Props) => { - const { claim, contentType, fileInfo, metadata, openFolder } = props; - - if (!claim || !metadata) { - return ( -
- {__('Empty claim or metadata info.')} -
- ); +class FileDetails extends PureComponent { + constructor() { + super(); + (this: any).handleCommentClick = this.handleCommentClick.bind(this); } - const { description, language, license } = metadata; + handleCommentClick() { + const { clickCommentButton, showSnackBar } = this.props; - const mediaType = contentType || 'unknown'; - const downloadPath = fileInfo ? path.normalize(fileInfo.download_path) : null; + clickCommentButton(); + Lbryio.call('user_tag', 'edit', { add: 'comments-waitlist' }); + showSnackBar(__('Thanks! This feature is so beta, all we had time for was this button.')); + } - return ( - - {description && ( - -
About
+ render() { + const { + claim, + contentType, + fileInfo, + metadata, + openFolder, + hasClickedComment, + user, + } = this.props; + + if (!claim || !metadata) { + return ( +
+ {__('Empty claim or metadata info.')} +
+ ); + } + + const { description, language, license } = metadata; + + const mediaType = contentType || 'unknown'; + const downloadPath = fileInfo ? path.normalize(fileInfo.download_path) : null; + + return ( + + + {description && ( + +
About
+
+ +
+
+ )} +
Info
- +
+ {__('Content-Type')} + {': '} + {mediaType} +
+
+ {__('Language')} + {': '} + {language} +
+
+ {__('License')} + {': '} + {license} +
+ {downloadPath && ( +
+ {__('Downloaded to')} + {': '} +
+ )}
-
- )} -
Info
-
-
- {__('Content-Type')} - {': '} - {mediaType} -
-
- {__('Language')} - {': '} - {language} -
-
- {__('License')} - {': '} - {license} -
- {downloadPath && ( -
- {__('Downloaded to')} - {': '} -
- )} -
-
- ); -}; + {hasClickedComment && ( +

+ {user + ? __( + 'Your support has been added. You will be notified when comments are available.' + ) + : __('Your support has been added. Comments are coming soon.')} +

+ )} + + + ); + } +} export default FileDetails; diff --git a/src/renderer/constants/action_types.js b/src/renderer/constants/action_types.js index 009eaa0f3..9c578e08c 100644 --- a/src/renderer/constants/action_types.js +++ b/src/renderer/constants/action_types.js @@ -13,6 +13,7 @@ export const DAEMON_READY = 'DAEMON_READY'; export const DAEMON_VERSION_MATCH = 'DAEMON_VERSION_MATCH'; export const DAEMON_VERSION_MISMATCH = 'DAEMON_VERSION_MISMATCH'; export const VOLUME_CHANGED = 'VOLUME_CHANGED'; +export const ADD_COMMENT = 'ADD_COMMENT'; // Navigation export const CHANGE_AFTER_AUTH_PATH = 'CHANGE_AFTER_AUTH_PATH'; @@ -36,6 +37,7 @@ export const SKIP_UPGRADE = 'SKIP_UPGRADE'; export const START_UPGRADE = 'START_UPGRADE'; export const AUTO_UPDATE_DECLINED = 'AUTO_UPDATE_DECLINED'; export const AUTO_UPDATE_DOWNLOADED = 'AUTO_UPDATE_DOWNLOADED'; +export const CLEAR_UPGRADE_TIMER = 'CLEAR_UPGRADE_TIMER'; // Wallet export const GET_NEW_ADDRESS_STARTED = 'GET_NEW_ADDRESS_STARTED'; diff --git a/src/renderer/index.js b/src/renderer/index.js index 5ed612fd0..4f79a6dc2 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -123,11 +123,16 @@ document.addEventListener('drop', event => { }); document.addEventListener('click', event => { let { target } = event; + while (target && target !== document) { if (target.matches('a') || target.matches('button')) { // TODO: Look into using accessiblity labels (this would also make the app more accessible) const hrefParts = window.location.href.split('#'); - const element = target.title || (target.textContent && target.textContent.trim()); + + // Buttons that we want to track should use `data-id` + // This prevents multiple buttons being grouped together if they have the same text + const element = + target.dataset.id || target.title || (target.textContent && target.textContent.trim()); if (element) { analytics.track('CLICK', { target: element, diff --git a/src/renderer/redux/actions/app.js b/src/renderer/redux/actions/app.js index 61e5b42f0..db973b3ce 100644 --- a/src/renderer/redux/actions/app.js +++ b/src/renderer/redux/actions/app.js @@ -2,8 +2,8 @@ import { execSync } from 'child_process'; import isDev from 'electron-is-dev'; import path from 'path'; import { ipcRenderer, remote } from 'electron'; +import * as ACTIONS from 'constants/action_types'; import { - ACTIONS, Lbry, doBalanceSubscribe, doFetchFileInfosAndPublishedClaims, @@ -387,6 +387,12 @@ export function doChangeVolume(volume) { }; } +export function doClickCommentButton() { + return { + type: ACTIONS.ADD_COMMENT, + }; +} + export function doConditionalAuthNavigate(newSession) { return (dispatch, getState) => { const state = getState(); diff --git a/src/renderer/redux/reducers/app.js b/src/renderer/redux/reducers/app.js index 2aa59f98d..aa1fd72c3 100644 --- a/src/renderer/redux/reducers/app.js +++ b/src/renderer/redux/reducers/app.js @@ -33,7 +33,7 @@ export type AppState = { checkUpgradeTimer: ?number, isUpgradeAvailable: ?boolean, isUpgradeSkipped: ?boolean, - snackBar: ?SnackBar, + hasClickedComment: boolean, }; const defaultState: AppState = { @@ -50,14 +50,13 @@ const defaultState: AppState = { autoUpdateDownloaded: false, autoUpdateDeclined: false, modalsAllowed: true, - + hasClickedComment: false, downloadProgress: undefined, upgradeDownloading: undefined, upgradeDownloadComplete: undefined, checkUpgradeTimer: undefined, isUpgradeAvailable: undefined, isUpgradeSkipped: undefined, - snackBar: undefined, }; reducers[ACTIONS.DAEMON_READY] = state => @@ -189,6 +188,11 @@ reducers[ACTIONS.CLEAR_UPGRADE_TIMER] = state => checkUpgradeTimer: undefined, }); +reducers[ACTIONS.ADD_COMMENT] = state => + Object.assign({}, state, { + hasClickedComment: true, + }); + export default function reducer(state: AppState = defaultState, action: any) { const handler = reducers[action.type]; if (handler) return handler(state, action); diff --git a/src/renderer/redux/selectors/app.js b/src/renderer/redux/selectors/app.js index 1f1655ea2..8fc21965f 100644 --- a/src/renderer/redux/selectors/app.js +++ b/src/renderer/redux/selectors/app.js @@ -19,6 +19,11 @@ export const selectUpdateUrl = createSelector(selectPlatform, platform => { } }); +export const selectHasClickedComment = createSelector( + selectState, + state => state.hasClickedComment +); + export const selectRemoteVersion = createSelector(selectState, state => state.remoteVersion); export const selectIsUpgradeAvailable = createSelector( diff --git a/src/renderer/scss/_gui.scss b/src/renderer/scss/_gui.scss index 751fc8463..47537dd83 100644 --- a/src/renderer/scss/_gui.scss +++ b/src/renderer/scss/_gui.scss @@ -130,7 +130,7 @@ p:not(:first-of-type) { bottom: 0; right: 0; - background-color: rgba($lbry-gray-1, 0.3); + background-color: mix($lbry-white, $lbry-gray-1, 70%); display: flex; position: absolute; z-index: 0; @@ -245,11 +245,6 @@ p:not(:first-of-type) { padding: 0; } -.divider__horizontal { - border-top: $lbry-gray-2; - margin: 16px 0; -} - .hidden { display: none; } diff --git a/src/renderer/scss/all.scss b/src/renderer/scss/all.scss index 8b421609c..8fa28771a 100644 --- a/src/renderer/scss/all.scss +++ b/src/renderer/scss/all.scss @@ -6,4 +6,4 @@ 'component/markdown-editor', 'component/scrollbar', 'component/spinner', 'component/nav', 'component/file-list', 'component/file-render', 'component/search', 'component/toggle', 'component/dat-gui', 'component/item-list', 'component/time', 'component/icon', - 'component/placeholder', 'component/badge', 'themes/dark'; + 'component/placeholder', 'component/badge', 'component/expandable', 'themes/dark'; diff --git a/src/renderer/scss/component/_expandable.scss b/src/renderer/scss/component/_expandable.scss new file mode 100644 index 000000000..c62a85c8c --- /dev/null +++ b/src/renderer/scss/component/_expandable.scss @@ -0,0 +1,28 @@ +.expandable { + border-bottom: var(--input-border-size) solid $lbry-gray-3; + padding-bottom: $spacing-vertical * 1/3; +} + +.expandable--open { + max-height: 100%; +} + +.expandable--closed { + max-height: 10em; + position: relative; + overflow: hidden; +} + +.expandable--closed::after { + content: ''; + width: 100%; + height: 20%; + position: absolute; + left: 0; + bottom: 0; + background-image: linear-gradient( + to bottom, + transparent 0%, + mix($lbry-white, $lbry-gray-1, 70%) 90% + ); +} diff --git a/src/renderer/scss/themes/_dark.scss b/src/renderer/scss/themes/_dark.scss index 6248f3e9f..94863ede3 100644 --- a/src/renderer/scss/themes/_dark.scss +++ b/src/renderer/scss/themes/_dark.scss @@ -83,7 +83,7 @@ html[data-theme='dark'] { } // - // BUTTON + // Button // .btn { &.btn--alt:not(:disabled) { @@ -178,4 +178,15 @@ html[data-theme='dark'] { } } } + + // + // Expandable + // + .expandable { + border-bottom: var(--input-border-size) solid $lbry-gray-5; + } + + .expandable--closed::after { + background-image: linear-gradient(to bottom, transparent 0%, $lbry-black 90%); + } } diff --git a/src/renderer/store.js b/src/renderer/store.js index b6e191885..d2237413d 100644 --- a/src/renderer/store.js +++ b/src/renderer/store.js @@ -108,6 +108,7 @@ const fileInfoFilter = createFilter('fileInfo', [ 'fileListDownloadedSort', 'fileListSubscriptionSort', ]); +const appFilter = createFilter('app', ['hasClickedComment']); // We only need to persist the receiveAddress for the wallet const walletFilter = createFilter('wallet', ['receiveAddress']); @@ -115,7 +116,14 @@ const persistOptions = { whitelist: ['subscriptions', 'publish', 'wallet', 'content', 'fileInfo'], // Order is important. Needs to be compressed last or other transforms can't // read the data - transforms: [subscriptionsFilter, walletFilter, contentFilter, fileInfoFilter, compressor], + transforms: [ + subscriptionsFilter, + walletFilter, + contentFilter, + fileInfoFilter, + appFilter, + compressor, + ], debounce: 10000, storage: localForage, }; diff --git a/src/renderer/types/claim.js b/src/renderer/types/claim.js index a7b86a39e..7db1d7e28 100644 --- a/src/renderer/types/claim.js +++ b/src/renderer/types/claim.js @@ -14,6 +14,8 @@ export type Metadata = { title: string, thumbnail: ?string, description: ?string, + license: ?string, + language: string, fee?: | { amount: number, // should be a string https://github.com/lbryio/lbry/issues/1576 diff --git a/src/renderer/types/file_info.js b/src/renderer/types/file_info.js index bec912921..07efda975 100644 --- a/src/renderer/types/file_info.js +++ b/src/renderer/types/file_info.js @@ -7,6 +7,7 @@ export type FileInfo = { pending?: boolean, channel_claim_id: string, file_name: string, + download_path: string, value?: { publisherSignature: { certificateId: string,