diff --git a/client/coral-embed-stream/src/Comment.css b/client/coral-embed-stream/src/Comment.css index 01022ae66..4383cbc5b 100644 --- a/client/coral-embed-stream/src/Comment.css +++ b/client/coral-embed-stream/src/Comment.css @@ -5,6 +5,7 @@ .Comment { margin-bottom: 15px; + position: relative; } .pendingComment { diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index caffb6894..d970a5f35 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -117,7 +117,6 @@ class Comment extends React.Component { const flag = getActionSummary('FlagActionSummary', comment); const dontagree = getActionSummary('DontAgreeActionSummary', comment); let commentClass = parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`; - commentClass += highlighted === comment.id ? ' highlighted-comment' : ''; commentClass += comment.id === 'pending' ? ` ${styles.pendingComment}` : ''; // call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar) @@ -147,18 +146,19 @@ class Comment extends React.Component { id={`c_${comment.id}`} style={{marginLeft: depth * 30}}>
- - { isStaff(comment.tags) - ? Staff +
+ + { isStaff(comment.tags) + ? Staff : null } - { commentIsBest(comment) - ? + { commentIsBest(comment) + ? : null } - + - +
-
- - - - - - +
+ + + + + + +
{ activeReplyBox === comment.id diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index 962eb1030..3f3c395f8 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -6,16 +6,17 @@ import I18n from 'coral-framework/modules/i18n/i18n'; import translations from 'coral-framework/translations'; const lang = new I18n(translations); -import {TabBar, Tab, TabContent, Spinner} from 'coral-ui'; +import {TabBar, Tab, TabContent, Spinner, Button} from 'coral-ui'; const {logout, showSignInDialog, requestConfirmEmail} = authActions; const {addNotification, clearNotification} = notificationActions; const {fetchAssetSuccess} = assetActions; +import {NEW_COMMENT_COUNT_POLL_INTERVAL} from 'coral-framework/constants/comments'; import {queryStream} from 'coral-framework/graphql/queries'; import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag} from 'coral-framework/graphql/mutations'; import {editName} from 'coral-framework/actions/user'; -import {updateCountCache} from 'coral-framework/actions/asset'; +import {updateCountCache, viewAllComments} from 'coral-framework/actions/asset'; import {notificationActions, authActions, assetActions, pym} from 'coral-framework'; import Stream from './Stream'; @@ -31,7 +32,7 @@ import ChangeUsernameContainer from '../../coral-sign-in/containers/ChangeUserna import ProfileContainer from 'coral-settings/containers/ProfileContainer'; import RestrictedContent from 'coral-framework/components/RestrictedContent'; import ConfigureStreamContainer from 'coral-configure/containers/ConfigureStreamContainer'; -import Comment from './Comment'; +import HighlightedComment from './Comment'; import LoadMore from './LoadMore'; import NewCount from './NewCount'; @@ -68,10 +69,30 @@ class Embed extends Component { pym.sendMessage('childReady'); } + componentWillUnmount () { + clearInterval(this.state.countPoll); + } + componentWillReceiveProps (nextProps) { const {loadAsset} = this.props; if(!isEqual(nextProps.data.asset, this.props.data.asset)) { loadAsset(nextProps.data.asset); + + const {getCounts, updateCountCache} = this.props; + const {asset} = nextProps.data; + + updateCountCache(asset.id, asset.commentCount); + + this.setState({ + countPoll: setInterval(() => { + const {asset} = this.props.data; + getCounts({ + asset_id: asset.id, + limit: asset.comments.length, + sort: 'REVERSE_CHRONOLOGICAL' + }); + }, NEW_COMMENT_COUNT_POLL_INTERVAL) + }); } } @@ -79,7 +100,7 @@ class Embed extends Component { if(!isEqual(prevProps.data.comment, this.props.data.comment)) { // Scroll to a permalinked comment if one is in the URL once the page is done rendering. - setTimeout(()=>pym.scrollParentToChildEl(`c_${this.props.data.comment.id}`), 0); + setTimeout(() => pym.scrollParentToChildEl('coralStream'), 0); } } @@ -97,6 +118,8 @@ class Embed extends Component { const {closedAt, countCache = {}} = this.props.asset; const {loading, asset, refetch, comment} = this.props.data; const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth; + + // even though the permalinked comment is the highlighted one, we're displaying its parent + replies const highlightedComment = comment && comment.parent ? comment.parent : comment; const openStream = closedAt === null; @@ -124,6 +147,16 @@ class Embed extends Component { {lang.t('MY_COMMENTS')} Configure Stream + { + highlightedComment && + + } {loggedIn && this.props.logout().then(refetch)} changeTab={this.changeTab}/>} { @@ -170,9 +203,11 @@ class Embed extends Component { offset={signInOffset}/>} {loggedIn && user && } {loggedIn && } + + {/* the highlightedComment is isolated after the user followed a permalink */} { - highlightedComment && - + :
+ +
+ +
+ asset.comments.length} + loadMore={this.props.loadMore} /> +
} - -
- -
- asset.comments.length} - loadMore={this.props.loadMore}/>
({ editName: (username) => dispatch(editName(username)), showSignInDialog: (offset) => dispatch(showSignInDialog(offset)), updateCountCache: (id, count) => dispatch(updateCountCache(id, count)), + viewAllComments: () => dispatch(viewAllComments()), logout: () => dispatch(logout()), dispatch: d => dispatch(d) }); diff --git a/client/coral-embed-stream/src/LoadMore.js b/client/coral-embed-stream/src/LoadMore.js index 89dc0104c..1a58c8d19 100644 --- a/client/coral-embed-stream/src/LoadMore.js +++ b/client/coral-embed-stream/src/LoadMore.js @@ -5,7 +5,7 @@ import {ADDTL_COMMENTS_ON_LOAD_MORE} from 'coral-framework/constants/comments'; import {Button} from 'coral-ui'; const lang = new I18n(translations); -const loadMoreComments = (assetId, comments, loadMore, parentId) => { +const loadMoreComments = (assetId, comments, loadMore, parentId, replyCount) => { let cursor = null; if (comments.length) { @@ -15,7 +15,7 @@ const loadMoreComments = (assetId, comments, loadMore, parentId) => { } loadMore({ - limit: ADDTL_COMMENTS_ON_LOAD_MORE, + limit: parentId ? replyCount : ADDTL_COMMENTS_ON_LOAD_MORE, cursor, asset_id: assetId, parent_id: parentId, @@ -48,7 +48,7 @@ class LoadMore extends React.Component { diff --git a/client/coral-embed-stream/src/Stream.js b/client/coral-embed-stream/src/Stream.js index defca1a61..34145ed4d 100644 --- a/client/coral-embed-stream/src/Stream.js +++ b/client/coral-embed-stream/src/Stream.js @@ -1,6 +1,5 @@ import React, {PropTypes} from 'react'; import Comment from './Comment'; -import {NEW_COMMENT_COUNT_POLL_INTERVAL} from 'coral-framework/constants/comments'; class Stream extends React.Component { @@ -27,26 +26,6 @@ class Stream extends React.Component { this.state = {activeReplyBox: '', countPoll: null}; } - componentDidMount() { - const {asset, getCounts, updateCountCache} = this.props; - - updateCountCache(asset.id, asset.commentCount); - - // Note: Apollo's built-in polling doesn't work with fetchMore queries, so a - // setInterval is being used instead. - this.setState({ - countPoll: setInterval(() => getCounts({ - asset_id: asset.id, - limit: asset.comments.length, - sort: 'REVERSE_CHRONOLOGICAL' - }), NEW_COMMENT_COUNT_POLL_INTERVAL), - }); - } - - componentWillUnmount() { - clearInterval(this.state.countPoll); - } - render () { const { comments, diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 697bebd29..c712d1696 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -307,10 +307,7 @@ button.comment__action-button[disabled], display: none; background-color: white; border: 1px solid black; - width: calc(100% - 15px); position: absolute; - top: 70px; - right: 0; padding: 5px; } diff --git a/client/coral-embed/src/index.js b/client/coral-embed/src/index.js index 8ebd403b8..e3350e0d3 100644 --- a/client/coral-embed/src/index.js +++ b/client/coral-embed/src/index.js @@ -74,6 +74,15 @@ function configurePymParent(pymParent, asset_url) { snackbar.style.opacity = 0; }); + // remove the permalink comment id from the hash + pymParent.onMessage('coral-view-all-comments', function () { + window.history.replaceState( + {}, + document.title, + location.origin + location.pathname + location.search + ); + }); + pymParent.onMessage('coral-alert', function (message) { const [type, text] = message.split('|'); snackbar.style.transform = 'translate(-50%, 20px)'; diff --git a/client/coral-framework/actions/asset.js b/client/coral-framework/actions/asset.js index 609525986..fa927e58a 100644 --- a/client/coral-framework/actions/asset.js +++ b/client/coral-framework/actions/asset.js @@ -1,6 +1,7 @@ import * as actions from '../constants/asset'; import coralApi from '../helpers/response'; import {addNotification} from '../actions/notification'; +import {pym} from 'coral-framework'; import I18n from '../../coral-framework/modules/i18n/i18n'; import translations from './../translations'; @@ -49,3 +50,37 @@ export const updateOpenStatus = status => dispatch => { dispatch(updateOpenStream({closedAt: new Date().getTime()})); } }; + +function removeParam(key, sourceURL) { + let rtn = sourceURL.split('?')[0]; + let param; + let params_arr = []; + let queryString = (sourceURL.indexOf('?') !== -1) ? sourceURL.split('?')[1] : ''; + if (queryString !== '') { + params_arr = queryString.split('&'); + for (let i = params_arr.length - 1; i >= 0; i -= 1) { + param = params_arr[i].split('=')[0]; + if (param === key) { + params_arr.splice(i, 1); + } + } + rtn = `${rtn}?${params_arr.join('&')}`; + } + return rtn; +} + +export const viewAllComments = () => { + + // remove the comment_id url param + const modifiedUrl = removeParam('comment_id', location.href); + try { + + // "window" here refers to the embedded iframe + window.history.replaceState({}, document.title, modifiedUrl); + + // also change the parent url + pym.sendMessage('coral-view-all-comments'); + } catch (e) { /* not sure if we're worried about old browsers */ } + + return {type: actions.VIEW_ALL_COMMENTS}; +}; diff --git a/client/coral-framework/constants/asset.js b/client/coral-framework/constants/asset.js index 234095d9d..5547c5284 100644 --- a/client/coral-framework/constants/asset.js +++ b/client/coral-framework/constants/asset.js @@ -9,3 +9,5 @@ export const UPDATE_ASSET_SETTINGS_FAILURE = 'UPDATE_ASSET_SETTINGS_FAILURE'; export const OPEN_COMMENTS = 'OPEN_COMMENTS'; export const CLOSE_COMMENTS = 'CLOSE_COMMENTS'; export const UPDATE_COUNT_CACHE = 'UPDATE_COUNT_CACHE'; + +export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS'; diff --git a/client/coral-framework/graphql/queries/index.js b/client/coral-framework/graphql/queries/index.js index cdf2356d8..ec0dcc336 100644 --- a/client/coral-framework/graphql/queries/index.js +++ b/client/coral-framework/graphql/queries/index.js @@ -5,6 +5,7 @@ import GET_COUNTS from './getCounts.graphql'; import MY_COMMENT_HISTORY from './myCommentHistory.graphql'; import uniqBy from 'lodash/uniqBy'; import sortBy from 'lodash/sortBy'; +import isNil from 'lodash/isNil'; function getQueryVariable(variable) { let query = window.location.search.substring(1); @@ -20,6 +21,7 @@ function getQueryVariable(variable) { return null; } +// get the counts of the top-level comments export const getCounts = (data) => ({asset_id, limit, sort}) => { return data.fetchMore({ query: GET_COUNTS, @@ -41,23 +43,48 @@ export const getCounts = (data) => ({asset_id, limit, sort}) => { }); }; +// handle paginated requests for more Comments pertaining to the Asset export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, sort}, newComments) => { return data.fetchMore({ query: LOAD_MORE, variables: { - limit, - cursor, - parent_id, - asset_id, - sort + limit, // how many comments are we returning + cursor, // the date of the first/last comment depending on the sort order + parent_id, // if null, we're loading more top-level comments, if not, we're loading more replies to a comment + asset_id, // the id of the asset we're currently on + sort // CHRONOLOGICAL or REVERSE_CHRONOLOGICAL }, updateQuery: (oldData, {fetchMoreResult:{data:{new_top_level_comments}}}) => { let updatedAsset; - if (parent_id) { + if (!isNil(oldData.comment)) { // loaded replies on a highlighted (permalinked) comment + + let comment = {}; + if (oldData.comment && oldData.comment.parent) { + + // put comments (replies) onto the oldData.comment.parent object + // the initial comment permalinked was a reply + const uniqReplies = uniqBy([...new_top_level_comments, ...oldData.comment.parent.replies], 'id'); + comment.parent = {...oldData.comment.parent, replies: sortBy(uniqReplies, 'created_at')}; + } else if (oldData.comment) { + + // put the comments (replies) directly onto oldData.comment + // the initial comment permalinked was a top-level comment + const uniqReplies = uniqBy([...new_top_level_comments, ...oldData.comment.replies], 'id'); + comment.replies = sortBy(uniqReplies, 'created_at'); + } + + updatedAsset = { + ...oldData, + comment: { + ...oldData.comment, + ...comment + } + }; + + } else if (parent_id) { // If loading more replies - // If loading more replies updatedAsset = { ...oldData, asset: { @@ -76,9 +103,8 @@ export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, s }) } }; - } else { + } else { // If loading more top-level comments - // If loading more top-level comments updatedAsset = { ...oldData, asset: { @@ -94,8 +120,11 @@ export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, s }); }; +// load the comment stream. export const queryStream = graphql(STREAM_QUERY, { options: () => { + + // where the query string is from the embeded iframe url let comment_id = getQueryVariable('comment_id'); let has_comment = comment_id != null; diff --git a/client/coral-framework/graphql/queries/streamQuery.graphql b/client/coral-framework/graphql/queries/streamQuery.graphql index 03af1de4c..c9f9d3d5b 100644 --- a/client/coral-framework/graphql/queries/streamQuery.graphql +++ b/client/coral-framework/graphql/queries/streamQuery.graphql @@ -1,10 +1,18 @@ #import "../fragments/commentView.graphql" query AssetQuery($asset_id: ID, $asset_url: String!, $comment_id: ID!, $has_comment: Boolean!) { + # the comment here is for loading one comment and it's children, probably after following a permalink + # $has_comment is derived from the comment_id query param in the iframe url, + # which is in turn pulled from the host page url comment(id: $comment_id) @include(if: $has_comment) { ...commentView + replyCount + replies { + ...commentView + } parent { ...commentView + replyCount replies { ...commentView } diff --git a/client/coral-framework/translations.json b/client/coral-framework/translations.json index f9038aba7..31c3e98ea 100644 --- a/client/coral-framework/translations.json +++ b/client/coral-framework/translations.json @@ -13,6 +13,7 @@ "error": "Usernames can contain letters, numbers and _ only" }, "viewMoreComments": "view more comments", + "showAllComments": "Show all comments", "viewReply": "view reply", "viewAllRepliesInitial": "view all {0} replies", "viewAllReplies": "view {0} replies", @@ -56,6 +57,7 @@ "newCount": "Ver {0} {1} más", "comment": "commentario", "comments": "commentarios", + "showAllComments": "Mostrar todos los comentarios", "error": { "emailNotVerified": "E-mail {0} no verificado.", "email": "No es un e-mail válido", diff --git a/client/coral-plugin-permalinks/PermalinkButton.js b/client/coral-plugin-permalinks/PermalinkButton.js index 4c6cbb571..464155b2e 100644 --- a/client/coral-plugin-permalinks/PermalinkButton.js +++ b/client/coral-plugin-permalinks/PermalinkButton.js @@ -23,6 +23,10 @@ class PermalinkButton extends React.Component { } toggle () { + + // I wish I could position this with a stylesheet, but top-level comments with + // nested replies throws everything off, as well as very long comments + this.popover.style.top = `${this.linkButton.offsetTop - 80}px`; this.setState({popoverOpen: !this.state.popoverOpen}); } @@ -48,11 +52,16 @@ class PermalinkButton extends React.Component { const {copySuccessful, copyFailure} = this.state; return (
- -
+
this.popover = ref} + className={`${name}-popover ${styles.container} ${this.state.popoverOpen ? 'active' : ''}`}>