diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 6959ff662..8ba34e3b4 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -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 ? :
{children}
; - 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={ All Comments {totalCommentCount} @@ -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 { /> )} - - {highlightedComment - ? this.renderHighlightedComment() - : this.renderTabPanel() - } - + {highlightedComment + ? this.renderHighlightedComment() + : this.renderTabPanel() + } ); } diff --git a/client/coral-embed-stream/src/components/StreamTabPanel.css b/client/coral-embed-stream/src/components/StreamTabPanel.css new file mode 100644 index 000000000..9398d373c --- /dev/null +++ b/client/coral-embed-stream/src/components/StreamTabPanel.css @@ -0,0 +1,3 @@ +.spinnerContainer { + margin-top: 16px; +} diff --git a/client/coral-embed-stream/src/components/StreamTabPanel.js b/client/coral-embed-stream/src/components/StreamTabPanel.js index 0f2092092..a511ac555 100644 --- a/client/coral-embed-stream/src/components/StreamTabPanel.js +++ b/client/coral-embed-stream/src/components/StreamTabPanel.js @@ -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 (
{tabs} - - {tabPanes} - + {loading + ?
+ : + {tabPanes} + + }
); } diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 271421dea..194bfc5d9 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -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) { diff --git a/client/coral-embed-stream/src/containers/StreamTabPanel.js b/client/coral-embed-stream/src/containers/StreamTabPanel.js index 152f34e39..fde6e3be4 100644 --- a/client/coral-embed-stream/src/containers/StreamTabPanel.js +++ b/client/coral-embed-stream/src/containers/StreamTabPanel.js @@ -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) => ({ diff --git a/client/coral-framework/components/Popup.js b/client/coral-framework/components/Popup.js index 1fe3408ea..33041442f 100644 --- a/client/coral-framework/components/Popup.js +++ b/client/coral-framework/components/Popup.js @@ -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); diff --git a/client/coral-framework/helpers/validate.js b/client/coral-framework/helpers/validate.js index dd36ae07f..ef64a2c84 100644 --- a/client/coral-framework/helpers/validate.js +++ b/client/coral-framework/helpers/validate.js @@ -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)), diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 533ed85db..26f6b627e 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -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); +} diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index f5ea64005..571952639 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -108,6 +108,11 @@ const CommentFragment = gql` nodes { id body + replyCount + action_summaries { + count + __typename + } asset { id title diff --git a/client/talk-plugin-history/Comment.css b/client/talk-plugin-history/Comment.css index 646e1f054..1200709a3 100644 --- a/client/talk-plugin-history/Comment.css +++ b/client/talk-plugin-history/Comment.css @@ -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; diff --git a/client/talk-plugin-history/Comment.js b/client/talk-plugin-history/Comment.js index 4cf9d875c..b813e514b 100644 --- a/client/talk-plugin-history/Comment.js +++ b/client/talk-plugin-history/Comment.js @@ -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 (
-

- + + + + {reactionCount} + + {reactionCount === 1 ? t('common.reaction') : t('common.reactions')} + + + + + {comment.replyCount} + + {comment.replyCount === 1 ? t('common.reply') : t('common.replies')} + +

+
+ - Story: {comment.asset.title ? comment.asset.title : comment.asset.url} + {t('common.story')}: {comment.asset.title ? comment.asset.title : comment.asset.url} -

+
    @@ -38,7 +57,7 @@ class Comment extends React.Component {
  • - + { 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) {