Comments support button #2095
16 changed files with 262 additions and 72 deletions
7
src/renderer/component/expandable/index.js
Normal file
7
src/renderer/component/expandable/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import Expandable from './view';
|
||||
|
||||
export default connect(
|
||||
|
||||
null,
|
||||
null
|
||||
)(Expandable);
|
57
src/renderer/component/expandable/view.jsx
Normal file
57
src/renderer/component/expandable/view.jsx
Normal file
|
@ -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<Node>,
|
||||
};
|
||||
|
||||
type State = {
|
||||
expanded: boolean,
|
||||
};
|
||||
|
||||
export default class Expandable extends PureComponent<Props, State> {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
Can also do:
and use arrow fn's or bind syntax https://babeljs.io/blog/2015/05/14/function-bind Can also do:
```
class MyComponent extends PureComponent {
state = {};
}
```
and use arrow fn's or bind syntax https://babeljs.io/blog/2015/05/14/function-bind
|
||||
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 (
|
||||
<div className="expandable">
|
||||
<div
|
||||
className={classnames({
|
||||
'expandable--open': expanded,
|
||||
'expandable--closed': !expanded,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<Button
|
||||
button="link"
|
||||
label={expanded ? __('Less') : __('More')}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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 (
|
||||
<div className="card__content">
|
||||
<span className="empty">{__('Empty claim or metadata info.')}</span>
|
||||
</div>
|
||||
);
|
||||
class FileDetails extends PureComponent<Props> {
|
||||
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! Comments are coming soon(ish).'));
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{description && (
|
||||
<React.Fragment>
|
||||
<div className="card__subtext-title">About</div>
|
||||
render() {
|
||||
const {
|
||||
claim,
|
||||
contentType,
|
||||
fileInfo,
|
||||
metadata,
|
||||
openFolder,
|
||||
hasClickedComment,
|
||||
user,
|
||||
} = this.props;
|
||||
|
||||
if (!claim || !metadata) {
|
||||
return (
|
||||
<div className="card__content">
|
||||
<span className="empty">{__('Empty claim or metadata info.')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { description, language, license } = metadata;
|
||||
|
||||
const mediaType = contentType || 'unknown';
|
||||
const downloadPath = fileInfo ? path.normalize(fileInfo.download_path) : null;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Expandable>
|
||||
{description && (
|
||||
<Fragment>
|
||||
<div className="card__subtext-title">About</div>
|
||||
<div className="card__subtext">
|
||||
<MarkdownPreview content={description} promptLinks />
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
<div className="card__subtext-title">Info</div>
|
||||
<div className="card__subtext">
|
||||
<MarkdownPreview content={description} promptLinks={true} />
|
||||
<div>
|
||||
{__('Content-Type')}
|
||||
{': '}
|
||||
{mediaType}
|
||||
</div>
|
||||
<div>
|
||||
{__('Language')}
|
||||
{': '}
|
||||
{language}
|
||||
</div>
|
||||
<div>
|
||||
Should Should `'unknown'` be a shared constant?
Probably. Although I'm not sure where else it's used. Probably. Although I'm not sure where else it's used.
|
||||
{__('License')}
|
||||
{': '}
|
||||
{license}
|
||||
</div>
|
||||
{downloadPath && (
|
||||
<div>
|
||||
{__('Downloaded to')}
|
||||
{': '}
|
||||
<Button
|
||||
button="link"
|
||||
onClick={() => openFolder(downloadPath)}
|
||||
label={downloadPath}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<div className="card__subtext-title">Info</div>
|
||||
<div className="card__subtext">
|
||||
<div>
|
||||
{__('Content-Type')}
|
||||
{': '}
|
||||
{mediaType}
|
||||
</div>
|
||||
<div>
|
||||
{__('Language')}
|
||||
{': '}
|
||||
{language}
|
||||
</div>
|
||||
<div>
|
||||
{__('License')}
|
||||
{': '}
|
||||
{license}
|
||||
</div>
|
||||
{downloadPath && (
|
||||
<div>
|
||||
{__('Downloaded to')}
|
||||
{': '}
|
||||
<Button button="link" onClick={() => openFolder(downloadPath)} label={downloadPath} />
|
||||
</Expandable>
|
||||
<div className="card__content">
|
||||
<div className="card__subtext-title">Comments</div>
|
||||
<div className="card__actions card__actions--center">
|
||||
<Button
|
||||
data-id="add-comment"
|
||||
disabled={hasClickedComment}
|
||||
button="primary"
|
||||
label={__('Want to comment?')}
|
||||
onClick={this.handleCommentClick}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
{hasClickedComment && (
|
||||
<p className="main--for-content">
|
||||
{user
|
||||
? __(
|
||||
'Your support has been added. You will be notified when comments are available.'
|
||||
)
|
||||
: __('Your support has been added. Comments are coming soon.')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FileDetails;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
28
src/renderer/scss/component/_expandable.scss
Normal file
28
src/renderer/scss/component/_expandable.scss
Normal file
|
@ -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%
|
||||
);
|
||||
}
|
|
@ -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%);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -108,14 +108,22 @@ 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']);
|
||||
|
||||
const persistOptions = {
|
||||
whitelist: ['subscriptions', 'publish', 'wallet', 'content', 'fileInfo'],
|
||||
whitelist: ['subscriptions', 'publish', 'wallet', 'content', 'fileInfo', 'app'],
|
||||
// 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,
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -7,6 +7,7 @@ export type FileInfo = {
|
|||
pending?: boolean,
|
||||
channel_claim_id: string,
|
||||
file_name: string,
|
||||
download_path: string,
|
||||
value?: {
|
||||
publisherSignature: {
|
||||
certificateId: string,
|
||||
|
|
Loading…
Reference in a new issue
😐