From 90a8a87eaf59d40b9c2eb17d3cb78f7a2f7e2e6a Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 28 Aug 2017 18:57:32 -0600 Subject: [PATCH 01/33] Added graph API for assets --- graph/mutators/asset.js | 55 ++++++++++++ graph/mutators/comment.js | 2 +- graph/mutators/index.js | 2 + graph/resolvers/root_mutation.js | 6 ++ graph/typeDefs.graphql | 85 ++++++++++++++++++- perms/constants.js | 2 + perms/mutationReducer.js | 4 + routes/api/assets/index.js | 20 ----- services/assets.js | 65 +++++++++++--- .../graph/mutations/updateAssetSettings.js | 70 +++++++++++++++ .../graph/mutations/updateAssetStatus.js | 67 +++++++++++++++ 11 files changed, 342 insertions(+), 36 deletions(-) create mode 100644 graph/mutators/asset.js create mode 100644 test/server/graph/mutations/updateAssetSettings.js create mode 100644 test/server/graph/mutations/updateAssetStatus.js diff --git a/graph/mutators/asset.js b/graph/mutators/asset.js new file mode 100644 index 000000000..462607f5f --- /dev/null +++ b/graph/mutators/asset.js @@ -0,0 +1,55 @@ +const errors = require('../../errors'); +const { + UPDATE_ASSET_SETTINGS, + UPDATE_ASSET_STATUS, +} = require('../../perms/constants'); + +const AssetsService = require('../../services/assets'); +const AssetModel = require('../../models/asset'); + +/** + * updateSettings will update the settings on an asset. + * + * @param {Object} ctx graphql context + * @param {String} id the asset's id to update + * @param {Object} settings the settings to update on the asset. + */ +const updateSettings = async (ctx, id, settings) => AssetsService.overrideSettings(id, settings); + +/** + * updateStatus will update the status of an asset. + * + * @param {Object} ctx graphql context + * @param {String} id the asset's id to update + * @param {Object} status the status to change on the asset relating to it's + * current state. + */ +const updateStatus = async (ctx, id, {closedAt, closedMessage}) => AssetModel.update({ + id, +}, { + $set: { + closedAt, + closedMessage + } +}); + +module.exports = (ctx) => { + let mutators = { + Asset: { + updateSettings: () => Promise.reject(errors.ErrNotAuthorized), + updateStatus: () => Promise.reject(errors.ErrNotAuthorized) + } + }; + + if (ctx.user) { + if (ctx.user.can(UPDATE_ASSET_SETTINGS)) { + mutators.Asset.updateSettings = (id, settings) => updateSettings(ctx, id, settings); + } + + if (ctx.user.can(UPDATE_ASSET_STATUS)) { + mutators.Asset.updateStatus = (id, status) => updateStatus(ctx, id, status); + } + } + + return mutators; +}; diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 3f9e72fc3..4ca044fcf 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -328,7 +328,7 @@ const createPublicComment = async (context, commentInput) => { * @param {String} id identifier of the comment (uuid) * @param {String} status the new status of the comment */ -const setStatus = async ({user, loaders: {Comments}, pubsub}, {id, status}) => { +const setStatus = async ({user, loaders: {Comments}}, {id, status}) => { let comment = await CommentsService.pushStatus(id, status, user ? user.id : null); // If the loaders are present, clear the caches for these values because we diff --git a/graph/mutators/index.js b/graph/mutators/index.js index e7e5df9e7..c48714baf 100644 --- a/graph/mutators/index.js +++ b/graph/mutators/index.js @@ -3,6 +3,7 @@ const debug = require('debug')('talk:graph:mutators'); const Comment = require('./comment'); const Action = require('./action'); +const Asset = require('./asset'); const Tag = require('./tag'); const Token = require('./token'); const User = require('./user'); @@ -14,6 +15,7 @@ let mutators = [ // Load in the core mutators. Comment, Action, + Asset, Tag, Token, User, diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index 685c26a17..9e9249792 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -25,6 +25,12 @@ const RootMutation = { rejectUsername(_, {input: {id, message}}, {mutators: {User}}) { return wrapResponse(null)(User.rejectUsername({id, message})); }, + updateAssetSettings(_, {id, input: settings}, {mutators: {Asset}}) { + return wrapResponse(null)(Asset.updateSettings(id, settings)); + }, + updateAssetStatus(_, {id, input: status}, {mutators: {Asset}}) { + return wrapResponse(null)(Asset.updateStatus(id, status)); + }, ignoreUser(_, {id}, {mutators: {User}}) { return wrapResponse(null)(User.ignoreUser({id})); }, diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 4dff623fd..5b29b2f0e 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -143,7 +143,19 @@ input AssetsQuery { # Limit the number of results to be returned limit: Int = 10 + + # open filters assets that are open/closed/all. Not providing this parameter + # will return all the assets, true will return assets that are open, and false + # will return assets that are closed. + open: Boolean + + # sortOrder specifies the order of the sort for the returned Assets. + sortOrder: SORT_ORDER = DESC + + # Skip results from the last created_at timestamp. + cursor: Cursor } + ################################################################################ ## Tags ################################################################################ @@ -627,6 +639,22 @@ type Asset { author: String } +# AssetConnection represents a paginable subset of a asset list. +type AssetConnection { + + # Indicates that there are more assets after this subset. + hasNextPage: Boolean! + + # Cursor of first asset in subset. + startCursor: Date + + # Cursor of last asset in subset. + endCursor: Date + + # Subset of assets. + nodes: [Asset!]! +} + ################################################################################ ## Errors ################################################################################ @@ -714,7 +742,7 @@ type RootQuery { comment(id: ID!): Comment # All assets. Requires the `ADMIN` role. - assets(query: AssetsQuery): [Asset] + assets(query: AssetsQuery): AssetConnection # Find or create an asset by url, or just find with the ID. asset(id: ID, url: String): Asset @@ -884,6 +912,54 @@ input RejectUsernameInput { message: String! } +# Configurable settings that can be overridden for the Asset. +input AssetSettingsInput { + + # premodLinksEnable will put all comments that contain links into premod. + premodLinksEnable: Boolean + + # moderation is the moderation mode for the asset. + moderation: MODERATION_MODE! + + # questionBoxEnable will enable the Question Boxs' content to be visable above + # the comment box. + questionBoxEnable: Boolean + + # questionBoxContent is the content of the Question Box. + questionBoxContent: String + + # questionBoxIcon is the icon for the Question Box. + questionBoxIcon: String +} + +# UpdateAssetStatusInput contains the input to change the status of a comment as +# it relates to being open/closed for commenting. +input UpdateAssetStatusInput { + + # closedAt is the time that the asset will be closed for commenting. If this + # is null or in the future, it will be open for commenting. + closedAt: Date + + # closedMessage is the message to be set on the asset when it is closed. + closedMessage: String +} + +# UpdateAssetStatusResponse is the response returned with possibly some errors +# relating to the update status attempt. +type UpdateAssetStatusResponse implements Response { + + # An array of errors relating to the mutation that occurred. + errors: [UserError!] +} + +# UpdateAssetSettingsResponse is the response returned with possibly some errors +# relating to the update settings attempt. +type UpdateAssetSettingsResponse implements Response { + + # An array of errors relating to the mutation that occurred. + errors: [UserError!] +} + # DeleteActionResponse is the response returned with possibly some errors # relating to the delete action attempt. type DeleteActionResponse implements Response { @@ -1044,6 +1120,13 @@ type RootMutation { # Removes a tag. removeTag(tag: ModifyTagInput!): ModifyTagResponse! + # Updates settings on a given asset. + updateAssetSettings(id: ID!, input: AssetSettingsInput!): UpdateAssetSettingsResponse + + # Updates the status of an asset allowing you to close/reopen an asset for + # commenting. + updateAssetStatus(id: ID!, input: UpdateAssetStatusInput!): UpdateAssetStatusResponse + # Ignore comments by another user ignoreUser(id: ID!): IgnoreUserResponse diff --git a/perms/constants.js b/perms/constants.js index 383f36daf..a139635d9 100644 --- a/perms/constants.js +++ b/perms/constants.js @@ -16,6 +16,8 @@ module.exports = { UPDATE_CONFIG: 'UPDATE_CONFIG', CREATE_TOKEN: 'CREATE_TOKEN', REVOKE_TOKEN: 'REVOKE_TOKEN', + UPDATE_ASSET_SETTINGS: 'UPDATE_ASSET_SETTINGS', + UPDATE_ASSET_STATUS: 'UPDATE_ASSET_STATUS', // queries SEARCH_ASSETS: 'SEARCH_ASSETS', diff --git a/perms/mutationReducer.js b/perms/mutationReducer.js index 897217348..5e0e68fcb 100644 --- a/perms/mutationReducer.js +++ b/perms/mutationReducer.js @@ -33,6 +33,10 @@ module.exports = (user, perm) => { return check(user, ['ADMIN']); case types.REVOKE_TOKEN: return check(user, ['ADMIN']); + case types.UPDATE_ASSET_SETTINGS: + return check(user, ['ADMIN', 'MODERATOR']); + case types.UPDATE_ASSET_STATUS: + return check(user, ['ADMIN', 'MODERATOR']); default: break; } diff --git a/routes/api/assets/index.js b/routes/api/assets/index.js index a306b36b4..87014b86e 100644 --- a/routes/api/assets/index.js +++ b/routes/api/assets/index.js @@ -1,7 +1,6 @@ const express = require('express'); const router = express.Router(); -const scraper = require('../../../services/scraper'); const errors = require('../../../errors'); const AssetsService = require('../../../services/assets'); @@ -88,25 +87,6 @@ router.get('/:asset_id', async (req, res, next) => { } }); -// Adds the asset id to the queue to be scraped. -router.post('/:asset_id/scrape', async (req, res, next) => { - try { - - // Send back the asset. - let asset = await AssetsService.findById(req.params.asset_id); - if (!asset) { - return next(errors.ErrNotFound); - } - - let job = await scraper.create(asset); - - // Send the job back for monitoring. - res.status(201).json(job); - } catch (e) { - return next(e); - } -}); - router.put('/:asset_id/settings', async (req, res, next) => { try { await AssetsService.overrideSettings(req.params.asset_id, req.body); diff --git a/services/assets.js b/services/assets.js index 74bc1aa3a..fd34a6f2b 100644 --- a/services/assets.js +++ b/services/assets.js @@ -108,19 +108,57 @@ module.exports = class AssetsService { * @param {String} value string to search by. * @return {Promise} */ - static search({value, skip, limit} = {}) { - if (!value) { - return AssetsService.all(skip, limit); - } else { - return AssetModel - .find({ - $text: { - $search: value - } - }) - .skip(skip) - .limit(limit); + static search({value, limit, open, sortOrder, cursor} = {}) { + let assets = AssetModel.find({}); + + if (value) { + assets.merge({ + $text: { + $search: value + } + }); } + + if (open != null) { + if (open) { + assets.merge({ + $or: [ + { + closedAt: null + }, + { + closedAt: { + $gt: Date.now() + } + } + ] + }); + } else { + assets.merge({ + closedAt: { + $lt: Date.now() + } + }); + } + } + + if (cursor) { + if (sortOrder === 'DESC') { + assets.merge({ + created_at: { + $lt: cursor, + }, + }); + } else { + assets.merge({ + created_at: { + $gt: cursor, + }, + }); + } + } + + return assets.limit(limit); } /** @@ -185,10 +223,9 @@ module.exports = class AssetsService { // That's it! } - static all(skip = null, limit = null) { + static all(limit = undefined) { return AssetModel .find({}) - .skip(skip) .limit(limit); } }; diff --git a/test/server/graph/mutations/updateAssetSettings.js b/test/server/graph/mutations/updateAssetSettings.js new file mode 100644 index 000000000..3fda0bd6c --- /dev/null +++ b/test/server/graph/mutations/updateAssetSettings.js @@ -0,0 +1,70 @@ +const {graphql} = require('graphql'); + +const schema = require('../../../../graph/schema'); +const Context = require('../../../../graph/context'); +const UserModel = require('../../../../models/user'); +const SettingsService = require('../../../../services/settings'); +const AssetModel = require('../../../../models/asset'); + +const {expect} = require('chai'); + +describe('graph.mutations.updateAssetSettings', () => { + let asset; + beforeEach(async () => { + await SettingsService.init(); + asset = await AssetModel.create({url: 'http://new.test.com/'}); + }); + + const QUERY = ` + mutation UpdateAssetStatus($id: ID!, $settings: AssetSettingsInput!) { + updateAssetSettings(id: $id, input: $settings) { + errors { + translation_key + } + } + } +`; + + describe('context with different user roles', () => { + + [ + {error: 'NOT_AUTHORIZED'}, + {roles: ['ADMIN', 'MODERATOR']}, + {roles: ['MODERATOR']}, + ].forEach(({roles, error}) => { + it(roles ? roles.join(', ') : '', async () => { + const user = new UserModel({roles}); + const ctx = new Context({user}); + + const settings = { + premodLinksEnable: false, + moderation: 'POST', + questionBoxEnable: true, + questionBoxContent: 'Question?', + questionBoxIcon: '', + }; + + const res = await graphql(schema, QUERY, {}, ctx, { + id: asset.id, + settings, + }); + if (res.errors) { + console.error(res.errors); + } + expect(res.errors).to.be.empty; + + if (error) { + expect(res.data.updateAssetSettings.errors).to.not.be.empty; + expect(res.data.updateAssetSettings.errors[0]).to.have.property('translation_key', error); + } else { + expect(res.data.updateAssetSettings.errors).to.be.null; + + const retrievedAsset = await AssetModel.findOne({id: asset.id}); + Object.keys(settings).forEach((key) => { + expect(retrievedAsset.settings).to.have.property(key, settings[key]); + }); + } + }); + }); + }); +}); diff --git a/test/server/graph/mutations/updateAssetStatus.js b/test/server/graph/mutations/updateAssetStatus.js new file mode 100644 index 000000000..b1eb8874c --- /dev/null +++ b/test/server/graph/mutations/updateAssetStatus.js @@ -0,0 +1,67 @@ +const {graphql} = require('graphql'); + +const schema = require('../../../../graph/schema'); +const Context = require('../../../../graph/context'); +const UserModel = require('../../../../models/user'); +const SettingsService = require('../../../../services/settings'); +const AssetModel = require('../../../../models/asset'); + +const {expect} = require('chai'); + +describe('graph.mutations.updateAssetStatus', () => { + let asset; + beforeEach(async () => { + await SettingsService.init(); + asset = await AssetModel.create({url: 'http://new.test.com/'}); + }); + + const QUERY = ` + mutation UpdateAssetStatus($id: ID!, $status: UpdateAssetStatusInput!) { + updateAssetStatus(id: $id, input: $status) { + errors { + translation_key + } + } + } + `; + + describe('context with different user roles', () => { + + [ + {error: 'NOT_AUTHORIZED'}, + {roles: ['ADMIN', 'MODERATOR']}, + {roles: ['MODERATOR']}, + ].forEach(({roles, error}) => { + it(roles ? roles.join(', ') : '', async () => { + const user = new UserModel({roles}); + const ctx = new Context({user}); + + const closedAt = (new Date()).toISOString(); + const closedMessage = 'my closed message!'; + + const res = await graphql(schema, QUERY, {}, ctx, { + id: asset.id, + status: { + closedAt, + closedMessage, + }, + }); + if (res.errors) { + console.error(res.errors); + } + expect(res.errors).to.be.empty; + + if (error) { + expect(res.data.updateAssetStatus.errors).to.not.be.empty; + expect(res.data.updateAssetStatus.errors[0]).to.have.property('translation_key', error); + } else { + expect(res.data.updateAssetStatus.errors).to.be.null; + + const retrievedAsset = await AssetModel.findOne({id: asset.id}); + expect(retrievedAsset.closedAt).to.not.be.null; + expect(retrievedAsset).to.have.property('closedMessage', closedMessage); + } + }); + }); + }); +}); From 173c2571265e73d6e689072258cfe7d302c63a95 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 29 Aug 2017 13:43:48 -0600 Subject: [PATCH 02/33] Added Graph API for Settings --- graph/loaders/settings.js | 4 +- graph/mutators/index.js | 2 + graph/mutators/settings.js | 33 ++++ graph/resolvers/root_mutation.js | 6 + graph/resolvers/settings.js | 18 +++ graph/resolvers/util.js | 28 +++- graph/typeDefs.graphql | 149 +++++++++++++++++- perms/constants.js | 3 + perms/mutationReducer.js | 14 +- perms/queryReducer.js | 2 + services/settings.js | 13 ++ test/server/graph/mutations/updateSettings.js | 74 +++++++++ test/server/graph/mutations/updateWordlist.js | 82 ++++++++++ test/server/graph/queries/settings.js | 97 ++++++++++++ 14 files changed, 506 insertions(+), 19 deletions(-) create mode 100644 graph/mutators/settings.js create mode 100644 test/server/graph/mutations/updateSettings.js create mode 100644 test/server/graph/mutations/updateWordlist.js create mode 100644 test/server/graph/queries/settings.js diff --git a/graph/loaders/settings.js b/graph/loaders/settings.js index 83545821d..2d6189b7d 100644 --- a/graph/loaders/settings.js +++ b/graph/loaders/settings.js @@ -1,5 +1,5 @@ const SettingsService = require('../../services/settings'); -const util = require('./util'); +const {SingletonResolver} = require('./util'); /** * Creates a set of loaders based on a GraphQL context. @@ -7,5 +7,5 @@ const util = require('./util'); * @return {Object} object of loaders */ module.exports = () => ({ - Settings: new util.SingletonResolver(() => SettingsService.retrieve()) + Settings: new SingletonResolver(() => SettingsService.retrieve()) }); diff --git a/graph/mutators/index.js b/graph/mutators/index.js index e7e5df9e7..7f57a773f 100644 --- a/graph/mutators/index.js +++ b/graph/mutators/index.js @@ -3,6 +3,7 @@ const debug = require('debug')('talk:graph:mutators'); const Comment = require('./comment'); const Action = require('./action'); +const Settings = require('./settings'); const Tag = require('./tag'); const Token = require('./token'); const User = require('./user'); @@ -14,6 +15,7 @@ let mutators = [ // Load in the core mutators. Comment, Action, + Settings, Tag, Token, User, diff --git a/graph/mutators/settings.js b/graph/mutators/settings.js new file mode 100644 index 000000000..f725d791d --- /dev/null +++ b/graph/mutators/settings.js @@ -0,0 +1,33 @@ +const errors = require('../../errors'); + +const { + UPDATE_SETTINGS, + UPDATE_WORDLIST, +} = require('../../perms/constants'); + +const SettingsService = require('../../services/settings'); + +const update = async (ctx, settings) => SettingsService.update(settings); + +const updateWordlist = async (ctx, wordlist) => SettingsService.updateWordlist(wordlist); + +module.exports = (ctx) => { + let mutators = { + Settings: { + update: () => Promise.reject(errors.ErrNotAuthorized), + updateWordlist: () => Promise.reject(errors.ErrNotAuthorized) + } + }; + + if (ctx.user) { + if (ctx.user.can(UPDATE_SETTINGS)) { + mutators.Settings.update = (id, settings) => update(ctx, id, settings); + } + + if (ctx.user.can(UPDATE_WORDLIST)) { + mutators.Settings.updateWordlist = (id, status) => updateWordlist(ctx, id, status); + } + } + + return mutators; +}; diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index 685c26a17..c11f657cc 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -50,6 +50,12 @@ const RootMutation = { removeTag(_, {tag}, {mutators: {Tag}}) { return wrapResponse(null)(Tag.remove(tag)); }, + updateSettings(_, {input: settings}, {mutators: {Settings}}) { + return wrapResponse(null)(Settings.update(settings)); + }, + updateWordlist(_, {input: wordlist}, {mutators: {Settings}}) { + return wrapResponse(null)(Settings.updateWordlist(wordlist)); + }, createToken(_, {input}, {mutators: {Token}}) { return wrapResponse('token')(Token.create(input)); }, diff --git a/graph/resolvers/settings.js b/graph/resolvers/settings.js index 72608a9c4..7d12c8a91 100644 --- a/graph/resolvers/settings.js +++ b/graph/resolvers/settings.js @@ -1,3 +1,21 @@ +const { + VIEW_PROTECTED_SETTINGS, +} = require('../../perms/constants'); + +const {decorateWithPermissionCheck} = require('./util'); + const Settings = {}; +// PROTECTED_SETTINGS are the settings keys that must be protected for only some +// eyes. +const PROTECTED_SETTINGS = [ + 'premodLinksEnable', + 'autoCloseStream', + 'wordlist', + 'domains', +]; + +// decorate the fields on the settings resolver with a permission check. +decorateWithPermissionCheck(Settings, VIEW_PROTECTED_SETTINGS, ...PROTECTED_SETTINGS); + module.exports = Settings; diff --git a/graph/resolvers/util.js b/graph/resolvers/util.js index ef3c70865..497f640a9 100644 --- a/graph/resolvers/util.js +++ b/graph/resolvers/util.js @@ -13,6 +13,32 @@ const decorateWithTags = (typeResolver) => { }; }; +/** + * decorateWithPermissionCheck will decorate the field resolver with + * permission checks. + * + * @param {Object} typeResolver the type resolver + * @param {String} permission the permission constant used to check against the user + * @param {Array} fields the fields to apply this check to + */ +const decorateWithPermissionCheck = (typeResolver, permission, ...fields) => { + for (const field of fields) { + let fieldResolver = (obj) => obj[field]; + if (field in typeResolver) { + fieldResolver = typeResolver[field]; + } + + typeResolver[field] = (obj, args, ctx, info) => { + if (!ctx.user || !ctx.user.can(permission)) { + return null; + } + + return fieldResolver(obj, args, ctx, info); + }; + } +}; + module.exports = { - decorateWithTags + decorateWithTags, + decorateWithPermissionCheck, }; diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 4dff623fd..7d5f032cf 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -551,27 +551,81 @@ enum MODERATION_MODE { POST } -# Site wide global settings. +# Wordlist describes all the available wordlists. +type Wordlist { + + # banned words will by default reject the comment if it is found. + banned: [String!]! + + # suspect words will simply flag the comment. + suspect: [String!]! +} + +# Domains describes all the available lists of domains. +type Domains { + + # whitelist is the list of domains that the embed is allowed to render on. + whitelist: [String!]! +} + +# Settings stores the global settings for a given installation. type Settings { - # Moderation mode for the site. + # moderation is the moderation mode for all Asset's on the site. moderation: MODERATION_MODE! # Enables a requirement for email confirmation before a user can login. requireEmailConfirmation: Boolean + # infoBoxEnable will enable the Info Box content visible above the question + # box. infoBoxEnable: Boolean + + # infoBoxContent is the content of the Info Box. infoBoxContent: String - premodLinksEnable: Boolean + + # questionBoxEnable will enable the Question Box's content to be visible above + # the comment box. questionBoxEnable: Boolean + + # questionBoxContent is the content of the Question Box. questionBoxContent: String + + # premodLinksEnable will put all comments that contain links into premod. + premodLinksEnable: Boolean + + # questionBoxIcon is the icon for the Question Box. questionBoxIcon: String - closeTimeout: Int + + # autoCloseStream when true will auto close the stream when the `closeTimeout` + # amount of seconds have been reached. + autoCloseStream: Boolean + + # customCssUrl is the URL of the custom CSS used to display on the frontend. + customCssUrl: String + + # closedTimeout is the amount of seconds from the created_at timestamp that a + # given asset will be considered closed. + closedTimeout: Int + + # closedMessage is the message shown to the user when the given Asset is + # closed. closedMessage: String + + # charCountEnable is true when the character count restriction is enabled. charCountEnable: Boolean + + # charCount is the maximum number of characters a comment may be. charCount: Int + # organizationName is the name of the organization. organizationName: String + + # wordlist will return a given list of words. + wordlist: Wordlist + + # domains will return a given list of domains. + domains: Domains } ################################################################################ @@ -977,6 +1031,87 @@ type EditCommentResponse implements Response { errors: [UserError!] } +# UpdateSettingsInput is the input used to input the global site settings. This +# will override the existing settings, so all fields must be included. +input UpdateSettingsInput { + + # moderation is the moderation mode for all Asset's on the site. + moderation: MODERATION_MODE! + + # Enables a requirement for email confirmation before a user can login. + requireEmailConfirmation: Boolean + + # infoBoxEnable will enable the Info Box content visible above the question + # box. + infoBoxEnable: Boolean + + # infoBoxContent is the content of the Info Box. + infoBoxContent: String + + # questionBoxEnable will enable the Question Box's content to be visible above + # the comment box. + questionBoxEnable: Boolean + + # questionBoxContent is the content of the Question Box. + questionBoxContent: String + + # premodLinksEnable will put all comments that contain links into premod. + premodLinksEnable: Boolean + + # questionBoxIcon is the icon for the Question Box. + questionBoxIcon: String + + # autoCloseStream when true will auto close the stream when the `closeTimeout` + # amount of seconds have been reached. + autoCloseStream: Boolean + + # customCssUrl is the URL of the custom CSS used to display on the frontend. + customCssUrl: String + + # closeTimeout is the amount of seconds from the created_at timestamp that a + # given asset will be considered closed. + closeTimeout: Int + + # closedMessage is the message shown to the user when the given Asset is + # closed. + closedMessage: String + + # charCountEnable is true when the character count restriction is enabled. + charCountEnable: Boolean + + # charCount is the maximum number of characters a comment may be. + charCount: Int + + # organizationName is the name of the organization. + organizationName: String +} + +# UpdateSettingsResponse contains any errors that were rendered as a result +# of the mutation. +type UpdateSettingsResponse implements Response { + + # An array of errors relating to the mutation that occurred. + errors: [UserError!] +} + +# UpdateWordlistInput is the list of words that composes the Wordlist. +input UpdateWordlistInput { + + # banned words will by default reject the comment if it is found. + banned: [String!]! + + # suspect words will simply flag the comment. + suspect: [String!]! +} + +# UpdateWordlistResponse contains any errors that were rendered as a result +# of the mutation. +type UpdateWordlistResponse implements Response { + + # An array of errors relating to the mutation that occurred. + errors: [UserError!] +} + # CreateTokenInput contains the input to create the token. input CreateTokenInput { @@ -1044,6 +1179,12 @@ type RootMutation { # Removes a tag. removeTag(tag: ModifyTagInput!): ModifyTagResponse! + # updateSettings will update the global settings. + updateSettings(input: UpdateSettingsInput!): UpdateSettingsResponse! + + # updateWordlist will update the given Wordlist. + updateWordlist(input: UpdateWordlistInput!): UpdateWordlistResponse! + # Ignore comments by another user ignoreUser(id: ID!): IgnoreUserResponse diff --git a/perms/constants.js b/perms/constants.js index 383f36daf..8ed1e2b4e 100644 --- a/perms/constants.js +++ b/perms/constants.js @@ -16,6 +16,8 @@ module.exports = { UPDATE_CONFIG: 'UPDATE_CONFIG', CREATE_TOKEN: 'CREATE_TOKEN', REVOKE_TOKEN: 'REVOKE_TOKEN', + UPDATE_SETTINGS: 'UPDATE_SETTINGS', + UPDATE_WORDLIST: 'UPDATE_WORDLIST', // queries SEARCH_ASSETS: 'SEARCH_ASSETS', @@ -27,6 +29,7 @@ module.exports = { LIST_OWN_TOKENS: 'LIST_OWN_TOKENS', SEARCH_COMMENT_STATUS_HISTORY: 'SEARCH_COMMENT_STATUS_HISTORY', VIEW_SUSPENSION_INFO: 'VIEW_SUSPENSION_INFO', + VIEW_PROTECTED_SETTINGS: 'VIEW_PROTECTED_SETTINGS', // subscriptions SUBSCRIBE_COMMENT_ACCEPTED: 'SUBSCRIBE_COMMENT_ACCEPTED', diff --git a/perms/mutationReducer.js b/perms/mutationReducer.js index 897217348..1ea46e9cf 100644 --- a/perms/mutationReducer.js +++ b/perms/mutationReducer.js @@ -4,33 +4,23 @@ const types = require('./constants'); module.exports = (user, perm) => { switch (perm) { case types.CREATE_COMMENT: - return true; case types.CREATE_ACTION: - return true; case types.DELETE_ACTION: - return true; case types.EDIT_NAME: - return true; case types.EDIT_COMMENT: return true; case types.UPDATE_USER_ROLES: - return check(user, ['ADMIN']); case types.REJECT_USERNAME: - return check(user, ['ADMIN', 'MODERATOR']); case types.SET_USER_STATUS: - return check(user, ['ADMIN', 'MODERATOR']); case types.SUSPEND_USER: - return check(user, ['ADMIN', 'MODERATOR']); case types.SET_COMMENT_STATUS: - return check(user, ['ADMIN', 'MODERATOR']); case types.ADD_COMMENT_TAG: - return check(user, ['ADMIN', 'MODERATOR']); case types.REMOVE_COMMENT_TAG: - return check(user, ['ADMIN', 'MODERATOR']); case types.UPDATE_CONFIG: + case types.UPDATE_SETTINGS: + case types.UPDATE_WORDLIST: return check(user, ['ADMIN', 'MODERATOR']); case types.CREATE_TOKEN: - return check(user, ['ADMIN']); case types.REVOKE_TOKEN: return check(user, ['ADMIN']); default: diff --git a/perms/queryReducer.js b/perms/queryReducer.js index 0b76c225a..b71c3d964 100644 --- a/perms/queryReducer.js +++ b/perms/queryReducer.js @@ -21,6 +21,8 @@ module.exports = (user, perm) => { return check(user, ['ADMIN', 'MODERATOR']); case types.VIEW_SUSPENSION_INFO: return check(user, ['ADMIN', 'MODERATOR']); + case types.VIEW_PROTECTED_SETTINGS: + return check(user, ['ADMIN', 'MODERATOR']); default: break; } diff --git a/services/settings.js b/services/settings.js index 2bb15319d..3033701d8 100644 --- a/services/settings.js +++ b/services/settings.js @@ -42,6 +42,19 @@ module.exports = class SettingsService { }); } + /** + * updateWordlist will update the wordlists. + * + * @param {Object} wordlist the Wordlist object + */ + static updateWordlist(wordlist) { + return SettingModel.findOneAndUpdate(selector, { + $set: { + wordlist, + }, + }); + } + /** * This is run once when the app starts to ensure settings are populated. */ diff --git a/test/server/graph/mutations/updateSettings.js b/test/server/graph/mutations/updateSettings.js new file mode 100644 index 000000000..27269b70a --- /dev/null +++ b/test/server/graph/mutations/updateSettings.js @@ -0,0 +1,74 @@ +const {graphql} = require('graphql'); + +const schema = require('../../../../graph/schema'); +const Context = require('../../../../graph/context'); +const UserModel = require('../../../../models/user'); +const SettingsService = require('../../../../services/settings'); + +const {expect} = require('chai'); + +describe('graph.mutations.updateSettings', () => { + beforeEach(async () => { + await SettingsService.init(); + }); + + const QUERY = ` + mutation UpdateSettings($settings: UpdateSettingsInput!) { + updateSettings(input: $settings) { + errors { + translation_key + } + } + } +`; + + describe('context with different user roles', () => { + + [ + {error: 'NOT_AUTHORIZED'}, + {error: 'NOT_AUTHORIZED', roles: []}, + {roles: ['ADMIN']}, + {roles: ['ADMIN', 'MODERATOR']}, + {roles: ['MODERATOR']}, + ].forEach(({roles, error}) => { + it(roles ? roles.join(', ') : '', async () => { + let user; + if (roles != null) { + user = new UserModel({roles}); + } + const ctx = new Context({user}); + + const newSettings = { + premodLinksEnable: false, + moderation: 'POST', + questionBoxEnable: true, + questionBoxContent: 'Question?', + questionBoxIcon: '', + }; + + const res = await graphql(schema, QUERY, {}, ctx, { + settings: newSettings, + }); + if (res.errors) { + console.error(res.errors); + } + expect(res.errors).to.be.empty; + + if (error) { + expect(res.data.updateSettings.errors).to.not.be.empty; + expect(res.data.updateSettings.errors[0]).to.have.property('translation_key', error); + } else { + if (res.data.updateSettings.errors) { + console.error(res.data.updateSettings.errors); + } + expect(res.data.updateSettings.errors).to.be.null; + + const retrievedSettings = await SettingsService.retrieve(); + Object.keys(newSettings).forEach((key) => { + expect(retrievedSettings).to.have.property(key, newSettings[key]); + }); + } + }); + }); + }); +}); diff --git a/test/server/graph/mutations/updateWordlist.js b/test/server/graph/mutations/updateWordlist.js new file mode 100644 index 000000000..b2f2e4abb --- /dev/null +++ b/test/server/graph/mutations/updateWordlist.js @@ -0,0 +1,82 @@ +const {graphql} = require('graphql'); + +const schema = require('../../../../graph/schema'); +const Context = require('../../../../graph/context'); +const UserModel = require('../../../../models/user'); +const SettingsService = require('../../../../services/settings'); + +const {expect} = require('chai'); + +describe('graph.mutations.updateWordlist', () => { + beforeEach(async () => { + await SettingsService.init(); + }); + + const QUERY = ` + mutation UpdateWordlist($wordlist: UpdateWordlistInput!) { + updateWordlist(input: $wordlist) { + errors { + translation_key + } + } + } +`; + + describe('context with different user roles', () => { + + [ + {error: 'NOT_AUTHORIZED'}, + {error: 'NOT_AUTHORIZED', roles: []}, + {roles: ['ADMIN']}, + {roles: ['ADMIN', 'MODERATOR']}, + {roles: ['MODERATOR']}, + ].forEach(({roles, error}) => { + it(roles && roles.length > 0 ? roles.join(', ') : '', async () => { + let user; + if (roles != null) { + user = new UserModel({roles}); + } + const ctx = new Context({user}); + + const wordlist = { + banned: [ + 'happy', + ], + suspect: [ + 'sad', + ], + }; + + const res = await graphql(schema, QUERY, {}, ctx, { + wordlist, + }); + if (res.errors) { + console.error(res.errors); + } + expect(res.errors).to.be.empty; + + if (error) { + expect(res.data.updateWordlist.errors).to.not.be.empty; + expect(res.data.updateWordlist.errors[0]).to.have.property('translation_key', error); + + const {wordlist: retrievedWordlist} = await SettingsService.retrieve(); + expect(retrievedWordlist).to.have.property('banned'); + expect(retrievedWordlist.banned).to.have.members([]); + expect(retrievedWordlist).to.have.property('suspect'); + expect(retrievedWordlist.suspect).to.have.members([]); + } else { + if (res.data.updateWordlist.errors) { + console.error(res.data.updateWordlist.errors); + } + expect(res.data.updateWordlist.errors).to.be.null; + + const {wordlist: retrievedWordlist} = await SettingsService.retrieve(); + expect(retrievedWordlist).to.have.property('banned'); + expect(retrievedWordlist.banned).to.have.members(wordlist.banned); + expect(retrievedWordlist).to.have.property('suspect'); + expect(retrievedWordlist.suspect).to.have.members(wordlist.suspect); + } + }); + }); + }); +}); diff --git a/test/server/graph/queries/settings.js b/test/server/graph/queries/settings.js new file mode 100644 index 000000000..338999145 --- /dev/null +++ b/test/server/graph/queries/settings.js @@ -0,0 +1,97 @@ +const {graphql} = require('graphql'); + +const schema = require('../../../../graph/schema'); +const Context = require('../../../../graph/context'); +const SettingsService = require('../../../../services/settings'); +const UserModel = require('../../../../models/user'); + +const {expect} = require('chai'); + +const defaultSettings = { + organizationName: 'The Coral Project' +}; + +describe('graph.queries.settings', () => { + let settings; + beforeEach(async () => { + settings = await SettingsService.init(defaultSettings); + }); + + const QUERY = ` + { + settings { + moderation + requireEmailConfirmation + infoBoxEnable + infoBoxContent + questionBoxEnable + questionBoxContent + premodLinksEnable + questionBoxIcon + autoCloseStream + customCssUrl + closedTimeout + closedMessage + charCountEnable + charCount + organizationName + wordlist { + banned + suspect + } + domains { + whitelist + } + } + } + `; + + describe('context with different user roles', () => { + + const BLACKLISTED_PROPERTIES = [ + 'premodLinksEnable', + 'autoCloseStream', + 'wordlist', + 'domains', + ]; + + [ + {bl: true}, + {bl: true, roles: []}, + {bl: false, roles: ['ADMIN']}, + {bl: false, roles: ['ADMIN', 'MODERATOR']}, + {bl: false, roles: ['MODERATOR']}, + ].forEach(({bl, roles}) => { + it(roles && roles.length > 0 ? roles.join(', ') : '', async () => { + let user; + if (roles != null) { + user = new UserModel({roles}); + } + + const ctx = new Context({user}); + + const res = await graphql(schema, QUERY, {}, ctx); + if (res.errors) { + console.error(res.errors); + } + + expect(res.errors).to.be.empty; + expect(res.data.settings).to.be.object; + Object.keys(res.data.settings).forEach((key) => { + if (bl && BLACKLISTED_PROPERTIES.includes(key)) { + expect(res.data.settings).to.have.property(key, null); + return; + } + + if (typeof settings[key] !== 'object') { + expect(res.data.settings).to.have.property(key, settings[key]); + } else { + expect(res.data.settings).to.have.property(key); + expect(res.data.settings[key]).to.not.be.null; + } + }); + }); + }); + }); + +}); From 8ca32c580800991f47d84e3598cd79a46b91e1b5 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 29 Aug 2017 13:54:14 -0600 Subject: [PATCH 03/33] added missing fields --- graph/typeDefs.graphql | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 7d5f032cf..7ee44eb19 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -612,6 +612,10 @@ type Settings { # closed. closedMessage: String + # editCommentWindowLength is the length of time (in milliseconds) after a + # comment is posted that it can still be edited by the author. + editCommentWindowLength: Int + # charCountEnable is true when the character count restriction is enabled. charCountEnable: Boolean @@ -1084,6 +1088,10 @@ input UpdateSettingsInput { # organizationName is the name of the organization. organizationName: String + + # editCommentWindowLength is the length of time (in milliseconds) after a + # comment is posted that it can still be edited by the author. + editCommentWindowLength: Int } # UpdateSettingsResponse contains any errors that were rendered as a result From f0dcef7b37956bac41802141406254cf472a587e Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 30 Aug 2017 14:23:47 -0600 Subject: [PATCH 04/33] finished connection, improved graph --- graph/loaders/assets.js | 31 +++++++++++++++-- graph/resolvers/root_query.js | 2 +- graph/typeDefs.graphql | 54 ++++++++++++++++++------------ test/server/graph/queries/asset.js | 6 ++-- 4 files changed, 63 insertions(+), 30 deletions(-) diff --git a/graph/loaders/assets.js b/graph/loaders/assets.js index 956d573ef..b2e315ec5 100644 --- a/graph/loaders/assets.js +++ b/graph/loaders/assets.js @@ -25,8 +25,33 @@ const genAssetsByID = (context, ids) => AssetModel.find({ * @param {Object} query the query * @return {Promise} resolves the assets */ -const getAssetsByQuery = (context, query) => { - return AssetsService.search(query); +const getAssetsByQuery = async (context, query) => { + + // If we are requesting based on a limit, ask for one more than we want. + const limit = query.limit; + if (limit) { + query.limit += 1; + } + + const nodes = await AssetsService.search(query); + + // 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); + } + + return { + startCursor: nodes && nodes.length > 0 ? nodes[0].created_at : null, + endCursor: nodes && nodes.length > 0 ? nodes[nodes.length - 1].created_at : null, + hasNextPage, + nodes, + }; }; /** @@ -84,7 +109,7 @@ module.exports = (context) => ({ getByURL: (url) => findOrCreateAssetByURL(context, url), findByUrl: (url) => findByUrl(context, url), - search: (query) => getAssetsByQuery(context, query), + getByQuery: (query) => getAssetsByQuery(context, query), getByID: new DataLoader((ids) => genAssetsByID(context, ids)), getForMetrics: () => getAssetsForMetrics(context), getAll: new util.SingletonResolver(() => AssetModel.find({})) diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 1e34e8ecd..25cd9a8e8 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -11,7 +11,7 @@ const RootQuery = { return null; } - return Assets.search(query); + return Assets.getByQuery(query); }, asset(_, query, {loaders: {Assets}}) { if (query.id) { diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 5b29b2f0e..2a813a02b 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -646,10 +646,10 @@ type AssetConnection { hasNextPage: Boolean! # Cursor of first asset in subset. - startCursor: Date + startCursor: Cursor # Cursor of last asset in subset. - endCursor: Date + endCursor: Cursor # Subset of assets. nodes: [Asset!]! @@ -912,24 +912,25 @@ input RejectUsernameInput { message: String! } -# Configurable settings that can be overridden for the Asset. +# Configurable settings that can be overridden for the Asset. You must specify +# all fields that should be updated. input AssetSettingsInput { # premodLinksEnable will put all comments that contain links into premod. - premodLinksEnable: Boolean + premodLinksEnable: Boolean! # moderation is the moderation mode for the asset. moderation: MODERATION_MODE! # questionBoxEnable will enable the Question Boxs' content to be visable above # the comment box. - questionBoxEnable: Boolean + questionBoxEnable: Boolean! # questionBoxContent is the content of the Question Box. - questionBoxContent: String + questionBoxContent: String! # questionBoxIcon is the icon for the Question Box. - questionBoxIcon: String + questionBoxIcon: String! } # UpdateAssetStatusInput contains the input to change the status of a comment as @@ -940,7 +941,8 @@ input UpdateAssetStatusInput { # is null or in the future, it will be open for commenting. closedAt: Date - # closedMessage is the message to be set on the asset when it is closed. + # closedMessage is the message to be set on the asset when it is closed. If it + # is null, then the message will default to the globally set `closedMessage`. closedMessage: String } @@ -1088,31 +1090,35 @@ type RevokeTokenResponse implements Response { type RootMutation { # Creates a comment on the asset. - createComment(comment: CreateCommentInput!): CreateCommentResponse + createComment(comment: CreateCommentInput!): CreateCommentResponse! # Creates a flag on an entity. - createFlag(flag: CreateFlagInput!): CreateFlagResponse + createFlag(flag: CreateFlagInput!): CreateFlagResponse! # Creates a don't agree action on an entity. - createDontAgree(dontagree: CreateDontAgreeInput!): CreateDontAgreeResponse + createDontAgree(dontagree: CreateDontAgreeInput!): CreateDontAgreeResponse! # Delete an action based on the action id. deleteAction(id: ID!): DeleteActionResponse # Edit a comment - editComment(id: ID!, asset_id: ID!, edit: EditCommentInput): EditCommentResponse + editComment(id: ID!, asset_id: ID!, edit: EditCommentInput): EditCommentResponse! # Sets User status. Requires the `ADMIN` role. - setUserStatus(id: ID!, status: USER_STATUS!): SetUserStatusResponse + # Mutation is restricted. + setUserStatus(id: ID!, status: USER_STATUS!): SetUserStatusResponse! # Suspends a user. Requires the `ADMIN` role. - suspendUser(input: SuspendUserInput!): SuspendUserResponse + # Mutation is restricted. + suspendUser(input: SuspendUserInput!): SuspendUserResponse! # Reject a username. Requires the `ADMIN` role. - rejectUsername(input: RejectUsernameInput!): RejectUsernameResponse + # Mutation is restricted. + rejectUsername(input: RejectUsernameInput!): RejectUsernameResponse! # Sets Comment status. Requires the `ADMIN` role. - setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse + # Mutation is restricted. + setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse! # Add a tag. addTag(tag: ModifyTagInput!): ModifyTagResponse! @@ -1121,23 +1127,27 @@ type RootMutation { removeTag(tag: ModifyTagInput!): ModifyTagResponse! # Updates settings on a given asset. - updateAssetSettings(id: ID!, input: AssetSettingsInput!): UpdateAssetSettingsResponse + # Mutation is restricted. + updateAssetSettings(id: ID!, input: AssetSettingsInput!): UpdateAssetSettingsResponse! # Updates the status of an asset allowing you to close/reopen an asset for # commenting. - updateAssetStatus(id: ID!, input: UpdateAssetStatusInput!): UpdateAssetStatusResponse + # Mutation is restricted. + updateAssetStatus(id: ID!, input: UpdateAssetStatusInput!): UpdateAssetStatusResponse! - # Ignore comments by another user - ignoreUser(id: ID!): IgnoreUserResponse + # Ignore comments by another user. + ignoreUser(id: ID!): IgnoreUserResponse! # CreateToken will create a token that is attached to the current user. + # Mutation is restricted. createToken(input: CreateTokenInput!): CreateTokenResponse! # RevokeToken will revoke an existing token. + # Mutation is restricted. revokeToken(input: RevokeTokenInput!): RevokeTokenResponse! - # Stop Ignoring comments by another user - stopIgnoringUser(id: ID!): StopIgnoringUserResponse + # Stop Ignoring comments by another user. + stopIgnoringUser(id: ID!): StopIgnoringUserResponse! } ################################################################################ diff --git a/test/server/graph/queries/asset.js b/test/server/graph/queries/asset.js index ba605dfd7..b49eb7429 100644 --- a/test/server/graph/queries/asset.js +++ b/test/server/graph/queries/asset.js @@ -74,12 +74,10 @@ describe('graph.queries.asset', () => { expect(asset.nodes).to.have.length(2); expect(asset.hasNextPage).to.be.false; - expect(asset.nodes[0]).to.have.property('id', comments[1].id); - expect(asset.nodes[1]).to.have.property('id', comments[0].id); + expect(asset.nodes.map(({id}) => id)).to.have.members(comments.slice(0, 2).map(({id}) => id)); expect(otherAsset.nodes).to.have.length(2); expect(otherAsset.hasNextPage).to.be.false; - expect(otherAsset.nodes[0]).to.have.property('id', comments[3].id); - expect(otherAsset.nodes[1]).to.have.property('id', comments[2].id); + expect(otherAsset.nodes.map(({id}) => id)).to.have.members(comments.slice(2, 4).map(({id}) => id)); for (let node of asset.nodes) { for (let otherNode of otherAsset.nodes) { From 7b71511caec2750f4150cb710a4ac95b460fdc32 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 31 Aug 2017 10:37:52 -0600 Subject: [PATCH 05/33] removed ! --- graph/typeDefs.graphql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index f038e3505..682f2c690 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -1061,7 +1061,7 @@ type EditCommentResponse implements Response { input UpdateSettingsInput { # moderation is the moderation mode for all Asset's on the site. - moderation: MODERATION_MODE! + moderation: MODERATION_MODE # Enables a requirement for email confirmation before a user can login. requireEmailConfirmation: Boolean From dfd77dbf63ef38a932a598d10abe3d82b5366975 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 31 Aug 2017 11:48:38 -0600 Subject: [PATCH 06/33] updated based on cvle's feedback --- graph/resolvers/settings.js | 14 +++++++------- graph/resolvers/util.js | 9 ++++----- graph/typeDefs.graphql | 2 ++ perms/index.js | 7 ++----- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/graph/resolvers/settings.js b/graph/resolvers/settings.js index 7d12c8a91..30e614c38 100644 --- a/graph/resolvers/settings.js +++ b/graph/resolvers/settings.js @@ -8,14 +8,14 @@ const Settings = {}; // PROTECTED_SETTINGS are the settings keys that must be protected for only some // eyes. -const PROTECTED_SETTINGS = [ - 'premodLinksEnable', - 'autoCloseStream', - 'wordlist', - 'domains', -]; +const PROTECTED_SETTINGS = { + 'premodLinksEnable': [VIEW_PROTECTED_SETTINGS], + 'autoCloseStream': [VIEW_PROTECTED_SETTINGS], + 'wordlist': [VIEW_PROTECTED_SETTINGS], + 'domains': [VIEW_PROTECTED_SETTINGS], +}; // decorate the fields on the settings resolver with a permission check. -decorateWithPermissionCheck(Settings, VIEW_PROTECTED_SETTINGS, ...PROTECTED_SETTINGS); +decorateWithPermissionCheck(Settings, PROTECTED_SETTINGS); module.exports = Settings; diff --git a/graph/resolvers/util.js b/graph/resolvers/util.js index 497f640a9..cf3162679 100644 --- a/graph/resolvers/util.js +++ b/graph/resolvers/util.js @@ -18,18 +18,17 @@ const decorateWithTags = (typeResolver) => { * permission checks. * * @param {Object} typeResolver the type resolver - * @param {String} permission the permission constant used to check against the user - * @param {Array} fields the fields to apply this check to + * @param {Object} protect the object with field -> Array of permissions */ -const decorateWithPermissionCheck = (typeResolver, permission, ...fields) => { - for (const field of fields) { +const decorateWithPermissionCheck = (typeResolver, protect) => { + for (const [field, permissions] of Object.entries(protect)) { let fieldResolver = (obj) => obj[field]; if (field in typeResolver) { fieldResolver = typeResolver[field]; } typeResolver[field] = (obj, args, ctx, info) => { - if (!ctx.user || !ctx.user.can(permission)) { + if (!ctx.user || !ctx.user.can(...permissions)) { return null; } diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 682f2c690..982ee04bd 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -1209,9 +1209,11 @@ type RootMutation { removeTag(tag: ModifyTagInput!): ModifyTagResponse! # updateSettings will update the global settings. + # Mutation is restricted. updateSettings(input: UpdateSettingsInput!): UpdateSettingsResponse! # updateWordlist will update the given Wordlist. + # Mutation is restricted. updateWordlist(input: UpdateWordlistInput!): UpdateWordlistResponse! # Ignore comments by another user diff --git a/perms/index.js b/perms/index.js index be6e7a2cd..a6b459efe 100644 --- a/perms/index.js +++ b/perms/index.js @@ -41,11 +41,8 @@ const findGrant = (user, perms) => { */ module.exports = (user, ...perms) => { - // make sure all the passed permissions are not typos - const missingPerms = perms.filter((perm) => { - return allPermissions.indexOf(perm) === -1; - }); - + // Make sure all the passed permissions are not typos. + const missingPerms = perms.filter((perm) => !allPermissions.includes(perm)); if (missingPerms.length > 0) { throw new Error(`${missingPerms.join(' ')} are not valid permissions.`); } From e368c50687762e70da0446a37ca9aa328639bc58 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 1 Sep 2017 08:44:25 -0600 Subject: [PATCH 07/33] ioredis changed config, fixes #901 --- services/redis.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/redis.js b/services/redis.js index 027d5bc20..59ab3eef8 100644 --- a/services/redis.js +++ b/services/redis.js @@ -65,7 +65,7 @@ const connectionOptions = { }; const createClient = () => { - let client = new Redis(connectionOptions); + let client = new Redis(REDIS_URL, connectionOptions); // Attach the monitors that will print debug messages to the console. attachMonitors(client); From 644788ea319ddb0edd8a741cb1f05ff40d2bd474 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 1 Sep 2017 08:47:08 -0600 Subject: [PATCH 08/33] removed unused option --- services/redis.js | 1 - 1 file changed, 1 deletion(-) diff --git a/services/redis.js b/services/redis.js index 59ab3eef8..5fd1b78d3 100644 --- a/services/redis.js +++ b/services/redis.js @@ -30,7 +30,6 @@ const attachMonitors = (client) => { }; const connectionOptions = { - url: REDIS_URL, retry_strategy: function(options) { if (options.error && options.error.code !== 'ECONNREFUSED') { From 7bb4b03daa8af7cdaffb277aeec3e285bd6c36b8 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Fri, 1 Sep 2017 12:17:48 -0300 Subject: [PATCH 09/33] Retrieving me from Comment triggers a change --- client/coral-embed-stream/src/containers/Comment.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index a4bfbc0eb..37f6e6ebc 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -91,6 +91,11 @@ const singleCommentFragment = gql` const withCommentFragments = withFragments({ root: gql` fragment CoralEmbedStream_Comment_root on RootQuery { + me { + ignoredUsers { + id + } + } __typename ${getSlotFragmentSpreads(slots, 'root')} } From 692d92f645e141c27e9620b7d2c08b7f71e4baf9 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 1 Sep 2017 22:20:49 +0700 Subject: [PATCH 10/33] Implement talk-plugin-author-menu --- .eslintignore | 1 + .gitignore | 1 + .../talk-plugin-author-menu/client/.babelrc | 14 ++++ .../client/.eslintrc.json | 23 ++++++ .../talk-plugin-author-menu/client/actions.js | 10 +++ .../client/components/AuthorName.css | 13 +++ .../client/components/AuthorName.js | 30 +++++++ .../client/components/Menu.css | 37 +++++++++ .../client/components/Menu.js | 18 +++++ .../client/constants.js | 5 ++ .../client/containers/AuthorName.js | 79 +++++++++++++++++++ .../talk-plugin-author-menu/client/index.js | 11 +++ .../talk-plugin-author-menu/client/reducer.js | 22 ++++++ .../client/translations.yml | 4 + plugins/talk-plugin-author-menu/index.js | 1 + plugins/talk-plugin-like/client/index.js | 2 +- 16 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 plugins/talk-plugin-author-menu/client/.babelrc create mode 100644 plugins/talk-plugin-author-menu/client/.eslintrc.json create mode 100644 plugins/talk-plugin-author-menu/client/actions.js create mode 100644 plugins/talk-plugin-author-menu/client/components/AuthorName.css create mode 100644 plugins/talk-plugin-author-menu/client/components/AuthorName.js create mode 100644 plugins/talk-plugin-author-menu/client/components/Menu.css create mode 100644 plugins/talk-plugin-author-menu/client/components/Menu.js create mode 100644 plugins/talk-plugin-author-menu/client/constants.js create mode 100644 plugins/talk-plugin-author-menu/client/containers/AuthorName.js create mode 100644 plugins/talk-plugin-author-menu/client/index.js create mode 100644 plugins/talk-plugin-author-menu/client/reducer.js create mode 100644 plugins/talk-plugin-author-menu/client/translations.yml create mode 100644 plugins/talk-plugin-author-menu/index.js diff --git a/.eslintignore b/.eslintignore index e09212b62..b9523387a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -19,5 +19,6 @@ plugins/* !plugins/talk-plugin-sort-most-liked !plugins/talk-plugin-sort-most-loved !plugins/talk-plugin-sort-most-respected +!plugins/talk-plugin-author-menu node_modules diff --git a/.gitignore b/.gitignore index 3fdb05c89..99eac3ade 100644 --- a/.gitignore +++ b/.gitignore @@ -35,5 +35,6 @@ plugins/* !plugins/talk-plugin-sort-most-liked !plugins/talk-plugin-sort-most-loved !plugins/talk-plugin-sort-most-respected +!plugins/talk-plugin-author-menu **/node_modules/* diff --git a/plugins/talk-plugin-author-menu/client/.babelrc b/plugins/talk-plugin-author-menu/client/.babelrc new file mode 100644 index 000000000..60be246eb --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/.babelrc @@ -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" + ] +} \ No newline at end of file diff --git a/plugins/talk-plugin-author-menu/client/.eslintrc.json b/plugins/talk-plugin-author-menu/client/.eslintrc.json new file mode 100644 index 000000000..9fe56bd14 --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/.eslintrc.json @@ -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"] }] + } +} diff --git a/plugins/talk-plugin-author-menu/client/actions.js b/plugins/talk-plugin-author-menu/client/actions.js new file mode 100644 index 000000000..0a806749d --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/actions.js @@ -0,0 +1,10 @@ +import {SET_CONTENT_SLOT, RESET_CONTENT_SLOT} from './constants'; + +export const setContentSlot = (slot) => ({ + type: SET_CONTENT_SLOT, + slot, +}); + +export const resetContentSlot = () => ({ + type: RESET_CONTENT_SLOT, +}); diff --git a/plugins/talk-plugin-author-menu/client/components/AuthorName.css b/plugins/talk-plugin-author-menu/client/components/AuthorName.css new file mode 100644 index 000000000..386d7cb45 --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/components/AuthorName.css @@ -0,0 +1,13 @@ +.root { + display: inline-block; + position: relative; +} + +.button { + composes: buttonReset from "coral-framework/styles/reset.css"; +} + +.name { + font-weight: bold; +} + diff --git a/plugins/talk-plugin-author-menu/client/components/AuthorName.js b/plugins/talk-plugin-author-menu/client/components/AuthorName.js new file mode 100644 index 000000000..d6e6c66d1 --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/components/AuthorName.js @@ -0,0 +1,30 @@ +import React from 'react'; +import Menu from './Menu'; +import styles from './AuthorName.css'; +import {ClickOutside} from 'plugin-api/beta/client/components'; + +export default ({data, root, asset, comment, contentSlot, menuVisible, toggleMenu, hideMenu}) => { + return ( + +
+ + {menuVisible && + + } +
+
+ ); +}; diff --git a/plugins/talk-plugin-author-menu/client/components/Menu.css b/plugins/talk-plugin-author-menu/client/components/Menu.css new file mode 100644 index 000000000..6ff52d81e --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/components/Menu.css @@ -0,0 +1,37 @@ + +.menu { + background-color: white; + border: solid 1px #999; + border-radius: 3px; + padding: 10px; + position: absolute; + -webkit-box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2); + box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2); + z-index: 10; + top: 26px; + left: 0px; + min-width: 20px; + text-align: left; + color: #616161; +} + +.menu::before{ + content: ''; + border: 10px solid transparent; + border-top-color: #999; + position: absolute; + left: 6px; + top: -21px; + transform: rotate(180deg); +} + +.menu::after{ + content: ''; + border: 10px solid transparent; + border-top-color: white; + position: absolute; + left: 6px; + top: -20px; + transform: rotate(180deg); +} + diff --git a/plugins/talk-plugin-author-menu/client/components/Menu.js b/plugins/talk-plugin-author-menu/client/components/Menu.js new file mode 100644 index 000000000..b3f6e2888 --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/components/Menu.js @@ -0,0 +1,18 @@ +import React from 'react'; +import styles from './Menu.css'; +import {Slot} from 'plugin-api/beta/client/components'; + +export default ({data, root, asset, comment}) => ( +
+ + +
+); diff --git a/plugins/talk-plugin-author-menu/client/constants.js b/plugins/talk-plugin-author-menu/client/constants.js new file mode 100644 index 000000000..a11de608a --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/constants.js @@ -0,0 +1,5 @@ +const prefix = 'TALK_AUTHOR_MENU'; + +export const SET_CONTENT_SLOT = `${prefix}_SET_CONTENT_SLOT`; +export const RESET_CONTENT_SLOT = `${prefix}_RESET_CONTENT_SLOT`; + diff --git a/plugins/talk-plugin-author-menu/client/containers/AuthorName.js b/plugins/talk-plugin-author-menu/client/containers/AuthorName.js new file mode 100644 index 000000000..9551e38d6 --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/containers/AuthorName.js @@ -0,0 +1,79 @@ +import React from 'react'; +import {connect, withFragments} from 'plugin-api/beta/client/hocs'; +import {bindActionCreators} from 'redux'; +import AuthorName from '../components/AuthorName'; +import {setContentSlot, resetContentSlot} from '../actions'; +import {compose, gql} from 'react-apollo'; +import {getSlotFragmentSpreads} from 'plugin-api/beta/client/utils'; + +class AuthorNameContainer extends React.Component { + state = { + menuVisible: false + }; + + toggleMenu = () => { + this.setState({ + menuVisible: !this.state.menuVisible, + }); + } + + hideMenu = () => { + if (this.state.menuVisible) { + this.setState({ + menuVisible: false + }); + } + } + + render() { + return ; + } +} + +const slots = [ + 'authorMenuInfos', + 'authorMenuActions', +]; + +const mapStateToProps = ({talkPluginAuthorMenu: state}) => ({ + contentSlot: state.contentSlot, +}); + +const mapDispatchToProps = (dispatch) => + bindActionCreators({setContentSlot, resetContentSlot}, dispatch); + +const withAuthorNameFragments = withFragments({ + root: gql` + fragment TalkAuthorMenu_AuthorName_root on RootQuery { + __typename + ${getSlotFragmentSpreads(slots, 'root')} + }`, + asset: gql` + fragment TalkAuthorMenu_AuthorName_asset on Asset { + __typename + ${getSlotFragmentSpreads(slots, 'asset')} + }`, + comment: gql` + fragment TalkAuthorMenu_AuthorName_comment on Comment { + __typename + user { + username + } + ${getSlotFragmentSpreads(slots, 'comment')} + }`, +}); + +const enhance = compose( + connect(mapStateToProps, mapDispatchToProps), + withAuthorNameFragments, +); + +export default enhance(AuthorNameContainer); diff --git a/plugins/talk-plugin-author-menu/client/index.js b/plugins/talk-plugin-author-menu/client/index.js new file mode 100644 index 000000000..a98a15822 --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/index.js @@ -0,0 +1,11 @@ +import AuthorName from './containers/AuthorName'; +import reducer from './reducer'; +import translations from './translations.yml'; + +export default { + reducer, + slots: { + commentAuthorName: [AuthorName] + }, + translations +}; diff --git a/plugins/talk-plugin-author-menu/client/reducer.js b/plugins/talk-plugin-author-menu/client/reducer.js new file mode 100644 index 000000000..4f04d07ed --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/reducer.js @@ -0,0 +1,22 @@ +import {SET_CONTENT_SLOT, RESET_CONTENT_SLOT} from './constants'; + +const initialState = { + contentSlot: null, +}; + +export default function reducer(state = initialState, action) { + switch (action.type) { + case SET_CONTENT_SLOT: + return { + ...state, + contentSlot: action.slot, + }; + case RESET_CONTENT_SLOT: + return { + ...state, + contentSlot: initialState.contentSlot, + }; + default : + return state; + } +} diff --git a/plugins/talk-plugin-author-menu/client/translations.yml b/plugins/talk-plugin-author-menu/client/translations.yml new file mode 100644 index 000000000..adac91ad4 --- /dev/null +++ b/plugins/talk-plugin-author-menu/client/translations.yml @@ -0,0 +1,4 @@ +en: + talk-plugin-author-menu: +es: + talk-plugin-author-menu: diff --git a/plugins/talk-plugin-author-menu/index.js b/plugins/talk-plugin-author-menu/index.js new file mode 100644 index 000000000..f053ebf79 --- /dev/null +++ b/plugins/talk-plugin-author-menu/index.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/plugins/talk-plugin-like/client/index.js b/plugins/talk-plugin-like/client/index.js index 68b7a2c46..aaea3aaf4 100644 --- a/plugins/talk-plugin-like/client/index.js +++ b/plugins/talk-plugin-like/client/index.js @@ -5,5 +5,5 @@ export default { translations, slots: { commentReactions: [LikeButton] - } + }, }; From 7c339aa510bcd77b8e807cfe6c52a942ee4dae0f Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 1 Sep 2017 22:25:26 +0700 Subject: [PATCH 11/33] Render contentSlot if set --- .../client/components/Menu.js | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/plugins/talk-plugin-author-menu/client/components/Menu.js b/plugins/talk-plugin-author-menu/client/components/Menu.js index b3f6e2888..fdedf6594 100644 --- a/plugins/talk-plugin-author-menu/client/components/Menu.js +++ b/plugins/talk-plugin-author-menu/client/components/Menu.js @@ -2,17 +2,29 @@ import React from 'react'; import styles from './Menu.css'; import {Slot} from 'plugin-api/beta/client/components'; -export default ({data, root, asset, comment}) => ( -
- - -
-); +export default ({data, root, asset, comment, contentSlot}) => { + if (contentSlot) { + return ( + + ); + } + + return ( +
+ + +
+ ); +}; From 9c2ea693b91b8398f12c9e8d993d6b0808e61d61 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 1 Sep 2017 22:29:30 +0700 Subject: [PATCH 12/33] Add hover styles --- .../talk-plugin-author-menu/client/components/AuthorName.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/talk-plugin-author-menu/client/components/AuthorName.css b/plugins/talk-plugin-author-menu/client/components/AuthorName.css index 386d7cb45..d9a8bdd71 100644 --- a/plugins/talk-plugin-author-menu/client/components/AuthorName.css +++ b/plugins/talk-plugin-author-menu/client/components/AuthorName.css @@ -5,6 +5,9 @@ .button { composes: buttonReset from "coral-framework/styles/reset.css"; + &:hover { + text-decoration: underline; + } } .name { From c07eaf0edbddc2a132818bec042ffe5dccaf2a48 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 1 Sep 2017 23:15:00 +0700 Subject: [PATCH 13/33] Add member since --- .eslintignore | 1 + .gitignore | 1 + package.json | 1 + .../client/components/Menu.css | 2 +- .../talk-plugin-member-since/client/.babelrc | 14 + .../client/.eslintrc.json | 23 ++ .../client/components/MemberSinceInfo.css | 16 ++ .../client/components/MemberSinceInfo.js | 14 + .../client/containers/MemberSinceInfo.js | 28 ++ .../talk-plugin-member-since/client/index.js | 9 + .../client/translations.yml | 6 + plugins/talk-plugin-member-since/index.js | 1 + yarn.lock | 250 +++--------------- 13 files changed, 150 insertions(+), 216 deletions(-) create mode 100644 plugins/talk-plugin-member-since/client/.babelrc create mode 100644 plugins/talk-plugin-member-since/client/.eslintrc.json create mode 100644 plugins/talk-plugin-member-since/client/components/MemberSinceInfo.css create mode 100644 plugins/talk-plugin-member-since/client/components/MemberSinceInfo.js create mode 100644 plugins/talk-plugin-member-since/client/containers/MemberSinceInfo.js create mode 100644 plugins/talk-plugin-member-since/client/index.js create mode 100644 plugins/talk-plugin-member-since/client/translations.yml create mode 100644 plugins/talk-plugin-member-since/index.js diff --git a/.eslintignore b/.eslintignore index b9523387a..688e285c5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -20,5 +20,6 @@ plugins/* !plugins/talk-plugin-sort-most-loved !plugins/talk-plugin-sort-most-respected !plugins/talk-plugin-author-menu +!plugins/talk-plugin-member-since node_modules diff --git a/.gitignore b/.gitignore index 99eac3ade..f8be44f20 100644 --- a/.gitignore +++ b/.gitignore @@ -36,5 +36,6 @@ plugins/* !plugins/talk-plugin-sort-most-loved !plugins/talk-plugin-sort-most-respected !plugins/talk-plugin-author-menu +!plugins/talk-plugin-member-since **/node_modules/* diff --git a/package.json b/package.json index a5127f6ac..8c45239e3 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,7 @@ "material-design-lite": "^1.2.1", "metascraper": "^1.0.7", "minimist": "^1.2.0", + "moment": "^2.18.1", "mongoose": "^4.11.7", "morgan": "^1.8.2", "ms": "^2.0.0", diff --git a/plugins/talk-plugin-author-menu/client/components/Menu.css b/plugins/talk-plugin-author-menu/client/components/Menu.css index 6ff52d81e..86befe54a 100644 --- a/plugins/talk-plugin-author-menu/client/components/Menu.css +++ b/plugins/talk-plugin-author-menu/client/components/Menu.css @@ -7,7 +7,7 @@ position: absolute; -webkit-box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2); box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2); - z-index: 10; + z-index: 9; top: 26px; left: 0px; min-width: 20px; diff --git a/plugins/talk-plugin-member-since/client/.babelrc b/plugins/talk-plugin-member-since/client/.babelrc new file mode 100644 index 000000000..60be246eb --- /dev/null +++ b/plugins/talk-plugin-member-since/client/.babelrc @@ -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" + ] +} \ No newline at end of file diff --git a/plugins/talk-plugin-member-since/client/.eslintrc.json b/plugins/talk-plugin-member-since/client/.eslintrc.json new file mode 100644 index 000000000..9fe56bd14 --- /dev/null +++ b/plugins/talk-plugin-member-since/client/.eslintrc.json @@ -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"] }] + } +} diff --git a/plugins/talk-plugin-member-since/client/components/MemberSinceInfo.css b/plugins/talk-plugin-member-since/client/components/MemberSinceInfo.css new file mode 100644 index 000000000..cefd32d40 --- /dev/null +++ b/plugins/talk-plugin-member-since/client/components/MemberSinceInfo.css @@ -0,0 +1,16 @@ +.root { + white-space: nowrap; +} + +.memberSince { + color: #484747; + margin-left: 4px; + letter-spacing: 0.2px; + font-size: 0.9em; + vertical-align: middle; +} + +.icon { + font-size: 1.4em; + vertical-align: middle; +} diff --git a/plugins/talk-plugin-member-since/client/components/MemberSinceInfo.js b/plugins/talk-plugin-member-since/client/components/MemberSinceInfo.js new file mode 100644 index 000000000..4263225b6 --- /dev/null +++ b/plugins/talk-plugin-member-since/client/components/MemberSinceInfo.js @@ -0,0 +1,14 @@ +import React from 'react'; +import styles from './MemberSinceInfo.css'; +import {t} from 'plugin-api/beta/client/services'; +import {Icon} from 'plugin-api/beta/client/components/ui'; +import moment from 'moment'; + +export default ({memberSinceDate}) => ( +
+ + + {t('talk-plugin-member-since.member_since')}: {moment(new Date(memberSinceDate)).format('MMM DD, YYYY')} + +
+); diff --git a/plugins/talk-plugin-member-since/client/containers/MemberSinceInfo.js b/plugins/talk-plugin-member-since/client/containers/MemberSinceInfo.js new file mode 100644 index 000000000..186e9c8c1 --- /dev/null +++ b/plugins/talk-plugin-member-since/client/containers/MemberSinceInfo.js @@ -0,0 +1,28 @@ +import React from 'react'; +import MemberSinceInfo from '../components/MemberSinceInfo'; +import {compose, gql} from 'react-apollo'; +import {withFragments} from 'plugin-api/beta/client/hocs'; + +class MemberSinceInfoContainer extends React.Component { + render() { + return ; + } +} + +const withMemberSinceInfoFragments = withFragments({ + comment: gql` + fragment TalkAuthorMenu_MemberSinceInfo_comment on Comment { + user { + username + created_at + } + }`, +}); + +const enhance = compose( + withMemberSinceInfoFragments, +); + +export default enhance(MemberSinceInfoContainer); diff --git a/plugins/talk-plugin-member-since/client/index.js b/plugins/talk-plugin-member-since/client/index.js new file mode 100644 index 000000000..cfb60d867 --- /dev/null +++ b/plugins/talk-plugin-member-since/client/index.js @@ -0,0 +1,9 @@ +import MemberSinceInfo from './containers/MemberSinceInfo'; +import translations from './translations.yml'; + +export default { + slots: { + authorMenuInfos: [MemberSinceInfo] + }, + translations +}; diff --git a/plugins/talk-plugin-member-since/client/translations.yml b/plugins/talk-plugin-member-since/client/translations.yml new file mode 100644 index 000000000..1329974d7 --- /dev/null +++ b/plugins/talk-plugin-member-since/client/translations.yml @@ -0,0 +1,6 @@ +en: + talk-plugin-member-since: + member_since: "Member Since" +es: + talk-plugin-member-since: + member_since: "Miembro desde" diff --git a/plugins/talk-plugin-member-since/index.js b/plugins/talk-plugin-member-since/index.js new file mode 100644 index 000000000..f053ebf79 --- /dev/null +++ b/plugins/talk-plugin-member-since/index.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/yarn.lock b/yarn.lock index d55aa9745..1ca8acdbb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -63,20 +63,13 @@ abbrev@1, abbrev@1.0.x: version "1.0.9" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" -accepts@^1.3.4: +accepts@^1.3.4, accepts@~1.3.3: version "1.3.4" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" dependencies: mime-types "~2.1.16" negotiator "0.6.1" -accepts@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" - dependencies: - mime-types "~2.1.11" - negotiator "0.6.1" - acorn-dynamic-import@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4" @@ -356,18 +349,12 @@ async@2.1.4: dependencies: lodash "^4.14.0" -async@2.4.1: +async@2.4.1, async@^2.1.2, async@^2.1.4: version "2.4.1" resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7" dependencies: lodash "^4.14.0" -async@^2.1.2, async@^2.1.4: - version "2.5.0" - resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" - dependencies: - lodash "^4.14.0" - async@~0.9.0: version "0.9.2" resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" @@ -416,15 +403,7 @@ babel-cli@^6.26.0: optionalDependencies: chokidar "^1.6.1" -babel-code-frame@^6.11.0, babel-code-frame@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" - dependencies: - chalk "^1.1.0" - esutils "^2.0.2" - js-tokens "^3.0.0" - -babel-code-frame@^6.26.0: +babel-code-frame@^6.11.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" dependencies: @@ -465,20 +444,7 @@ babel-eslint@^7.2.1: babel-types "^6.23.0" babylon "^6.17.0" -babel-generator@^6.18.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.24.1.tgz#e715f486c58ded25649d888944d52aa07c5d9497" - dependencies: - babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.2.0" - source-map "^0.5.0" - trim-right "^1.0.1" - -babel-generator@^6.26.0: +babel-generator@^6.18.0, babel-generator@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5" dependencies: @@ -956,31 +922,14 @@ babel-register@^6.26.0: mkdirp "^0.5.1" source-map-support "^0.4.15" -babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.6.1: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b" - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.10.0" - -babel-runtime@^6.26.0: +babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.6.1: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" dependencies: core-js "^2.4.0" regenerator-runtime "^0.11.0" -babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.3.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.24.1.tgz#04ae514f1f93b3a2537f2a0f60a5a45fb8308333" - dependencies: - babel-runtime "^6.22.0" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - babylon "^6.11.0" - lodash "^4.2.0" - -babel-template@^6.26.0: +babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0, babel-template@^6.3.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" dependencies: @@ -990,21 +939,7 @@ babel-template@^6.26.0: babylon "^6.18.0" lodash "^4.17.4" -babel-traverse@^6.18.0, babel-traverse@^6.23.1, babel-traverse@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.24.1.tgz#ab36673fd356f9a0948659e7b338d5feadb31695" - dependencies: - babel-code-frame "^6.22.0" - babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - babylon "^6.15.0" - debug "^2.2.0" - globals "^9.0.0" - invariant "^2.2.0" - lodash "^4.2.0" - -babel-traverse@^6.26.0: +babel-traverse@^6.18.0, babel-traverse@^6.23.1, babel-traverse@^6.24.1, babel-traverse@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" dependencies: @@ -1018,16 +953,7 @@ babel-traverse@^6.26.0: invariant "^2.2.2" lodash "^4.17.4" -babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.24.1.tgz#a136879dc15b3606bda0d90c1fc74304c2ff0975" - dependencies: - babel-runtime "^6.22.0" - esutils "^2.0.2" - lodash "^4.2.0" - to-fast-properties "^1.0.1" - -babel-types@^6.26.0: +babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1, babel-types@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" dependencies: @@ -1036,11 +962,7 @@ babel-types@^6.26.0: lodash "^4.17.4" to-fast-properties "^1.0.3" -babylon@^6.11.0, babylon@^6.13.0, babylon@^6.15.0, babylon@^6.17.0: - version "6.17.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.0.tgz#37da948878488b9c4e3c4038893fa3314b3fc932" - -babylon@^6.18.0: +babylon@^6.13.0, babylon@^6.17.0, babylon@^6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" @@ -1056,7 +978,7 @@ balanced-match@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.2.1.tgz#7bc658b4bed61eee424ad74f75f5c3e2c4df3cc7" -balanced-match@^0.4.1, balanced-match@^0.4.2: +balanced-match@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" @@ -1112,7 +1034,7 @@ bluebird@2.9.24: version "2.9.24" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.9.24.tgz#14a2e75f0548323dc35aa440d92007ca154e967c" -bluebird@3.5.0, bluebird@^3.3.4: +bluebird@3.5.0, bluebird@^3.3.4, bluebird@^3.4.6: version "3.5.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" @@ -1120,30 +1042,11 @@ bluebird@^2.10.2: version "2.11.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" -bluebird@^3.4.6: - version "3.4.7" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" - bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.6" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" -body-parser@^1.12.2: - version "1.17.1" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.1.tgz#75b3bc98ddd6e7e0d8ffe750dfaca5c66993fa47" - dependencies: - bytes "2.4.0" - content-type "~1.0.2" - debug "2.6.1" - depd "~1.1.0" - http-errors "~1.6.1" - iconv-lite "0.4.15" - on-finished "~2.3.0" - qs "6.4.0" - raw-body "~2.2.0" - type-is "~1.6.14" - -body-parser@^1.17.2: +body-parser@^1.12.2, body-parser@^1.17.2: version "1.17.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.2.tgz#f8892abc8f9e627d42aedafbca66bf5ab99104ee" dependencies: @@ -1172,13 +1075,6 @@ bowser@^1.7.2: version "1.7.2" resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.7.2.tgz#b94cc6925ba6b5e07c421a58e601ce4611264572" -brace-expansion@^1.0.0: - version "1.1.7" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.7.tgz#3effc3c50e000531fb720eaff80f0ae8ef23cf59" - dependencies: - balanced-match "^0.4.1" - concat-map "0.0.1" - brace-expansion@^1.1.7: version "1.1.8" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" @@ -1394,7 +1290,7 @@ chalk@2.0.1: escape-string-regexp "^1.0.5" supports-color "^4.0.0" -chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: +chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" dependencies: @@ -1867,11 +1763,7 @@ core-js@^1.0.0: version "1.2.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" -core-js@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" - -core-js@^2.5.0: +core-js@^2.4.0, core-js@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.0.tgz#569c050918be6486b3837552028ae0466b717086" @@ -2131,9 +2023,9 @@ date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" -debug@*: - version "3.0.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.0.0.tgz#1d2feae53349047b08b264ec41906ba17a8516e4" +debug@*, debug@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.0.1.tgz#0564c612b521dc92d9f2988f0549e34f9c98db64" dependencies: ms "2.0.0" @@ -2143,12 +2035,6 @@ debug@2.6.0: dependencies: ms "0.7.2" -debug@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.1.tgz#79855090ba2c4e3115cc7d8769491d58f0491351" - dependencies: - ms "0.7.2" - debug@2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e" @@ -2161,12 +2047,6 @@ debug@2.6.8, debug@^2.2.0, debug@^2.6.8: dependencies: ms "2.0.0" -debug@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.0.1.tgz#0564c612b521dc92d9f2988f0549e34f9c98db64" - dependencies: - ms "2.0.0" - debug@~0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" @@ -3200,7 +3080,7 @@ glob@7.0.x: once "^1.3.0" path-is-absolute "^1.0.0" -glob@7.1.1, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1: +glob@7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" dependencies: @@ -3211,7 +3091,7 @@ glob@7.1.1, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" -glob@7.1.2, glob@^7.1.2: +glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -3242,10 +3122,6 @@ glob@^6.0.4: once "^1.3.0" path-is-absolute "^1.0.0" -globals@^9.0.0: - version "9.17.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.17.0.tgz#0c0ca696d9b9bb694d2e5470bd37777caad50286" - globals@^9.17.0, globals@^9.18.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" @@ -3372,15 +3248,7 @@ graphql-server-module-graphiql@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/graphql-server-module-graphiql/-/graphql-server-module-graphiql-0.6.0.tgz#e37634b05f000731981e8ed13103f9a5861e5da0" -graphql-subscriptions@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.4.4.tgz#39cff32d08dd3c990113864bab77154403727e9b" - dependencies: - "@types/graphql" "^0.9.1" - es6-promise "^4.0.5" - iterall "^1.1.1" - -graphql-subscriptions@^0.4.3: +graphql-subscriptions@^0.4.2, graphql-subscriptions@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.4.3.tgz#2aed6ba87551cc747742b793497ff24b22991867" dependencies: @@ -4240,22 +4108,18 @@ js-stringify@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db" -js-tokens@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" - -js-tokens@^3.0.2: +js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" -js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.5.2, js-yaml@^3.7.0: +js-yaml@3.x, js-yaml@^3.5.2: version "3.8.3" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.3.tgz#33a05ec481c850c8875929166fe1beb61c728766" dependencies: argparse "^1.0.7" esprima "^3.1.1" -js-yaml@^3.9.1: +js-yaml@^3.4.3, js-yaml@^3.7.0, js-yaml@^3.9.1: version "3.9.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.1.tgz#08775cebdfdd359209f0d2acd383c8f86a6904a0" dependencies: @@ -4318,11 +4182,7 @@ jshint@^2.8.0: shelljs "0.3.x" strip-json-comments "1.0.x" -json-loader@^0.5.4: - version "0.5.4" - resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.4.tgz#8baa1365a632f58a3c46d20175fc6002c96e37de" - -json-loader@^0.5.7: +json-loader@^0.5.4, json-loader@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" @@ -4954,15 +4814,11 @@ mime-db@~1.12.0: version "1.12.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.12.0.tgz#3d0c63180f458eb10d325aaa37d7c58ae312e9d7" -mime-db@~1.27.0: - version "1.27.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" - -mime-types@^2.1.10, mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7: - version "2.1.15" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" +mime-types@^2.1.10, mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.7: + version "2.1.16" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23" dependencies: - mime-db "~1.27.0" + mime-db "~1.29.0" mime-types@~2.0.3: version "2.0.14" @@ -4970,12 +4826,6 @@ mime-types@~2.0.3: dependencies: mime-db "~1.12.0" -mime-types@~2.1.16: - version "2.1.16" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23" - dependencies: - mime-db "~1.29.0" - mime@1.3.4, mime@1.3.x, mime@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" @@ -4992,13 +4842,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" -"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" - dependencies: - brace-expansion "^1.0.0" - -minimatch@^3.0.4, minimatch@~3.0.2: +"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" dependencies: @@ -5043,7 +4887,7 @@ mocha@^3.1.2: mkdirp "0.5.1" supports-color "3.1.2" -moment@2.18.1, moment@2.x.x, moment@^2.10.3: +moment@2.18.1, moment@2.x.x, moment@^2.10.3, moment@^2.18.1: version "2.18.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" @@ -6429,7 +6273,7 @@ pym.js@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/pym.js/-/pym.js-1.2.0.tgz#feb1e2c9b396613e5172192b0cdd75408f9c8322" -q@1.5.0: +q@1.5.0, q@^1.1.2: version "1.5.0" resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1" @@ -6441,10 +6285,6 @@ q@2.0.3: pop-iterate "^1.0.1" weak-map "^1.0.5" -q@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/q/-/q-1.1.2.tgz#6357e291206701d99f197ab84e57e8ad196f2a89" - qs@6.4.0, qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" @@ -6796,10 +6636,6 @@ regenerate@^1.2.1: version "1.3.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260" -regenerator-runtime@^0.10.0: - version "0.10.4" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.4.tgz#74cb6598d3ba2eb18694e968a40e2b3b4df9cf93" - regenerator-runtime@^0.10.5: version "0.10.5" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" @@ -7127,7 +6963,7 @@ shelljs@0.3.x: version "0.3.0" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.3.0.tgz#3596e6307a781544f591f37da618360f31db57b1" -shelljs@0.7.8: +shelljs@0.7.8, shelljs@^0.7.0: version "0.7.8" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" dependencies: @@ -7135,14 +6971,6 @@ shelljs@0.7.8: interpret "^1.0.0" rechoir "^0.6.2" -shelljs@^0.7.0: - version "0.7.7" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.7.tgz#b2f5c77ef97148f4b4f6e22682e10bba8667cff1" - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -7275,7 +7103,7 @@ source-map@0.4.x, source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@0.5.x, source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3: +source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3: version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" @@ -7648,10 +7476,6 @@ to-capital-case@^1.0.0: dependencies: to-space-case "^1.0.0" -to-fast-properties@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320" - to-fast-properties@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" @@ -7753,7 +7577,7 @@ type-detect@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.3.tgz#0e3f2670b44099b0b46c284d136a7ef49c74c2ea" -type-is@~1.6.14, type-is@~1.6.15: +type-is@~1.6.15: version "1.6.15" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" dependencies: @@ -8051,14 +7875,10 @@ word-wrap@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.1.0.tgz#356153d61d10610d600785c5d701288e0ae764a6" -word-wrap@1.2.3: +word-wrap@1.2.3, word-wrap@^1.0.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" -word-wrap@^1.0.3: - version "1.2.1" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.1.tgz#248f459b465d179a17bc407c854d3151d07e45d8" - wordwrap@0.0.2, wordwrap@~0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" From 9a549a2b63373c3bdb7f73055a5713823adc4918 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 1 Sep 2017 11:21:17 -0600 Subject: [PATCH 14/33] fixed some bugs from review --- graph/typeDefs.graphql | 10 +++++----- services/assets.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 46f089eab..8ea71a157 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -938,20 +938,20 @@ input RejectUsernameInput { input AssetSettingsInput { # premodLinksEnable will put all comments that contain links into premod. - premodLinksEnable: Boolean! + premodLinksEnable: Boolean # moderation is the moderation mode for the asset. - moderation: MODERATION_MODE! + moderation: MODERATION_MODE # questionBoxEnable will enable the Question Boxs' content to be visable above # the comment box. - questionBoxEnable: Boolean! + questionBoxEnable: Boolean # questionBoxContent is the content of the Question Box. - questionBoxContent: String! + questionBoxContent: String # questionBoxIcon is the icon for the Question Box. - questionBoxIcon: String! + questionBoxIcon: String } # UpdateAssetStatusInput contains the input to change the status of a comment as diff --git a/services/assets.js b/services/assets.js index fd34a6f2b..34c8ff465 100644 --- a/services/assets.js +++ b/services/assets.js @@ -158,7 +158,7 @@ module.exports = class AssetsService { } } - return assets.limit(limit); + return assets.sort({created_at: sortOrder === 'DESC' ? -1 : 1}).limit(limit); } /** From 26bae69454ec0e51bc86c3f2b36f568b5095e9bf Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Sun, 3 Sep 2017 19:36:02 +0700 Subject: [PATCH 15/33] Fix bug when determining moderation queues for live updates --- .../coral-admin/src/routes/Moderation/graphql.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/coral-admin/src/routes/Moderation/graphql.js b/client/coral-admin/src/routes/Moderation/graphql.js index 28ca259d8..866218d73 100644 --- a/client/coral-admin/src/routes/Moderation/graphql.js +++ b/client/coral-admin/src/routes/Moderation/graphql.js @@ -69,15 +69,15 @@ function getCommentQueues(comment, queueConfig) { const queues = []; Object.keys(queueConfig).forEach((key) => { const {action_type, statuses, tags} = queueConfig[key]; - let addToQueues = false; - if (statuses && statuses.indexOf(comment.status) >= 0) { - addToQueues = true; + let addToQueues = true; + if (statuses && statuses.indexOf(comment.status) === -1) { + addToQueues = false; } - if (tags && comment.tags && comment.tags.some((tagLink) => tags.indexOf(tagLink.tag.name) >= 0)) { - addToQueues = true; + if (tags && (!comment.tags || !comment.tags.some((tagLink) => tags.indexOf(tagLink.tag.name) >= 0))) { + addToQueues = false; } - if (action_type && comment.actions && comment.actions.some((a) => a.__typename.toLowerCase() === `${action_type}action`)) { - addToQueues = true; + if (action_type && (!comment.actions || !comment.actions.some((a) => a.__typename.toLowerCase() === `${action_type.toLowerCase()}action`))) { + addToQueues = false; } if (addToQueues) { queues.push(key); From ffef2f640da5c4aa48944f586821bf42abc50b80 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 4 Sep 2017 15:40:39 +0700 Subject: [PATCH 16/33] First implementation of ignore user plugin --- .eslintignore | 1 + .gitignore | 1 + .../src/components/AllCommentsPane.js | 2 - .../src/components/Comment.css | 13 +--- .../src/components/Comment.js | 16 ----- .../src/components/Stream.js | 7 --- .../src/components/TopRightMenu.js | 60 ------------------- .../src/containers/Stream.js | 3 +- .../coral-embed-stream/src/graphql/index.js | 12 ---- client/coral-framework/graphql/mutations.js | 2 +- plugin-api/beta/client/hocs/index.js | 1 + .../client/components/Menu.css | 4 ++ .../client/components/Menu.js | 13 ++-- .../client/containers/AuthorName.js | 4 ++ .../talk-plugin-ignore-user/client/.babelrc | 14 +++++ .../client/.eslintrc.json | 23 +++++++ .../client/components/IgnoreUserAction.css | 16 +++++ .../client/components/IgnoreUserAction.js | 9 +++ .../components/IgnoreUserConfirmation.css | 9 +++ .../components/IgnoreUserConfirmation.js | 14 +++++ .../client/containers/IgnoreUserAction.js | 34 +++++++++++ .../containers/IgnoreUserConfirmation.js | 44 ++++++++++++++ .../talk-plugin-ignore-user/client/index.js | 26 ++++++++ .../client/translations.yml | 5 ++ plugins/talk-plugin-ignore-user/index.js | 1 + 25 files changed, 217 insertions(+), 117 deletions(-) delete mode 100644 client/coral-embed-stream/src/components/TopRightMenu.js create mode 100644 plugins/talk-plugin-ignore-user/client/.babelrc create mode 100644 plugins/talk-plugin-ignore-user/client/.eslintrc.json create mode 100644 plugins/talk-plugin-ignore-user/client/components/IgnoreUserAction.css create mode 100644 plugins/talk-plugin-ignore-user/client/components/IgnoreUserAction.js create mode 100644 plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.css create mode 100644 plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.js create mode 100644 plugins/talk-plugin-ignore-user/client/containers/IgnoreUserAction.js create mode 100644 plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js create mode 100644 plugins/talk-plugin-ignore-user/client/index.js create mode 100644 plugins/talk-plugin-ignore-user/client/translations.yml create mode 100644 plugins/talk-plugin-ignore-user/index.js diff --git a/.eslintignore b/.eslintignore index 688e285c5..4805e34a4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -21,5 +21,6 @@ plugins/* !plugins/talk-plugin-sort-most-respected !plugins/talk-plugin-author-menu !plugins/talk-plugin-member-since +!plugins/talk-plugin-ignore-user node_modules diff --git a/.gitignore b/.gitignore index f8be44f20..0a2ac00df 100644 --- a/.gitignore +++ b/.gitignore @@ -37,5 +37,6 @@ plugins/* !plugins/talk-plugin-sort-most-respected !plugins/talk-plugin-author-menu !plugins/talk-plugin-member-since +!plugins/talk-plugin-ignore-user **/node_modules/* diff --git a/client/coral-embed-stream/src/components/AllCommentsPane.js b/client/coral-embed-stream/src/components/AllCommentsPane.js index 427d3a68b..1617b8c4f 100644 --- a/client/coral-embed-stream/src/components/AllCommentsPane.js +++ b/client/coral-embed-stream/src/components/AllCommentsPane.js @@ -126,7 +126,6 @@ class AllCommentsPane extends React.Component { root, comments, commentClassNames, - ignoreUser, setActiveReplyBox, activeReplyBox, notify, @@ -173,7 +172,6 @@ class AllCommentsPane extends React.Component { currentUser={currentUser} postFlag={postFlag} postDontAgree={postDontAgree} - ignoreUser={ignoreUser} commentIsIgnored={commentIsIgnored} loadMore={loadNewReplies} deleteAction={deleteAction} diff --git a/client/coral-embed-stream/src/components/Comment.css b/client/coral-embed-stream/src/components/Comment.css index 578df2d80..6dc8eee80 100644 --- a/client/coral-embed-stream/src/components/Comment.css +++ b/client/coral-embed-stream/src/components/Comment.css @@ -66,17 +66,6 @@ border-bottom: 1px solid rgba(255, 255, 255, 0.3); } -.topRight .popover { - margin-top: 1em; - right: 0px; -} - -.topRight .link.active, -.topRight .active .link { - padding-bottom: 0.125em; - border-bottom: 2px solid currentColor; -} - .editCommentForm { margin-bottom: 10px; } @@ -175,4 +164,4 @@ .username { margin-right: 5px; -} \ No newline at end of file +} diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 205bc498b..87dc4e776 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -16,7 +16,6 @@ import mapValues from 'lodash/mapValues'; import LoadMore from './LoadMore'; import {getEditableUntilDate} from './util'; import {findCommentWithId} from '../graphql/utils'; -import {TopRightMenu} from './TopRightMenu'; import CommentContent from './CommentContent'; import Slot from 'coral-framework/components/Slot'; import IgnoredCommentTombstone from './IgnoredCommentTombstone'; @@ -187,9 +186,6 @@ export default class Comment extends React.Component { // given a comment, return whether it should be rendered as ignored commentIsIgnored: PropTypes.func, - // dispatch action to ignore another user - ignoreUser: PropTypes.func, - // edit a comment, passed (id, asset_id, { body }) editComment: PropTypes.func, @@ -317,7 +313,6 @@ export default class Comment extends React.Component { comment, postFlag, parentId, - ignoreUser, highlighted, postComment, currentUser, @@ -480,16 +475,6 @@ export default class Comment extends React.Component { } } - { isActive && (currentUser && (comment.user.id !== currentUser.id)) && - - /* TopRightMenu allows currentUser to ignore other users' comments */ - - - - } { !isActive && } @@ -600,7 +585,6 @@ export default class Comment extends React.Component { postFlag={postFlag} deleteAction={deleteAction} loadMore={loadMore} - ignoreUser={ignoreUser} charCountEnable={charCountEnable} maxCharCount={maxCharCount} showSignInDialog={showSignInDialog} diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 4d46c8e87..575c1fd00 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -64,7 +64,6 @@ class Stream extends React.Component { postDontAgree, deleteAction, showSignInDialog, - ignoreUser, loadNewReplies, auth: {user}, emit, @@ -90,7 +89,6 @@ class Stream extends React.Component { data={data} root={root} commentClassNames={commentClassNames} - ignoreUser={ignoreUser} setActiveReplyBox={setActiveReplyBox} activeReplyBox={activeReplyBox} notify={notify} @@ -133,7 +131,6 @@ class Stream extends React.Component { postDontAgree, deleteAction, showSignInDialog, - ignoreUser, activeStreamTab, setActiveStreamTab, loadNewReplies, @@ -183,7 +180,6 @@ class Stream extends React.Component { root={root} comments={comments} commentClassNames={commentClassNames} - ignoreUser={ignoreUser} setActiveReplyBox={setActiveReplyBox} activeReplyBox={activeReplyBox} notify={notify} @@ -322,9 +318,6 @@ Stream.propTypes = { notify: PropTypes.func.isRequired, postComment: PropTypes.func.isRequired, - // dispatch action to ignore another user - ignoreUser: React.PropTypes.func, - // edit a comment, passed (id, asset_id, { body }) editComment: React.PropTypes.func }; diff --git a/client/coral-embed-stream/src/components/TopRightMenu.js b/client/coral-embed-stream/src/components/TopRightMenu.js deleted file mode 100644 index 47147e853..000000000 --- a/client/coral-embed-stream/src/components/TopRightMenu.js +++ /dev/null @@ -1,60 +0,0 @@ -import React, {PropTypes} from 'react'; -import {IgnoreUserWizard} from './IgnoreUserWizard'; -import Toggleable from './Toggleable'; - -// TopRightMenu appears as a dropdown in the top right of the comment. -// when you click the down cehvron, it expands and shows IgnoreUserWizard -// when you click 'cancel' in the wizard, it closes the menu -export class TopRightMenu extends React.Component { - static propTypes = { - - // comment on which this menu appears - comment: PropTypes.shape({ - user: PropTypes.shape({ - id: PropTypes.string.isRequired, - username: PropTypes.string.isRequired - }).isRequired - }).isRequired, - ignoreUser: PropTypes.func, - - // show notification to the user (e.g. for errors) - notify: PropTypes.func.isRequired, - } - constructor(props) { - super(props); - this.state = { - timesReset: 0 - }; - } - render() { - const {comment, ignoreUser, notify} = this.props; - - // timesReset is used as Toggleable key so it re-renders on reset (closing the toggleable) - const reset = () => this.setState({timesReset: this.state.timesReset + 1}); - const ignoreUserAndCloseMenuAndNotifyOnError = async ({id}) => { - - // close menu - reset(); - - // ignore user - try { - await ignoreUser({id}); - } catch (error) { - notify('error', 'Failed to ignore user'); - throw error; - } - }; - return ( - -
- -
-
- ); - } -} - diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 194bfc5d9..d3279896c 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux'; import {ADDTL_COMMENTS_ON_LOAD_MORE, THREADING_LEVEL} from '../constants/stream'; import { withPostComment, withPostFlag, withPostDontAgree, - withDeleteAction, withIgnoreUser, withEditComment + withDeleteAction, withEditComment } from 'coral-framework/graphql/mutations'; import * as authActions from 'coral-embed-stream/src/actions/auth'; @@ -374,7 +374,6 @@ export default compose( withPostComment, withPostFlag, withPostDontAgree, - withIgnoreUser, withDeleteAction, withEditComment, )(StreamContainer); diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index 9ec9a2132..d01e353fc 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -108,18 +108,6 @@ export default { `, }, mutations: { - IgnoreUser: ({variables}) => ({ - updateQueries: { - CoralEmbedStream_Embed: (previousData) => { - const ignoredUserId = variables.id; - const updated = update(previousData, {me: {ignoredUsers: {$push: [{ - id: ignoredUserId, - __typename: 'User', - }]}}}); - return updated; - } - } - }), StopIgnoringUser: ({variables}) => ({ updateQueries: { CoralEmbedStream_Profile: (previousData) => { diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index 91d2ae0ab..4254400bb 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -293,7 +293,7 @@ export const withIgnoreUser = withMutation( } `, { props: ({mutate}) => ({ - ignoreUser: ({id}) => { + ignoreUser: (id) => { return mutate({ variables: { id, diff --git a/plugin-api/beta/client/hocs/index.js b/plugin-api/beta/client/hocs/index.js index 1bc47868e..2659237f3 100644 --- a/plugin-api/beta/client/hocs/index.js +++ b/plugin-api/beta/client/hocs/index.js @@ -5,3 +5,4 @@ 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'; export {default as withEmit} from 'coral-framework/hocs/withEmit'; +export {withIgnoreUser} from 'coral-framework/graphql/mutations'; diff --git a/plugins/talk-plugin-author-menu/client/components/Menu.css b/plugins/talk-plugin-author-menu/client/components/Menu.css index 86befe54a..e3a111235 100644 --- a/plugins/talk-plugin-author-menu/client/components/Menu.css +++ b/plugins/talk-plugin-author-menu/client/components/Menu.css @@ -35,3 +35,7 @@ transform: rotate(180deg); } +.actions { + display: flex; + justify-content: flex-end; +} diff --git a/plugins/talk-plugin-author-menu/client/components/Menu.js b/plugins/talk-plugin-author-menu/client/components/Menu.js index fdedf6594..620bd59c9 100644 --- a/plugins/talk-plugin-author-menu/client/components/Menu.js +++ b/plugins/talk-plugin-author-menu/client/components/Menu.js @@ -5,11 +5,13 @@ import {Slot} from 'plugin-api/beta/client/components'; export default ({data, root, asset, comment, contentSlot}) => { if (contentSlot) { return ( - +
+ +
); } @@ -21,6 +23,7 @@ export default ({data, root, asset, comment, contentSlot}) => { queryData={{asset, root, comment}} /> ( + +); diff --git a/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.css b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.css new file mode 100644 index 000000000..5cd38f244 --- /dev/null +++ b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.css @@ -0,0 +1,9 @@ +.root { + width: 250px; +} + +.actions { + display: flex; + justify-content: flex-end; +} + diff --git a/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.js b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.js new file mode 100644 index 000000000..ba2f62b0b --- /dev/null +++ b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.js @@ -0,0 +1,14 @@ +import React from 'react'; +import styles from './IgnoreUserConfirmation.css'; +import {t} from 'plugin-api/beta/client/services'; + +export default ({ignoreUser}) => ( +
+ Do you really want to ignore this user? +
+ +
+
+); diff --git a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserAction.js b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserAction.js new file mode 100644 index 000000000..422162a6e --- /dev/null +++ b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserAction.js @@ -0,0 +1,34 @@ +import React from 'react'; +import IgnoreUserAction from '../components/IgnoreUserAction'; +import {compose} from 'react-apollo'; +import {connect, withFragments} from 'plugin-api/beta/client/hocs'; +import {bindActionCreators} from 'redux'; +import {setContentSlot} from 'plugins/talk-plugin-author-menu/client/actions'; +import IgnoreUserConfirmation from './IgnoreUserConfirmation'; + +class IgnoreUserActionContainer extends React.Component { + + ignoreUser = () => { + this.props.setContentSlot('ignoreUserConfirmation'); + }; + + render() { + return ; + } +} + +const mapDispatchToProps = (dispatch) => + bindActionCreators({ + setContentSlot, + }, dispatch); + +const withIgnoreUserActionFragments = withFragments(IgnoreUserConfirmation.fragments); + +const enhance = compose( + connect(null, mapDispatchToProps), + withIgnoreUserActionFragments, +); + +export default enhance(IgnoreUserActionContainer); diff --git a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js new file mode 100644 index 000000000..5d014277c --- /dev/null +++ b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js @@ -0,0 +1,44 @@ +import React from 'react'; +import IgnoreUserConfirmation from '../components/IgnoreUserConfirmation'; +import {compose, gql} from 'react-apollo'; +import {connect, withFragments, withIgnoreUser} from 'plugin-api/beta/client/hocs'; +import {bindActionCreators} from 'redux'; +import {setContentSlot} from 'plugins/talk-plugin-author-menu/client/actions'; + +class IgnoreUserConfirmationContainer extends React.Component { + + ignoreUser = () => { + this.props.ignoreUser(this.props.comment.user.id); + this.props.setContentSlot(null); + }; + + render() { + return ; + } +} + +const mapDispatchToProps = (dispatch) => + bindActionCreators({ + setContentSlot, + }, dispatch); + +const withIgnoreUserConfirmationFragments = withFragments({ + comment: gql` + fragment TalkIgnoreUser_IgnoreUserConfirmation_comment on Comment { + user { + id + username + } + }`, +}); + +const enhance = compose( + connect(null, mapDispatchToProps), + withIgnoreUserConfirmationFragments, + withIgnoreUser, +); + +export default enhance(IgnoreUserConfirmationContainer); diff --git a/plugins/talk-plugin-ignore-user/client/index.js b/plugins/talk-plugin-ignore-user/client/index.js new file mode 100644 index 000000000..79f440126 --- /dev/null +++ b/plugins/talk-plugin-ignore-user/client/index.js @@ -0,0 +1,26 @@ +import IgnoreUserAction from './containers/IgnoreUserAction'; +import IgnoreUserConfirmation from './containers/IgnoreUserConfirmation'; +import translations from './translations.yml'; +import update from 'immutability-helper'; + +export default { + mutations: { + IgnoreUser: ({variables}) => ({ + updateQueries: { + CoralEmbedStream_Embed: (previousData) => { + const ignoredUserId = variables.id; + const updated = update(previousData, {me: {ignoredUsers: {$push: [{ + id: ignoredUserId, + __typename: 'User', + }]}}}); + return updated; + } + } + }), + }, + slots: { + authorMenuActions: [IgnoreUserAction], + ignoreUserConfirmation: [IgnoreUserConfirmation] + }, + translations +}; diff --git a/plugins/talk-plugin-ignore-user/client/translations.yml b/plugins/talk-plugin-ignore-user/client/translations.yml new file mode 100644 index 000000000..1e40c7ba1 --- /dev/null +++ b/plugins/talk-plugin-ignore-user/client/translations.yml @@ -0,0 +1,5 @@ +en: + talk-plugin-ignore-user: + ignore: "ignore" +es: + talk-plugin-ignore-user: diff --git a/plugins/talk-plugin-ignore-user/index.js b/plugins/talk-plugin-ignore-user/index.js new file mode 100644 index 000000000..f053ebf79 --- /dev/null +++ b/plugins/talk-plugin-ignore-user/index.js @@ -0,0 +1 @@ +module.exports = {}; From b2ef7fa795377cdb2f4cfb50a58a365a619b3f68 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 4 Sep 2017 15:51:04 +0700 Subject: [PATCH 17/33] Fix tombstone bug --- .../src/components/AllCommentsPane.js | 11 +++----- .../src/components/Comment.js | 28 ++++++++++++------- .../src/components/Stream.js | 11 -------- .../src/containers/Comment.js | 5 ++++ 4 files changed, 27 insertions(+), 28 deletions(-) diff --git a/client/coral-embed-stream/src/components/AllCommentsPane.js b/client/coral-embed-stream/src/components/AllCommentsPane.js index 1617b8c4f..7602902df 100644 --- a/client/coral-embed-stream/src/components/AllCommentsPane.js +++ b/client/coral-embed-stream/src/components/AllCommentsPane.js @@ -1,7 +1,6 @@ import React from 'react'; import LoadMore from './LoadMore'; -import IgnoredCommentTombstone from './IgnoredCommentTombstone'; import NewCount from './NewCount'; import {TransitionGroup} from 'react-transition-group'; import {forEachError} from 'coral-framework/utils'; @@ -138,7 +137,6 @@ class AllCommentsPane extends React.Component { loadNewReplies, deleteAction, showSignInDialog, - commentIsIgnored, charCountEnable, maxCharCount, editComment, @@ -156,9 +154,8 @@ class AllCommentsPane extends React.Component { /> {view.map((comment) => { - return commentIsIgnored(comment) - ? - : ; + /> + ); })} u.id === comment.user.id) + ); + } + hasIgnoredReplies() { return this.props.comment.replies && - this.props.comment.replies.nodes.some((reply) => this.props.commentIsIgnored(reply)); + this.props.comment.replies.nodes.some((reply) => this.commentIsIgnored(reply)); } loadNewReplies = () => { @@ -327,12 +333,15 @@ export default class Comment extends React.Component { charCountEnable, showSignInDialog, liveUpdates, - commentIsIgnored, animateEnter, emit, commentClassNames = [] } = this.props; + if (this.commentIsIgnored(comment)) { + return ; + } + const view = this.getVisibileReplies(); // Inactive comments can be viewed by moderators and admins (e.g. using permalinks). @@ -566,9 +575,8 @@ export default class Comment extends React.Component { {view.map((reply) => { - return commentIsIgnored(reply) - ? - : ; + /> + ); })}
diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 575c1fd00..89e7b13ae 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -40,15 +40,6 @@ class Stream extends React.Component { } } - commentIsIgnored = (comment) => { - const me = this.props.root.me; - return ( - me && - me.ignoredUsers && - me.ignoredUsers.find((u) => u.id === comment.user.id) - ); - }; - renderHighlightedComment() { const { data, @@ -104,7 +95,6 @@ class Stream extends React.Component { deleteAction={deleteAction} showSignInDialog={showSignInDialog} key={topLevelComment.id} - commentIsIgnored={this.commentIsIgnored} comment={topLevelComment} charCountEnable={asset.settings.charCountEnable} maxCharCount={asset.settings.charCount} @@ -193,7 +183,6 @@ class Stream extends React.Component { loadNewReplies={loadNewReplies} deleteAction={deleteAction} showSignInDialog={showSignInDialog} - commentIsIgnored={this.commentIsIgnored} charCountEnable={asset.settings.charCountEnable} maxCharCount={asset.settings.charCount} editComment={editComment} diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index a4bfbc0eb..f24dc8b01 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -93,6 +93,11 @@ const withCommentFragments = withFragments({ fragment CoralEmbedStream_Comment_root on RootQuery { __typename ${getSlotFragmentSpreads(slots, 'root')} + me { + ignoredUsers { + id + } + } } `, asset: gql` From 98cb34f55456b191c32d374f65166391e2f15182 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 4 Sep 2017 16:25:19 +0700 Subject: [PATCH 18/33] Don't ignore myself --- .../client/containers/IgnoreUserAction.js | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserAction.js b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserAction.js index 422162a6e..7fc32d0e8 100644 --- a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserAction.js +++ b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserAction.js @@ -1,10 +1,11 @@ import React from 'react'; import IgnoreUserAction from '../components/IgnoreUserAction'; -import {compose} from 'react-apollo'; -import {connect, withFragments} from 'plugin-api/beta/client/hocs'; +import {compose, gql} from 'react-apollo'; +import {connect, withFragments, excludeIf} from 'plugin-api/beta/client/hocs'; import {bindActionCreators} from 'redux'; import {setContentSlot} from 'plugins/talk-plugin-author-menu/client/actions'; import IgnoreUserConfirmation from './IgnoreUserConfirmation'; +import {getDefinitionName} from 'plugin-api/beta/client/utils'; class IgnoreUserActionContainer extends React.Component { @@ -24,11 +25,29 @@ const mapDispatchToProps = (dispatch) => setContentSlot, }, dispatch); -const withIgnoreUserActionFragments = withFragments(IgnoreUserConfirmation.fragments); +const withIgnoreUserActionFragments = withFragments({ + root: gql` + fragment TalkIgnoreUser_IgnoreUserAction_root on RootQuery { + me { + id + } + } + `, + comment: gql` + fragment TalkIgnoreUser_IgnoreUserAction_comment on Comment { + user { + id + } + ...${getDefinitionName(IgnoreUserConfirmation.fragments.comment)} + } + ${IgnoreUserConfirmation.fragments.comment} + `, +}); const enhance = compose( connect(null, mapDispatchToProps), withIgnoreUserActionFragments, + excludeIf(({root: {me}, comment}) => me.id === comment.user.id), ); export default enhance(IgnoreUserActionContainer); From c0252e160bb4c05754a6863f567d2e875c567679 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 4 Sep 2017 16:44:53 +0700 Subject: [PATCH 19/33] Fix reply fragments --- client/coral-embed-stream/src/containers/Comment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index f24dc8b01..11a668371 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -85,6 +85,7 @@ const singleCommentFragment = gql` edited editableUntil } + ${getSlotFragmentSpreads(slots, 'comment')} } `; @@ -121,7 +122,6 @@ const withCommentFragments = withFragments({ endCursor } `, THREADING_LEVEL)} - ${getSlotFragmentSpreads(slots, 'comment')} } ${singleCommentFragment} ` From 89258638d4a8a7a7bec75ea66f188ad51fcbcda8 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 4 Sep 2017 22:35:05 +0700 Subject: [PATCH 20/33] Correct exclude if logic --- .../client/containers/IgnoreUserAction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserAction.js b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserAction.js index 7fc32d0e8..d7e9d38b5 100644 --- a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserAction.js +++ b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserAction.js @@ -47,7 +47,7 @@ const withIgnoreUserActionFragments = withFragments({ const enhance = compose( connect(null, mapDispatchToProps), withIgnoreUserActionFragments, - excludeIf(({root: {me}, comment}) => me.id === comment.user.id), + excludeIf(({root: {me}, comment}) => !me || me.id === comment.user.id), ); export default enhance(IgnoreUserActionContainer); From 74e8c28f45cfba45002f04766366fba94f0977ad Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 4 Sep 2017 23:13:00 +0700 Subject: [PATCH 21/33] Apply styling --- .../client/components/Menu.css | 6 +++- .../client/components/IgnoreUserAction.css | 21 ++++++------ .../client/components/IgnoreUserAction.js | 2 +- .../components/IgnoreUserConfirmation.css | 32 ++++++++++++++++++- .../components/IgnoreUserConfirmation.js | 11 +++++-- .../containers/IgnoreUserConfirmation.js | 5 +++ .../client/translations.yml | 6 +++- .../client/components/MemberSinceInfo.css | 9 ++++-- 8 files changed, 72 insertions(+), 20 deletions(-) diff --git a/plugins/talk-plugin-author-menu/client/components/Menu.css b/plugins/talk-plugin-author-menu/client/components/Menu.css index e3a111235..dd149ce18 100644 --- a/plugins/talk-plugin-author-menu/client/components/Menu.css +++ b/plugins/talk-plugin-author-menu/client/components/Menu.css @@ -12,7 +12,6 @@ left: 0px; min-width: 20px; text-align: left; - color: #616161; } .menu::before{ @@ -38,4 +37,9 @@ .actions { display: flex; justify-content: flex-end; + margin-top: 8px; + + &:empty { + display: none; + } } diff --git a/plugins/talk-plugin-ignore-user/client/components/IgnoreUserAction.css b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserAction.css index cefd32d40..ac818278c 100644 --- a/plugins/talk-plugin-ignore-user/client/components/IgnoreUserAction.css +++ b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserAction.css @@ -2,15 +2,16 @@ white-space: nowrap; } -.memberSince { - color: #484747; - margin-left: 4px; - letter-spacing: 0.2px; - font-size: 0.9em; - vertical-align: middle; -} +.button { + composes: buttonReset from "coral-framework/styles/reset.css"; + border: 1px solid rgba(1, 1, 1, 0.8); + border-radius: 1px; + padding: 3px 6px; + font-size: 12px; + transition: color 100ms, background 100ms; -.icon { - font-size: 1.4em; - vertical-align: middle; + &:hover { + background-color: rgba(1, 1, 1, 0.8); + color: white; + } } diff --git a/plugins/talk-plugin-ignore-user/client/components/IgnoreUserAction.js b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserAction.js index 00817f38b..0649999ca 100644 --- a/plugins/talk-plugin-ignore-user/client/components/IgnoreUserAction.js +++ b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserAction.js @@ -4,6 +4,6 @@ import {t} from 'plugin-api/beta/client/services'; export default ({ignoreUser}) => ( ); diff --git a/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.css b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.css index 5cd38f244..0439da88c 100644 --- a/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.css +++ b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.css @@ -1,9 +1,39 @@ .root { - width: 250px; + width: 200px; } .actions { display: flex; justify-content: flex-end; + margin-top: 8px; } +.message { + font-size: 13px; +} + +.button { + composes: buttonReset from "coral-framework/styles/reset.css"; + border: 1px solid rgba(1, 1, 1, 0.8); + border-radius: 1px; + padding: 3px 6px; + font-size: 12px; + transition: color 100ms, background 100ms; + + &:hover { + background-color: rgba(1, 1, 1, 0.8); + color: white; + } +} + +.cancel { + composes: buttonReset from "coral-framework/styles/reset.css"; + padding: 3px 6px; + font-size: 12px; + transition: color 100ms; + margin-right: 4px; + + &:hover { + color: rgba(1, 1, 1, 0.75); + } +} diff --git a/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.js b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.js index ba2f62b0b..10a4aac5f 100644 --- a/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.js +++ b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.js @@ -2,12 +2,17 @@ import React from 'react'; import styles from './IgnoreUserConfirmation.css'; import {t} from 'plugin-api/beta/client/services'; -export default ({ignoreUser}) => ( +export default ({ignoreUser, cancel}) => (
- Do you really want to ignore this user? +
+ {t('talk-plugin-ignore-user.confirmation')} +
+
diff --git a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js index 5d014277c..a97ebe228 100644 --- a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js +++ b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js @@ -12,10 +12,15 @@ class IgnoreUserConfirmationContainer extends React.Component { this.props.setContentSlot(null); }; + cancel = () => { + this.props.setContentSlot(null); + } + render() { return ; } } diff --git a/plugins/talk-plugin-ignore-user/client/translations.yml b/plugins/talk-plugin-ignore-user/client/translations.yml index 1e40c7ba1..d3d3c2aba 100644 --- a/plugins/talk-plugin-ignore-user/client/translations.yml +++ b/plugins/talk-plugin-ignore-user/client/translations.yml @@ -1,5 +1,9 @@ en: talk-plugin-ignore-user: - ignore: "ignore" + ignore_user: Ignore User + cancel: Cancel + confirmation: | + When you ignore a user, all comments they wrote on the site will be hidden from you. You can + undo this later from My Profile. es: talk-plugin-ignore-user: diff --git a/plugins/talk-plugin-member-since/client/components/MemberSinceInfo.css b/plugins/talk-plugin-member-since/client/components/MemberSinceInfo.css index cefd32d40..3cf2fb75d 100644 --- a/plugins/talk-plugin-member-since/client/components/MemberSinceInfo.css +++ b/plugins/talk-plugin-member-since/client/components/MemberSinceInfo.css @@ -2,15 +2,18 @@ white-space: nowrap; } +.root:not(:last-child) { + margin-bottom: 4px; +} + .memberSince { - color: #484747; margin-left: 4px; letter-spacing: 0.2px; - font-size: 0.9em; + font-size: 13px; vertical-align: middle; } .icon { - font-size: 1.4em; + font-size: 18px; vertical-align: middle; } From f8297e96524f68893c9b727f2a621d321f7dc6a4 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 4 Sep 2017 23:29:36 +0700 Subject: [PATCH 22/33] Hide menu after action --- .../talk-plugin-author-menu/client/actions.js | 11 ++++++- .../client/constants.js | 2 ++ .../client/containers/AuthorName.js | 33 ++++++++++--------- .../talk-plugin-author-menu/client/reducer.js | 14 +++++++- .../containers/IgnoreUserConfirmation.js | 8 ++--- 5 files changed, 46 insertions(+), 22 deletions(-) diff --git a/plugins/talk-plugin-author-menu/client/actions.js b/plugins/talk-plugin-author-menu/client/actions.js index 0a806749d..960e66bb4 100644 --- a/plugins/talk-plugin-author-menu/client/actions.js +++ b/plugins/talk-plugin-author-menu/client/actions.js @@ -1,4 +1,4 @@ -import {SET_CONTENT_SLOT, RESET_CONTENT_SLOT} from './constants'; +import {SET_CONTENT_SLOT, RESET_CONTENT_SLOT, OPEN_MENU, CLOSE_MENU} from './constants'; export const setContentSlot = (slot) => ({ type: SET_CONTENT_SLOT, @@ -8,3 +8,12 @@ export const setContentSlot = (slot) => ({ export const resetContentSlot = () => ({ type: RESET_CONTENT_SLOT, }); + +export const openMenu = (id) => ({ + type: OPEN_MENU, + id, +}); + +export const closeMenu = () => ({ + type: CLOSE_MENU, +}); diff --git a/plugins/talk-plugin-author-menu/client/constants.js b/plugins/talk-plugin-author-menu/client/constants.js index a11de608a..1d14e94d4 100644 --- a/plugins/talk-plugin-author-menu/client/constants.js +++ b/plugins/talk-plugin-author-menu/client/constants.js @@ -2,4 +2,6 @@ const prefix = 'TALK_AUTHOR_MENU'; export const SET_CONTENT_SLOT = `${prefix}_SET_CONTENT_SLOT`; export const RESET_CONTENT_SLOT = `${prefix}_RESET_CONTENT_SLOT`; +export const OPEN_MENU = `${prefix}_OPEN_MENU`; +export const CLOSE_MENU = `${prefix}_CLOSE_MENU`; diff --git a/plugins/talk-plugin-author-menu/client/containers/AuthorName.js b/plugins/talk-plugin-author-menu/client/containers/AuthorName.js index cbb8c84ee..9d9188167 100644 --- a/plugins/talk-plugin-author-menu/client/containers/AuthorName.js +++ b/plugins/talk-plugin-author-menu/client/containers/AuthorName.js @@ -2,29 +2,23 @@ import React from 'react'; import {connect, withFragments} from 'plugin-api/beta/client/hocs'; import {bindActionCreators} from 'redux'; import AuthorName from '../components/AuthorName'; -import {setContentSlot, resetContentSlot} from '../actions'; +import {setContentSlot, resetContentSlot, openMenu, closeMenu} from '../actions'; import {compose, gql} from 'react-apollo'; import {getSlotFragmentSpreads} from 'plugin-api/beta/client/utils'; class AuthorNameContainer extends React.Component { - state = { - menuVisible: false - }; toggleMenu = () => { - this.setState({ - menuVisible: !this.state.menuVisible, - }); + if (this.props.showMenuForComment === this.props.comment.id) { + this.props.closeMenu(); + } else { + this.props.openMenu(this.props.comment.id); + } } hideMenu = () => { - if (this.state.menuVisible) { - this.setState({ - menuVisible: false - }); - if (this.props.contentSlot) { - this.props.resetContentSlot(); - } + if (this.props.showMenuForComment === this.props.comment.id) { + this.props.closeMenu(); } } @@ -35,7 +29,7 @@ class AuthorNameContainer extends React.Component { asset={this.props.asset} comment={this.props.comment} contentSlot={this.props.contentSlot} - menuVisible={this.state.menuVisible} + menuVisible={this.props.showMenuForComment === this.props.comment.id} toggleMenu={this.toggleMenu} hideMenu={this.hideMenu} />; @@ -49,10 +43,16 @@ const slots = [ const mapStateToProps = ({talkPluginAuthorMenu: state}) => ({ contentSlot: state.contentSlot, + showMenuForComment: state.showMenuForComment, }); const mapDispatchToProps = (dispatch) => - bindActionCreators({setContentSlot, resetContentSlot}, dispatch); + bindActionCreators({ + setContentSlot, + resetContentSlot, + openMenu, + closeMenu, + }, dispatch); const withAuthorNameFragments = withFragments({ root: gql` @@ -68,6 +68,7 @@ const withAuthorNameFragments = withFragments({ comment: gql` fragment TalkAuthorMenu_AuthorName_comment on Comment { __typename + id user { username } diff --git a/plugins/talk-plugin-author-menu/client/reducer.js b/plugins/talk-plugin-author-menu/client/reducer.js index 4f04d07ed..822c2a6a8 100644 --- a/plugins/talk-plugin-author-menu/client/reducer.js +++ b/plugins/talk-plugin-author-menu/client/reducer.js @@ -1,7 +1,8 @@ -import {SET_CONTENT_SLOT, RESET_CONTENT_SLOT} from './constants'; +import {SET_CONTENT_SLOT, RESET_CONTENT_SLOT, OPEN_MENU, CLOSE_MENU} from './constants'; const initialState = { contentSlot: null, + showMenuForComment: null, }; export default function reducer(state = initialState, action) { @@ -16,6 +17,17 @@ export default function reducer(state = initialState, action) { ...state, contentSlot: initialState.contentSlot, }; + case OPEN_MENU: + return { + ...state, + showMenuForComment: action.id, + }; + case CLOSE_MENU: + return { + ...state, + showMenuForComment: null, + contentSlot: null, + }; default : return state; } diff --git a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js index a97ebe228..4821fae9b 100644 --- a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js +++ b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js @@ -3,17 +3,17 @@ import IgnoreUserConfirmation from '../components/IgnoreUserConfirmation'; import {compose, gql} from 'react-apollo'; import {connect, withFragments, withIgnoreUser} from 'plugin-api/beta/client/hocs'; import {bindActionCreators} from 'redux'; -import {setContentSlot} from 'plugins/talk-plugin-author-menu/client/actions'; +import {closeMenu} from 'plugins/talk-plugin-author-menu/client/actions'; class IgnoreUserConfirmationContainer extends React.Component { ignoreUser = () => { this.props.ignoreUser(this.props.comment.user.id); - this.props.setContentSlot(null); + this.props.closeMenu(); }; cancel = () => { - this.props.setContentSlot(null); + this.props.closeMenu(); } render() { @@ -27,7 +27,7 @@ class IgnoreUserConfirmationContainer extends React.Component { const mapDispatchToProps = (dispatch) => bindActionCreators({ - setContentSlot, + closeMenu, }, dispatch); const withIgnoreUserConfirmationFragments = withFragments({ From 2d4adb60be38bab47a50c93f385645dd3e8e6a27 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 4 Sep 2017 23:55:44 +0700 Subject: [PATCH 23/33] Show notification on success --- client/coral-embed/src/Snackbar.js | 2 +- client/coral-framework/services/notification.js | 12 ++++++------ plugin-api/beta/client/actions/notification.js | 2 +- .../client/containers/IgnoreUserConfirmation.js | 4 ++++ .../talk-plugin-ignore-user/client/translations.yml | 2 ++ 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/client/coral-embed/src/Snackbar.js b/client/coral-embed/src/Snackbar.js index c518288c6..11219102d 100644 --- a/client/coral-embed/src/Snackbar.js +++ b/client/coral-embed/src/Snackbar.js @@ -38,7 +38,7 @@ export default class Snackbar { } alert(message) { - const [type, text] = message.split('|'); + const {type, text} = JSON.parse(message); this.el.style.transform = 'translate(-50%, 20px)'; this.el.style.opacity = 0; this.el.className = `coral-notif-${type}`; diff --git a/client/coral-framework/services/notification.js b/client/coral-framework/services/notification.js index 5730200ad..a3f1a6520 100644 --- a/client/coral-framework/services/notification.js +++ b/client/coral-framework/services/notification.js @@ -5,14 +5,14 @@ */ export function createNotificationService(pym) { return { - success(msg) { - pym.sendMessage('coral-alert', `success|${msg}`); + success(text) { + pym.sendMessage('coral-alert', JSON.stringify({kind: 'success', text})); }, - error(msg) { - pym.sendMessage('coral-alert', `error|${msg}`); + error(text) { + pym.sendMessage('coral-alert', JSON.stringify({kind: 'error', text})); }, - info(msg) { - pym.sendMessage('coral-alert', `info|${msg}`); + info(text) { + pym.sendMessage('coral-alert', JSON.stringify({kind: 'info', text})); }, }; } diff --git a/plugin-api/beta/client/actions/notification.js b/plugin-api/beta/client/actions/notification.js index c1a75cb7a..0fae2b983 100644 --- a/plugin-api/beta/client/actions/notification.js +++ b/plugin-api/beta/client/actions/notification.js @@ -1 +1 @@ -export {addNotification} from 'coral-framework/actions/notification'; +export {notify} from 'coral-framework/actions/notification'; diff --git a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js index 4821fae9b..2e1c7a820 100644 --- a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js +++ b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js @@ -4,11 +4,14 @@ import {compose, gql} from 'react-apollo'; import {connect, withFragments, withIgnoreUser} from 'plugin-api/beta/client/hocs'; import {bindActionCreators} from 'redux'; import {closeMenu} from 'plugins/talk-plugin-author-menu/client/actions'; +import {notify} from 'plugin-api/beta/client/actions/notification'; +import {t} from 'plugin-api/beta/client/services'; class IgnoreUserConfirmationContainer extends React.Component { ignoreUser = () => { this.props.ignoreUser(this.props.comment.user.id); + this.props.notify('success', t('talk-plugin-ignore-user.notify_success', this.props.comment.user.username)); this.props.closeMenu(); }; @@ -28,6 +31,7 @@ class IgnoreUserConfirmationContainer extends React.Component { const mapDispatchToProps = (dispatch) => bindActionCreators({ closeMenu, + notify, }, dispatch); const withIgnoreUserConfirmationFragments = withFragments({ diff --git a/plugins/talk-plugin-ignore-user/client/translations.yml b/plugins/talk-plugin-ignore-user/client/translations.yml index d3d3c2aba..a10c68a8d 100644 --- a/plugins/talk-plugin-ignore-user/client/translations.yml +++ b/plugins/talk-plugin-ignore-user/client/translations.yml @@ -5,5 +5,7 @@ en: confirmation: | When you ignore a user, all comments they wrote on the site will be hidden from you. You can undo this later from My Profile. + notify_success: | + You are now ignoring {0}. You can undo this action from My Profile. es: talk-plugin-ignore-user: From 123935c8bcdf16608c2b85e41e37691d05c0a0ba Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 5 Sep 2017 00:00:27 +0700 Subject: [PATCH 24/33] Also notify on errors --- .../client/containers/IgnoreUserConfirmation.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js index 2e1c7a820..68492414e 100644 --- a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js +++ b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js @@ -6,13 +6,20 @@ import {bindActionCreators} from 'redux'; import {closeMenu} from 'plugins/talk-plugin-author-menu/client/actions'; import {notify} from 'plugin-api/beta/client/actions/notification'; import {t} from 'plugin-api/beta/client/services'; +import {getErrorMessages} from 'plugin-api/beta/client/utils'; class IgnoreUserConfirmationContainer extends React.Component { ignoreUser = () => { - this.props.ignoreUser(this.props.comment.user.id); - this.props.notify('success', t('talk-plugin-ignore-user.notify_success', this.props.comment.user.username)); - this.props.closeMenu(); + const {ignoreUser, notify, comment, closeMenu} = this.props; + ignoreUser(comment.user.id) + .then(() => { + notify('success', t('talk-plugin-ignore-user.notify_success', comment.user.username)); + }) + .catch((err) => { + notify('error', getErrorMessages(err)); + }); + closeMenu(); }; cancel = () => { From 07f5f42870aab517d8d6d387e5a894a972fadd85 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 5 Sep 2017 00:16:27 +0700 Subject: [PATCH 25/33] Optimized render --- plugin-api/beta/client/utils/index.js | 1 + .../client/containers/AuthorName.js | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/plugin-api/beta/client/utils/index.js b/plugin-api/beta/client/utils/index.js index 6dd18f3a1..3fe742569 100644 --- a/plugin-api/beta/client/utils/index.js +++ b/plugin-api/beta/client/utils/index.js @@ -7,4 +7,5 @@ export { capitalize, getErrorMessages, getDefinitionName, + getShallowChanges, } from 'coral-framework/utils'; diff --git a/plugins/talk-plugin-author-menu/client/containers/AuthorName.js b/plugins/talk-plugin-author-menu/client/containers/AuthorName.js index 9d9188167..78663b603 100644 --- a/plugins/talk-plugin-author-menu/client/containers/AuthorName.js +++ b/plugins/talk-plugin-author-menu/client/containers/AuthorName.js @@ -4,10 +4,28 @@ import {bindActionCreators} from 'redux'; import AuthorName from '../components/AuthorName'; import {setContentSlot, resetContentSlot, openMenu, closeMenu} from '../actions'; import {compose, gql} from 'react-apollo'; -import {getSlotFragmentSpreads} from 'plugin-api/beta/client/utils'; +import {getSlotFragmentSpreads, getShallowChanges} from 'plugin-api/beta/client/utils'; class AuthorNameContainer extends React.Component { + shouldComponentUpdate(nextProps) { + + // Specifically handle `showMenuForComment` if it is the only change. + const changes = getShallowChanges(this.props, nextProps); + if (changes.length === 1 && changes[0] === 'showMenuForComment') { + const commentId = this.props.comment.id; + if ( + commentId !== this.props.showMenuForComment && + commentId !== nextProps.showMenuForComment + ) { + return false; + } + } + + // Prevent Slot from rerendering when no props has shallowly changed. + return changes.length !== 0; + } + toggleMenu = () => { if (this.props.showMenuForComment === this.props.comment.id) { this.props.closeMenu(); From 44e3d0b811702a02c9e45e215b4495a4c521bb3d Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 5 Sep 2017 00:28:07 +0700 Subject: [PATCH 26/33] Add title --- .../client/components/IgnoreUserConfirmation.css | 6 ++++++ .../client/components/IgnoreUserConfirmation.js | 9 ++++++--- .../client/containers/IgnoreUserConfirmation.js | 2 +- plugins/talk-plugin-ignore-user/client/translations.yml | 1 + 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.css b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.css index 0439da88c..4f3d8e9da 100644 --- a/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.css +++ b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.css @@ -12,6 +12,12 @@ font-size: 13px; } +.title { + padding: 0; + margin: 0 0 8px 0; + font-size: 15px; +} + .button { composes: buttonReset from "coral-framework/styles/reset.css"; border: 1px solid rgba(1, 1, 1, 0.8); diff --git a/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.js b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.js index 10a4aac5f..9265dfda2 100644 --- a/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.js +++ b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.js @@ -2,9 +2,12 @@ import React from 'react'; import styles from './IgnoreUserConfirmation.css'; import {t} from 'plugin-api/beta/client/services'; -export default ({ignoreUser, cancel}) => ( -
+export default ({ignoreUser, cancel, username}) => ( +
+ ); diff --git a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js index 68492414e..fdf6d1986 100644 --- a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js +++ b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js @@ -28,7 +28,7 @@ class IgnoreUserConfirmationContainer extends React.Component { render() { return ; diff --git a/plugins/talk-plugin-ignore-user/client/translations.yml b/plugins/talk-plugin-ignore-user/client/translations.yml index a10c68a8d..0eb07d101 100644 --- a/plugins/talk-plugin-ignore-user/client/translations.yml +++ b/plugins/talk-plugin-ignore-user/client/translations.yml @@ -7,5 +7,6 @@ en: undo this later from My Profile. notify_success: | You are now ignoring {0}. You can undo this action from My Profile. + confirmation_title: Ignore {0}? es: talk-plugin-ignore-user: From e1a5a64b3995012506707ecc17837d348e82b37d Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 5 Sep 2017 01:09:43 +0700 Subject: [PATCH 27/33] Pluginize stop ignoring user feature in profile --- .../coral-embed-stream/src/graphql/index.js | 15 ------- .../components/IgnoredUsers.css | 24 ----------- .../coral-settings/components/IgnoredUsers.js | 43 ------------------- .../containers/ProfileContainer.js | 29 +++++-------- locales/en.yml | 3 -- locales/es.yml | 2 - locales/fr.yml | 1 - locales/pt_BR.yml | 3 -- plugin-api/beta/client/hocs/index.js | 5 ++- .../client/components/IgnoredUserSection.css | 22 ++++++++++ .../client/components/IgnoredUserSection.js | 23 ++++++++++ .../client/containers/IgnoredUserSection.js | 36 ++++++++++++++++ .../talk-plugin-ignore-user/client/index.js | 19 +++++++- .../client/translations.yml | 13 ++++++ 14 files changed, 126 insertions(+), 112 deletions(-) delete mode 100644 client/coral-settings/components/IgnoredUsers.css delete mode 100644 client/coral-settings/components/IgnoredUsers.js create mode 100644 plugins/talk-plugin-ignore-user/client/components/IgnoredUserSection.css create mode 100644 plugins/talk-plugin-ignore-user/client/components/IgnoredUserSection.js create mode 100644 plugins/talk-plugin-ignore-user/client/containers/IgnoredUserSection.js diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index d01e353fc..09470de14 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -108,21 +108,6 @@ export default { `, }, mutations: { - StopIgnoringUser: ({variables}) => ({ - updateQueries: { - CoralEmbedStream_Profile: (previousData) => { - const noLongerIgnoredUserId = variables.id; - - // remove noLongerIgnoredUserId from ignoredUsers - const updated = update(previousData, {me: {ignoredUsers: { - $apply: (ignoredUsers) => { - return ignoredUsers.filter((u) => u.id !== noLongerIgnoredUserId); - } - }}}); - return updated; - } - } - }), PostComment: ({ variables: {comment: {asset_id, body, parent_id, tags = []}}, state: {auth}, diff --git a/client/coral-settings/components/IgnoredUsers.css b/client/coral-settings/components/IgnoredUsers.css deleted file mode 100644 index 716140256..000000000 --- a/client/coral-settings/components/IgnoredUsers.css +++ /dev/null @@ -1,24 +0,0 @@ -.ignoredUser { - display: table-row; -} - -.ignoredUserList { - display: table; -} - -.ignoredUser > * { - display: table-cell; -} - -.stopListening { - color: #D0011B; -} - -.link { - text-decoration: underline; - cursor: pointer; -} - -.stopListening:before { - content: '\00a0\00a0\00a0\00a0'; -} \ No newline at end of file diff --git a/client/coral-settings/components/IgnoredUsers.js b/client/coral-settings/components/IgnoredUsers.js deleted file mode 100644 index a8723161f..000000000 --- a/client/coral-settings/components/IgnoredUsers.js +++ /dev/null @@ -1,43 +0,0 @@ -import React, {Component, PropTypes} from 'react'; -import t from 'coral-framework/services/i18n'; -import styles from './IgnoredUsers.css'; - -class IgnoredUsers extends Component { - static propTypes = { - users: PropTypes.arrayOf(PropTypes.shape({ - username: PropTypes.string, - id: PropTypes.string, - })).isRequired, - - // accepts { id } - stopIgnoring: PropTypes.func.isRequired, - } - render() { - const {users, stopIgnoring} = this.props; - return ( -
- { - users.length - ?

{t('framework.because_you_ignored')}

- : null - } -
- { - users.map(({username, id}) => ( - -
{ username }
-
- stopIgnoring({id})} - className={styles.link}>{t('framework.stop_ignoring')} -
-
- )) - } -
-
- ); - } -} - -export default IgnoredUsers; diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index db47de8e2..aab0e2a6a 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -3,12 +3,10 @@ import {compose, gql} from 'react-apollo'; import React, {Component} from 'react'; import {bindActionCreators} from 'redux'; import {withQuery} from 'coral-framework/hocs'; - -import {withStopIgnoringUser} from 'coral-framework/graphql/mutations'; +import Slot from 'coral-framework/components/Slot'; import {link} from 'coral-framework/services/pym'; import NotLoggedIn from '../components/NotLoggedIn'; -import IgnoredUsers from '../components/IgnoredUsers'; import {Spinner} from 'coral-ui'; import CommentHistory from 'talk-plugin-history/CommentHistory'; @@ -55,7 +53,7 @@ class ProfileContainer extends Component { }; render() { - const {auth, auth: {user}, showSignInDialog, stopIgnoringUser, root, data} = this.props; + const {auth, auth: {user}, showSignInDialog, root, data} = this.props; const {me} = this.props.root; const loading = this.props.data.loading; @@ -77,15 +75,11 @@ class ProfileContainer extends Component {

{user.username}

{emailAddress ?

{emailAddress}

: null} - {me.ignoredUsers && me.ignoredUsers.length - ?
-

{t('framework.ignored_users')}

- -
- : null} +
@@ -98,8 +92,10 @@ class ProfileContainer extends Component { } } -// TODO: This Slot should be included in `talk-plugin-history` instead. const slots = [ + 'profileSections', + + // TODO: This Slot should be included in `talk-plugin-history` instead. 'commentContent', ]; @@ -143,10 +139,6 @@ const withProfileQuery = withQuery( query CoralEmbedStream_Profile { me { id - ignoredUsers { - id, - username, - } comments(query: {limit: 10}) { ...TalkSettings_CommentConnectionFragment } @@ -165,6 +157,5 @@ const mapDispatchToProps = (dispatch) => export default compose( connect(mapStateToProps, mapDispatchToProps), - withStopIgnoringUser, withProfileQuery )(ProfileContainer); diff --git a/locales/en.yml b/locales/en.yml index 791bc1121..4d215fd06 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -219,7 +219,6 @@ en: framework: banned_account_header: "Your account is currently banned." banned_account_body: "This means that you cannot Like, Report, or write comments." - because_you_ignored: "Because you ignored the following commenters, their comments are hidden." comment: comment comment_is_ignored: "This comment is hidden because you ignored this user." comments: comments @@ -230,13 +229,11 @@ en: error: "Usernames can contain letters numbers and _ only" label: "New Username" msg: "Your account is currently suspended because your username has been deemed inappropriate. To restore your account please enter a new username. Please contact us if you have any questions." - ignored_users: "Ignored users" my_comments: "My Comments" my_profile: "My profile" new_count: "View {0} new {1}" profile: Profile show_all_comments: "Show all comments" - stop_ignoring: "Stop ignoring" success_bio_update: "Your biography has been updated" success_name_update: "Your username has been updated" success_update_settings: "The changes you have made have been applied to the comment stream on this article" diff --git a/locales/es.yml b/locales/es.yml index 99f55d32b..f9b675a10 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -227,13 +227,11 @@ es: error: "Nombres de usuarios pueden solamente incluir letras, números y _" label: "Nuevo Nombre" msg: "Tu cuenta está suspendida porque tu nombre de usuario ha sido considerado no apropiado para el espacio. Para recuperar la cuenta, por favor ingresar un nuevo nombre de usuario. Contáctanos si tienes alguna pregunta." - ignored_users: "Usuarios ignorados" my_comments: "Mis Comentarios" my_profile: "Mi perfil" new_count: "Ver {0} {1} nuevo" profile: Perfil show_all_comments: "Mostrar todos los comentarios" - stop_ignoring: "No ignorar más" success_bio_update: "Tu biografia fue actualizada" success_name_update: "Tu nombre de usuario ha sido actualizado" success_update_settings: "La configuración de este articulo fue actualizada" diff --git a/locales/fr.yml b/locales/fr.yml index 77231e17b..5ece19c67 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -188,7 +188,6 @@ fr: error: "Les noms d'utilisateur ne peuvent contenir que des chiffres, des lettres et \"_\"" label: "Nouveau nom d'utilisateur" msg: "Votre compte est actuellement suspendu car votre nom d'utilisateur a été jugé inapproprié. Pour restaurer votre compte, entrez un nouveau nom d'utilisateur. Contactez-nous si vous avez des questions." - ignored_users: "Utilisateurs ignorés" my_comments: "Mes commentaires" my_profile: "Mon profil" new_count: "Voir {0} nouveau {1}" diff --git a/locales/pt_BR.yml b/locales/pt_BR.yml index 0e606fd81..e2e93c531 100644 --- a/locales/pt_BR.yml +++ b/locales/pt_BR.yml @@ -214,7 +214,6 @@ pt_BR: framework: banned_account_header: "Sua conta está atualmente proibida." banned_account_body: "Isso significa que você não pode gostar, informar ou escrever comentários." - because_you_ignored: "Porque você ignorou os seguintes comentadores, seus comentários estão ocultos." comment: comentário comment_is_ignored: "Este comentário está oculto porque você ignorou esse usuário." comments: comentários @@ -225,13 +224,11 @@ pt_BR: error: "Nomes de usuários podem conter números de letras e _ somente" label: "Novo usuário" msg: "Sua conta está suspensa porque seu nome de usuário foi considerado inapropriado. Para restaurar sua conta, insira um novo nome de usuário. Entre em contato conosco se você tiver alguma dúvida." - ignored_users: "Usuários ignorados" my_comments: "Meus comentários" my_profile: "Meu perfil" new_count: "Ver {0} {1}" profile: Perfil show_all_comments: "Exibir todos os comentários" - stop_ignoring: "Pare de ignorar" success_bio_update: "Sua biografia foi atualizada" success_name_update: "Seu nome de usuário foi atualizado" success_update_settings: "As alterações que você fez foram aplicadas no hilo de comentários neste artigo" diff --git a/plugin-api/beta/client/hocs/index.js b/plugin-api/beta/client/hocs/index.js index 2659237f3..15c8b3544 100644 --- a/plugin-api/beta/client/hocs/index.js +++ b/plugin-api/beta/client/hocs/index.js @@ -5,4 +5,7 @@ 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'; export {default as withEmit} from 'coral-framework/hocs/withEmit'; -export {withIgnoreUser} from 'coral-framework/graphql/mutations'; +export { + withIgnoreUser, + withStopIgnoringUser, +} from 'coral-framework/graphql/mutations'; diff --git a/plugins/talk-plugin-ignore-user/client/components/IgnoredUserSection.css b/plugins/talk-plugin-ignore-user/client/components/IgnoredUserSection.css new file mode 100644 index 000000000..b279101ea --- /dev/null +++ b/plugins/talk-plugin-ignore-user/client/components/IgnoredUserSection.css @@ -0,0 +1,22 @@ +.list { + display: table; + list-style-type: none; + margin-left: 0; + padding-left: 0; +} + +.listItem { + display: table-row; +} + +.username { + display: table-cell; +} + +.button { + composes: buttonReset from "coral-framework/styles/reset.css"; + + margin-left: 16px; + color: #D0011B; + text-decoration: underline; +} diff --git a/plugins/talk-plugin-ignore-user/client/components/IgnoredUserSection.js b/plugins/talk-plugin-ignore-user/client/components/IgnoredUserSection.js new file mode 100644 index 000000000..111568d34 --- /dev/null +++ b/plugins/talk-plugin-ignore-user/client/components/IgnoredUserSection.js @@ -0,0 +1,23 @@ +import React from 'react'; +import styles from './IgnoredUserSection.css'; +import {t} from 'plugin-api/beta/client/services'; + +export default ({ignoredUsers, stopIgnoringUser}) => ( +
+

{t('talk-plugin-ignore-user.section_title')}

+

{t('talk-plugin-ignore-user.section_info')}

+
    + {ignoredUsers.map(({username, id}) => ( +
  • + {username} + +
  • + ))} +
+
+); diff --git a/plugins/talk-plugin-ignore-user/client/containers/IgnoredUserSection.js b/plugins/talk-plugin-ignore-user/client/containers/IgnoredUserSection.js new file mode 100644 index 000000000..e420f180b --- /dev/null +++ b/plugins/talk-plugin-ignore-user/client/containers/IgnoredUserSection.js @@ -0,0 +1,36 @@ +import React from 'react'; +import IgnoredUserSection from '../components/IgnoredUserSection'; +import {compose, gql} from 'react-apollo'; +import {withFragments, excludeIf, withStopIgnoringUser} from 'plugin-api/beta/client/hocs'; + +class IgnoredUserSectionContainer extends React.Component { + + render() { + return ; + } +} + +const withIgnoredUserSectionFragments = withFragments({ + root: gql` + fragment TalkIgnoreUser_IgnoredUserSection_root on RootQuery { + me { + id + ignoredUsers { + id, + username, + } + } + } + `, +}); + +const enhance = compose( + withIgnoredUserSectionFragments, + withStopIgnoringUser, + excludeIf(({root: {me}}) => me.ignoredUsers.length === 0), +); + +export default enhance(IgnoredUserSectionContainer); diff --git a/plugins/talk-plugin-ignore-user/client/index.js b/plugins/talk-plugin-ignore-user/client/index.js index 79f440126..798728ed1 100644 --- a/plugins/talk-plugin-ignore-user/client/index.js +++ b/plugins/talk-plugin-ignore-user/client/index.js @@ -1,5 +1,6 @@ import IgnoreUserAction from './containers/IgnoreUserAction'; import IgnoreUserConfirmation from './containers/IgnoreUserConfirmation'; +import IgnoredUserSection from './containers/IgnoredUserSection'; import translations from './translations.yml'; import update from 'immutability-helper'; @@ -17,10 +18,26 @@ export default { } } }), + StopIgnoringUser: ({variables}) => ({ + updateQueries: { + CoralEmbedStream_Profile: (previousData) => { + const noLongerIgnoredUserId = variables.id; + + // remove noLongerIgnoredUserId from ignoredUsers + const updated = update(previousData, {me: {ignoredUsers: { + $apply: (ignoredUsers) => { + return ignoredUsers.filter((u) => u.id !== noLongerIgnoredUserId); + } + }}}); + return updated; + } + } + }), }, slots: { authorMenuActions: [IgnoreUserAction], - ignoreUserConfirmation: [IgnoreUserConfirmation] + ignoreUserConfirmation: [IgnoreUserConfirmation], + profileSections: [IgnoredUserSection], }, translations }; diff --git a/plugins/talk-plugin-ignore-user/client/translations.yml b/plugins/talk-plugin-ignore-user/client/translations.yml index 0eb07d101..a6c051ada 100644 --- a/plugins/talk-plugin-ignore-user/client/translations.yml +++ b/plugins/talk-plugin-ignore-user/client/translations.yml @@ -1,5 +1,8 @@ en: talk-plugin-ignore-user: + section_title: Ignored users + section_info: Because you ignored the following commenters, their comments are hidden. + stop_ignoring: Stop ignoring ignore_user: Ignore User cancel: Cancel confirmation: | @@ -10,3 +13,13 @@ en: confirmation_title: Ignore {0}? es: talk-plugin-ignore-user: + section_title: "Usuarios ignorados" + stop_ignoring: "No ignorar más" +fr: + talk-plugin-ignore-user: + section_title: "Utilisateurs ignorés" +pt_Br: + talk-plugin-ignore-user: + section_title: "Usuários ignorados" + section_info: "Porque você ignorou os seguintes comentadores, seus comentários estão ocultos." + stop_ignoring: "Pare de ignorar" From 77396ccc31425270854b5d4a674c14436d17dd0d Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 5 Sep 2017 01:17:05 +0700 Subject: [PATCH 28/33] Fix graphql tree bug --- client/coral-embed-stream/src/containers/Comment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index 11a668371..5e2766b9a 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -93,12 +93,12 @@ const withCommentFragments = withFragments({ root: gql` fragment CoralEmbedStream_Comment_root on RootQuery { __typename - ${getSlotFragmentSpreads(slots, 'root')} me { ignoredUsers { id } } + ${getSlotFragmentSpreads(slots, 'root')} } `, asset: gql` From 943c9decbaf2a07f33dd05caf14e412c9f74524a Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 5 Sep 2017 01:18:14 +0700 Subject: [PATCH 29/33] Add to default plugins --- plugins.default.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins.default.json b/plugins.default.json index 0fcc70681..950cec229 100644 --- a/plugins.default.json +++ b/plugins.default.json @@ -17,6 +17,9 @@ "talk-plugin-sort-newest", "talk-plugin-sort-oldest", "talk-plugin-sort-most-respected", - "talk-plugin-sort-most-replied" + "talk-plugin-sort-most-replied", + "talk-plugin-author-menu", + "talk-plugin-member-since", + "talk-plugin-ignore-user" ] } From a7224563598cda2f90f72684ff8b98179787f61f Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 5 Sep 2017 01:25:16 +0700 Subject: [PATCH 30/33] reorder --- plugins/talk-plugin-ignore-user/client/index.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/talk-plugin-ignore-user/client/index.js b/plugins/talk-plugin-ignore-user/client/index.js index 798728ed1..6583f68e6 100644 --- a/plugins/talk-plugin-ignore-user/client/index.js +++ b/plugins/talk-plugin-ignore-user/client/index.js @@ -5,6 +5,12 @@ import translations from './translations.yml'; import update from 'immutability-helper'; export default { + slots: { + authorMenuActions: [IgnoreUserAction], + ignoreUserConfirmation: [IgnoreUserConfirmation], + profileSections: [IgnoredUserSection], + }, + translations, mutations: { IgnoreUser: ({variables}) => ({ updateQueries: { @@ -34,10 +40,4 @@ export default { } }), }, - slots: { - authorMenuActions: [IgnoreUserAction], - ignoreUserConfirmation: [IgnoreUserConfirmation], - profileSections: [IgnoredUserSection], - }, - translations }; From a5acf8ba82589beb2ab2ed8484e367eb782d94f2 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 5 Sep 2017 02:22:10 +0700 Subject: [PATCH 31/33] Add some global css classes --- .../client/components/AuthorName.js | 5 ++-- .../client/components/Menu.js | 8 +++--- .../client/components/IgnoreUserAction.js | 5 +++- .../components/IgnoreUserConfirmation.js | 9 ++++--- .../client/components/IgnoredUserSection.js | 2 +- .../client/components/MemberSinceInfo.js | 7 +++--- pluginsa.json | 25 +++++++++++++++++++ 7 files changed, 47 insertions(+), 14 deletions(-) create mode 100644 pluginsa.json diff --git a/plugins/talk-plugin-author-menu/client/components/AuthorName.js b/plugins/talk-plugin-author-menu/client/components/AuthorName.js index d6e6c66d1..191f93490 100644 --- a/plugins/talk-plugin-author-menu/client/components/AuthorName.js +++ b/plugins/talk-plugin-author-menu/client/components/AuthorName.js @@ -2,13 +2,14 @@ import React from 'react'; import Menu from './Menu'; import styles from './AuthorName.css'; import {ClickOutside} from 'plugin-api/beta/client/components'; +import cn from 'classnames'; export default ({data, root, asset, comment, contentSlot, menuVisible, toggleMenu, hideMenu}) => { return ( -
+
); diff --git a/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.js b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.js index 9265dfda2..8da879f62 100644 --- a/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.js +++ b/plugins/talk-plugin-ignore-user/client/components/IgnoreUserConfirmation.js @@ -1,20 +1,21 @@ import React from 'react'; import styles from './IgnoreUserConfirmation.css'; import {t} from 'plugin-api/beta/client/services'; +import cn from 'classnames'; export default ({ignoreUser, cancel, username}) => ( -