Merge branch 'master' into asset-graph-api

This commit is contained in:
Kim Gardner
2017-08-29 18:52:17 +01:00
committed by GitHub
14 changed files with 116 additions and 55 deletions
@@ -13,7 +13,7 @@ import t, {timeago} from 'coral-framework/services/i18n';
import CommentBox from 'talk-plugin-commentbox/CommentBox';
import QuestionBox from 'talk-plugin-questionbox/QuestionBox';
import {isCommentActive} from 'coral-framework/utils';
import {Spinner, Button, Tab, TabCount, TabPane} from 'coral-ui';
import {Button, Tab, TabCount, TabPane} from 'coral-ui';
import cn from 'classnames';
import {getTopLevelParent, attachCommentToParent} from '../graphql/utils';
@@ -23,8 +23,6 @@ import StreamTabPanel from '../containers/StreamTabPanel';
import styles from './Stream.css';
const SpinnerWhenLoading = ({loading, children}) => loading ? <Spinner /> : <div>{children}</div>;
class Stream extends React.Component {
constructor(props) {
@@ -144,6 +142,7 @@ class Stream extends React.Component {
emit,
sortOrder,
sortBy,
loading,
} = this.props;
const slotProps = {data};
@@ -171,6 +170,7 @@ class Stream extends React.Component {
tabPaneSlot={'streamTabPanes'}
slotProps={slotProps}
queryData={slotQueryData}
loading={loading}
appendTabs={
<Tab tabId={'all'} key='all'>
All Comments <TabCount active={activeStreamTab === 'all'} sub>{totalCommentCount}</TabCount>
@@ -223,7 +223,6 @@ class Stream extends React.Component {
viewAllComments,
auth: {loggedIn, user},
editName,
loading,
} = this.props;
const {keepCommentBox} = this.state;
const open = !asset.isClosed;
@@ -310,12 +309,10 @@ class Stream extends React.Component {
/>
)}
<SpinnerWhenLoading loading={loading}>
{highlightedComment
? this.renderHighlightedComment()
: this.renderTabPanel()
}
</SpinnerWhenLoading>
{highlightedComment
? this.renderHighlightedComment()
: this.renderTabPanel()
}
</div>
);
}
@@ -0,0 +1,3 @@
.spinnerContainer {
margin-top: 16px;
}
@@ -1,19 +1,23 @@
import React from 'react';
import {TabBar, TabContent} from 'coral-ui';
import {Spinner, TabBar, TabContent} from 'coral-ui';
import PropTypes from 'prop-types';
import styles from './StreamTabPanel.css';
class StreamTabPanel extends React.Component {
render() {
const {activeTab, setActiveTab, tabs, tabPanes, sub} = this.props;
const {activeTab, setActiveTab, tabs, tabPanes, sub, loading} = this.props;
return (
<div>
<TabBar activeTab={activeTab} onTabClick={setActiveTab} sub={sub}>
{tabs}
</TabBar>
<TabContent activeTab={activeTab} sub={sub}>
{tabPanes}
</TabContent>
{loading
? <div className={styles.spinnerContainer}><Spinner /></div>
: <TabContent activeTab={activeTab} sub={sub}>
{tabPanes}
</TabContent>
}
</div>
);
}
@@ -84,6 +84,10 @@ class StreamContainer extends React.Component {
return prev;
}
// Newest top-level comments are only added when sorting by 'newest first'.
if (!commentAdded.parent && !this.isSortedByNewestFirst()) {
return prev;
}
return insertCommentIntoEmbedQuery(prev, commentAdded);
},
});
@@ -163,32 +167,9 @@ class StreamContainer extends React.Component {
}
componentWillReceiveProps(nextProps) {
const prevSortedNewest = this.isSortedByNewestFirst(this.props);
const nextSortedNewest = this.isSortedByNewestFirst(nextProps);
// When switching to 'Newest first' we refetch and subscribe so that
// we always have the newest comments.
if (!prevSortedNewest && nextSortedNewest) {
if (this.props.sortOrder !== nextProps.sortOrder || this.props.sortBy !== nextProps.sortBy) {
nextProps.data.refetch();
this.subscribeToCommentsAdded();
}
// When switching away from 'Newest first' unsubscribe from newest comments.
if (prevSortedNewest && !nextSortedNewest) {
this.unsubscribeCommentsAdded();
}
}
shouldComponentUpdate(nextProps) {
const prevSortedNewest = this.isSortedByNewestFirst(this.props);
const nextSortedNewest = this.isSortedByNewestFirst(nextProps);
if (!prevSortedNewest && nextSortedNewest) {
// When switching to 'Newest first' we refetch => skip
// rendering this frame and wait for refetch to kick in.
return false;
}
return true;
}
userIsDegraged({auth: {user}} = this.props) {
@@ -86,6 +86,7 @@ class StreamTabPanelContainer extends React.Component {
setActiveTab={this.props.setActiveTab}
tabs={this.getPluginTabElements().concat(this.props.appendTabs)}
tabPanes={this.getPluginTabPaneElements().concat(this.props.appendTabPanes)}
loading={this.props.loading}
sub={this.props.sub}
/>
);
@@ -110,6 +111,7 @@ StreamTabPanelContainer.propTypes = {
queryData: PropTypes.object,
className: PropTypes.string,
sub: PropTypes.bool,
loading: PropTypes.bool,
};
const mapStateToProps = (state) => ({
+13 -2
View File
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
export default class Popup extends Component {
ref = null;
detectCloseInterval = null;
resetCallbackInterval = null;
constructor(props) {
super(props);
@@ -41,16 +42,26 @@ export default class Popup extends Component {
this.ref.onunload = () => {
this.onUnload();
const interval = setInterval(() => {
if (this.resetCallbackInterval) {
clearInterval(this.resetCallbackInterval);
}
this.resetCallbackInterval = setInterval(() => {
if (this.ref && this.ref.onload === null) {
clearInterval(this.resetCallbackInterval);
this.resetCallbackInterval = null;
this.setCallbacks();
clearInterval(interval);
}
}, 50);
if (this.detectCloseInterval) {
clearInterval(this.detectCloseInterval);
}
this.detectCloseInterval = setInterval(() => {
if (!this.ref || this.ref.closed) {
clearInterval(this.detectCloseInterval);
this.detectCloseInterval = null;
this.onClose();
}
}, 50);
+1 -1
View File
@@ -1,5 +1,5 @@
export default {
email: (email) => (/^([A-Za-z0-9_\-\.\+])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(email)),
email: (email) => (/^.+@.+\..+$/.test(email)),
password: (pass) => (/^(?=.{8,}).*$/.test(pass)),
confirmPassword: () => true,
username: (username) => (/^[a-zA-Z0-9_]+$/.test(username)),
+13
View File
@@ -184,3 +184,16 @@ export function getShallowChanges(a, b) {
return union(Object.keys(a), Object.keys(b))
.filter((key) => a[key] !== b[key]);
}
// TODO: replace with something less fragile.
// NOT_REACTION_TYPES are the action summaries that are not reactions.
const NOT_REACTION_TYPES = [
'FlagActionSummary',
'DontAgreeActionSummary',
];
export function getTotalReactionsCount(actionSummaries) {
return actionSummaries
.filter(({__typename}) => !NOT_REACTION_TYPES.includes(__typename))
.reduce((total, {count}) => total + count, 0);
}
@@ -108,6 +108,11 @@ const CommentFragment = gql`
nodes {
id
body
replyCount
action_summaries {
count
__typename
}
asset {
id
title
+19 -3
View File
@@ -6,6 +6,7 @@
display: flex;
align-items: baseline;
justify-content: space-between;
padding-bottom: 20px;
}
.myComment:last-child {
@@ -16,13 +17,28 @@
text-decoration: none;
font-weight: bold;
font-size: 12px;
color: #2c3e50;
color: #757575;
}
.commentBody {
.commentSummary {
font-size: 14px;
margin: 30px 0 10px;
color: #424242;
}
.commentSummaryReactions {
margin-right: 10px;
}
.reactionCount, .replyCount {
margin: 0 4px;
}
.countZero {
color: #9E9E9E;
}
.sidebar {
ul {
margin-top: 0;
+26 -7
View File
@@ -4,6 +4,8 @@ import styles from './Comment.css';
import Slot from 'coral-framework/components/Slot';
import PubDate from '../talk-plugin-pubdate/PubDate';
import CommentContent from '../coral-embed-stream/src/components/CommentContent';
import cn from 'classnames';
import {getTotalReactionsCount} from 'coral-framework/utils';
import t from 'coral-framework/services/i18n';
@@ -11,24 +13,41 @@ class Comment extends React.Component {
render() {
const {comment, link, data, root} = this.props;
const reactionCount = getTotalReactionsCount(comment.action_summaries);
return (
<div className={styles.myComment}>
<div>
<Slot
fill="commentContent"
defaultComponent={CommentContent}
className={`${styles.commentBody} myCommentBody`}
className={cn(styles.commentBody, 'my-comment-body')}
data={data}
queryData={{root, comment, asset: comment.asset}}
/>
<p className="myCommentAsset">
<a
className={`${styles.assetURL} myCommentAnchor`}
<div className={cn(styles.commentSummary, 'comment-summary')}>
<span className={cn(styles.commentSummaryReactions, 'comment-summary-reactions', {[styles.countZero]: reactionCount === 0})}>
<Icon name="thumb_up" />
<span className={cn(styles.reactionCount, 'comment-summary-reaction-count')}>
{reactionCount}
</span>
{reactionCount === 1 ? t('common.reaction') : t('common.reactions')}
</span>
<span className={cn('comment-summary-replies', {[styles.countZero]: comment.replyCount === 0})}>
<Icon name="reply" />
<span className={cn(styles.replyCount, 'comment-summary-reply-count')}>
{comment.replyCount}
</span>
{comment.replyCount === 1 ? t('common.reply') : t('common.replies')}
</span>
</div>
<div className="my-comment-asset">
<a className={cn(styles.assetURL, 'my-comment-anchor')}
href="#"
onClick={link(`${comment.asset.url}`)}>
Story: {comment.asset.title ? comment.asset.title : comment.asset.url}
{t('common.story')}: {comment.asset.title ? comment.asset.title : comment.asset.url}
</a>
</p>
</div>
</div>
<div className={styles.sidebar}>
<ul>
@@ -38,7 +57,7 @@ class Comment extends React.Component {
</a>
</li>
<li>
<Icon name="schedule" className={styles.iconDate}/>
<Icon name="schedule" className={styles.iconDate} />
<PubDate
className={styles.pubdate}
created_at={comment.created_at}
+5
View File
@@ -41,6 +41,11 @@ en:
common:
copy: 'Copy'
error: 'An error has occurred.'
reply: 'reply'
replies: 'replies'
reaction: 'reaction'
reactions: 'reactions'
story: 'Story'
community:
account_creation_date: "Account Creation Date"
active: Active
+5
View File
@@ -41,6 +41,11 @@ es:
common:
copy: 'Copiar'
error: 'Ha ocurrido un error.'
reply: 'respuesta'
replies: 'respuestas'
reaction: 'reacción'
reactions: 'reacciones'
story: 'Artículo'
community:
account_creation_date: "Fecha de creación de la cuenta"
active: Activa
+3 -3
View File
@@ -34,7 +34,7 @@ describe('graph.queries.asset', () => {
username: 'usernameC'
}
]);
comments = await CommentsService.publicCreate([0, 1, 0, 1].map((idx) => ({
comments = await CommentsService.publicCreate([0, 0, 1, 1].map((idx) => ({
author_id: users[idx].id,
asset_id: assets[idx].id,
body: `hello there! ${String(Math.random()).slice(2)}`,
@@ -74,12 +74,12 @@ describe('graph.queries.asset', () => {
expect(asset.nodes).to.have.length(2);
expect(asset.hasNextPage).to.be.false;
expect(asset.nodes[0]).to.have.property('id', comments[2].id);
expect(asset.nodes[0]).to.have.property('id', comments[1].id);
expect(asset.nodes[1]).to.have.property('id', comments[0].id);
expect(otherAsset.nodes).to.have.length(2);
expect(otherAsset.hasNextPage).to.be.false;
expect(otherAsset.nodes[0]).to.have.property('id', comments[3].id);
expect(otherAsset.nodes[1]).to.have.property('id', comments[1].id);
expect(otherAsset.nodes[1]).to.have.property('id', comments[2].id);
for (let node of asset.nodes) {
for (let otherNode of otherAsset.nodes) {