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 (
-