mirror of
https://github.com/wassname/talk.git
synced 2026-06-28 13:20:44 +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 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) => ({
|
||||
|
||||
@@ -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,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)),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user