import update from 'immutability-helper'; import { appendNewNodes } from 'coral-framework/utils'; function determineCommentDepth(comment) { let depth = 0; let cur = comment; while (cur.parent) { cur = cur.parent; depth++; } return depth; } function applyToCommentsOrigin(root, callback) { if (root.asset.comment) { let comment = root.asset.comment; for (let depth = 0; depth <= determineCommentDepth(comment); depth++) { let changes = { $apply: node => (node ? callback(node) : node) }; for (let i = 0; i < depth; i++) { changes = { parent: changes }; } comment = update(comment, changes); } return { ...root, asset: { ...root.asset, comment, }, }; } return update(root, { asset: { $apply: asset => callback(asset) }, }); } function findAndInsertComment(parent, comment) { const isAsset = parent.__typename === 'Asset'; const [connectionField, countField, action] = isAsset ? ['comments', 'totalCommentCount', '$unshift'] : ['replies', 'replyCount', '$push']; if ( (!comment.parent && isAsset) || // A top level comment in the asset. (comment.parent && parent.id === comment.parent.id) // A reply at the correct parent. ) { return update(parent, { [connectionField]: { nodes: { [action]: [comment] }, }, [countField]: { $apply: c => c + 1 }, }); } const connection = parent[connectionField]; if (!connection) { return parent; } return update(parent, { [connectionField]: { nodes: { $apply: nodes => nodes.map(node => findAndInsertComment(node, comment)), }, }, }); } export function insertCommentIntoEmbedQuery(root, comment) { return applyToCommentsOrigin(root, origin => findAndInsertComment(origin, comment) ); } function findAndRemoveComment(parent, id) { const [connectionField, countField] = parent.__typename === 'Asset' ? ['comments', 'totalCommentCount'] : ['replies', 'replyCount']; const connection = parent[connectionField]; if (!connection) { return parent; } let next = connection.nodes.filter(node => node.id !== id); if (next.length === connection.nodes.length) { next = next.map(node => findAndRemoveComment(node, id)); } let changes = { [connectionField]: { nodes: { $set: next }, }, }; if (parent[countField] && next.length !== connection.nodes.length) { changes[countField] = { $set: parent[countField] - 1 }; } return update(parent, changes); } export function removeCommentFromEmbedQuery(root, id) { return applyToCommentsOrigin(root, origin => findAndRemoveComment(origin, id) ); } export function getTopLevelParent(comment) { if (comment.parent) { return getTopLevelParent(comment.parent); } return comment; } export function findComment(nodes, callback) { for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (callback(node)) { return node; } if (node.replies) { const find = findComment(node.replies.nodes, callback); if (find) { return find; } } } return false; } export function findCommentWithId(nodes, id) { return findComment(nodes, node => node.id === id); } export function findCommentInEmbedQuery(root, callbackOrId) { return findCommentInAsset(root.asset, callbackOrId); } export function findCommentInAsset(asset, callbackOrId) { let callback = callbackOrId; if (typeof callbackOrId === 'string') { callback = node => node.id === callbackOrId; } if (asset.comment) { return findComment([reverseCommentParentTree(asset.comment)], callback); } if (!asset.comments) { return false; } return findComment(asset.comments.nodes, callback); } function findAndInsertFetchedComments(parent, comments, parent_id) { const isAsset = parent.__typename === 'Asset'; const connectionField = isAsset ? 'comments' : 'replies'; if ( (!parent_id && connectionField === 'comments') || parent.id === parent_id ) { return update(parent, { [connectionField]: { hasNextPage: { $set: comments.hasNextPage }, endCursor: { $set: comments.endCursor }, nodes: { $apply: nodes => appendNewNodes(nodes, comments.nodes) }, }, }); } const connection = parent[connectionField]; if (!connection) { return parent; } return update(parent, { [connectionField]: { nodes: { $apply: nodes => nodes.map(node => findAndInsertFetchedComments(node, comments, parent_id) ), }, }, }); } export function insertFetchedCommentsIntoEmbedQuery(root, comments, parent_id) { return applyToCommentsOrigin(root, origin => findAndInsertFetchedComments(origin, comments, parent_id) ); } function attachComment(topLevelComment, comment) { if (!topLevelComment.replies) { topLevelComment = update(topLevelComment, { replies: { $set: { nodes: [] } }, }); } if (topLevelComment.id === comment.parent.id) { return update(topLevelComment, { replies: { nodes: { $apply: nodes => appendNewNodes(nodes, [comment]), }, }, replyCount: { $set: count => count + 1, }, }); } if (!topLevelComment.replies.nodes.length) { return topLevelComment; } return update(topLevelComment, { replies: { nodes: { $apply: nodes => nodes.map(node => attachComment(node, comment)), }, }, }); } /** * attachCommentToParent recurses through the comment tree starting at `topLevelComment` * to find the ancestor of `comment` and attach it to the replies. */ export function attachCommentToParent(topLevelComment, comment) { let result = topLevelComment; if (comment.parent.parent) { result = attachCommentToParent(result, comment.parent); } return attachComment(result, comment); } /** * reverseCommentParentTree reverses a comment parent relationship tree * like `comment -> parent -> parent` into `parent -> parent -> comment -> replies`. */ export function reverseCommentParentTree(comment) { return comment.parent ? attachCommentToParent(getTopLevelParent(comment), comment) : comment; } /** * Nest a string in itself repeatly until `level` has been reached. * * Example: * nest(` * a * ...nest * b * `, 2) * * Output: * ` * a * a * b * b * ` */ export function nest(document, level) { let result = ''; for (let x = 0; x < level; x++) { if (x === 0) { result += document; continue; } result = result.replace('...nest', document); } return result.replace('...nest', ''); }