Comments support button #2095

Merged
neb-b merged 2 commits from comments into master 2018-11-14 02:11:33 +01:00
16 changed files with 262 additions and 72 deletions

View file

@ -0,0 +1,7 @@
import { connect } from 'react-redux';
import Expandable from './view';
export default connect(
skhameneh commented 2018-11-14 01:37:14 +01:00 (Migrated from github.com)
Review

😐

😐
null,
null
)(Expandable);

View 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 = {
skhameneh commented 2018-11-14 01:39:37 +01:00 (Migrated from github.com)
Review

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

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>
);
}
}

View file

@ -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(

View file

@ -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>
skhameneh commented 2018-11-14 01:40:42 +01:00 (Migrated from github.com)
Review

Should 'unknown' be a shared constant?

Should `'unknown'` be a shared constant?
neb-b commented 2018-11-14 01:54:14 +01:00 (Migrated from github.com)
Review

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;

View file

@ -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';

View file

@ -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,

View file

@ -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();

View file

@ -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);

View file

@ -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(

View file

@ -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;
}

View file

@ -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';

View 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%
);
}

View file

@ -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%);
}
}

View file

@ -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,
};

View file

@ -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

View file

@ -7,6 +7,7 @@ export type FileInfo = {
pending?: boolean,
channel_claim_id: string,
file_name: string,
download_path: string,
value?: {
publisherSignature: {
certificateId: string,