mirror of
https://github.com/wassname/talk.git
synced 2026-07-28 11:27:05 +08:00
Merge branch 'master' into fix-permalink-liveupdates
This commit is contained in:
+2
-1
@@ -6,7 +6,7 @@ node_modules
|
||||
scripts
|
||||
!scripts/generateIntrospectionResult.js
|
||||
|
||||
# documentation should not be visable in production.
|
||||
# documentation should not be visible in production.
|
||||
docs
|
||||
|
||||
# static assets are rebuild in the docker container.
|
||||
@@ -14,6 +14,7 @@ dist
|
||||
|
||||
# tests are not run in the docker container.
|
||||
test
|
||||
__tests__
|
||||
|
||||
# we won't use the .git folder in production.
|
||||
.git
|
||||
|
||||
+6
-1
@@ -1,6 +1,5 @@
|
||||
dist
|
||||
docs
|
||||
client/lib
|
||||
**/*.html
|
||||
plugins/*
|
||||
!plugins/talk-plugin-facebook-auth
|
||||
@@ -14,5 +13,11 @@ plugins/*
|
||||
!plugins/talk-plugin-comment-content
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-featured-comments
|
||||
!plugins/talk-plugin-sort-newest
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
!plugins/talk-plugin-sort-most-replied
|
||||
!plugins/talk-plugin-sort-most-liked
|
||||
!plugins/talk-plugin-sort-most-loved
|
||||
!plugins/talk-plugin-sort-most-respected
|
||||
|
||||
node_modules
|
||||
|
||||
+6
-1
@@ -14,7 +14,6 @@ client/coral-framework/graphql/introspection.json
|
||||
*.swp
|
||||
*.DS_STORE
|
||||
|
||||
test/e2e/reports
|
||||
coverage/
|
||||
|
||||
plugins.json
|
||||
@@ -30,5 +29,11 @@ plugins/*
|
||||
!plugins/talk-plugin-comment-content
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-featured-comments
|
||||
!plugins/talk-plugin-sort-newest
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
!plugins/talk-plugin-sort-most-replied
|
||||
!plugins/talk-plugin-sort-most-liked
|
||||
!plugins/talk-plugin-sort-most-loved
|
||||
!plugins/talk-plugin-sort-most-respected
|
||||
|
||||
**/node_modules/*
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
FROM node:8
|
||||
FROM node:8-alpine
|
||||
|
||||
# Create app directory
|
||||
RUN mkdir -p /usr/src/app
|
||||
@@ -12,6 +12,9 @@ EXPOSE 5000
|
||||
# Bundle app source
|
||||
COPY . /usr/src/app
|
||||
|
||||
# Ensure the runtime of the container is in production mode.
|
||||
ENV NODE_ENV production
|
||||
|
||||
# Install app dependencies and build static assets.
|
||||
RUN yarn global add node-gyp && \
|
||||
yarn install --frozen-lockfile && \
|
||||
@@ -19,7 +22,4 @@ RUN yarn global add node-gyp && \
|
||||
yarn build && \
|
||||
yarn cache clean
|
||||
|
||||
# Ensure the runtime of the container is in production mode.
|
||||
ENV NODE_ENV production
|
||||
|
||||
CMD ["yarn", "start"]
|
||||
|
||||
+2
-5
@@ -7,8 +7,5 @@ ONBUILD COPY . /usr/src/app
|
||||
# we need to have webpack available. We then build the new dependancies and
|
||||
# clear out the development dependancies again. After this we of course need to
|
||||
# clear out the yarn cache, this saves quite a lot of size.
|
||||
ONBUILD RUN NODE_ENV=development yarn install --frozen-lockfile && \
|
||||
NODE_ENV=production cli plugins reconcile && \
|
||||
NODE_ENV=production yarn build && \
|
||||
NODE_ENV=production yarn install --production --force && \
|
||||
yarn cache clean
|
||||
ONBUILD RUN cli plugins reconcile && \
|
||||
yarn build
|
||||
|
||||
@@ -16,6 +16,7 @@ program
|
||||
.command('token', 'work with the access tokens')
|
||||
.command('users', 'work with the application auth')
|
||||
.command('migration', 'provides utilities for migrating the database')
|
||||
.command('verify', 'provides utilities for performing data verification')
|
||||
.command(
|
||||
'plugins',
|
||||
'provides utilities for interacting with the plugin system'
|
||||
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const util = require('./util');
|
||||
const databaseVerifications = require('./verifications/database');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
async function database({fix = false, limit = Infinity, batch = 1000}) {
|
||||
try {
|
||||
for (const verification of databaseVerifications) {
|
||||
await verification({fix, limit, batch});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to process all the ${databaseVerifications.length} verifications`, err);
|
||||
util.shutdown(1);
|
||||
return;
|
||||
}
|
||||
|
||||
util.shutdown();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.command('db')
|
||||
.description('verifies the database integrity')
|
||||
.option('-f, --fix', 'fix the problems found with database inconsistencies')
|
||||
.option('-l, --limit [size]', 'limit the amount of documents to process in a single pass, this will ensure only a maximum number of batch operations are issued [default: inf]', parseInt)
|
||||
.option('-b, --batch [size]', 'batch size to process verifications and repairs of documents [default: 1000]', parseInt)
|
||||
.action(database);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp();
|
||||
util.shutdown();
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
const CommentModel = require('../../../models/comment');
|
||||
const ActionsService = require('../../../services/actions');
|
||||
const {arrayJoinBy, singleJoinBy} = require('../../../graph/loaders/util');
|
||||
const sc = require('snake-case');
|
||||
const debug = require('debug')('talk:cli:verify');
|
||||
|
||||
const getBatch = async (limit, offset) => CommentModel
|
||||
.find({})
|
||||
.select({'id': 1, 'action_counts': 1, 'reply_count': 1})
|
||||
.limit(limit)
|
||||
.skip(offset)
|
||||
.sort('created_at');
|
||||
|
||||
module.exports = async ({fix, limit, batch}) => {
|
||||
let operations = [];
|
||||
|
||||
// Count how many comments there are to process.
|
||||
const totalCount = await CommentModel.count();
|
||||
|
||||
let offset = 0;
|
||||
let comments = [];
|
||||
let commentIDs = [];
|
||||
|
||||
console.log(`Processing ${totalCount} comments in batches of ${limit}...`);
|
||||
|
||||
// Keep processing documents until there are is none left.
|
||||
while (offset < totalCount) {
|
||||
|
||||
// Get a batch of comments.
|
||||
comments = await getBatch(batch, offset);
|
||||
commentIDs = comments.map(({id}) => id);
|
||||
|
||||
// Get their reply counts.
|
||||
let allReplyCounts = await CommentModel
|
||||
.aggregate([
|
||||
{
|
||||
$match: {
|
||||
parent_id: {
|
||||
$in: commentIDs,
|
||||
},
|
||||
status: {
|
||||
$in: ['NONE', 'ACCEPTED']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$parent_id',
|
||||
count: {
|
||||
$sum: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
.then(singleJoinBy(commentIDs, '_id'))
|
||||
.then((results) => results.map((result) => result ? result.count : 0));
|
||||
|
||||
// Get their action summaries.
|
||||
let allActionSummaries = await ActionsService
|
||||
.getActionSummaries(commentIDs)
|
||||
.then(arrayJoinBy(commentIDs, 'item_id'));
|
||||
|
||||
// Loop over the comments, with their action summaries.
|
||||
for (let i = 0; i < comments.length; i++) {
|
||||
let comment = comments[i];
|
||||
let actionSummaries = allActionSummaries[i];
|
||||
let replyCount = allReplyCounts[i];
|
||||
|
||||
// And check to see if the action summaries we just computed match what is
|
||||
// currently set for the comments.
|
||||
let commentOperations = [];
|
||||
|
||||
// If the reply count needs to be updated, then update it!
|
||||
if (comment.reply_count !== replyCount) {
|
||||
commentOperations.push({
|
||||
reply_count: replyCount,
|
||||
});
|
||||
}
|
||||
|
||||
// First we process all the group id's.
|
||||
for (let actionSummary of actionSummaries) {
|
||||
if (actionSummary.group_id === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// And we generate the group id.
|
||||
const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase());
|
||||
const GROUP_ID = sc(actionSummary.group_id.toLowerCase());
|
||||
|
||||
if (GROUP_ID.length <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// And we add a new batch operation if the action summary is associated
|
||||
// with a group.
|
||||
const ACTION_COUNT_FIELD = `${ACTION_TYPE}_${GROUP_ID}`;
|
||||
|
||||
// Check that the action summaries match the cached counts.
|
||||
if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== actionSummary.count) {
|
||||
|
||||
// Batch updates for those changes.
|
||||
commentOperations.push({
|
||||
[`action_counts.${ACTION_COUNT_FIELD}`]: actionSummary.count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Group all the action summaries together from all the different group
|
||||
// ids.
|
||||
let groupedActionSummaries = actionSummaries.reduce((acc, actionSummary) => {
|
||||
const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase());
|
||||
|
||||
if (!(ACTION_TYPE in acc)) {
|
||||
acc[ACTION_TYPE] = 0;
|
||||
}
|
||||
|
||||
acc[ACTION_TYPE] += actionSummary.count;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
for (const ACTION_COUNT_FIELD of Object.keys(groupedActionSummaries)) {
|
||||
const count = groupedActionSummaries[ACTION_COUNT_FIELD];
|
||||
|
||||
// Check that the action summaries match the cached counts.
|
||||
if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== count) {
|
||||
|
||||
// Batch updates for those changes.
|
||||
commentOperations.push({
|
||||
[`action_counts.${ACTION_COUNT_FIELD}`]: count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If this comment has action summaries that should be updated, then
|
||||
// perform an update!
|
||||
if (commentOperations.length > 0) {
|
||||
operations.push({
|
||||
updateOne: {
|
||||
filter: {
|
||||
id: comment.id
|
||||
},
|
||||
update: {
|
||||
$set: Object.assign({}, ...commentOperations),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
debug(`Processed batch of ${comments.length} comments.`);
|
||||
|
||||
if (operations.length >= limit) {
|
||||
debug(`Queued operations are ${operations.length}, reached limit of ${limit}, not processing any more.`);
|
||||
|
||||
if (operations.length > limit) {
|
||||
debug(`${operations.length - limit} operations have been truncated to enforce the limit`);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
offset += batch;
|
||||
}
|
||||
|
||||
const OPERATIONS_LENGTH = operations.length;
|
||||
|
||||
if (limit < Infinity && offset + comments.length < totalCount) {
|
||||
console.log(`Processed ${offset + comments.length}/${totalCount} comments because we reached the update limit of ${limit}.`);
|
||||
} else {
|
||||
console.log(`Processed all ${totalCount} comments.`);
|
||||
}
|
||||
|
||||
console.log(`${OPERATIONS_LENGTH} documents need fixing.`);
|
||||
|
||||
// If fix was enabled, execute the batch writes.
|
||||
if (OPERATIONS_LENGTH > 0) {
|
||||
if (fix) {
|
||||
debug(`Fixing ${OPERATIONS_LENGTH} documents...`);
|
||||
|
||||
while (operations.length) {
|
||||
let batchOperations = operations.splice(0, batch);
|
||||
let result = await CommentModel.collection.bulkWrite(batchOperations);
|
||||
|
||||
debug(`Fixed batch of ${result.modifiedCount} documents.`);
|
||||
}
|
||||
|
||||
console.log(`Applied all ${OPERATIONS_LENGTH} fixes.`);
|
||||
} else {
|
||||
console.warn('Skipping fixing, --fix was not enabled, pass --fix to fix these errors');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
// This will import all the verifications that should be run by the:
|
||||
//
|
||||
// cli verify database
|
||||
//
|
||||
// command. They exist in the form:
|
||||
//
|
||||
// async ({fix = false, batch = 1000}) => {}
|
||||
//
|
||||
// where their options are derrived.
|
||||
module.exports = [
|
||||
require('./comments'),
|
||||
];
|
||||
@@ -40,8 +40,6 @@ test:
|
||||
override:
|
||||
# Run the tests using the junit reporter.
|
||||
- MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml MOCHA_REPORTER=mocha-junit-reporter yarn test
|
||||
# Run the e2e test suite.
|
||||
# - E2E_REPORT_PATH=$CIRCLE_TEST_REPORTS/e2e yarn e2e
|
||||
|
||||
deployment:
|
||||
release:
|
||||
|
||||
@@ -124,7 +124,7 @@ class UserDetailContainer extends React.Component {
|
||||
}
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Date, $author_id: ID!, $statuses: [COMMENT_STATUS!]) {
|
||||
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $author_id: ID!, $statuses: [COMMENT_STATUS!]) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, author_id: $author_id, statuses: $statuses}) {
|
||||
...CoralAdmin_Moderation_CommentConnection
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ const initialState = {
|
||||
storySearchVisible: false,
|
||||
storySearchString: '',
|
||||
shortcutsNoteVisible: 'show',
|
||||
sortOrder: 'REVERSE_CHRONOLOGICAL',
|
||||
sortOrder: 'DESC',
|
||||
};
|
||||
|
||||
export default function moderation (state = initialState, action) {
|
||||
|
||||
@@ -22,10 +22,10 @@ class DashboardContainer extends React.Component {
|
||||
|
||||
export const witDashboardQuery = withQuery(gql`
|
||||
query CoralAdmin_Dashboard($from: Date!, $to: Date!) {
|
||||
assetsByFlag: assetMetrics(from: $from, to: $to, sort: FLAG) {
|
||||
assetsByFlag: assetMetrics(from: $from, to: $to, sortBy: FLAG) {
|
||||
...CoralAdmin_Metrics
|
||||
}
|
||||
assetsByActivity: assetMetrics(from: $from, to: $to, sort: ACTIVITY) {
|
||||
assetsByActivity: assetMetrics(from: $from, to: $to, sortBy: ACTIVITY) {
|
||||
...CoralAdmin_Metrics
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,8 @@ const ModerationMenu = ({
|
||||
label="Sort"
|
||||
value={sort}
|
||||
onChange={(sort) => selectSort(sort)}>
|
||||
<Option value={'REVERSE_CHRONOLOGICAL'}>{t('modqueue.newest_first')}</Option>
|
||||
<Option value={'CHRONOLOGICAL'}>{t('modqueue.oldest_first')}</Option>
|
||||
<Option value={'DESC'}>{t('modqueue.newest_first')}</Option>
|
||||
<Option value={'ASC'}>{t('modqueue.oldest_first')}</Option>
|
||||
</SelectField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -59,7 +59,7 @@ class ModerationContainer extends Component {
|
||||
return handleCommentChange(
|
||||
root,
|
||||
comment,
|
||||
this.props.data.variables.sort,
|
||||
this.props.data.variables.sortOrder,
|
||||
() => notifyText && this.props.notify('info', notifyText),
|
||||
this.props.queueConfig,
|
||||
this.activeTab
|
||||
@@ -168,7 +168,7 @@ class ModerationContainer extends Component {
|
||||
const variables = {
|
||||
limit: 10,
|
||||
cursor: this.props.root[tab].endCursor,
|
||||
sort: this.props.data.variables.sort,
|
||||
sortOrder: this.props.data.variables.sortOrder,
|
||||
asset_id: this.props.data.variables.asset_id,
|
||||
statuses: this.props.queueConfig[tab].statuses,
|
||||
action_type: this.props.queueConfig[tab].action_type,
|
||||
@@ -288,8 +288,8 @@ const COMMENT_REJECTED_SUBSCRIPTION = gql`
|
||||
`;
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Date, $sort: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sort: $sort, action_type: $action_type}) {
|
||||
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $sortOrder: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type}) {
|
||||
nodes {
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
}
|
||||
@@ -314,14 +314,14 @@ const commentConnectionFragment = gql`
|
||||
`;
|
||||
|
||||
const withModQueueQuery = withQuery(({queueConfig}) => gql`
|
||||
query CoralAdmin_Moderation($asset_id: ID, $sort: SORT_ORDER, $allAssets: Boolean!) {
|
||||
query CoralAdmin_Moderation($asset_id: ID, $sortOrder: SORT_ORDER, $allAssets: Boolean!) {
|
||||
${Object.keys(queueConfig).map((queue) => `
|
||||
${queue}: comments(query: {
|
||||
${queueConfig[queue].statuses ? `statuses: [${queueConfig[queue].statuses.join(', ')}],` : ''}
|
||||
${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''}
|
||||
${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''}
|
||||
asset_id: $asset_id,
|
||||
sort: $sort
|
||||
sortOrder: $sortOrder
|
||||
}) {
|
||||
...CoralAdmin_Moderation_CommentConnection
|
||||
}
|
||||
@@ -354,7 +354,7 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
|
||||
return {
|
||||
variables: {
|
||||
asset_id: id,
|
||||
sort: props.moderation.sortOrder,
|
||||
sortOrder: props.moderation.sortOrder,
|
||||
allAssets: id === null
|
||||
}
|
||||
};
|
||||
|
||||
@@ -28,29 +28,29 @@ function removeCommentFromQueue(root, queue, id) {
|
||||
});
|
||||
}
|
||||
|
||||
function shouldCommentBeAdded(root, queue, comment, sort) {
|
||||
function shouldCommentBeAdded(root, queue, comment, sortOrder) {
|
||||
if (root[`${queue}Count`] < limit) {
|
||||
|
||||
// Adding all comments until first limit has reached.
|
||||
return true;
|
||||
}
|
||||
const cursor = new Date(root[queue].endCursor);
|
||||
return sort === 'CHRONOLOGICAL'
|
||||
return sortOrder === 'ASC'
|
||||
? new Date(comment.created_at) <= cursor
|
||||
: new Date(comment.created_at) >= cursor;
|
||||
}
|
||||
|
||||
function addCommentToQueue(root, queue, comment, sort) {
|
||||
function addCommentToQueue(root, queue, comment, sortOrder) {
|
||||
if (queueHasComment(root, queue, comment.id)) {
|
||||
return root;
|
||||
}
|
||||
|
||||
const sortAlgo = sort === 'CHRONOLOGICAL' ? ascending : descending;
|
||||
const sortAlgo = sortOrder === 'ASC' ? ascending : descending;
|
||||
const changes = {
|
||||
[`${queue}Count`]: {$set: root[`${queue}Count`] + 1},
|
||||
};
|
||||
|
||||
if (shouldCommentBeAdded(root, queue, comment, sort)) {
|
||||
if (shouldCommentBeAdded(root, queue, comment, sortOrder)) {
|
||||
const nodes = root[queue].nodes.concat(comment).sort(sortAlgo);
|
||||
changes[queue] = {
|
||||
nodes: {$set: nodes},
|
||||
@@ -90,13 +90,13 @@ function getCommentQueues(comment, queueConfig) {
|
||||
* Assimilate comment changes into current store.
|
||||
* @param {Object} root current state of the store
|
||||
* @param {Object} comment comment that was changed
|
||||
* @param {string} sort current sort order of the queues
|
||||
* @param {string} sortOrder current sort order of the queues
|
||||
* @param {string} notify callback to show notification
|
||||
* in the current active queue besides the 'all' queue.
|
||||
* @param {Object} queueConfig queue configuration
|
||||
* @return {Object} next state of the store
|
||||
*/
|
||||
export function handleCommentChange(root, comment, sort, notify, queueConfig, activeQueue) {
|
||||
export function handleCommentChange(root, comment, sortOrder, notify, queueConfig, activeQueue) {
|
||||
let next = root;
|
||||
|
||||
const nextQueues = getCommentQueues(comment, queueConfig);
|
||||
@@ -113,8 +113,8 @@ export function handleCommentChange(root, comment, sort, notify, queueConfig, ac
|
||||
Object.keys(queueConfig).forEach((queue) => {
|
||||
if (nextQueues.indexOf(queue) >= 0) {
|
||||
if (!queueHasComment(next, queue, comment.id)) {
|
||||
next = addCommentToQueue(next, queue, comment, sort);
|
||||
if (notify && activeQueue === queue && shouldCommentBeAdded(next, queue, comment, sort)) {
|
||||
next = addCommentToQueue(next, queue, comment, sortOrder);
|
||||
if (notify && activeQueue === queue && shouldCommentBeAdded(next, queue, comment, sortOrder)) {
|
||||
showNotificationOnce();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import queryString from 'query-string';
|
||||
|
||||
export const setActiveReplyBox = (id) => ({type: actions.SET_ACTIVE_REPLY_BOX, id});
|
||||
|
||||
export const setSort = ({sortOrder, sortBy}) => ({type: actions.SET_SORT, sortOrder, sortBy});
|
||||
|
||||
export const viewAllComments = () => (dispatch, _, {pym}) => {
|
||||
|
||||
const search = queryString.stringify({
|
||||
|
||||
@@ -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 {Button, Tab, TabCount, TabPane} from 'coral-ui';
|
||||
import {Spinner, Button, Tab, TabCount, TabPane} from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
|
||||
import {getTopLevelParent, attachCommentToParent} from '../graphql/utils';
|
||||
@@ -23,6 +23,8 @@ 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) {
|
||||
@@ -49,15 +51,14 @@ class Stream extends React.Component {
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
renderHighlightedComment() {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
activeReplyBox,
|
||||
setActiveReplyBox,
|
||||
appendItemArray,
|
||||
commentClassNames,
|
||||
root: {asset, asset: {comment, comments, totalCommentCount}},
|
||||
root: {asset, asset: {comment}},
|
||||
postComment,
|
||||
notify,
|
||||
editComment,
|
||||
@@ -65,23 +66,15 @@ class Stream extends React.Component {
|
||||
postDontAgree,
|
||||
deleteAction,
|
||||
showSignInDialog,
|
||||
updateItem,
|
||||
ignoreUser,
|
||||
activeStreamTab,
|
||||
setActiveStreamTab,
|
||||
loadNewReplies,
|
||||
loadMoreComments,
|
||||
viewAllComments,
|
||||
auth: {loggedIn, user},
|
||||
editName,
|
||||
auth: {user},
|
||||
emit,
|
||||
} = this.props;
|
||||
const {keepCommentBox} = this.state;
|
||||
const open = !asset.isClosed;
|
||||
|
||||
// even though the permalinked comment is the highlighted one, we're displaying its parent + replies
|
||||
let highlightedComment = comment && getTopLevelParent(comment);
|
||||
if (highlightedComment) {
|
||||
let topLevelComment = getTopLevelParent(comment);
|
||||
if (topLevelComment) {
|
||||
|
||||
// Inactive comments can be viewed by moderators and admins (e.g. using permalinks).
|
||||
const isInactive = !isCommentActive(comment.status);
|
||||
@@ -89,10 +82,152 @@ class Stream extends React.Component {
|
||||
|
||||
// the highlighted comment is not active and as such not in the replies, so we
|
||||
// attach it to the right parent.
|
||||
highlightedComment = attachCommentToParent(highlightedComment, comment);
|
||||
topLevelComment = attachCommentToParent(topLevelComment, comment);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('talk-stream-highlighted-container', styles.highlightedContainer)}>
|
||||
<Comment
|
||||
data={data}
|
||||
root={root}
|
||||
commentClassNames={commentClassNames}
|
||||
ignoreUser={ignoreUser}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
activeReplyBox={activeReplyBox}
|
||||
notify={notify}
|
||||
depth={0}
|
||||
disableReply={!open}
|
||||
postComment={postComment}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
highlighted={comment.id}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
loadMore={loadNewReplies}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
key={topLevelComment.id}
|
||||
commentIsIgnored={this.commentIsIgnored}
|
||||
comment={topLevelComment}
|
||||
charCountEnable={asset.settings.charCountEnable}
|
||||
maxCharCount={asset.settings.charCount}
|
||||
editComment={editComment}
|
||||
emit={emit}
|
||||
liveUpdates={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderTabPanel() {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
activeReplyBox,
|
||||
setActiveReplyBox,
|
||||
commentClassNames,
|
||||
root: {asset, asset: {comments, totalCommentCount}},
|
||||
postComment,
|
||||
notify,
|
||||
editComment,
|
||||
postFlag,
|
||||
postDontAgree,
|
||||
deleteAction,
|
||||
showSignInDialog,
|
||||
ignoreUser,
|
||||
activeStreamTab,
|
||||
setActiveStreamTab,
|
||||
loadNewReplies,
|
||||
loadMoreComments,
|
||||
auth: {user},
|
||||
emit,
|
||||
sortOrder,
|
||||
sortBy,
|
||||
} = this.props;
|
||||
|
||||
const slotProps = {data};
|
||||
const slotQueryData = {root, asset};
|
||||
|
||||
// `key` of `StreamTabPanel` depends on sorting so that we always reset
|
||||
// the state when changing sorting.
|
||||
return (
|
||||
<div className={cn('talk-stream-tab-container', styles.tabContainer)}>
|
||||
<div
|
||||
className={cn('talk-stream-filter-wrapper', styles.filterWrapper)}
|
||||
>
|
||||
<Slot
|
||||
fill="streamFilter"
|
||||
queryData={slotQueryData}
|
||||
{...slotProps}
|
||||
/>
|
||||
</div>
|
||||
<StreamTabPanel
|
||||
key={`${sortBy}_${sortOrder}`}
|
||||
activeTab={activeStreamTab}
|
||||
setActiveTab={setActiveStreamTab}
|
||||
fallbackTab={'all'}
|
||||
tabSlot={'streamTabs'}
|
||||
tabPaneSlot={'streamTabPanes'}
|
||||
slotProps={slotProps}
|
||||
queryData={slotQueryData}
|
||||
appendTabs={
|
||||
<Tab tabId={'all'} key='all'>
|
||||
All Comments <TabCount active={activeStreamTab === 'all'} sub>{totalCommentCount}</TabCount>
|
||||
</Tab>
|
||||
}
|
||||
appendTabPanes={
|
||||
<TabPane tabId={'all'} key='all'>
|
||||
<AllCommentsPane
|
||||
data={data}
|
||||
root={root}
|
||||
comments={comments}
|
||||
commentClassNames={commentClassNames}
|
||||
ignoreUser={ignoreUser}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
activeReplyBox={activeReplyBox}
|
||||
notify={notify}
|
||||
disableReply={!open}
|
||||
postComment={postComment}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
loadMore={loadMoreComments}
|
||||
loadNewReplies={loadNewReplies}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
commentIsIgnored={this.commentIsIgnored}
|
||||
charCountEnable={asset.settings.charCountEnable}
|
||||
maxCharCount={asset.settings.charCount}
|
||||
editComment={editComment}
|
||||
emit={emit}
|
||||
/>
|
||||
</TabPane>
|
||||
}
|
||||
sub
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
appendItemArray,
|
||||
root: {asset, asset: {comment: highlightedComment, comments}},
|
||||
postComment,
|
||||
notify,
|
||||
updateItem,
|
||||
viewAllComments,
|
||||
auth: {loggedIn, user},
|
||||
editName,
|
||||
loading,
|
||||
} = this.props;
|
||||
const {keepCommentBox} = this.state;
|
||||
const open = !asset.isClosed;
|
||||
|
||||
const banned = user && user.status === 'BANNED';
|
||||
const temporarilySuspended =
|
||||
user &&
|
||||
@@ -103,7 +238,7 @@ class Stream extends React.Component {
|
||||
const slotProps = {data};
|
||||
const slotQueryData = {root, asset};
|
||||
|
||||
if (!comment && !comments) {
|
||||
if (!highlightedComment && !comments) {
|
||||
console.error('Talk: No comments came back from the graph given that query. Please, check the query params.');
|
||||
return <StreamError />;
|
||||
}
|
||||
@@ -111,7 +246,7 @@ class Stream extends React.Component {
|
||||
return (
|
||||
<div id="stream" className={styles.root}>
|
||||
<AutomaticAssetClosure assetId={asset.id} closedAt={asset.closedAt}/>
|
||||
{comment &&
|
||||
{highlightedComment &&
|
||||
<Button
|
||||
cStyle="darkGrey"
|
||||
className={cn('talk-stream-show-all-comments-button', styles.viewAllButton)}
|
||||
@@ -175,96 +310,12 @@ class Stream extends React.Component {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* the highlightedComment is isolated after the user followed a permalink */}
|
||||
{highlightedComment
|
||||
? (
|
||||
<div className={cn('talk-stream-highlighted-container', styles.highlightedContainer)}>
|
||||
<Comment
|
||||
data={data}
|
||||
root={root}
|
||||
commentClassNames={commentClassNames}
|
||||
ignoreUser={ignoreUser}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
activeReplyBox={activeReplyBox}
|
||||
notify={notify}
|
||||
depth={0}
|
||||
disableReply={!open}
|
||||
postComment={postComment}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
highlighted={comment.id}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
loadMore={loadNewReplies}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
key={highlightedComment.id}
|
||||
commentIsIgnored={this.commentIsIgnored}
|
||||
comment={highlightedComment}
|
||||
charCountEnable={asset.settings.charCountEnable}
|
||||
maxCharCount={asset.settings.charCount}
|
||||
editComment={editComment}
|
||||
emit={emit}
|
||||
liveUpdates={true}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: <div className={cn('talk-stream-tab-container', styles.tabContainer)}>
|
||||
<div
|
||||
className={cn('talk-stream-filter-wrapper', styles.filterWrapper)}
|
||||
>
|
||||
<Slot
|
||||
fill="streamFilter"
|
||||
queryData={slotQueryData}
|
||||
{...slotProps}
|
||||
/>
|
||||
</div>
|
||||
<StreamTabPanel
|
||||
activeTab={activeStreamTab}
|
||||
setActiveTab={setActiveStreamTab}
|
||||
fallbackTab={'all'}
|
||||
tabSlot={'streamTabs'}
|
||||
tabPaneSlot={'streamTabPanes'}
|
||||
slotProps={slotProps}
|
||||
queryData={slotQueryData}
|
||||
appendTabs={
|
||||
<Tab tabId={'all'} key='all'>
|
||||
All Comments <TabCount active={activeStreamTab === 'all'} sub>{totalCommentCount}</TabCount>
|
||||
</Tab>
|
||||
}
|
||||
appendTabPanes={
|
||||
<TabPane tabId={'all'} key='all'>
|
||||
<AllCommentsPane
|
||||
data={data}
|
||||
root={root}
|
||||
comments={comments}
|
||||
commentClassNames={commentClassNames}
|
||||
ignoreUser={ignoreUser}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
activeReplyBox={activeReplyBox}
|
||||
notify={notify}
|
||||
disableReply={!open}
|
||||
postComment={postComment}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
loadMore={loadMoreComments}
|
||||
loadNewReplies={loadNewReplies}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
commentIsIgnored={this.commentIsIgnored}
|
||||
charCountEnable={asset.settings.charCountEnable}
|
||||
maxCharCount={asset.settings.charCount}
|
||||
editComment={editComment}
|
||||
emit={emit}
|
||||
/>
|
||||
</TabPane>
|
||||
}
|
||||
sub
|
||||
/>
|
||||
</div>
|
||||
<SpinnerWhenLoading loading={loading}>
|
||||
{highlightedComment
|
||||
? this.renderHighlightedComment()
|
||||
: this.renderTabPanel()
|
||||
}
|
||||
</SpinnerWhenLoading>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,3 +6,4 @@ export const ADD_COMMENT_CLASSNAME = 'ADD_COMMENT_CLASSNAME';
|
||||
export const REMOVE_COMMENT_CLASSNAME = 'REMOVE_COMMENT_CLASSNAME';
|
||||
export const THREADING_LEVEL = process.env.TALK_THREADING_LEVEL;
|
||||
export const SET_ACTIVE_TAB = 'CORAL_STREAM_SET_ACTIVE_TAB';
|
||||
export const SET_SORT = 'CORAL_STREAM_SET_SORT';
|
||||
|
||||
@@ -105,7 +105,7 @@ const withCommentFragments = withFragments({
|
||||
fragment CoralEmbedStream_Comment_comment on Comment {
|
||||
...CoralEmbedStream_Comment_SingleComment
|
||||
${nest(`
|
||||
replies(limit: 3, excludeIgnored: $excludeIgnored) {
|
||||
replies(query: {limit: 3, excludeIgnored: $excludeIgnored}) {
|
||||
nodes {
|
||||
...CoralEmbedStream_Comment_SingleComment
|
||||
...nest
|
||||
|
||||
@@ -154,7 +154,15 @@ const slots = [
|
||||
];
|
||||
|
||||
const EMBED_QUERY = gql`
|
||||
query CoralEmbedStream_Embed($assetId: ID, $assetUrl: String, $commentId: ID!, $hasComment: Boolean!, $excludeIgnored: Boolean) {
|
||||
query CoralEmbedStream_Embed(
|
||||
$assetId: ID,
|
||||
$assetUrl: String,
|
||||
$commentId: ID!,
|
||||
$hasComment: Boolean!,
|
||||
$excludeIgnored: Boolean,
|
||||
$sortBy: SORT_COMMENTS_BY!,
|
||||
$sortOrder: SORT_ORDER!,
|
||||
) {
|
||||
me {
|
||||
id
|
||||
status
|
||||
@@ -166,13 +174,15 @@ const EMBED_QUERY = gql`
|
||||
`;
|
||||
|
||||
export const withEmbedQuery = withQuery(EMBED_QUERY, {
|
||||
options: ({auth, commentId, assetId, assetUrl}) => ({
|
||||
options: ({auth, commentId, assetId, assetUrl, sortBy, sortOrder}) => ({
|
||||
variables: {
|
||||
assetId,
|
||||
assetUrl,
|
||||
commentId,
|
||||
hasComment: commentId !== '',
|
||||
excludeIgnored: Boolean(auth && auth.user && auth.user.id),
|
||||
sortBy,
|
||||
sortOrder,
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -183,7 +193,9 @@ const mapStateToProps = (state) => ({
|
||||
assetId: state.stream.assetId,
|
||||
assetUrl: state.stream.assetUrl,
|
||||
activeTab: state.embed.activeTab,
|
||||
config: state.config
|
||||
config: state.config,
|
||||
sortOrder: state.stream.sortOrder,
|
||||
sortBy: state.stream.sortBy,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
|
||||
@@ -29,11 +29,15 @@ const {showSignInDialog, editName} = authActions;
|
||||
const {notify} = notificationActions;
|
||||
|
||||
class StreamContainer extends React.Component {
|
||||
subscriptions = [];
|
||||
commentsAddedSubscription = null;
|
||||
commentsEditedSubscription = null;
|
||||
|
||||
subscribeToUpdates() {
|
||||
const newSubscriptions = [{
|
||||
subscribeToCommentsEdited() {
|
||||
this.commentsEditedSubscription = this.props.data.subscribeToMore({
|
||||
document: COMMENTS_EDITED_SUBSCRIPTION,
|
||||
variables: {
|
||||
assetId: this.props.root.asset.id,
|
||||
},
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentEdited}}}) => {
|
||||
|
||||
// Ignore mutations from me.
|
||||
@@ -51,9 +55,15 @@ class StreamContainer extends React.Component {
|
||||
return removeCommentFromEmbedQuery(prev, commentEdited.id);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
});
|
||||
}
|
||||
|
||||
subscribeToCommentsAdded() {
|
||||
this.commentsAddedSubscription = this.props.data.subscribeToMore({
|
||||
document: COMMENTS_ADDED_SUBSCRIPTION,
|
||||
variables: {
|
||||
assetId: this.props.root.asset.id,
|
||||
},
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentAdded}}}) => {
|
||||
|
||||
// Ignore mutations from me.
|
||||
@@ -75,21 +85,22 @@ class StreamContainer extends React.Component {
|
||||
}
|
||||
|
||||
return insertCommentIntoEmbedQuery(prev, commentAdded);
|
||||
}
|
||||
}];
|
||||
|
||||
this.subscriptions = newSubscriptions.map((s) => this.props.data.subscribeToMore({
|
||||
document: s.document,
|
||||
variables: {
|
||||
assetId: this.props.root.asset.id,
|
||||
},
|
||||
updateQuery: s.updateQuery,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
unsubscribe() {
|
||||
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
||||
this.subscriptions = [];
|
||||
unsubscribeCommentsAdded() {
|
||||
if (this.commentsAddedSubscription) {
|
||||
this.commentsAddedSubscription();
|
||||
this.commentsAddedSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribeCommentsEdited() {
|
||||
if (this.commentsEditedSubscription) {
|
||||
this.commentsEditedSubscription();
|
||||
this.commentsEditedSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
loadNewReplies = (parent_id) => {
|
||||
@@ -102,7 +113,7 @@ class StreamContainer extends React.Component {
|
||||
cursor: comment.replies.endCursor,
|
||||
parent_id,
|
||||
asset_id: this.props.root.asset.id,
|
||||
sort: 'CHRONOLOGICAL',
|
||||
sortOrder: 'ASC',
|
||||
excludeIgnored: this.props.data.variables.excludeIgnored,
|
||||
},
|
||||
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
|
||||
@@ -119,7 +130,8 @@ class StreamContainer extends React.Component {
|
||||
cursor: this.props.root.asset.comments.endCursor,
|
||||
parent_id: null,
|
||||
asset_id: this.props.root.asset.id,
|
||||
sort: 'REVERSE_CHRONOLOGICAL',
|
||||
sortOrder: this.props.data.variables.sortOrder,
|
||||
sortBy: this.props.data.variables.sortBy,
|
||||
excludeIgnored: this.props.data.variables.excludeIgnored,
|
||||
},
|
||||
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
|
||||
@@ -128,36 +140,78 @@ class StreamContainer extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
isSortedByNewestFirst({sortBy, sortOrder} = this.props) {
|
||||
return sortBy === 'CREATED_AT' && sortOrder === 'DESC';
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.previousTab) {
|
||||
this.props.data.refetch();
|
||||
}
|
||||
this.subscribeToUpdates();
|
||||
|
||||
if (this.isSortedByNewestFirst()) {
|
||||
this.subscribeToCommentsAdded();
|
||||
}
|
||||
|
||||
this.subscribeToCommentsEdited();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubscribe();
|
||||
this.unsubscribeCommentsAdded();
|
||||
this.unsubscribeCommentsEdited();
|
||||
clearInterval(this.countPoll);
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
return !can(user, 'INTERACT_WITH_COMMUNITY');
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.refetching
|
||||
|| !this.props.root.asset
|
||||
if (!this.props.root.asset
|
||||
|| !this.props.root.asset.comment
|
||||
&& !this.props.root.asset.comments
|
||||
) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const streamLoading = this.props.refetching || this.props.data.loading;
|
||||
|
||||
return <Stream
|
||||
{...this.props}
|
||||
loadMore={this.loadMore}
|
||||
loadMoreComments={this.loadMoreComments}
|
||||
loadNewReplies={this.loadNewReplies}
|
||||
userIsDegraged={this.userIsDegraged()}
|
||||
loading={streamLoading}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
@@ -203,8 +257,26 @@ const COMMENTS_EDITED_SUBSCRIPTION = gql`
|
||||
`;
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query CoralEmbedStream_LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) {
|
||||
query CoralEmbedStream_LoadMoreComments(
|
||||
$limit: Int = 5
|
||||
$cursor: Cursor
|
||||
$parent_id: ID
|
||||
$asset_id: ID
|
||||
$sortOrder: SORT_ORDER
|
||||
$sortBy: SORT_COMMENTS_BY = CREATED_AT
|
||||
$excludeIgnored: Boolean
|
||||
) {
|
||||
comments(
|
||||
query: {
|
||||
limit: $limit
|
||||
cursor: $cursor
|
||||
parent_id: $parent_id
|
||||
asset_id: $asset_id
|
||||
sortOrder: $sortOrder
|
||||
sortBy: $sortBy
|
||||
excludeIgnored: $excludeIgnored
|
||||
}
|
||||
) {
|
||||
nodes {
|
||||
...CoralEmbedStream_Stream_comment
|
||||
}
|
||||
@@ -219,6 +291,7 @@ const LOAD_MORE_QUERY = gql`
|
||||
const slots = [
|
||||
'streamTabs',
|
||||
'streamTabPanes',
|
||||
'streamFilter',
|
||||
];
|
||||
|
||||
const fragments = {
|
||||
@@ -254,9 +327,9 @@ const fragments = {
|
||||
charCount
|
||||
requireEmailConfirmation
|
||||
}
|
||||
commentCount(excludeIgnored: $excludeIgnored) @skip(if: $hasComment)
|
||||
totalCommentCount(excludeIgnored: $excludeIgnored) @skip(if: $hasComment)
|
||||
comments(limit: 10, excludeIgnored: $excludeIgnored) @skip(if: $hasComment) {
|
||||
commentCount @skip(if: $hasComment)
|
||||
totalCommentCount @skip(if: $hasComment)
|
||||
comments(query: {limit: 10, excludeIgnored: $excludeIgnored, sortOrder: $sortOrder, sortBy: $sortBy}) @skip(if: $hasComment) {
|
||||
nodes {
|
||||
...CoralEmbedStream_Stream_comment
|
||||
}
|
||||
@@ -299,6 +372,8 @@ const mapStateToProps = (state) => ({
|
||||
previousStreamTab: state.stream.previousTab,
|
||||
commentClassNames: state.stream.commentClassNames,
|
||||
pluginConfig: state.config.plugin_config,
|
||||
sortOrder: state.stream.sortOrder,
|
||||
sortBy: state.stream.sortBy,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import update from 'immutability-helper';
|
||||
import {insertCommentsSorted} from 'coral-framework/utils';
|
||||
import {appendNewNodes} from 'coral-framework/utils';
|
||||
|
||||
function determineCommentDepth(comment) {
|
||||
let depth = 0;
|
||||
@@ -163,14 +163,7 @@ function findAndInsertFetchedComments(parent, comments, parent_id) {
|
||||
[connectionField]: {
|
||||
hasNextPage: {$set: comments.hasNextPage},
|
||||
endCursor: {$set: comments.endCursor},
|
||||
nodes: {$apply: (nodes) => {
|
||||
if (isAsset) {
|
||||
return nodes.concat(comments.nodes);
|
||||
}
|
||||
return insertCommentsSorted(nodes, comments.nodes.filter(
|
||||
(comment) => !nodes.some((node) => node.id === comment.id)
|
||||
));
|
||||
}},
|
||||
nodes: {$apply: (nodes) => appendNewNodes(nodes, comments.nodes)},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -202,7 +195,7 @@ export function attachCommentToParent(topLevelComment, comment) {
|
||||
return update(topLevelComment, {
|
||||
replies: {
|
||||
nodes: {
|
||||
$apply: (nodes) => insertCommentsSorted(nodes, comment),
|
||||
$apply: (nodes) => appendNewNodes(nodes, [comment]),
|
||||
},
|
||||
},
|
||||
replyCount: {
|
||||
|
||||
@@ -23,6 +23,8 @@ const initialState = {
|
||||
commentClassNames: [],
|
||||
activeTab: process.env.TALK_DEFAULT_STREAM_TAB,
|
||||
previousTab: '',
|
||||
sortBy: 'CREATED_AT',
|
||||
sortOrder: 'DESC',
|
||||
};
|
||||
|
||||
export default function stream(state = initialState, action) {
|
||||
@@ -66,6 +68,12 @@ export default function stream(state = initialState, action) {
|
||||
...state.commentClassNames.slice(action.idx + 1)
|
||||
]
|
||||
};
|
||||
case actions.SET_SORT :
|
||||
return {
|
||||
...state,
|
||||
sortOrder: action.sortOrder ? action.sortOrder : state.sortOrder,
|
||||
sortBy: action.sortBy ? action.sortBy : state.sortBy,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,18 @@ class Slot extends React.Component {
|
||||
return changes.length !== 0;
|
||||
}
|
||||
|
||||
getSlotProps({fill: _a, inline: _b, className: _c, reduxState: _d, defaultComponent_: _e, queryData: _f, ...rest} = this.props) {
|
||||
getSlotProps(props = this.props) {
|
||||
const {
|
||||
fill: _a,
|
||||
inline: _b,
|
||||
className: _c,
|
||||
reduxState: _d,
|
||||
defaultComponent_: _e,
|
||||
queryData: _f,
|
||||
childFactory: _g,
|
||||
component: _h,
|
||||
...rest
|
||||
} = props;
|
||||
return rest;
|
||||
}
|
||||
|
||||
@@ -39,8 +50,16 @@ class Slot extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
inline = false,
|
||||
className,
|
||||
reduxState,
|
||||
component: Component,
|
||||
childFactory,
|
||||
defaultComponent: DefaultComponent,
|
||||
queryData,
|
||||
} = this.props;
|
||||
const {plugins} = this.context;
|
||||
const {inline = false, className, reduxState, defaultComponent: DefaultComponent, queryData} = this.props;
|
||||
let children = this.getChildren();
|
||||
const pluginConfig = reduxState.config.pluginConfig || emptyConfig;
|
||||
if (children.length === 0 && DefaultComponent) {
|
||||
@@ -48,19 +67,45 @@ class Slot extends React.Component {
|
||||
children = <DefaultComponent {...props} />;
|
||||
}
|
||||
|
||||
if (childFactory) {
|
||||
children = children.map(childFactory);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn({[styles.inline]: inline, [styles.debug]: pluginConfig.debug}, className)}>
|
||||
<Component className={cn({[styles.inline]: inline, [styles.debug]: pluginConfig.debug}, className)}>
|
||||
{children}
|
||||
</div>
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Slot.defaultProps = {
|
||||
component: 'div',
|
||||
};
|
||||
|
||||
Slot.propTypes = {
|
||||
fill: React.PropTypes.string.isRequired,
|
||||
fill: PropTypes.string.isRequired,
|
||||
|
||||
/**
|
||||
* You may specify the component to use as the root wrapper.
|
||||
* Defaults to 'div'.
|
||||
*/
|
||||
component: PropTypes.any,
|
||||
|
||||
// props coming from graphql must be passed through this property.
|
||||
queryData: React.PropTypes.object,
|
||||
queryData: PropTypes.object,
|
||||
|
||||
/**
|
||||
* You may need to apply reactive updates to a child as it is exiting.
|
||||
* This is generally done by using `cloneElement` however in the case of an exiting
|
||||
* child the element has already been removed and not accessible to the consumer.
|
||||
*
|
||||
* If you do need to update a child as it leaves you can provide a `childFactory`
|
||||
* to wrap every child, even the ones that are leaving.
|
||||
*
|
||||
* @type Function(child: ReactElement) -> ReactElement
|
||||
*/
|
||||
childFactory: PropTypes.func,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
|
||||
@@ -2,6 +2,7 @@ import {gql} from 'react-apollo';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import union from 'lodash/union';
|
||||
import {capitalize} from 'coral-framework/helpers/strings';
|
||||
export * from 'coral-framework/helpers/strings';
|
||||
|
||||
export const getTotalActionCount = (type, comment) => {
|
||||
return comment.action_summaries
|
||||
@@ -153,25 +154,12 @@ export function getErrorMessages(error) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const ascending = (a, b) => {
|
||||
const dateA = new Date(a.created_at);
|
||||
const dateB = new Date(b.created_at);
|
||||
if (dateA < dateB) { return -1; }
|
||||
if (dateA > dateB) { return 1; }
|
||||
return 0;
|
||||
};
|
||||
export function appendNewNodes(nodesA, nodesB) {
|
||||
return nodesA.concat(nodesB.filter((nodeB) => !nodesA.some((nodeA) => nodeA.id === nodeB.id)));
|
||||
}
|
||||
|
||||
const descending = (a, b) => ascending(a, b) * -1;
|
||||
|
||||
export function insertCommentsSorted(nodes, comments, sortOrder = 'CHRONOLOGICAL') {
|
||||
const added = nodes.concat(comments);
|
||||
if (sortOrder === 'CHRONOLOGICAL') {
|
||||
return added.sort(ascending);
|
||||
}
|
||||
if (sortOrder === 'REVERSE_CHRONOLOGICAL') {
|
||||
return added.sort(descending);
|
||||
}
|
||||
throw new Error(`Unknown sort order ${sortOrder}`);
|
||||
export function prependNewNodes(nodesA, nodesB) {
|
||||
return nodesB.filter((nodeB) => !nodesA.some((nodeA) => nodeA.id === nodeB.id)).concat(nodesA);
|
||||
}
|
||||
|
||||
export const isTagged = (tags, which) => tags.some((t) => t.tag.name === which);
|
||||
|
||||
@@ -15,7 +15,7 @@ import CommentHistory from 'talk-plugin-history/CommentHistory';
|
||||
// TODO: Auth logic needs refactoring.
|
||||
import {showSignInDialog, checkLogin} from 'coral-embed-stream/src/actions/auth';
|
||||
|
||||
import {insertCommentsSorted} from 'plugin-api/beta/client/utils';
|
||||
import {appendNewNodes} from 'plugin-api/beta/client/utils';
|
||||
import update from 'immutability-helper';
|
||||
import {getSlotFragmentSpreads} from 'coral-framework/utils';
|
||||
|
||||
@@ -42,7 +42,7 @@ class ProfileContainer extends Component {
|
||||
me: {
|
||||
comments: {
|
||||
nodes: {
|
||||
$apply: (nodes) => insertCommentsSorted(nodes, comments.nodes, 'REVERSE_CHRONOLOGICAL'),
|
||||
$apply: (nodes) => appendNewNodes(nodes, comments.nodes),
|
||||
},
|
||||
hasNextPage: {$set: comments.hasNextPage},
|
||||
endCursor: {$set: comments.endCursor},
|
||||
@@ -123,7 +123,7 @@ const CommentFragment = gql`
|
||||
`;
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query TalkSettings_LoadMoreComments($limit: Int, $cursor: Date) {
|
||||
query TalkSettings_LoadMoreComments($limit: Int, $cursor: Cursor) {
|
||||
comments(query: {limit: $limit, cursor: $cursor}) {
|
||||
...TalkSettings_CommentConnectionFragment
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ require('env-rewrite').rewrite();
|
||||
|
||||
const uniq = require('lodash/uniq');
|
||||
const ms = require('ms');
|
||||
const debug = require('debug')('talk:config');
|
||||
|
||||
//==============================================================================
|
||||
// CONFIG INITIALIZATION
|
||||
@@ -240,33 +241,23 @@ CONFIG.RECAPTCHA_ENABLED =
|
||||
CONFIG.RECAPTCHA_SECRET.length > 0 &&
|
||||
CONFIG.RECAPTCHA_PUBLIC &&
|
||||
CONFIG.RECAPTCHA_PUBLIC.length > 0;
|
||||
if (!CONFIG.RECAPTCHA_ENABLED) {
|
||||
console.warn(
|
||||
'Recaptcha is not enabled for login/signup abuse prevention, set TALK_RECAPTCHA_SECRET and TALK_RECAPTCHA_PUBLIC to enable Recaptcha.'
|
||||
);
|
||||
}
|
||||
|
||||
debug(`reCAPTCHA is ${CONFIG.RECAPTCHA_ENABLED ? 'enabled' : 'disabled, required config is not present'}`);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// SMTP Server configuration
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
{
|
||||
const requiredProps = [
|
||||
'SMTP_FROM_ADDRESS',
|
||||
'SMTP_USERNAME',
|
||||
'SMTP_PASSWORD',
|
||||
'SMTP_HOST'
|
||||
];
|
||||
CONFIG.EMAIL_ENABLED =
|
||||
CONFIG.SMTP_FROM_ADDRESS &&
|
||||
CONFIG.SMTP_FROM_ADDRESS.length > 0 &&
|
||||
CONFIG.SMTP_USERNAME &&
|
||||
CONFIG.SMTP_USERNAME.length > 0 &&
|
||||
CONFIG.SMTP_PASSWORD &&
|
||||
CONFIG.SMTP_PASSWORD.length > 0 &&
|
||||
CONFIG.SMTP_HOST &&
|
||||
CONFIG.SMTP_HOST.length > 0;
|
||||
|
||||
if (requiredProps.some((prop) => !CONFIG[prop])) {
|
||||
console.warn(
|
||||
`${requiredProps
|
||||
.map((v) => `TALK_${v}`)
|
||||
.join(
|
||||
', '
|
||||
)} should be defined in the environment if you would like to send password reset emails from Talk`
|
||||
);
|
||||
}
|
||||
}
|
||||
debug(`Email is ${CONFIG.EMAIL_ENABLED ? 'enabled' : 'disabled, required config is not present'}`);
|
||||
|
||||
module.exports = CONFIG;
|
||||
|
||||
@@ -72,7 +72,6 @@ You can see other scripts we've made available by consulting the `package.json`
|
||||
file under the `scripts` key including:
|
||||
|
||||
- `yarn test` run unit tests
|
||||
- `yarn e2e` run end to end tests
|
||||
- `yarn build-watch` watch for changes to client files and build static assets
|
||||
- `yarn dev-start` watch for changes to server files and reload the server while
|
||||
also sourcing a `.env` file in your local directory for configuration
|
||||
|
||||
@@ -85,6 +85,52 @@ configure the context plugin before it would be mounted at `context.plugins`.
|
||||
This plugin above would mount at: `context.plugins.Slack`, or, if you're using
|
||||
[object destructuring](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), `{plugins: {Slack}}`.
|
||||
|
||||
##### `Sort`
|
||||
|
||||
A special context hook, `Sort` will allow plugin authors to provide new
|
||||
methods to sort data. An example is as follows:
|
||||
|
||||
```js
|
||||
{
|
||||
Sort: () => ({
|
||||
Comments: { // <-- (1)
|
||||
likes: { // <-- (2)
|
||||
startCursor(ctx, nodes, {cursor}) { // <-- (3)
|
||||
return cursor != null ? cursor : 0;
|
||||
},
|
||||
endCursor(ctx, nodes, {cursor}) { // <-- (4)
|
||||
return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null;
|
||||
},
|
||||
sort(ctx, query, {cursor, sort}) { // <-- (5)
|
||||
if (cursor) {
|
||||
query = query.skip(cursor);
|
||||
}
|
||||
|
||||
return query.sort({
|
||||
'action_counts.like': sort === 'DESC' ? -1 : 1,
|
||||
created_at: sort === 'DESC' ? -1 : 1,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
This has a bunch of special features:
|
||||
|
||||
1. `Comments` is the name of the type being sorted, this is pluralized and
|
||||
capitalized.
|
||||
2. `likes` is the `sortBy` field in lowercase.
|
||||
3. `startCursor` will retrieve the start cursor based on the current set of
|
||||
nodes and the current cursor.
|
||||
4. `endCursor` will retrieve the end cursor based on the current set of nodes
|
||||
and the current cursor.
|
||||
5. `sort` will mutate the `query` to apply the sort operations.
|
||||
|
||||
All the `startCursor`, `endCursor`, and `sort` functions must be provided in
|
||||
order for the sorting to apply properly.
|
||||
|
||||
#### Field: `loaders`
|
||||
|
||||
```js
|
||||
|
||||
+15
-7
@@ -1,6 +1,7 @@
|
||||
const loaders = require('./loaders');
|
||||
const mutators = require('./mutators');
|
||||
const uuid = require('uuid');
|
||||
const merge = require('lodash/merge');
|
||||
|
||||
const plugins = require('../services/plugins');
|
||||
const pubsub = require('../services/pubsub');
|
||||
@@ -17,17 +18,24 @@ const contextPlugins = plugins.get('server', 'context').map(({plugin, context})
|
||||
});
|
||||
|
||||
/**
|
||||
* This should itterate over the passed in plugins and load them all with the
|
||||
* This should iterate over the passed in plugins and load them all with the
|
||||
* current graph context.
|
||||
* @return {Object} the saturated plugins object
|
||||
*/
|
||||
const decorateContextPlugins = (context, contextPlugins) => contextPlugins.reduce((acc, plugin) => {
|
||||
Object.keys(plugin.context).forEach((service) => {
|
||||
acc[service] = plugin.context[service](context);
|
||||
});
|
||||
const decorateContextPlugins = (context, contextPlugins) => {
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
// For each of the plugins, we execute with the context to get the context
|
||||
// based plugin. We then merge that into an object for the plugin. Once the
|
||||
// plugin is assembled, we merge that object with all the other objects
|
||||
// provided from the other plugins.
|
||||
return merge(...contextPlugins.map((plugin) => {
|
||||
return Object.keys(plugin.context).reduce((services, serviceName) => {
|
||||
services[serviceName] = plugin.context[serviceName](context);
|
||||
|
||||
return services;
|
||||
}, {});
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Stores the request context.
|
||||
|
||||
@@ -54,12 +54,8 @@ const findOrCreateAssetByURL = async (context, asset_url) => {
|
||||
return asset;
|
||||
};
|
||||
|
||||
const getAssetsForMetrics = async ({loaders: {Actions, Comments}}) => {
|
||||
let actions = await Actions.getByTypes({action_type: 'FLAG', item_type: 'COMMENT'});
|
||||
|
||||
const ids = actions.map(({item_id}) => item_id);
|
||||
|
||||
return Comments.getByQuery({ids})
|
||||
const getAssetsForMetrics = async ({loaders: {Comments}}) => {
|
||||
return Comments.getByQuery({action_type: 'FLAG'})
|
||||
.then((connection) => connection.nodes);
|
||||
};
|
||||
|
||||
@@ -86,7 +82,7 @@ module.exports = (context) => ({
|
||||
// TODO: decide whether we want to move these to mutators or not, as in fact
|
||||
// this operation create a new asset if one isn't found.
|
||||
getByURL: (url) => findOrCreateAssetByURL(context, url),
|
||||
|
||||
|
||||
findByUrl: (url) => findByUrl(context, url),
|
||||
search: (query) => getAssetsByQuery(context, query),
|
||||
getByID: new DataLoader((ids) => genAssetsByID(context, ids)),
|
||||
|
||||
+170
-275
@@ -1,7 +1,6 @@
|
||||
const {
|
||||
SharedCounterDataLoader,
|
||||
singleJoinBy,
|
||||
arrayJoinBy
|
||||
} = require('./util');
|
||||
const DataLoader = require('dataloader');
|
||||
const {
|
||||
@@ -14,7 +13,6 @@ const {
|
||||
const ms = require('ms');
|
||||
|
||||
const CommentModel = require('../../models/comment');
|
||||
const UsersService = require('../../services/users');
|
||||
|
||||
/**
|
||||
* Returns the comment count for all comments that are public based on their
|
||||
@@ -48,38 +46,6 @@ const getCountsByAssetID = (context, asset_ids) => {
|
||||
.then((results) => results.map((result) => result ? result.count : 0));
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the count of all public comments on an asset id, also filtering by personalization options.
|
||||
*
|
||||
* @param {Array<String>} id The ID of the asset
|
||||
* @param {Array<String>} excludeIgnored Exclude comments ignored by the requesting user
|
||||
*/
|
||||
const getCountsByAssetIDPersonalized = async (context, {assetId, excludeIgnored, tags}) => {
|
||||
const query = {
|
||||
asset_id: assetId,
|
||||
status: {
|
||||
$in: ['NONE', 'ACCEPTED'],
|
||||
},
|
||||
};
|
||||
|
||||
if (tags) {
|
||||
query['tags.tag.name'] = {
|
||||
$in: tags,
|
||||
};
|
||||
}
|
||||
|
||||
const user = context.user;
|
||||
if (excludeIgnored && user) {
|
||||
|
||||
// load afresh, as `user` may be from cache and not have recent ignores
|
||||
const freshUser = await UsersService.findById(user.id);
|
||||
const ignoredUsers = freshUser.ignoresUsers;
|
||||
query.author_id = {$nin: ignoredUsers};
|
||||
}
|
||||
const count = await CommentModel.where(query).count();
|
||||
return count;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the comment count for all comments that are public based on their
|
||||
* asset ids.
|
||||
@@ -113,98 +79,6 @@ const getParentCountsByAssetID = (context, asset_ids) => {
|
||||
.then((results) => results.map((result) => result ? result.count : 0));
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the count of top-level comments on an asset id, also filtering by personalization options.
|
||||
*
|
||||
* @param {Array<String>} id The ID of the asset
|
||||
* @param {Array<String>} excludeIgnored Exclude comments ignored by the requesting user
|
||||
*/
|
||||
const getParentCountByAssetIDPersonalized = async (context, {assetId, excludeIgnored, tags}) => {
|
||||
const query = {
|
||||
asset_id: assetId,
|
||||
parent_id: null,
|
||||
status: {
|
||||
$in: ['NONE', 'ACCEPTED'],
|
||||
},
|
||||
};
|
||||
|
||||
if (tags) {
|
||||
query['tags.tag.name'] = {
|
||||
$in: tags,
|
||||
};
|
||||
}
|
||||
|
||||
const user = context.user;
|
||||
if (excludeIgnored && user) {
|
||||
|
||||
// load afresh, as `user` may be from cache and not have recent ignores
|
||||
const freshUser = await UsersService.findById(user.id);
|
||||
const ignoredUsers = freshUser.ignoresUsers;
|
||||
query.author_id = {$nin: ignoredUsers};
|
||||
}
|
||||
|
||||
return CommentModel.where(query).count();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the comment count for all comments that are public based on their
|
||||
* parent ids.
|
||||
*
|
||||
* @param {Array<String>} parent_ids the ids of parents for which there are
|
||||
* comments that we want to get
|
||||
*/
|
||||
const getCountsByParentID = (context, parent_ids) => {
|
||||
return CommentModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
parent_id: {
|
||||
$in: parent_ids
|
||||
},
|
||||
status: {
|
||||
$in: ['NONE', 'ACCEPTED']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$parent_id',
|
||||
count: {
|
||||
$sum: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
.then(singleJoinBy(parent_ids, '_id'))
|
||||
.then((results) => results.map((result) => result ? result.count : 0));
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the count of comments for the provided parent_id, also filtering by personalization options.
|
||||
*
|
||||
* @param {Array<String>} id The ID of the parent comment
|
||||
* @param {Array<String>} excludeIgnored Exclude comments ignored by context.user
|
||||
*/
|
||||
const getCountByParentIDPersonalized = async (context, {id, excludeIgnored}) => {
|
||||
const query = {
|
||||
parent_id: {
|
||||
$in: [id]
|
||||
},
|
||||
status: {
|
||||
$in: ['NONE', 'ACCEPTED']
|
||||
}
|
||||
};
|
||||
const user = context.user;
|
||||
if (excludeIgnored && user) {
|
||||
|
||||
// load afresh, as `user` may be from cache and not have recent ignores
|
||||
const freshUser = await UsersService.findById(user.id);
|
||||
const ignoredUsers = freshUser.ignoresUsers;
|
||||
query.author_id = {$nin: ignoredUsers};
|
||||
}
|
||||
const count = await CommentModel.where(query).count();
|
||||
return count;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the count of comments based on the passed in query.
|
||||
* @param {Object} context graph context
|
||||
@@ -213,7 +87,7 @@ const getCountByParentIDPersonalized = async (context, {id, excludeIgnored}) =>
|
||||
* @return {Promise} resolves to the counts of the comments from the
|
||||
* query
|
||||
*/
|
||||
const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, author_id, tags}) => {
|
||||
const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, author_id, tags, action_type}) => {
|
||||
let query = CommentModel.find();
|
||||
|
||||
if (ids) {
|
||||
@@ -236,6 +110,16 @@ const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, au
|
||||
query = query.where({author_id});
|
||||
}
|
||||
|
||||
if (context.user != null && context.user.can(SEARCH_OTHERS_COMMENTS) && action_type) {
|
||||
query = query.where({
|
||||
action_counts: {
|
||||
[action_type.toLowerCase()]: {
|
||||
$gt: 0,
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
query = query.find({
|
||||
'tags.tag.name': {
|
||||
@@ -249,18 +133,156 @@ const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, au
|
||||
.count();
|
||||
};
|
||||
|
||||
/**
|
||||
* getStartCursor will retrieve the start cursor based on the sortBy field.
|
||||
*
|
||||
* @param {Object} ctx the graph context
|
||||
* @param {Object} nodes the result set of retrieved comments
|
||||
* @param {Object} params the params from the client describing the query
|
||||
*/
|
||||
const getStartCursor = (ctx, nodes, {cursor, sortBy}) => {
|
||||
switch (sortBy) {
|
||||
case 'CREATED_AT':
|
||||
return nodes.length ? nodes[0].created_at : null;
|
||||
case 'REPLIES':
|
||||
|
||||
// The cursor is the start! This is using numeric pagination.
|
||||
return cursor != null ? cursor : 0;
|
||||
}
|
||||
|
||||
const SORT_KEY = sortBy.toLowerCase();
|
||||
if (!ctx.plugins || !ctx.plugins.Sort.Comments || !ctx.plugins.Sort.Comments[SORT_KEY] || !ctx.plugins.Sort.Comments[SORT_KEY].startCursor) {
|
||||
throw new Error(`unable to sort by ${sortBy}, no plugin was provided to handle this type`);
|
||||
}
|
||||
|
||||
return ctx.plugins.Sort.Comments[SORT_KEY].startCursor(ctx, nodes, {cursor});
|
||||
};
|
||||
|
||||
/**
|
||||
* getEndCursor will fetch the end cursor based on the desired sortBy parameter.
|
||||
*
|
||||
* @param {Object} ctx the graph context
|
||||
* @param {Object} nodes the result set of retrieved comments
|
||||
* @param {Object} params the params from the client describing the query
|
||||
*/
|
||||
const getEndCursor = (ctx, nodes, {cursor, sortBy}) => {
|
||||
switch (sortBy) {
|
||||
case 'CREATED_AT':
|
||||
return nodes.length ? nodes[nodes.length - 1].created_at : null;
|
||||
case 'REPLIES':
|
||||
return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null;
|
||||
}
|
||||
|
||||
const SORT_KEY = sortBy.toLowerCase();
|
||||
if (!ctx.plugins || !ctx.plugins.Sort.Comments || !ctx.plugins.Sort.Comments[SORT_KEY] || !ctx.plugins.Sort.Comments[SORT_KEY].endCursor) {
|
||||
throw new Error(`unable to sort by ${sortBy}, no plugin was provided to handle this type`);
|
||||
}
|
||||
|
||||
return ctx.plugins.Sort.Comments[SORT_KEY].endCursor(ctx, nodes, {cursor});
|
||||
};
|
||||
|
||||
/**
|
||||
* applySort will add the actual `.sort` and `.skip/.where` clauses to the query
|
||||
* to apply the desired sort.
|
||||
*
|
||||
* @param {Object} ctx the graph context
|
||||
* @param {Object} query the current mongoose query object
|
||||
* @param {Object} params the params from the client describing the query
|
||||
*/
|
||||
const applySort = (ctx, query, {cursor, sortOrder, sortBy}) => {
|
||||
switch (sortBy) {
|
||||
case 'CREATED_AT': {
|
||||
if (cursor) {
|
||||
if (sortOrder === 'DESC') {
|
||||
query = query.where({
|
||||
created_at: {
|
||||
$lt: cursor,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
query = query.where({
|
||||
created_at: {
|
||||
$gt: cursor,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return query.sort({created_at: sortOrder === 'DESC' ? -1 : 1});
|
||||
}
|
||||
case 'REPLIES': {
|
||||
if (cursor) {
|
||||
query = query.skip(cursor);
|
||||
}
|
||||
|
||||
return query.sort({reply_count: sortOrder === 'DESC' ? -1 : 1, created_at: sortOrder === 'DESC' ? -1 : 1});
|
||||
}
|
||||
}
|
||||
|
||||
const SORT_KEY = sortBy.toLowerCase();
|
||||
if (!ctx.plugins || !ctx.plugins.Sort.Comments || !ctx.plugins.Sort.Comments[SORT_KEY] || !ctx.plugins.Sort.Comments[SORT_KEY].sort) {
|
||||
throw new Error(`unable to sort by ${sortBy}, no plugin was provided to handle this type`);
|
||||
}
|
||||
|
||||
return ctx.plugins.Sort.Comments[SORT_KEY].sort(ctx, query, {cursor, sortOrder});
|
||||
};
|
||||
|
||||
/**
|
||||
* executeWithSort will actually retrieve the comments based on the pre-assembled
|
||||
* query and will compose on top the sort operators necessary to get the desired
|
||||
* result.
|
||||
*
|
||||
* @param {Object} ctx the graph context
|
||||
* @param {Object} query the current mongoose query object
|
||||
* @param {Object} params the params from the client describing the query
|
||||
*/
|
||||
const executeWithSort = async (ctx, query, {cursor, sortOrder, sortBy, limit}) => {
|
||||
|
||||
// Apply the sort to the query.
|
||||
query = applySort(ctx, query, {cursor, sortOrder, sortBy});
|
||||
|
||||
// Apply the limit (if it exists, as it's applied universally).
|
||||
if (limit) {
|
||||
query = query.limit(limit + 1);
|
||||
}
|
||||
|
||||
// Fetch the nodes based on the source query.
|
||||
const nodes = await query.exec();
|
||||
|
||||
// The hasNextPage is always handled the same (ask for one more than we need,
|
||||
// if there is one more, than there is more).
|
||||
let hasNextPage = false;
|
||||
if (limit && nodes.length > limit) {
|
||||
|
||||
// There was one more than we expected! Set hasNextPage = true and remove
|
||||
// the last item from the array that we requested.
|
||||
hasNextPage = true;
|
||||
nodes.splice(limit, 1);
|
||||
}
|
||||
|
||||
// Use the generator functions below to extract the cursor details based on
|
||||
// the current sortBy parameter.
|
||||
return {
|
||||
startCursor: getStartCursor(ctx, nodes, {cursor, sortOrder, sortBy, limit}),
|
||||
endCursor: getEndCursor(ctx, nodes, {cursor, sortOrder, sortBy, limit}),
|
||||
hasNextPage,
|
||||
nodes,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves comments based on the passed in query that is filtered by the
|
||||
* current used passed in via the context.
|
||||
*
|
||||
* @param {Object} context graph context
|
||||
* @param {Object} query query terms to apply to the comments query
|
||||
*/
|
||||
const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, author_id, limit, cursor, sort, excludeIgnored, tags}) => {
|
||||
const getCommentsByQuery = async (ctx, {ids, statuses, asset_id, parent_id, author_id, limit, cursor, sortOrder, sortBy, excludeIgnored, tags, action_type}) => {
|
||||
let comments = CommentModel.find();
|
||||
|
||||
// Only administrators can search for comments with statuses that are not
|
||||
// `null`, or `'ACCEPTED'`.
|
||||
if (user != null && user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS) && statuses) {
|
||||
if (ctx.user != null && ctx.user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS) && statuses && statuses.length > 0) {
|
||||
comments = comments.where({
|
||||
status: {
|
||||
$in: statuses
|
||||
@@ -274,6 +296,16 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.user != null && ctx.user.can(SEARCH_OTHERS_COMMENTS) && action_type) {
|
||||
comments = comments.where({
|
||||
action_counts: {
|
||||
[action_type.toLowerCase()]: {
|
||||
$gt: 0,
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (ids) {
|
||||
comments = comments.find({
|
||||
id: {
|
||||
@@ -291,7 +323,7 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a
|
||||
}
|
||||
|
||||
// Only let an admin request any user or the current user request themself.
|
||||
if (user && (user.can(SEARCH_OTHERS_COMMENTS) || user.id === author_id) && author_id != null) {
|
||||
if (ctx.user && (ctx.user.can(SEARCH_OTHERS_COMMENTS) || ctx.user.id === author_id) && author_id != null) {
|
||||
comments = comments.where({author_id});
|
||||
}
|
||||
|
||||
@@ -305,151 +337,19 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a
|
||||
comments = comments.where({parent_id});
|
||||
}
|
||||
|
||||
if (excludeIgnored && user && user.ignoresUsers) {
|
||||
if (excludeIgnored && ctx.user && ctx.user.ignoresUsers && ctx.user.ignoresUsers.length > 0) {
|
||||
comments = comments.where({
|
||||
author_id: {$nin: user.ignoresUsers}
|
||||
author_id: {$nin: ctx.user.ignoresUsers}
|
||||
});
|
||||
}
|
||||
|
||||
if (cursor) {
|
||||
if (sort === 'REVERSE_CHRONOLOGICAL') {
|
||||
comments = comments.where({
|
||||
created_at: {
|
||||
$lt: cursor
|
||||
}
|
||||
});
|
||||
} else {
|
||||
comments = comments.where({
|
||||
created_at: {
|
||||
$gt: cursor
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let query = comments
|
||||
.sort({created_at: sort === 'REVERSE_CHRONOLOGICAL' ? -1 : 1});
|
||||
if (limit) {
|
||||
query = query.limit(limit + 1);
|
||||
}
|
||||
return query.then((nodes) => {
|
||||
let hasNextPage = false;
|
||||
if (limit && nodes.length > limit) {
|
||||
hasNextPage = true;
|
||||
nodes.splice(limit, 1);
|
||||
}
|
||||
return Promise.resolve({
|
||||
startCursor: nodes.length ? nodes[0].created_at : null,
|
||||
endCursor: nodes.length ? nodes[nodes.length - 1].created_at : null,
|
||||
hasNextPage,
|
||||
nodes,
|
||||
});
|
||||
});
|
||||
return executeWithSort(ctx, comments, {cursor, sortOrder, sortBy, limit});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the recent replies.
|
||||
* @param {Object} context graph context
|
||||
* @param {Array<String>} ids ids of parent ids
|
||||
* @return {Promise} resolves to recent replies
|
||||
*/
|
||||
const genRecentReplies = (context, ids) => {
|
||||
return CommentModel.aggregate([
|
||||
|
||||
// get all the replies for the comments in question
|
||||
{$match: {
|
||||
parent_id: {
|
||||
$in: ids
|
||||
}
|
||||
}},
|
||||
|
||||
// sort these by their created at timestamp, CHRONOLOGICAL'y as these are
|
||||
// replies
|
||||
{$sort: {
|
||||
created_at: 1
|
||||
}},
|
||||
|
||||
// group these replies by their parent_id
|
||||
{$group: {
|
||||
_id: '$parent_id',
|
||||
replies: {
|
||||
$push: '$$ROOT'
|
||||
}
|
||||
}},
|
||||
|
||||
// project it so that we only retain the first 3 replies of each parent
|
||||
// comment
|
||||
{$project: {
|
||||
_id: '$_id',
|
||||
replies: {
|
||||
$slice: [
|
||||
'$replies',
|
||||
0,
|
||||
3
|
||||
]
|
||||
}
|
||||
}},
|
||||
|
||||
{$unwind: '$replies'},
|
||||
|
||||
])
|
||||
.then((replies) => replies.map((reply) => reply.replies))
|
||||
.then(arrayJoinBy(ids, 'parent_id'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the recent comments.
|
||||
* @param {Object} context graph context
|
||||
* @param {Array<String>} ids ids of asset ids
|
||||
* @return {Promise} resolves to recent comments from assets
|
||||
*/
|
||||
const genRecentComments = (_, ids) => {
|
||||
return CommentModel.aggregate([
|
||||
|
||||
// get all the replies for the comments in question
|
||||
{$match: {
|
||||
asset_id: {
|
||||
$in: ids
|
||||
}
|
||||
}},
|
||||
|
||||
// sort these by their created at timestamp, CHRONOLOGICAL'y as these are
|
||||
// replies
|
||||
{$sort: {
|
||||
created_at: 1
|
||||
}},
|
||||
|
||||
// group these replies by their parent_id
|
||||
{$group: {
|
||||
_id: '$asset_id',
|
||||
comments: {
|
||||
$push: '$$ROOT'
|
||||
}
|
||||
}},
|
||||
|
||||
// project it so that we only retain the first 3 replies of each parent
|
||||
// comment
|
||||
{$project: {
|
||||
_id: '$_id',
|
||||
comments: {
|
||||
$slice: [
|
||||
'$comments',
|
||||
0,
|
||||
3
|
||||
]
|
||||
}
|
||||
}},
|
||||
|
||||
// Unwind these comments.
|
||||
{$unwind: '$comments'},
|
||||
|
||||
])
|
||||
.then((replies) => replies.map((reply) => reply.comments))
|
||||
.then(arrayJoinBy(ids, 'asset_id'));
|
||||
};
|
||||
|
||||
/**
|
||||
* getComments returns the comments by the id's. Only admins can see non-public comments.
|
||||
* getComments returns the comments by the id's. Only admins can see non-public
|
||||
* comments.
|
||||
*
|
||||
* @param {Object} context graph context
|
||||
* @param {Array<String>} ids the comment id's to fetch
|
||||
* @return {Promise} resolves to the comments
|
||||
@@ -477,6 +377,7 @@ const getComments = ({user}, ids) => {
|
||||
|
||||
/**
|
||||
* Creates a set of loaders based on a GraphQL context.
|
||||
*
|
||||
* @param {Object} context the context of the GraphQL request
|
||||
* @return {Object} object of loaders
|
||||
*/
|
||||
@@ -486,12 +387,6 @@ module.exports = (context) => ({
|
||||
getByQuery: (query) => getCommentsByQuery(context, query),
|
||||
getCountByQuery: (query) => getCommentCountByQuery(context, query),
|
||||
countByAssetID: new SharedCounterDataLoader('Comments.totalCommentCount', ms(CACHE_EXPIRY_COMMENT_COUNT), (ids) => getCountsByAssetID(context, ids)),
|
||||
countByAssetIDPersonalized: (query) => getCountsByAssetIDPersonalized(context, query),
|
||||
parentCountByAssetID: new SharedCounterDataLoader('Comments.countByAssetID', ms(CACHE_EXPIRY_COMMENT_COUNT), (ids) => getParentCountsByAssetID(context, ids)),
|
||||
parentCountByAssetIDPersonalized: (query) => getParentCountByAssetIDPersonalized(context, query),
|
||||
countByParentID: new SharedCounterDataLoader('Comments.countByParentID', ms(CACHE_EXPIRY_COMMENT_COUNT), (ids) => getCountsByParentID(context, ids)),
|
||||
countByParentIDPersonalized: (query) => getCountByParentIDPersonalized(context, query),
|
||||
genRecentReplies: new DataLoader((ids) => genRecentReplies(context, ids)),
|
||||
genRecentComments: new DataLoader((ids) => genRecentComments(context, ids))
|
||||
parentCountByAssetID: new SharedCounterDataLoader('Comments.countByAssetID', ms(CACHE_EXPIRY_COMMENT_COUNT), (ids) => getParentCountsByAssetID(context, ids))
|
||||
}
|
||||
});
|
||||
|
||||
+12
-12
@@ -54,13 +54,13 @@ const getAssetActivityMetrics = ({loaders: {Assets}}, {from, to, limit}) => {
|
||||
/**
|
||||
* Returns a list of assets with action metadata included on the models.
|
||||
*/
|
||||
const getAssetMetrics = async ({loaders: {Metrics, Assets, Comments}}, {from, to, sort, limit}) => {
|
||||
const getAssetMetrics = async ({loaders: {Metrics, Assets, Comments}}, {from, to, sortBy, limit}) => {
|
||||
|
||||
// Get the recent actions.
|
||||
let actionSummaries = await Metrics.getRecentActions.load({from, to});
|
||||
|
||||
let commentMetrics = actionSummaries.reduce((acc, {item_id, action_type, count}) => {
|
||||
if (action_type !== sort) {
|
||||
if (action_type !== sortBy) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
@@ -94,9 +94,9 @@ const getAssetMetrics = async ({loaders: {Metrics, Assets, Comments}}, {from, to
|
||||
|
||||
return {action_summaries, id: asset_id};
|
||||
})
|
||||
|
||||
|
||||
.filter((asset) => {
|
||||
let contextActionSummary = asset.action_summaries.find((({action_type}) => action_type === sort));
|
||||
let contextActionSummary = asset.action_summaries.find((({action_type}) => action_type === sortBy));
|
||||
if (contextActionSummary === null || contextActionSummary.actionCount === 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -108,8 +108,8 @@ const getAssetMetrics = async ({loaders: {Metrics, Assets, Comments}}, {from, to
|
||||
// if the action summary does not exist on the object, that it is less
|
||||
// prefered over the one that does have it.
|
||||
.sort((a, b) => {
|
||||
let aActionSummary = a.action_summaries.find((({action_type}) => action_type === sort));
|
||||
let bActionSummary = b.action_summaries.find((({action_type}) => action_type === sort));
|
||||
let aActionSummary = a.action_summaries.find((({action_type}) => action_type === sortBy));
|
||||
let bActionSummary = b.action_summaries.find((({action_type}) => action_type === sortBy));
|
||||
|
||||
// Both of them had an actionCount, hence we can determine that we could
|
||||
// compare the actual values directly.
|
||||
@@ -144,15 +144,15 @@ const getAssetMetrics = async ({loaders: {Metrics, Assets, Comments}}, {from, to
|
||||
* Returns a list of comments that are retrieved based on most activity within
|
||||
* the indicated time range.
|
||||
*/
|
||||
const getCommentMetrics = async ({loaders: {Metrics, Comments}}, {from, to, sort, limit}) => {
|
||||
const getCommentMetrics = async ({loaders: {Metrics, Comments}}, {from, to, sortBy, limit}) => {
|
||||
|
||||
let commentActionSummaries = {};
|
||||
|
||||
let actionSummaries = await Metrics.getRecentActions.load({from, to});
|
||||
|
||||
actionSummaries.sort((a, b) => {
|
||||
let aActionSummary = a.action_type === sort ? a : null;
|
||||
let bActionSummary = b.action_type === sort ? b : null;
|
||||
let aActionSummary = a.action_type === sortBy ? a : null;
|
||||
let bActionSummary = b.action_type === sortBy ? b : null;
|
||||
|
||||
// If either a or b don't have this action type, then one of them will
|
||||
// automatically win.
|
||||
@@ -178,7 +178,7 @@ const getCommentMetrics = async ({loaders: {Metrics, Comments}}, {from, to, sort
|
||||
// Grab the comment id's for comment where they have at least one of the
|
||||
// actions being sorted by.
|
||||
let commentIDs = Object.keys(commentActionSummaries).filter((item_id) => {
|
||||
let contextActionSummary = commentActionSummaries[item_id].find(({action_type}) => action_type === sort);
|
||||
let contextActionSummary = commentActionSummaries[item_id].find(({action_type}) => action_type === sortBy);
|
||||
if (contextActionSummary == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -247,11 +247,11 @@ module.exports = (context) => ({
|
||||
cacheKeyFn: objectCacheKeyFn('from', 'to')
|
||||
}),
|
||||
Assets: {
|
||||
get: ({from, to, sort, limit}) => getAssetMetrics(context, {from, to, sort, limit}),
|
||||
get: ({from, to, sortBy, limit}) => getAssetMetrics(context, {from, to, sortBy, limit}),
|
||||
getActivity: ({from, to, limit}) => getAssetActivityMetrics(context, {from, to, limit}),
|
||||
},
|
||||
Comments: {
|
||||
get: ({from, to, sort, limit}) => getCommentMetrics(context, {from, to, sort, limit}),
|
||||
get: ({from, to, sortBy, limit}) => getCommentMetrics(context, {from, to, sortBy, limit}),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ const genUserByIDs = (context, ids) => UsersService
|
||||
* @param {Object} context graph context
|
||||
* @param {Object} query query terms to apply to the users query
|
||||
*/
|
||||
const getUsersByQuery = ({user}, {ids, limit, cursor, statuses = null, sort}) => {
|
||||
const getUsersByQuery = ({user}, {ids, limit, cursor, statuses = null, sortOrder}) => {
|
||||
|
||||
let users = UserModel.find();
|
||||
|
||||
@@ -36,7 +36,7 @@ const getUsersByQuery = ({user}, {ids, limit, cursor, statuses = null, sort}) =>
|
||||
}
|
||||
|
||||
if (cursor) {
|
||||
if (sort === 'REVERSE_CHRONOLOGICAL') {
|
||||
if (sortOrder === 'DESC') {
|
||||
users = users.where({
|
||||
created_at: {
|
||||
$lt: cursor
|
||||
@@ -52,7 +52,7 @@ const getUsersByQuery = ({user}, {ids, limit, cursor, statuses = null, sort}) =>
|
||||
}
|
||||
|
||||
return users
|
||||
.sort({created_at: sort === 'REVERSE_CHRONOLOGICAL' ? -1 : 1})
|
||||
.sort({created_at: sortOrder === 'DESC' ? -1 : 1})
|
||||
.limit(limit);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const ActionModel = require('../../models/action');
|
||||
const ActionsService = require('../../services/actions');
|
||||
const UsersService = require('../../services/users');
|
||||
const errors = require('../../errors');
|
||||
@@ -23,7 +22,7 @@ const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id,
|
||||
}
|
||||
}
|
||||
|
||||
let action = await ActionsService.insertUserAction({
|
||||
let action = await ActionsService.create({
|
||||
item_id,
|
||||
item_type,
|
||||
user_id: user.id,
|
||||
@@ -52,10 +51,7 @@ const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id,
|
||||
* @return {Promise} resolves to the deleted action, or null if not found.
|
||||
*/
|
||||
const deleteAction = ({user}, {id}) => {
|
||||
return ActionModel.findOneAndRemove({
|
||||
id,
|
||||
user_id: user.id
|
||||
});
|
||||
return ActionsService.delete({id, user_id: user.id});
|
||||
};
|
||||
|
||||
module.exports = (context) => {
|
||||
|
||||
@@ -174,9 +174,7 @@ const createComment = async (context, {tags = [], body, asset_id, parent_id = nu
|
||||
// perform these increments in the event that we do have a new comment that
|
||||
// is approved or without a comment.
|
||||
if (status === 'NONE' || status === 'APPROVED') {
|
||||
if (parent_id != null) {
|
||||
Comments.countByParentID.incr(parent_id);
|
||||
} else {
|
||||
if (parent_id === null) {
|
||||
Comments.parentCountByAssetID.incr(asset_id);
|
||||
}
|
||||
Comments.countByAssetID.incr(asset_id);
|
||||
@@ -309,7 +307,7 @@ const createPublicComment = async (context, commentInput) => {
|
||||
// TODO: this is kind of fragile, we should refactor this to resolve
|
||||
// all these const's that we're using like 'COMMENTS', 'FLAG' to be
|
||||
// defined in a checkable schema.
|
||||
await ActionsService.insertUserAction({
|
||||
await ActionsService.create({
|
||||
item_id: comment.id,
|
||||
item_type: 'COMMENTS',
|
||||
action_type: 'FLAG',
|
||||
@@ -338,9 +336,7 @@ const setStatus = async ({user, loaders: {Comments}, pubsub}, {id, status}) => {
|
||||
// be nice if we could decrement the counters here, but that would result
|
||||
// in us having to know the initial state of the comment, which would
|
||||
// require another database query.
|
||||
if (comment.parent_id != null) {
|
||||
Comments.countByParentID.clear(comment.parent_id);
|
||||
} else {
|
||||
if (comment.parent_id === null) {
|
||||
Comments.parentCountByAssetID.clear(comment.asset_id);
|
||||
}
|
||||
|
||||
|
||||
+47
-39
@@ -1,55 +1,63 @@
|
||||
const {decorateWithTags} = require('./util');
|
||||
const {
|
||||
SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS,
|
||||
} = require('../../perms/constants');
|
||||
|
||||
const Asset = {
|
||||
recentComments({id}, _, {loaders: {Comments}}) {
|
||||
return Comments.genRecentComments.load(id);
|
||||
},
|
||||
async comment({id}, {id: commentId}, {loaders: {Comments}, user}) {
|
||||
const statuses = user && user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS)
|
||||
? ['NONE', 'ACCEPTED', 'PREMOD', 'REJECTED']
|
||||
: ['NONE', 'ACCEPTED'];
|
||||
async comment({id}, {id: commentId}, {loaders: {Comments}}) {
|
||||
|
||||
const comments = await Comments.getByQuery({
|
||||
asset_id: id,
|
||||
ids: commentId,
|
||||
statuses,
|
||||
});
|
||||
|
||||
return comments.nodes[0];
|
||||
},
|
||||
comments({id}, {sort, limit, deep, excludeIgnored, tags}, {loaders: {Comments}}) {
|
||||
return Comments.getByQuery({
|
||||
asset_id: id,
|
||||
sort,
|
||||
limit,
|
||||
parent_id: deep ? undefined : null,
|
||||
tags,
|
||||
excludeIgnored,
|
||||
});
|
||||
},
|
||||
commentCount({id, commentCount}, {excludeIgnored, tags}, {user, loaders: {Comments}}) {
|
||||
|
||||
// TODO: remove
|
||||
if ((user && excludeIgnored) || tags) {
|
||||
return Comments.parentCountByAssetIDPersonalized({assetId: id, excludeIgnored, tags});
|
||||
// Load the comment from the database.
|
||||
const comment = await Comments.get.load(commentId);
|
||||
if (!comment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the comment asset mismatches, then don't return it!
|
||||
if (comment.asset_id !== id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return comment;
|
||||
},
|
||||
comments({id}, {query, deep}, {loaders: {Comments}}) {
|
||||
if (!deep) {
|
||||
query.parent_id = null;
|
||||
}
|
||||
|
||||
return Comments.getByQuery(query);
|
||||
},
|
||||
commentCount({id, commentCount}, {tags}, {loaders: {Comments}}) {
|
||||
if (commentCount != null) {
|
||||
return commentCount;
|
||||
}
|
||||
|
||||
// If we are filtering by a tag.
|
||||
if (tags && tags.length > 0) {
|
||||
|
||||
// Then count the comments with those tags.
|
||||
return Comments.getCountByQuery({
|
||||
tags,
|
||||
asset_id: id,
|
||||
parent_id: null,
|
||||
statuses: ['NONE', 'ACCEPTED'],
|
||||
});
|
||||
}
|
||||
|
||||
return Comments.parentCountByAssetID.load(id);
|
||||
},
|
||||
totalCommentCount({id, totalCommentCount}, {excludeIgnored, tags}, {user, loaders: {Comments}}) {
|
||||
|
||||
// TODO: remove
|
||||
if ((user && excludeIgnored) || tags) {
|
||||
return Comments.countByAssetIDPersonalized({assetId: id, excludeIgnored, tags});
|
||||
}
|
||||
totalCommentCount({id, totalCommentCount}, {tags}, {loaders: {Comments}}) {
|
||||
if (totalCommentCount != null) {
|
||||
return totalCommentCount;
|
||||
}
|
||||
|
||||
// If we are filtering by a tag.
|
||||
if (tags && tags.length > 0) {
|
||||
|
||||
// Then count the comments with those tags.
|
||||
return Comments.getCountByQuery({
|
||||
tags,
|
||||
asset_id: id,
|
||||
statuses: ['NONE', 'ACCEPTED'],
|
||||
});
|
||||
}
|
||||
|
||||
return Comments.countByAssetID.load(id);
|
||||
},
|
||||
async settings({settings = null}, _, {loaders: {Settings}}) {
|
||||
|
||||
+20
-21
@@ -14,33 +14,32 @@ const Comment = {
|
||||
user({author_id}, _, {loaders: {Users}}) {
|
||||
return Users.getByID.load(author_id);
|
||||
},
|
||||
recentReplies({id}, _, {loaders: {Comments}}) {
|
||||
return Comments.genRecentReplies.load(id);
|
||||
},
|
||||
replies({id, asset_id}, {sort, limit, excludeIgnored}, {loaders: {Comments}}) {
|
||||
return Comments.getByQuery({
|
||||
asset_id,
|
||||
parent_id: id,
|
||||
sort,
|
||||
limit,
|
||||
excludeIgnored,
|
||||
});
|
||||
},
|
||||
replyCount({id}, {excludeIgnored}, {user, loaders: {Comments}}) {
|
||||
replies({id, asset_id, reply_count}, {query}, {loaders: {Comments}}) {
|
||||
|
||||
// TODO: remove
|
||||
if (user && excludeIgnored) {
|
||||
return Comments.countByParentIDPersonalized({id, excludeIgnored});
|
||||
// Don't bother looking up replies if there aren't any there!
|
||||
if (reply_count === 0) {
|
||||
return {
|
||||
nodes: [],
|
||||
hasNextPage: false,
|
||||
};
|
||||
}
|
||||
return Comments.countByParentID.load(id);
|
||||
|
||||
query.asset_id = asset_id;
|
||||
query.parent_id = id;
|
||||
|
||||
return Comments.getByQuery(query);
|
||||
},
|
||||
replyCount({reply_count}) {
|
||||
|
||||
// A simple remap from the underlying database model to the graph model.
|
||||
return reply_count;
|
||||
},
|
||||
actions({id}, _, {user, loaders: {Actions}}) {
|
||||
|
||||
if (user && user.can('SEARCH_ACTIONS')) {
|
||||
return Actions.getByID.load(id);
|
||||
if (!user || !user.can('SEARCH_ACTIONS')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
return Actions.getByID.load(id);
|
||||
},
|
||||
action_summaries({id, action_summaries}, _, {loaders: {Actions}}) {
|
||||
if (action_summaries) {
|
||||
|
||||
@@ -2,9 +2,11 @@ const {SEARCH_OTHER_USERS} = require('../../perms/constants');
|
||||
|
||||
const CommentStatusHistory = {
|
||||
assigned_by({assigned_by}, _, {user, loaders: {Users}}) {
|
||||
if (user && user.can(SEARCH_OTHER_USERS) && assigned_by != null) {
|
||||
return Users.getByID.load(assigned_by);
|
||||
if (!user || !user.can(SEARCH_OTHER_USERS) || assigned_by == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Users.getByID.load(assigned_by);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
const GraphQLScalarType = require('graphql').GraphQLScalarType;
|
||||
const Kind = require('graphql/language').Kind;
|
||||
|
||||
module.exports = new GraphQLScalarType({
|
||||
name: 'Cursor',
|
||||
description: 'Cursor represents a paginating cursor.',
|
||||
serialize(value) {
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
parseValue(value) {
|
||||
if (typeof value === 'string') {
|
||||
return new Date(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
parseLiteral(ast) {
|
||||
switch (ast.kind) {
|
||||
case Kind.STRING:
|
||||
|
||||
// This handles an empty string.
|
||||
if (ast.value && ast.value.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Date(ast.value);
|
||||
default:
|
||||
return ast.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -8,9 +8,11 @@ const FlagAction = {
|
||||
return group_id;
|
||||
},
|
||||
user({user_id}, _, {loaders: {Users}}) {
|
||||
if (user_id) {
|
||||
return Users.getByID.load(user_id);
|
||||
if (!user_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Users.getByID.load(user_id);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@ const ActionSummary = require('./action_summary');
|
||||
const Action = require('./action');
|
||||
const AssetActionSummary = require('./asset_action_summary');
|
||||
const Asset = require('./asset');
|
||||
const Comment = require('./comment');
|
||||
const CommentStatusHistory = require('./comment_status_history');
|
||||
const Comment = require('./comment');
|
||||
const Cursor = require('./cursor');
|
||||
const Date = require('./date');
|
||||
const FlagActionSummary = require('./flag_action_summary');
|
||||
const FlagAction = require('./flag_action');
|
||||
@@ -31,8 +32,9 @@ let resolvers = {
|
||||
Action,
|
||||
AssetActionSummary,
|
||||
Asset,
|
||||
Comment,
|
||||
CommentStatusHistory,
|
||||
Comment,
|
||||
Cursor,
|
||||
Date,
|
||||
FlagActionSummary,
|
||||
FlagAction,
|
||||
|
||||
@@ -26,13 +26,7 @@ const RootQuery = {
|
||||
|
||||
// This endpoint is used for loading moderation queues, so hide it in the
|
||||
// event that we aren't an admin.
|
||||
async comments(_, {query}, {user, loaders: {Comments, Actions}}) {
|
||||
let {action_type} = query;
|
||||
|
||||
if (user != null && user.can(SEARCH_OTHERS_COMMENTS) && action_type) {
|
||||
query.ids = await Actions.getByTypes({action_type, item_type: 'COMMENTS'});
|
||||
}
|
||||
|
||||
async comments(_, {query}, {loaders: {Comments}}) {
|
||||
return Comments.getByQuery(query);
|
||||
},
|
||||
|
||||
@@ -40,19 +34,13 @@ const RootQuery = {
|
||||
return Comments.get.load(id);
|
||||
},
|
||||
|
||||
async commentCount(_, {query}, {user, loaders: {Actions, Comments, Assets}}) {
|
||||
|
||||
async commentCount(_, {query}, {user, loaders: {Comments, Assets}}) {
|
||||
if (user == null || !user.can(SEARCH_OTHERS_COMMENTS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {action_type, asset_url} = query;
|
||||
|
||||
if (action_type) {
|
||||
query.ids = await Actions.getByTypes({action_type, item_type: 'COMMENTS'});
|
||||
}
|
||||
|
||||
if (asset_url) {
|
||||
const {asset_url, asset_id} = query;
|
||||
if ((!asset_id || asset_id.length === 0) && asset_url && asset_url.length > 0) {
|
||||
let asset = await Assets.findByUrl(asset_url);
|
||||
if (asset) {
|
||||
query.asset_id = asset.id;
|
||||
@@ -62,24 +50,25 @@ const RootQuery = {
|
||||
return Comments.getCountByQuery(query);
|
||||
},
|
||||
|
||||
assetMetrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics: {Assets}}}) {
|
||||
assetMetrics(_, query, {user, loaders: {Metrics: {Assets}}}) {
|
||||
if (user == null || !user.can(SEARCH_ASSETS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (sort === 'ACTIVITY') {
|
||||
return Assets.getActivity({from, to, limit});
|
||||
const {sortBy} = query;
|
||||
if (sortBy === 'ACTIVITY') {
|
||||
return Assets.getActivity(query);
|
||||
}
|
||||
|
||||
return Assets.get({from, to, sort, limit});
|
||||
return Assets.get(query);
|
||||
},
|
||||
|
||||
commentMetrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics: {Comments}}}) {
|
||||
commentMetrics(_, query, {user, loaders: {Metrics: {Comments}}}) {
|
||||
if (user == null || !user.can(SEARCH_COMMENT_METRICS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Comments.get({from, to, sort, limit});
|
||||
return Comments.get(query);
|
||||
},
|
||||
|
||||
// This returns the current user, ensure that if we aren't logged in, we
|
||||
@@ -109,7 +98,6 @@ const RootQuery = {
|
||||
}
|
||||
|
||||
const {action_type} = query;
|
||||
|
||||
if (action_type) {
|
||||
query.ids = await Actions.getByTypes({action_type, item_type: 'USERS'});
|
||||
query.statuses = ['PENDING'];
|
||||
|
||||
@@ -45,7 +45,7 @@ const User = {
|
||||
return null;
|
||||
},
|
||||
tokens({id, tokens}, args, {user}) {
|
||||
if (!user || ((user.id !== id) && !user.can(LIST_OWN_TOKENS))) {
|
||||
if (!user || ((user.id !== id) && !user.can(LIST_OWN_TOKENS))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+62
-40
@@ -5,6 +5,9 @@
|
||||
# Date represented as an ISO8601 string.
|
||||
scalar Date
|
||||
|
||||
# Cursor represents a paginating cursor.
|
||||
scalar Cursor
|
||||
|
||||
################################################################################
|
||||
## Reliability
|
||||
################################################################################
|
||||
@@ -17,7 +20,7 @@ type Reliability {
|
||||
# `null` if the reliability cannot be determined.
|
||||
flagger: Boolean
|
||||
|
||||
# commenter will be `true` when the commenter is reliable, `false` if not, or
|
||||
# Commenter will be `true` when the commenter is reliable, `false` if not, or
|
||||
# `null` if the reliability cannot be determined.
|
||||
commenter: Boolean
|
||||
}
|
||||
@@ -126,10 +129,10 @@ input UsersQuery {
|
||||
limit: Int = 10
|
||||
|
||||
# Skip results from the last created_at timestamp.
|
||||
cursor: Date
|
||||
cursor: Cursor
|
||||
|
||||
# Sort the results by created_at.
|
||||
sort: SORT_ORDER = REVERSE_CHRONOLOGICAL
|
||||
sortOrder: SORT_ORDER = DESC
|
||||
}
|
||||
|
||||
# AssetsQuery allows teh ability to query assets by specific fields
|
||||
@@ -208,7 +211,7 @@ enum COMMENT_STATUS {
|
||||
PREMOD
|
||||
}
|
||||
|
||||
# The types of action there are as enum's.
|
||||
# The types of action there are as enums.
|
||||
enum ACTION_TYPE {
|
||||
|
||||
# Represents a FlagAction.
|
||||
@@ -241,10 +244,14 @@ input CommentsQuery {
|
||||
limit: Int = 10
|
||||
|
||||
# Skip results from the last created_at timestamp.
|
||||
cursor: Date
|
||||
cursor: Cursor
|
||||
|
||||
# Sort the results by created_at.
|
||||
sort: SORT_ORDER = REVERSE_CHRONOLOGICAL
|
||||
# Sort the results by from largest first.
|
||||
sortOrder: SORT_ORDER = DESC
|
||||
|
||||
# The order to sort the comments by, sorting by default the created at
|
||||
# timestamp.
|
||||
sortBy: SORT_COMMENTS_BY = CREATED_AT
|
||||
|
||||
# Filter by a specific tag name.
|
||||
tags: [String!]
|
||||
@@ -253,6 +260,22 @@ input CommentsQuery {
|
||||
excludeIgnored: Boolean
|
||||
}
|
||||
|
||||
input RepliesQuery {
|
||||
|
||||
# Sort the results by from smallest first.
|
||||
sortOrder: SORT_ORDER = ASC
|
||||
|
||||
# The order to sort the comments by, sorting by default the created at
|
||||
# timestamp.
|
||||
sortBy: SORT_COMMENTS_BY = CREATED_AT
|
||||
|
||||
# Limit the number of results to be returned.
|
||||
limit: Int = 3
|
||||
|
||||
# Exclude comments ignored by the requesting user
|
||||
excludeIgnored: Boolean
|
||||
}
|
||||
|
||||
# CommentCountQuery allows the ability to query comment counts by specific
|
||||
# methods.
|
||||
input CommentCountQuery {
|
||||
@@ -310,14 +333,11 @@ type Comment {
|
||||
# the user who authored the comment.
|
||||
user: User
|
||||
|
||||
# the recent replies made against this comment.
|
||||
recentReplies: [Comment!]
|
||||
|
||||
# the replies that were made to the comment.
|
||||
replies(sort: SORT_ORDER = CHRONOLOGICAL, limit: Int = 3, excludeIgnored: Boolean): CommentConnection!
|
||||
replies(query: RepliesQuery = {}): CommentConnection!
|
||||
|
||||
# The count of replies on a comment.
|
||||
replyCount(excludeIgnored: Boolean): Int
|
||||
replyCount: Int
|
||||
|
||||
# Actions completed on the parent. Requires the `ADMIN` role.
|
||||
actions: [Action]
|
||||
@@ -351,10 +371,10 @@ type CommentConnection {
|
||||
hasNextPage: Boolean!
|
||||
|
||||
# Cursor of first comment in subset.
|
||||
startCursor: Date
|
||||
startCursor: Cursor
|
||||
|
||||
# Cursor of last comment in subset.
|
||||
endCursor: Date
|
||||
endCursor: Cursor
|
||||
|
||||
# Subset of comments.
|
||||
nodes: [Comment!]!
|
||||
@@ -570,28 +590,18 @@ type Asset {
|
||||
# The URL that the asset is located on.
|
||||
url: String
|
||||
|
||||
# Returns recent comments
|
||||
recentComments: [Comment!]
|
||||
|
||||
# The comments that are attached to the asset.
|
||||
# If `deep` is true, it will return comments of all depths,
|
||||
# otherwise only top-level comments are returned.
|
||||
comments(
|
||||
sort: SORT_ORDER = REVERSE_CHRONOLOGICAL,
|
||||
limit: Int = 10,
|
||||
excludeIgnored: Boolean,
|
||||
tags: [String!]
|
||||
deep: Boolean,
|
||||
): CommentConnection!
|
||||
# The comments that are attached to the asset. When `deep` is true, the
|
||||
# comments returned will be at all depths.
|
||||
comments(query: CommentsQuery = {}, deep: Boolean = false): CommentConnection!
|
||||
|
||||
# A Comment from the Asset by comment's ID
|
||||
comment(id: ID!): Comment
|
||||
|
||||
# The count of top level comments on the asset.
|
||||
commentCount(excludeIgnored: Boolean, tags: [String!]): Int
|
||||
commentCount(tags: [String!]): Int
|
||||
|
||||
# The total count of all comments made on the asset.
|
||||
totalCommentCount(excludeIgnored: Boolean, tags: [String!]): Int
|
||||
totalCommentCount(tags: [String!]): Int
|
||||
|
||||
# The settings (rectified with the global settings) that should be applied to
|
||||
# this asset.
|
||||
@@ -656,10 +666,22 @@ type ValidationUserError implements UserError {
|
||||
enum SORT_ORDER {
|
||||
|
||||
# newest to oldest order.
|
||||
REVERSE_CHRONOLOGICAL
|
||||
DESC
|
||||
|
||||
# oldest to newer order.
|
||||
CHRONOLOGICAL
|
||||
ASC
|
||||
}
|
||||
|
||||
# SORT_COMMENTS_BY selects the means for which comments are ordered when
|
||||
# sorting.
|
||||
enum SORT_COMMENTS_BY {
|
||||
|
||||
# Comments will be sorted by their created at date.
|
||||
CREATED_AT
|
||||
|
||||
# Comments will be sorted by their immediate reply count (replies to the comment
|
||||
# in question only, not including descendants).
|
||||
REPLIES
|
||||
}
|
||||
|
||||
# All queries that can be executed.
|
||||
@@ -716,11 +738,11 @@ type RootQuery {
|
||||
|
||||
# Asset metrics related to user actions are saturated into the assets
|
||||
# returned. Parameters `from` and `to` are related to the action created_at field.
|
||||
assetMetrics(from: Date!, to: Date!, sort: ASSET_METRICS_SORT!, limit: Int = 10): [Asset!]
|
||||
assetMetrics(from: Date!, to: Date!, sortBy: ASSET_METRICS_SORT!, limit: Int = 10): [Asset!]
|
||||
|
||||
# Comment metrics related to user actions are saturated into the comments
|
||||
# returned. Parameters `from` and `to` are related to the action created_at field.
|
||||
commentMetrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Comment!]
|
||||
commentMetrics(from: Date!, to: Date!, sortBy: ACTION_TYPE!, limit: Int = 10): [Comment!]
|
||||
}
|
||||
|
||||
################################################################################
|
||||
@@ -922,19 +944,19 @@ input ModifyTagInput {
|
||||
# Response to the addTag or removeTag mutations.
|
||||
type ModifyTagResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occured.
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# Response to ignoreUser mutation
|
||||
type IgnoreUserResponse implements Response {
|
||||
# An array of errors relating to the mutation that occured.
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# Response to stopIgnoringUser mutation
|
||||
type StopIgnoringUserResponse implements Response {
|
||||
# An array of errors relating to the mutation that occured.
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
@@ -945,13 +967,13 @@ input EditCommentInput {
|
||||
body: String!
|
||||
}
|
||||
|
||||
# EditCommentResponse contains the updated comment and any errors that occured.
|
||||
# EditCommentResponse contains the updated comment and any errors that occurred.
|
||||
type EditCommentResponse implements Response {
|
||||
|
||||
# The edited comment.
|
||||
comment: Comment
|
||||
|
||||
# An array of errors relating to the mutation that occured.
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
@@ -968,7 +990,7 @@ type CreateTokenResponse implements Response {
|
||||
# Token is the Token that was created, or null if it failed.
|
||||
token: Token
|
||||
|
||||
# An array of errors relating to the mutation that occured.
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
@@ -982,7 +1004,7 @@ input RevokeTokenInput {
|
||||
# RevokeTokenResponse contains the errors related to revoking a token.
|
||||
type RevokeTokenResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occured.
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ AssetSchema.index({
|
||||
subsection: 'text',
|
||||
author: 'text'
|
||||
}, {
|
||||
background: true
|
||||
background: true,
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
+38
-2
@@ -55,7 +55,7 @@ const CommentSchema = new Schema({
|
||||
body: {
|
||||
type: String,
|
||||
required: [true, 'The body is required.'],
|
||||
minlength: 2
|
||||
minlength: 2,
|
||||
},
|
||||
body_history: [BodyHistoryItemSchema],
|
||||
asset_id: String,
|
||||
@@ -66,15 +66,29 @@ const CommentSchema = new Schema({
|
||||
enum: COMMENT_STATUS,
|
||||
default: 'NONE'
|
||||
},
|
||||
|
||||
// parent_id is the id of the parent comment (null if there is none).
|
||||
parent_id: String,
|
||||
|
||||
// The number of replies to this comment directly.
|
||||
reply_count: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
|
||||
// Counts to store related to actions taken on the given comment.
|
||||
action_counts: {
|
||||
default: {},
|
||||
type: Object,
|
||||
},
|
||||
|
||||
// Tags are added by the self or by administrators.
|
||||
tags: [TagLinkSchema],
|
||||
|
||||
// Additional metadata stored on the field.
|
||||
metadata: {
|
||||
default: {},
|
||||
type: Object
|
||||
type: Object,
|
||||
}
|
||||
}, {
|
||||
timestamps: {
|
||||
@@ -94,10 +108,32 @@ CommentSchema.index({
|
||||
background: false
|
||||
});
|
||||
|
||||
// Add an index that is optimized for sorting based on the action count data.
|
||||
CommentSchema.index({
|
||||
'created_at': 1,
|
||||
'action_counts': 1,
|
||||
}, {
|
||||
background: true,
|
||||
});
|
||||
|
||||
// Add an index that is optimized for sorting based on the created_at timestamp
|
||||
// but also good at locating comments that have a specific asset id.
|
||||
CommentSchema.index({
|
||||
'asset_id': 1,
|
||||
'created_at': 1,
|
||||
}, {
|
||||
background: true,
|
||||
});
|
||||
|
||||
CommentSchema.virtual('edited').get(function() {
|
||||
return this.body_history.length > 1;
|
||||
});
|
||||
|
||||
// Visable is true when the comment is visible to the public.
|
||||
CommentSchema.virtual('visible').get(function() {
|
||||
return ['ACCEPTED', 'NONE'].includes(this.status);
|
||||
});
|
||||
|
||||
// Comment model.
|
||||
const Comment = mongoose.model('Comment', CommentSchema);
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
require('babel-core/register');
|
||||
|
||||
let E2E_REPORT_PATH = './test/e2e/reports';
|
||||
if (process.env.E2E_REPORT_PATH && process.env.E2E_REPORT_PATH.length > 0) {
|
||||
E2E_REPORT_PATH = process.env.E2E_REPORT_PATH;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
'src_folders': './test/e2e/tests',
|
||||
'output_folder': E2E_REPORT_PATH,
|
||||
'page_objects_path': './test/e2e/pages',
|
||||
'globals_path': './test/e2e/globals',
|
||||
'custom_commands_path' : '',
|
||||
'custom_assertions_path' : '',
|
||||
'selenium': {
|
||||
'start_process': true,
|
||||
'server_path': 'node_modules/selenium-standalone/.selenium/selenium-server/2.53.1-server.jar',
|
||||
'log_path': E2E_REPORT_PATH,
|
||||
'host': '127.0.0.1',
|
||||
'port': 6666,
|
||||
'cli_args': {
|
||||
'webdriver.chrome.driver': 'node_modules/selenium-standalone/.selenium/chromedriver/2.25-x64-chromedriver'
|
||||
}
|
||||
},
|
||||
'test_settings': {
|
||||
'default': {
|
||||
'launch_url' : 'http://localhost:3011',
|
||||
'selenium_port': 6666,
|
||||
'selenium_host': 'localhost',
|
||||
'silent': true,
|
||||
'desiredCapabilities': {
|
||||
'browserName': 'chrome',
|
||||
'javascriptEnabled': true,
|
||||
'acceptSslCerts': true,
|
||||
'webStorageEnabled': true,
|
||||
'databaseEnabled': true,
|
||||
'applicationCacheEnabled': false,
|
||||
'nativeEvents': true
|
||||
},
|
||||
'screenshots' : {
|
||||
'enabled': true,
|
||||
'on_failure': true,
|
||||
'on_error': true,
|
||||
'path': E2E_REPORT_PATH
|
||||
},
|
||||
'exclude': [
|
||||
'./tests/e2e/tests/EmbedStreamTests.js'
|
||||
]
|
||||
},
|
||||
'integration': {
|
||||
'launch_url': 'http://localhost:3011'
|
||||
}
|
||||
}
|
||||
};
|
||||
+116
-130
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"postinstall": "./bin/cli plugins reconcile --skip-remote",
|
||||
"start": "./bin/cli serve -j -w",
|
||||
"dev-start": "nodemon -w . -w bin/cli -w bin/cli-serve --config .nodemon.json --exec \"WEBPACK=TRUE NODE_ENV=test ./scripts/generateIntrospectionResult.js && ./bin/cli -c .env serve -j -w\"",
|
||||
"dev-start": "nodemon -w . -w bin/cli -w bin/cli-serve --config .nodemon.json --exec \"yarn generate-introspection && ./bin/cli -c .env serve -j -w\"",
|
||||
"prebuild": "yarn generate-introspection",
|
||||
"build": "WEBPACK=TRUE NODE_ENV=production webpack -p --config webpack.config.js --bail",
|
||||
"prebuild-watch": "yarn generate-introspection",
|
||||
@@ -15,10 +15,6 @@
|
||||
"lint-fix": "eslint bin/* . --fix",
|
||||
"test": "TEST_MODE=unit NODE_ENV=test mocha -R ${MOCHA_REPORTER:-spec}",
|
||||
"test-cover": "TEST_MODE=unit NODE_ENV=test istanbul cover _mocha --report text --check-coverage -- -R spec",
|
||||
"pree2e": "NODE_ENV=test TALK_PORT=3011 scripts/pree2e.sh",
|
||||
"e2e": "NODE_ENV=test nightwatch",
|
||||
"poste2e": "NODE_ENV=test scripts/poste2e.sh",
|
||||
"embed-start": "NODE_ENV=development yarn build && ./bin/cli serve --jobs",
|
||||
"heroku-postbuild": "./bin/cli plugins reconcile && yarn build",
|
||||
"generate-introspection": "WEBPACK=TRUE NODE_ENV=test ./scripts/generateIntrospectionResult.js"
|
||||
},
|
||||
@@ -59,89 +55,12 @@
|
||||
"homepage": "https://github.com/coralproject/talk#readme",
|
||||
"dependencies": {
|
||||
"accepts": "^1.3.3",
|
||||
"app-module-path": "^2.2.0",
|
||||
"async": "^2.5.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.17.1",
|
||||
"bowser": "^1.7.0",
|
||||
"cli-table": "^0.3.1",
|
||||
"clipboard": "^1.7.1",
|
||||
"colors": "^1.1.2",
|
||||
"commander": "^2.9.0",
|
||||
"compression": "^1.6.2",
|
||||
"connect-redis": "^3.1.0",
|
||||
"cookie-parser": "^1.4.3",
|
||||
"cross-spawn": "^5.1.0",
|
||||
"csurf": "^1.9.0",
|
||||
"dataloader": "^1.3.0",
|
||||
"debug": "^2.6.3",
|
||||
"dotenv": "^4.0.0",
|
||||
"ejs": "^2.5.6",
|
||||
"env-rewrite": "^1.0.2",
|
||||
"eventemitter2": "^4.1.2",
|
||||
"express": "^4.15.2",
|
||||
"express-session": "^1.15.1",
|
||||
"file-loader": "^0.11.2",
|
||||
"form-data": "^2.1.2",
|
||||
"fs-extra": "^3.0.1",
|
||||
"gql-merge": "^0.0.4",
|
||||
"graphql": "^0.9.1",
|
||||
"graphql-errors": "^2.1.0",
|
||||
"graphql-redis-subscriptions": "^1.1.5",
|
||||
"graphql-server-express": "^0.6.0",
|
||||
"graphql-subscriptions": "^0.4.3",
|
||||
"graphql-tools": "^0.10.1",
|
||||
"helmet": "^3.5.0",
|
||||
"immutability-helper": "^2.2.0",
|
||||
"inquirer": "^3.2.1",
|
||||
"joi": "^10.4.1",
|
||||
"jsonwebtoken": "^7.3.0",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"kue": "^0.11.5",
|
||||
"linkify-it": "^2.0.3",
|
||||
"lodash": "^4.16.6",
|
||||
"marked": "^0.3.6",
|
||||
"metascraper": "^1.0.6",
|
||||
"minimist": "^1.2.0",
|
||||
"mongoose": "^4.9.8",
|
||||
"morgan": "^1.8.1",
|
||||
"ms": "^2.0.0",
|
||||
"murmurhash-js": "^1.0.0",
|
||||
"natural": "^0.5.0",
|
||||
"node-emoji": "^1.5.1",
|
||||
"node-fetch": "^1.6.3",
|
||||
"nodemailer": "^2.6.4",
|
||||
"passport": "^0.3.2",
|
||||
"passport-jwt": "^2.2.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"query-strings": "^0.0.1",
|
||||
"react-apollo": "^1.4.12",
|
||||
"react-input-autosize": "^1.1.4",
|
||||
"react-recaptcha": "^2.2.6",
|
||||
"react-toastify": "^1.5.0",
|
||||
"react-transition-group": "^1.1.3",
|
||||
"recompose": "^0.23.1",
|
||||
"redis": "^2.7.1",
|
||||
"resolve": "^1.3.2",
|
||||
"semver": "^5.3.0",
|
||||
"simplemde": "^1.11.2",
|
||||
"smoothscroll-polyfill": "^0.3.5",
|
||||
"snake-case": "^2.1.0",
|
||||
"subscriptions-transport-ws": "^0.7.2",
|
||||
"timekeeper": "^1.0.0",
|
||||
"url-search-params": "^0.9.0",
|
||||
"uuid": "^3.0.1",
|
||||
"yaml-loader": "^0.4.0",
|
||||
"yamljs": "^0.2.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"apollo-client": "^1.9.1",
|
||||
"app-module-path": "^2.2.0",
|
||||
"autoprefixer": "^6.5.2",
|
||||
"babel-cli": "^6.24.0",
|
||||
"babel-core": "^6.24.0",
|
||||
"babel-eslint": "^7.2.1",
|
||||
"babel-jest": "^19.0.0",
|
||||
"babel-loader": "^6.4.1",
|
||||
"babel-plugin-add-module-exports": "^0.2.1",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
@@ -154,16 +73,120 @@
|
||||
"babel-polyfill": "^6.23.0",
|
||||
"babel-preset-es2015": "^6.24.0",
|
||||
"babel-preset-react": "^6.23.0",
|
||||
"babel-preset-stage-0": "^6.16.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.17.1",
|
||||
"bowser": "^1.7.0",
|
||||
"cli-table": "^0.3.1",
|
||||
"clipboard": "^1.7.1",
|
||||
"colors": "^1.1.2",
|
||||
"commander": "^2.11.0",
|
||||
"common-tags": "^1.4.0",
|
||||
"compression": "^1.7.0",
|
||||
"compression-webpack-plugin": "^0.4.0",
|
||||
"cookie-parser": "^1.4.3",
|
||||
"copy-webpack-plugin": "^4.0.0",
|
||||
"cross-spawn": "^5.1.0",
|
||||
"css-loader": "^0.27.3",
|
||||
"dataloader": "^1.3.0",
|
||||
"debug": "^3.0.0",
|
||||
"dialog-polyfill": "^0.4.4",
|
||||
"dotenv": "^4.0.0",
|
||||
"ejs": "^2.5.7",
|
||||
"env-rewrite": "^1.0.2",
|
||||
"eventemitter2": "^4.1.2",
|
||||
"exports-loader": "^0.6.4",
|
||||
"express": "^4.15.4",
|
||||
"file-loader": "^0.11.2",
|
||||
"form-data": "^2.2.0",
|
||||
"fs-extra": "^4.0.1",
|
||||
"gql-merge": "^0.0.4",
|
||||
"graphql": "^0.9.1",
|
||||
"graphql-docs": "^0.2.0",
|
||||
"graphql-errors": "^2.1.0",
|
||||
"graphql-redis-subscriptions": "^1.1.5",
|
||||
"graphql-server-express": "^0.6.0",
|
||||
"graphql-subscriptions": "^0.4.3",
|
||||
"graphql-tag": "^1.2.3",
|
||||
"graphql-tools": "^0.10.1",
|
||||
"hammerjs": "^2.0.8",
|
||||
"helmet": "^3.8.1",
|
||||
"history": "^3.0.0",
|
||||
"immutability-helper": "^2.2.0",
|
||||
"imports-loader": "^0.7.1",
|
||||
"inquirer": "^3.2.2",
|
||||
"istanbul": "^1.1.0-alpha.1",
|
||||
"joi": "^10.6.0",
|
||||
"json-loader": "^0.5.4",
|
||||
"jsonwebtoken": "^7.4.3",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"keymaster": "^1.6.2",
|
||||
"kue": "^0.11.6",
|
||||
"license-webpack-plugin": "^1.0.0",
|
||||
"linkify-it": "^2.0.3",
|
||||
"lodash": "^4.16.6",
|
||||
"marked": "^0.3.6",
|
||||
"material-design-lite": "^1.2.1",
|
||||
"metascraper": "^1.0.7",
|
||||
"minimist": "^1.2.0",
|
||||
"mongoose": "^4.11.7",
|
||||
"morgan": "^1.8.2",
|
||||
"ms": "^2.0.0",
|
||||
"murmurhash-js": "^1.0.0",
|
||||
"natural": "^0.5.4",
|
||||
"node-emoji": "^1.8.1",
|
||||
"node-fetch": "^1.7.2",
|
||||
"nodemailer": "^2.6.4",
|
||||
"passport": "^0.4.0",
|
||||
"passport-jwt": "^3.0.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"pluralize": "^7.0.0",
|
||||
"postcss-loader": "^1.3.3",
|
||||
"postcss-modules": "^0.5.2",
|
||||
"postcss-smart-import": "^0.5.1",
|
||||
"precss": "^1.4.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"pym.js": "^1.1.1",
|
||||
"query-strings": "^0.0.1",
|
||||
"react": "^15.4.2",
|
||||
"react-apollo": "^1.4.12",
|
||||
"react-dom": "^15.4.2",
|
||||
"react-highlight-words": "^0.6.0",
|
||||
"react-input-autosize": "^1.1.4",
|
||||
"react-linkify": "^0.1.3",
|
||||
"react-mdl": "^1.7.2",
|
||||
"react-mdl-selectfield": "^0.2.0",
|
||||
"react-recaptcha": "^2.2.6",
|
||||
"react-redux": "^4.4.5",
|
||||
"react-router": "^3.0.0",
|
||||
"react-tagsinput": "^3.17.0",
|
||||
"react-toastify": "^1.5.0",
|
||||
"react-transition-group": "^1.1.3",
|
||||
"recompose": "^0.23.1",
|
||||
"redis": "^2.8.0",
|
||||
"redux": "^3.6.0",
|
||||
"redux-thunk": "^2.1.0",
|
||||
"resolve": "^1.4.0",
|
||||
"semver": "^5.4.1",
|
||||
"simplemde": "^1.11.2",
|
||||
"smoothscroll-polyfill": "^0.3.5",
|
||||
"snake-case": "^2.1.0",
|
||||
"style-loader": "^0.16.0",
|
||||
"subscriptions-transport-ws": "^0.7.2",
|
||||
"timeago.js": "^2.0.3",
|
||||
"timekeeper": "^1.0.0",
|
||||
"url-loader": "^0.5.9",
|
||||
"url-search-params": "^0.9.0",
|
||||
"uuid": "^3.1.0",
|
||||
"webpack": "^2.3.1",
|
||||
"webpack-sources": "^1.0.1",
|
||||
"yaml-loader": "^0.4.0",
|
||||
"yamljs": "^0.2.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^3.5.0",
|
||||
"chai-as-promised": "^6.0.0",
|
||||
"chai-http": "^3.0.0",
|
||||
"common-tags": "^1.4.0",
|
||||
"compression-webpack-plugin": "^0.4.0",
|
||||
"copy-webpack-plugin": "^4.0.0",
|
||||
"css-loader": "^0.27.3",
|
||||
"dialog-polyfill": "^0.4.4",
|
||||
"enzyme": "^2.6.0",
|
||||
"enzyme": "^2.9.1",
|
||||
"eslint": "^3.12.1",
|
||||
"eslint-config-postcss": "^2.0.2",
|
||||
"eslint-config-standard": "^6.2.1",
|
||||
@@ -174,50 +197,13 @@
|
||||
"eslint-plugin-promise": "^3.3.1",
|
||||
"eslint-plugin-react": "^6.6.0",
|
||||
"eslint-plugin-standard": "^2.0.1",
|
||||
"exports-loader": "^0.6.4",
|
||||
"fetch-mock": "^5.5.0",
|
||||
"graphql-docs": "^0.2.0",
|
||||
"graphql-tag": "^1.2.3",
|
||||
"hammerjs": "^2.0.8",
|
||||
"history": "^3.0.0",
|
||||
"ignore-styles": "^5.0.1",
|
||||
"imports-loader": "^0.7.1",
|
||||
"istanbul": "^1.1.0-alpha.1",
|
||||
"jsdom": "^9.8.3",
|
||||
"json-loader": "^0.5.4",
|
||||
"keymaster": "^1.6.2",
|
||||
"license-webpack-plugin": "^0.4.2",
|
||||
"material-design-lite": "^1.2.1",
|
||||
"mocha": "^3.1.2",
|
||||
"mocha-junit-reporter": "^1.12.1",
|
||||
"nightwatch": "^0.9.11",
|
||||
"nodemon": "^1.11.0",
|
||||
"postcss-loader": "^1.3.3",
|
||||
"postcss-modules": "^0.5.2",
|
||||
"postcss-smart-import": "^0.5.1",
|
||||
"pre-git": "^3.10.0",
|
||||
"precss": "^1.4.0",
|
||||
"pym.js": "^1.1.1",
|
||||
"react": "^15.4.2",
|
||||
"react-addons-test-utils": "^15.4.2",
|
||||
"react-dom": "^15.4.2",
|
||||
"react-highlight-words": "^0.6.0",
|
||||
"react-linkify": "^0.1.3",
|
||||
"react-mdl": "^1.7.2",
|
||||
"react-mdl-selectfield": "^0.2.0",
|
||||
"react-redux": "^4.4.5",
|
||||
"react-router": "^3.0.0",
|
||||
"react-tagsinput": "^3.17.0",
|
||||
"redux": "^3.6.0",
|
||||
"redux-mock-store": "^1.2.1",
|
||||
"redux-thunk": "^2.1.0",
|
||||
"regenerator": "^0.8.46",
|
||||
"selenium-standalone": "^5.11.2",
|
||||
"style-loader": "^0.16.0",
|
||||
"supertest": "^2.0.1",
|
||||
"timeago.js": "^2.0.3",
|
||||
"url-loader": "^0.5.9",
|
||||
"webpack": "^2.3.1"
|
||||
"sinon": "^3.2.1",
|
||||
"sinon-chai": "^2.13.0",
|
||||
"supertest": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^8"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export {setSort} from 'coral-embed-stream/src/actions/stream';
|
||||
@@ -0,0 +1,16 @@
|
||||
.label {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
padding: 6px 16px 6px 10px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
cursor: pointer;
|
||||
margin-right: 6px;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import styles from './SortOption.css';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class SortOption extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<label className={styles.label}>
|
||||
<input
|
||||
type="radio"
|
||||
onChange={this.props.setSort}
|
||||
checked={this.props.active}
|
||||
className={styles.input}
|
||||
/>
|
||||
{this.props.label}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SortOption.propTypes = {
|
||||
|
||||
// A simple callback to be called when clicking on this sort option.
|
||||
setSort: PropTypes.func.isRequired,
|
||||
|
||||
// Whether or not this sort option is active.
|
||||
active: PropTypes.bool.isRequired,
|
||||
|
||||
// Label to show next to the input control.
|
||||
label: PropTypes.string.isRequired,
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
export {Slot} from 'coral-framework/components';
|
||||
export {default as ClickOutside} from 'coral-framework/components/ClickOutside';
|
||||
export {default as IfSlotIsEmpty} from 'coral-framework/components/IfSlotIsEmpty';
|
||||
export {default as IfSlotIsNotEmpty} from 'coral-framework/components/IfSlotIsNotEmpty';
|
||||
export {default as CommentAuthorName} from 'coral-framework/components/CommentAuthorName';
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import withSortOption from '../hocs/withSortOption';
|
||||
import SortOption from '../components/SortOption';
|
||||
|
||||
/**
|
||||
* A factory creating a sort option component.
|
||||
* @param {string|function} label label to display, can be a callback for lazy evaluation.
|
||||
* @param {Object} sort sort parameters
|
||||
* @param {string} sort.sortBy
|
||||
* @param {string} sort.sortOrder
|
||||
* @return {Object} Component
|
||||
*/
|
||||
export const createSortOption = (label, sort) => withSortOption({...sort, label})(SortOption);
|
||||
@@ -1,5 +1,6 @@
|
||||
export {default as withReaction} from './withReaction';
|
||||
export {default as withTags} from './withTags';
|
||||
export {default as withSortOption} from './withSortOption';
|
||||
export {default as withFragments} from 'coral-framework/hocs/withFragments';
|
||||
export {default as excludeIf} from 'coral-framework/hocs/excludeIf';
|
||||
export {default as connect} from 'coral-framework/hocs/connect';
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'plugin-api/beta/client/hocs';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {sortOrderSelector, sortBySelector} from 'plugin-api/beta/client/selectors/stream';
|
||||
import {setSort} from 'plugin-api/beta/client/actions/stream';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
sortOrder: sortOrderSelector(state),
|
||||
sortBy: sortBySelector(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators(
|
||||
{
|
||||
setSort
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
/**
|
||||
* A HOC providing props to implement a sort option.
|
||||
* Provides the props `active`, `setSort`, `label`.
|
||||
* @param {Object} sort
|
||||
* @param {Object} sort.sortBy
|
||||
* @param {string} sort.sortOrder
|
||||
* @return {Object} HOC
|
||||
*/
|
||||
export default ({sortBy = 'created_at', sortOrder = 'DESC', label}) => hoistStatics((WrappedComponent) => {
|
||||
class WithSortOption extends React.Component {
|
||||
setSort = () => {
|
||||
this.props.setSort({sortBy, sortOrder});
|
||||
}
|
||||
|
||||
render() {
|
||||
const active = this.props.sortOrder === sortOrder && this.props.sortBy === sortBy;
|
||||
const resolvedLabel = typeof label === 'function' ? label() : label;
|
||||
return (
|
||||
<WrappedComponent
|
||||
active={active}
|
||||
setSort={this.setSort}
|
||||
label={resolvedLabel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return connect(mapStateToProps, mapDispatchToProps)(WithSortOption);
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export const sortOrderSelector = (state) => state.stream.sortOrder;
|
||||
export const sortBySelector = (state) => state.stream.sortBy;
|
||||
@@ -1,8 +1,10 @@
|
||||
export {
|
||||
isTagged,
|
||||
insertCommentsSorted,
|
||||
prependNewNodes,
|
||||
appendNewNodes,
|
||||
getSlotFragmentSpreads,
|
||||
forEachError,
|
||||
capitalize,
|
||||
getErrorMessages,
|
||||
getDefinitionName,
|
||||
} from 'coral-framework/utils';
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
const wrapResponse = require('../../../graph/helpers/response');
|
||||
const {SEARCH_OTHER_USERS} = require('../../../perms/constants');
|
||||
const errors = require('../../../errors');
|
||||
const pluralize = require('pluralize');
|
||||
|
||||
function getReactionConfig(reaction) {
|
||||
reaction = reaction.toLowerCase();
|
||||
|
||||
const reactionPlural = pluralize(reaction);
|
||||
const Reaction = reaction.charAt(0).toUpperCase() + reaction.slice(1);
|
||||
const REACTION = reaction.toUpperCase();
|
||||
const REACTION_PLURAL = reactionPlural.toUpperCase();
|
||||
const typeDefs = `
|
||||
enum ACTION_TYPE {
|
||||
|
||||
@@ -26,6 +29,13 @@ function getReactionConfig(reaction) {
|
||||
item_id: ID!
|
||||
}
|
||||
|
||||
enum SORT_COMMENTS_BY {
|
||||
|
||||
# Comments will be sorted by their count of ${reactionPlural}
|
||||
# on the comment.
|
||||
${REACTION_PLURAL}
|
||||
}
|
||||
|
||||
input Delete${Reaction}ActionInput {
|
||||
|
||||
# The item's id for which we are deleting a ${reaction}.
|
||||
@@ -107,6 +117,29 @@ function getReactionConfig(reaction) {
|
||||
|
||||
return {
|
||||
typeDefs,
|
||||
context: {
|
||||
Sort: () => ({
|
||||
Comments: {
|
||||
[reactionPlural]: {
|
||||
startCursor(ctx, nodes, {cursor}) {
|
||||
|
||||
// The cursor is the start! This is using numeric pagination.
|
||||
return cursor != null ? cursor : 0;
|
||||
},
|
||||
endCursor(ctx, nodes, {cursor}) {
|
||||
return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null;
|
||||
},
|
||||
sort(ctx, query, {cursor, sortOrder}) {
|
||||
if (cursor) {
|
||||
query = query.skip(cursor);
|
||||
}
|
||||
|
||||
return query.sort({[`action_counts.${reaction}`]: sortOrder === 'DESC' ? -1 : 1, created_at: sortOrder === 'DESC' ? -1 : 1});
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
resolvers: {
|
||||
Subscription: {
|
||||
[`${reaction}ActionCreated`]: ({action}) => {
|
||||
|
||||
+10
-2
@@ -4,7 +4,11 @@
|
||||
"talk-plugin-respect",
|
||||
"talk-plugin-offtopic",
|
||||
"talk-plugin-facebook-auth",
|
||||
"talk-plugin-featured-comments"
|
||||
"talk-plugin-featured-comments",
|
||||
"talk-plugin-sort-newest",
|
||||
"talk-plugin-sort-oldest",
|
||||
"talk-plugin-sort-most-liked",
|
||||
"talk-plugin-sort-most-replied"
|
||||
],
|
||||
"client": [
|
||||
"talk-plugin-respect",
|
||||
@@ -13,6 +17,10 @@
|
||||
"talk-plugin-viewing-options",
|
||||
"talk-plugin-comment-content",
|
||||
"talk-plugin-permalink",
|
||||
"talk-plugin-featured-comments"
|
||||
"talk-plugin-featured-comments",
|
||||
"talk-plugin-sort-newest",
|
||||
"talk-plugin-sort-oldest",
|
||||
"talk-plugin-sort-most-respected",
|
||||
"talk-plugin-sort-most-replied"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export const FakeComment = ({username, created_at, body}) => (
|
||||
<div className={styles.root}>
|
||||
<span className={styles.authorName}>
|
||||
{username}
|
||||
</span>;
|
||||
</span>
|
||||
<PubDate created_at={created_at} />
|
||||
<div className={styles.body}>
|
||||
{body}
|
||||
|
||||
@@ -17,7 +17,7 @@ const enhance = compose(
|
||||
withFragments({
|
||||
asset: gql`
|
||||
fragment TalkFeaturedComments_Tab_asset on Asset {
|
||||
featuredCommentsCount: totalCommentCount(tags: ["FEATURED"], excludeIgnored: $excludeIgnored) @skip(if: $hasComment)
|
||||
featuredCommentsCount: totalCommentCount(tags: ["FEATURED"]) @skip(if: $hasComment)
|
||||
}`,
|
||||
}),
|
||||
excludeIf((props) => props.asset.featuredCommentsCount === 0),
|
||||
|
||||
@@ -6,7 +6,7 @@ import {withFragments, connect} from 'plugin-api/beta/client/hocs';
|
||||
import Comment from '../containers/Comment';
|
||||
import {addNotification} from 'plugin-api/beta/client/actions/notification';
|
||||
import {viewComment} from 'coral-embed-stream/src/actions/stream';
|
||||
import {insertCommentsSorted, getDefinitionName} from 'plugin-api/beta/client/utils';
|
||||
import {appendNewNodes, getDefinitionName} from 'plugin-api/beta/client/utils';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
class TabPaneContainer extends React.Component {
|
||||
@@ -18,7 +18,8 @@ class TabPaneContainer extends React.Component {
|
||||
limit: 5,
|
||||
cursor: this.props.asset.featuredComments.endCursor,
|
||||
asset_id: this.props.asset.id,
|
||||
sort: 'REVERSE_CHRONOLOGICAL',
|
||||
sortOrder: this.props.data.variables.sortOrder,
|
||||
sortBy: this.props.data.variables.sortBy,
|
||||
excludeIgnored: this.props.data.variables.excludeIgnored,
|
||||
},
|
||||
updateQuery: (previous, {fetchMoreResult:{comments}}) => {
|
||||
@@ -26,7 +27,7 @@ class TabPaneContainer extends React.Component {
|
||||
asset: {
|
||||
featuredComments: {
|
||||
nodes: {
|
||||
$apply: (nodes) => insertCommentsSorted(nodes, comments.nodes, 'REVERSE_CHRONOLOGICAL'),
|
||||
$apply: (nodes) => appendNewNodes(nodes, comments.nodes),
|
||||
},
|
||||
hasNextPage: {$set: comments.hasNextPage},
|
||||
endCursor: {$set: comments.endCursor},
|
||||
@@ -47,8 +48,25 @@ class TabPaneContainer extends React.Component {
|
||||
}
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query TalkFeaturedComments_LoadMoreComments($limit: Int = 5, $cursor: Date, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, tags: ["FEATURED"], asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) {
|
||||
query TalkFeaturedComments_LoadMoreComments(
|
||||
$limit: Int = 5
|
||||
$cursor: Cursor
|
||||
$asset_id: ID
|
||||
$sortOrder: SORT_ORDER
|
||||
$sortBy: SORT_COMMENTS_BY
|
||||
$excludeIgnored: Boolean
|
||||
) {
|
||||
comments(
|
||||
query: {
|
||||
limit: $limit
|
||||
cursor: $cursor
|
||||
tags: ["FEATURED"]
|
||||
asset_id: $asset_id,
|
||||
sortOrder: $sortOrder
|
||||
sortBy: $sortBy
|
||||
excludeIgnored: $excludeIgnored
|
||||
}
|
||||
) {
|
||||
nodes {
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
}
|
||||
@@ -79,7 +97,14 @@ const enhance = compose(
|
||||
asset: gql`
|
||||
fragment TalkFeaturedComments_TabPane_asset on Asset {
|
||||
id
|
||||
featuredComments: comments(tags: ["FEATURED"], excludeIgnored: $excludeIgnored, deep: true) @skip(if: $hasComment) {
|
||||
featuredComments: comments(
|
||||
query: {
|
||||
tags: ["FEATURED"]
|
||||
sortOrder: $sortOrder
|
||||
sortBy: $sortBy
|
||||
}
|
||||
deep: true
|
||||
) @skip(if: $hasComment) {
|
||||
nodes {
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import ModTag from './containers/ModTag';
|
||||
import ModSubscription from './containers/ModSubscription';
|
||||
|
||||
import {findCommentInEmbedQuery} from 'coral-embed-stream/src/graphql/utils';
|
||||
import {insertCommentsSorted} from 'plugin-api/beta/client/utils';
|
||||
import {prependNewNodes} from 'plugin-api/beta/client/utils';
|
||||
|
||||
export default {
|
||||
reducer,
|
||||
@@ -60,7 +60,7 @@ export default {
|
||||
asset: {
|
||||
featuredComments: {
|
||||
nodes: {
|
||||
$apply: (nodes) => insertCommentsSorted(nodes, comment, 'REVERSE_CHRONOLOGICAL')
|
||||
$apply: (nodes) => prependNewNodes(nodes, [comment]),
|
||||
}
|
||||
},
|
||||
featuredCommentsCount: {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.offTopic {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.offTopicLabel {
|
||||
padding: 10px 20px;
|
||||
display: block;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import styles from './styles.css';
|
||||
import styles from './OffTopicCheckbox.css';
|
||||
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
.label {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
padding: 6px 16px 6px 10px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
cursor: pointer;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
:global(.talk-plugin-off-topic-comment) {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import styles from './styles.css';
|
||||
import styles from './OffTopicFilter.css';
|
||||
|
||||
export default class OffTopicFilter extends React.Component {
|
||||
|
||||
@@ -20,12 +20,15 @@ export default class OffTopicFilter extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.viewingOption}>
|
||||
<label>
|
||||
<input type="checkbox" onChange={this.handleChange} checked={this.props.checked} />
|
||||
Hide Off-Topic Comments
|
||||
</label>
|
||||
</div>
|
||||
<label className={styles.label}>
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={this.handleChange}
|
||||
checked={this.props.checked}
|
||||
className={styles.input}
|
||||
/>
|
||||
Hide Off-Topic Comments
|
||||
</label>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.tag {
|
||||
background: #D2D7D3;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #4A4A4A;
|
||||
display: inline-block;
|
||||
margin: 0px 5px;
|
||||
padding: 5px 5px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import styles from './styles.css';
|
||||
import styles from './OffTopicTag.css';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
import {isTagged} from 'plugin-api/beta/client/utils';
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
.offTopic {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.offTopicLabel {
|
||||
padding: 10px 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: #D2D7D3;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #4A4A4A;
|
||||
display: inline-block;
|
||||
margin: 0px 5px;
|
||||
padding: 5px 5px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.viewingOption {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
:global(.talk-plugin-off-topic-comment) {
|
||||
display: none;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import reducer from './reducer';
|
||||
|
||||
/**
|
||||
* talk-plugin-offtopic depends on talk-plugin-viewing-options
|
||||
* in other to display filter and use the streamViewingOptions slot
|
||||
* in other to display filter.
|
||||
*/
|
||||
|
||||
export default {
|
||||
@@ -15,6 +15,6 @@ export default {
|
||||
slots: {
|
||||
commentInputDetailArea: [OffTopicCheckbox],
|
||||
commentInfoBar: [OffTopicTag],
|
||||
viewingOptions: [OffTopicFilter]
|
||||
viewingOptionsFilter: [OffTopicFilter]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"add-module-exports",
|
||||
"transform-class-properties",
|
||||
"transform-decorators-legacy",
|
||||
"transform-object-assign",
|
||||
"transform-object-rest-spread",
|
||||
"transform-async-to-generator",
|
||||
"transform-react-jsx"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import translations from './translations.yml';
|
||||
import {createSortOption} from 'plugin-api/beta/client/factories';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
|
||||
const SortOption = createSortOption(
|
||||
() => t('talk-plugin-sort-most-liked.label'),
|
||||
{sortBy: 'LIKES', sortOrder: 'DESC'},
|
||||
);
|
||||
|
||||
/**
|
||||
* This plugin depends on talk-plugin-viewing-options.
|
||||
*/
|
||||
|
||||
export default {
|
||||
translations,
|
||||
slots: {
|
||||
viewingOptionsSort: [SortOption]
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
en:
|
||||
talk-plugin-sort-most-liked:
|
||||
label: Most liked first
|
||||
es:
|
||||
talk-plugin-sort-most-liked:
|
||||
@@ -0,0 +1,2 @@
|
||||
module.exports = {
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"add-module-exports",
|
||||
"transform-class-properties",
|
||||
"transform-decorators-legacy",
|
||||
"transform-object-assign",
|
||||
"transform-object-rest-spread",
|
||||
"transform-async-to-generator",
|
||||
"transform-react-jsx"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import translations from './translations.yml';
|
||||
import {createSortOption} from 'plugin-api/beta/client/factories';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
|
||||
const SortOption = createSortOption(
|
||||
() => t('talk-plugin-sort-most-loved.label'),
|
||||
{sortBy: 'LOVES', sortOrder: 'DESC'},
|
||||
);
|
||||
|
||||
/**
|
||||
* This plugin depends on talk-plugin-viewing-options.
|
||||
*/
|
||||
|
||||
export default {
|
||||
translations,
|
||||
slots: {
|
||||
viewingOptionsSort: [SortOption]
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
en:
|
||||
talk-plugin-sort-most-loved:
|
||||
label: Most loved first
|
||||
es:
|
||||
talk-plugin-sort-most-loved:
|
||||
@@ -0,0 +1,2 @@
|
||||
module.exports = {
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"add-module-exports",
|
||||
"transform-class-properties",
|
||||
"transform-decorators-legacy",
|
||||
"transform-object-assign",
|
||||
"transform-object-rest-spread",
|
||||
"transform-async-to-generator",
|
||||
"transform-react-jsx"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import translations from './translations.yml';
|
||||
import {createSortOption} from 'plugin-api/beta/client/factories';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
|
||||
const SortOption = createSortOption(
|
||||
() => t('talk-plugin-sort-most-replied.label'),
|
||||
{sortBy: 'REPLIES', sortOrder: 'DESC'},
|
||||
);
|
||||
|
||||
/**
|
||||
* This plugin depends on talk-plugin-viewing-options.
|
||||
*/
|
||||
|
||||
export default {
|
||||
translations,
|
||||
slots: {
|
||||
viewingOptionsSort: [SortOption]
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
en:
|
||||
talk-plugin-sort-most-replied:
|
||||
label: Most replied first
|
||||
es:
|
||||
talk-plugin-sort-most-replied:
|
||||
@@ -0,0 +1,2 @@
|
||||
module.exports = {
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"add-module-exports",
|
||||
"transform-class-properties",
|
||||
"transform-decorators-legacy",
|
||||
"transform-object-assign",
|
||||
"transform-object-rest-spread",
|
||||
"transform-async-to-generator",
|
||||
"transform-react-jsx"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import translations from './translations.yml';
|
||||
import {createSortOption} from 'plugin-api/beta/client/factories';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
|
||||
const SortOption = createSortOption(
|
||||
() => t('talk-plugin-sort-most-respected.label'),
|
||||
{sortBy: 'RESPECTS', sortOrder: 'DESC'},
|
||||
);
|
||||
|
||||
/**
|
||||
* This plugin depends on talk-plugin-viewing-options.
|
||||
*/
|
||||
|
||||
export default {
|
||||
translations,
|
||||
slots: {
|
||||
viewingOptionsSort: [SortOption]
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
en:
|
||||
talk-plugin-sort-most-respected:
|
||||
label: Most respected first
|
||||
es:
|
||||
talk-plugin-sort-most-respected:
|
||||
@@ -0,0 +1,2 @@
|
||||
module.exports = {
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"add-module-exports",
|
||||
"transform-class-properties",
|
||||
"transform-decorators-legacy",
|
||||
"transform-object-assign",
|
||||
"transform-object-rest-spread",
|
||||
"transform-async-to-generator",
|
||||
"transform-react-jsx"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import translations from './translations.yml';
|
||||
import {createSortOption} from 'plugin-api/beta/client/factories';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
|
||||
const SortOption = createSortOption(
|
||||
() => t('talk-plugin-sort-newest.label'),
|
||||
{sortBy: 'CREATED_AT', sortOrder: 'DESC'},
|
||||
);
|
||||
|
||||
/**
|
||||
* This plugin depends on talk-plugin-viewing-options.
|
||||
*/
|
||||
|
||||
export default {
|
||||
translations,
|
||||
slots: {
|
||||
viewingOptionsSort: [SortOption]
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
en:
|
||||
talk-plugin-sort-newest:
|
||||
label: Newest first
|
||||
es:
|
||||
talk-plugin-sort-newest:
|
||||
@@ -0,0 +1,2 @@
|
||||
module.exports = {
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"add-module-exports",
|
||||
"transform-class-properties",
|
||||
"transform-decorators-legacy",
|
||||
"transform-object-assign",
|
||||
"transform-object-rest-spread",
|
||||
"transform-async-to-generator",
|
||||
"transform-react-jsx"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user