mirror of
https://github.com/wassname/talk.git
synced 2026-09-09 11:38:08 +08:00
Merge branch 'master' into asset-graph-api
This commit is contained in:
@@ -13,7 +13,7 @@ import t, {timeago} from 'coral-framework/services/i18n';
|
|||||||
import CommentBox from 'talk-plugin-commentbox/CommentBox';
|
import CommentBox from 'talk-plugin-commentbox/CommentBox';
|
||||||
import QuestionBox from 'talk-plugin-questionbox/QuestionBox';
|
import QuestionBox from 'talk-plugin-questionbox/QuestionBox';
|
||||||
import {isCommentActive} from 'coral-framework/utils';
|
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 cn from 'classnames';
|
||||||
|
|
||||||
import {getTopLevelParent, attachCommentToParent} from '../graphql/utils';
|
import {getTopLevelParent, attachCommentToParent} from '../graphql/utils';
|
||||||
@@ -23,8 +23,6 @@ import StreamTabPanel from '../containers/StreamTabPanel';
|
|||||||
|
|
||||||
import styles from './Stream.css';
|
import styles from './Stream.css';
|
||||||
|
|
||||||
const SpinnerWhenLoading = ({loading, children}) => loading ? <Spinner /> : <div>{children}</div>;
|
|
||||||
|
|
||||||
class Stream extends React.Component {
|
class Stream extends React.Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -144,6 +142,7 @@ class Stream extends React.Component {
|
|||||||
emit,
|
emit,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
sortBy,
|
sortBy,
|
||||||
|
loading,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const slotProps = {data};
|
const slotProps = {data};
|
||||||
@@ -171,6 +170,7 @@ class Stream extends React.Component {
|
|||||||
tabPaneSlot={'streamTabPanes'}
|
tabPaneSlot={'streamTabPanes'}
|
||||||
slotProps={slotProps}
|
slotProps={slotProps}
|
||||||
queryData={slotQueryData}
|
queryData={slotQueryData}
|
||||||
|
loading={loading}
|
||||||
appendTabs={
|
appendTabs={
|
||||||
<Tab tabId={'all'} key='all'>
|
<Tab tabId={'all'} key='all'>
|
||||||
All Comments <TabCount active={activeStreamTab === 'all'} sub>{totalCommentCount}</TabCount>
|
All Comments <TabCount active={activeStreamTab === 'all'} sub>{totalCommentCount}</TabCount>
|
||||||
@@ -223,7 +223,6 @@ class Stream extends React.Component {
|
|||||||
viewAllComments,
|
viewAllComments,
|
||||||
auth: {loggedIn, user},
|
auth: {loggedIn, user},
|
||||||
editName,
|
editName,
|
||||||
loading,
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {keepCommentBox} = this.state;
|
const {keepCommentBox} = this.state;
|
||||||
const open = !asset.isClosed;
|
const open = !asset.isClosed;
|
||||||
@@ -310,12 +309,10 @@ class Stream extends React.Component {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SpinnerWhenLoading loading={loading}>
|
{highlightedComment
|
||||||
{highlightedComment
|
? this.renderHighlightedComment()
|
||||||
? this.renderHighlightedComment()
|
: this.renderTabPanel()
|
||||||
: this.renderTabPanel()
|
}
|
||||||
}
|
|
||||||
</SpinnerWhenLoading>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.spinnerContainer {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
@@ -1,19 +1,23 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {TabBar, TabContent} from 'coral-ui';
|
import {Spinner, TabBar, TabContent} from 'coral-ui';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import styles from './StreamTabPanel.css';
|
||||||
|
|
||||||
class StreamTabPanel extends React.Component {
|
class StreamTabPanel extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {activeTab, setActiveTab, tabs, tabPanes, sub} = this.props;
|
const {activeTab, setActiveTab, tabs, tabPanes, sub, loading} = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<TabBar activeTab={activeTab} onTabClick={setActiveTab} sub={sub}>
|
<TabBar activeTab={activeTab} onTabClick={setActiveTab} sub={sub}>
|
||||||
{tabs}
|
{tabs}
|
||||||
</TabBar>
|
</TabBar>
|
||||||
<TabContent activeTab={activeTab} sub={sub}>
|
{loading
|
||||||
{tabPanes}
|
? <div className={styles.spinnerContainer}><Spinner /></div>
|
||||||
</TabContent>
|
: <TabContent activeTab={activeTab} sub={sub}>
|
||||||
|
{tabPanes}
|
||||||
|
</TabContent>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,10 @@ class StreamContainer extends React.Component {
|
|||||||
return prev;
|
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);
|
return insertCommentIntoEmbedQuery(prev, commentAdded);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -163,32 +167,9 @@ class StreamContainer extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
const prevSortedNewest = this.isSortedByNewestFirst(this.props);
|
if (this.props.sortOrder !== nextProps.sortOrder || this.props.sortBy !== nextProps.sortBy) {
|
||||||
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) {
|
|
||||||
nextProps.data.refetch();
|
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) {
|
userIsDegraged({auth: {user}} = this.props) {
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ class StreamTabPanelContainer extends React.Component {
|
|||||||
setActiveTab={this.props.setActiveTab}
|
setActiveTab={this.props.setActiveTab}
|
||||||
tabs={this.getPluginTabElements().concat(this.props.appendTabs)}
|
tabs={this.getPluginTabElements().concat(this.props.appendTabs)}
|
||||||
tabPanes={this.getPluginTabPaneElements().concat(this.props.appendTabPanes)}
|
tabPanes={this.getPluginTabPaneElements().concat(this.props.appendTabPanes)}
|
||||||
|
loading={this.props.loading}
|
||||||
sub={this.props.sub}
|
sub={this.props.sub}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -110,6 +111,7 @@ StreamTabPanelContainer.propTypes = {
|
|||||||
queryData: PropTypes.object,
|
queryData: PropTypes.object,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
sub: PropTypes.bool,
|
sub: PropTypes.bool,
|
||||||
|
loading: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
const mapStateToProps = (state) => ({
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
|
|||||||
export default class Popup extends Component {
|
export default class Popup extends Component {
|
||||||
ref = null;
|
ref = null;
|
||||||
detectCloseInterval = null;
|
detectCloseInterval = null;
|
||||||
|
resetCallbackInterval = null;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -41,16 +42,26 @@ export default class Popup extends Component {
|
|||||||
this.ref.onunload = () => {
|
this.ref.onunload = () => {
|
||||||
this.onUnload();
|
this.onUnload();
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
if (this.resetCallbackInterval) {
|
||||||
|
clearInterval(this.resetCallbackInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resetCallbackInterval = setInterval(() => {
|
||||||
if (this.ref && this.ref.onload === null) {
|
if (this.ref && this.ref.onload === null) {
|
||||||
|
clearInterval(this.resetCallbackInterval);
|
||||||
|
this.resetCallbackInterval = null;
|
||||||
this.setCallbacks();
|
this.setCallbacks();
|
||||||
clearInterval(interval);
|
|
||||||
}
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
|
if (this.detectCloseInterval) {
|
||||||
|
clearInterval(this.detectCloseInterval);
|
||||||
|
}
|
||||||
|
|
||||||
this.detectCloseInterval = setInterval(() => {
|
this.detectCloseInterval = setInterval(() => {
|
||||||
if (!this.ref || this.ref.closed) {
|
if (!this.ref || this.ref.closed) {
|
||||||
clearInterval(this.detectCloseInterval);
|
clearInterval(this.detectCloseInterval);
|
||||||
|
this.detectCloseInterval = null;
|
||||||
this.onClose();
|
this.onClose();
|
||||||
}
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export default {
|
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)),
|
password: (pass) => (/^(?=.{8,}).*$/.test(pass)),
|
||||||
confirmPassword: () => true,
|
confirmPassword: () => true,
|
||||||
username: (username) => (/^[a-zA-Z0-9_]+$/.test(username)),
|
username: (username) => (/^[a-zA-Z0-9_]+$/.test(username)),
|
||||||
|
|||||||
@@ -184,3 +184,16 @@ export function getShallowChanges(a, b) {
|
|||||||
return union(Object.keys(a), Object.keys(b))
|
return union(Object.keys(a), Object.keys(b))
|
||||||
.filter((key) => a[key] !== b[key]);
|
.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 {
|
nodes {
|
||||||
id
|
id
|
||||||
body
|
body
|
||||||
|
replyCount
|
||||||
|
action_summaries {
|
||||||
|
count
|
||||||
|
__typename
|
||||||
|
}
|
||||||
asset {
|
asset {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.myComment:last-child {
|
.myComment:last-child {
|
||||||
@@ -16,13 +17,28 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 12px;
|
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 {
|
.sidebar {
|
||||||
ul {
|
ul {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import styles from './Comment.css';
|
|||||||
import Slot from 'coral-framework/components/Slot';
|
import Slot from 'coral-framework/components/Slot';
|
||||||
import PubDate from '../talk-plugin-pubdate/PubDate';
|
import PubDate from '../talk-plugin-pubdate/PubDate';
|
||||||
import CommentContent from '../coral-embed-stream/src/components/CommentContent';
|
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';
|
import t from 'coral-framework/services/i18n';
|
||||||
|
|
||||||
@@ -11,24 +13,41 @@ class Comment extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {comment, link, data, root} = this.props;
|
const {comment, link, data, root} = this.props;
|
||||||
|
const reactionCount = getTotalReactionsCount(comment.action_summaries);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.myComment}>
|
<div className={styles.myComment}>
|
||||||
<div>
|
<div>
|
||||||
<Slot
|
<Slot
|
||||||
fill="commentContent"
|
fill="commentContent"
|
||||||
defaultComponent={CommentContent}
|
defaultComponent={CommentContent}
|
||||||
className={`${styles.commentBody} myCommentBody`}
|
className={cn(styles.commentBody, 'my-comment-body')}
|
||||||
data={data}
|
data={data}
|
||||||
queryData={{root, comment, asset: comment.asset}}
|
queryData={{root, comment, asset: comment.asset}}
|
||||||
/>
|
/>
|
||||||
<p className="myCommentAsset">
|
<div className={cn(styles.commentSummary, 'comment-summary')}>
|
||||||
<a
|
<span className={cn(styles.commentSummaryReactions, 'comment-summary-reactions', {[styles.countZero]: reactionCount === 0})}>
|
||||||
className={`${styles.assetURL} myCommentAnchor`}
|
<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="#"
|
href="#"
|
||||||
onClick={link(`${comment.asset.url}`)}>
|
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>
|
</a>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.sidebar}>
|
<div className={styles.sidebar}>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -38,7 +57,7 @@ class Comment extends React.Component {
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Icon name="schedule" className={styles.iconDate}/>
|
<Icon name="schedule" className={styles.iconDate} />
|
||||||
<PubDate
|
<PubDate
|
||||||
className={styles.pubdate}
|
className={styles.pubdate}
|
||||||
created_at={comment.created_at}
|
created_at={comment.created_at}
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ en:
|
|||||||
common:
|
common:
|
||||||
copy: 'Copy'
|
copy: 'Copy'
|
||||||
error: 'An error has occurred.'
|
error: 'An error has occurred.'
|
||||||
|
reply: 'reply'
|
||||||
|
replies: 'replies'
|
||||||
|
reaction: 'reaction'
|
||||||
|
reactions: 'reactions'
|
||||||
|
story: 'Story'
|
||||||
community:
|
community:
|
||||||
account_creation_date: "Account Creation Date"
|
account_creation_date: "Account Creation Date"
|
||||||
active: Active
|
active: Active
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ es:
|
|||||||
common:
|
common:
|
||||||
copy: 'Copiar'
|
copy: 'Copiar'
|
||||||
error: 'Ha ocurrido un error.'
|
error: 'Ha ocurrido un error.'
|
||||||
|
reply: 'respuesta'
|
||||||
|
replies: 'respuestas'
|
||||||
|
reaction: 'reacción'
|
||||||
|
reactions: 'reacciones'
|
||||||
|
story: 'Artículo'
|
||||||
community:
|
community:
|
||||||
account_creation_date: "Fecha de creación de la cuenta"
|
account_creation_date: "Fecha de creación de la cuenta"
|
||||||
active: Activa
|
active: Activa
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ describe('graph.queries.asset', () => {
|
|||||||
username: 'usernameC'
|
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,
|
author_id: users[idx].id,
|
||||||
asset_id: assets[idx].id,
|
asset_id: assets[idx].id,
|
||||||
body: `hello there! ${String(Math.random()).slice(2)}`,
|
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.nodes).to.have.length(2);
|
||||||
expect(asset.hasNextPage).to.be.false;
|
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(asset.nodes[1]).to.have.property('id', comments[0].id);
|
||||||
expect(otherAsset.nodes).to.have.length(2);
|
expect(otherAsset.nodes).to.have.length(2);
|
||||||
expect(otherAsset.hasNextPage).to.be.false;
|
expect(otherAsset.hasNextPage).to.be.false;
|
||||||
expect(otherAsset.nodes[0]).to.have.property('id', comments[3].id);
|
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 node of asset.nodes) {
|
||||||
for (let otherNode of otherAsset.nodes) {
|
for (let otherNode of otherAsset.nodes) {
|
||||||
|
|||||||
Reference in New Issue
Block a user