- : 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/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/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/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/index.js b/graph/mutators/index.js
index e7e5df9e7..0f290c69a 100644
--- a/graph/mutators/index.js
+++ b/graph/mutators/index.js
@@ -3,6 +3,8 @@ const debug = require('debug')('talk:graph:mutators');
const Comment = require('./comment');
const Action = require('./action');
+const Asset = require('./asset');
+const Settings = require('./settings');
const Tag = require('./tag');
const Token = require('./token');
const User = require('./user');
@@ -14,6 +16,8 @@ let mutators = [
// Load in the core mutators.
Comment,
Action,
+ Asset,
+ 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..bfdc3991e 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}));
},
@@ -50,6 +56,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/root_query.js b/graph/resolvers/root_query.js
index 239b052c0..13e41a312 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/resolvers/settings.js b/graph/resolvers/settings.js
index 72608a9c4..30e614c38 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': [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, PROTECTED_SETTINGS);
+
module.exports = Settings;
diff --git a/graph/resolvers/util.js b/graph/resolvers/util.js
index ef3c70865..cf3162679 100644
--- a/graph/resolvers/util.js
+++ b/graph/resolvers/util.js
@@ -13,6 +13,31 @@ const decorateWithTags = (typeResolver) => {
};
};
+/**
+ * decorateWithPermissionCheck will decorate the field resolver with
+ * permission checks.
+ *
+ * @param {Object} typeResolver the type resolver
+ * @param {Object} protect the object with field -> Array of permissions
+ */
+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(...permissions)) {
+ return null;
+ }
+
+ return fieldResolver(obj, args, ctx, info);
+ };
+ }
+};
+
module.exports = {
- decorateWithTags
+ decorateWithTags,
+ decorateWithPermissionCheck,
};
diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql
index 9d8e25f12..24d814311 100644
--- a/graph/typeDefs.graphql
+++ b/graph/typeDefs.graphql
@@ -164,7 +164,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
################################################################################
@@ -572,27 +584,85 @@ 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
+
+ # 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
+
+ # 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
}
################################################################################
@@ -648,6 +718,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: Cursor
+
+ # Cursor of last asset in subset.
+ endCursor: Cursor
+
+ # Subset of assets.
+ nodes: [Asset!]!
+}
+
################################################################################
## Errors
################################################################################
@@ -735,7 +821,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
@@ -905,6 +991,56 @@ input RejectUsernameInput {
message: String!
}
+# 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
+
+ # 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. If it
+ # is null, then the message will default to the globally set `closedMessage`.
+ 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 {
@@ -998,6 +1134,91 @@ 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
+
+ # 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
+
+ # 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
+# 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 {
@@ -1033,31 +1254,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!
@@ -1065,17 +1290,36 @@ type RootMutation {
# Removes a tag.
removeTag(tag: ModifyTagInput!): ModifyTagResponse!
+ # Updates settings on a given asset.
+ # Mutation is restricted.
+ updateAssetSettings(id: ID!, input: AssetSettingsInput!): UpdateAssetSettingsResponse!
+
+ # Updates the status of an asset allowing you to close/reopen an asset for
+ # commenting.
+ # Mutation is restricted.
+ updateAssetStatus(id: ID!, input: UpdateAssetStatusInput!): UpdateAssetStatusResponse!
+
+ # 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
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/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/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/perms/constants.js b/perms/constants.js
index 383f36daf..070fe80bf 100644
--- a/perms/constants.js
+++ b/perms/constants.js
@@ -16,6 +16,10 @@ 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',
+ UPDATE_SETTINGS: 'UPDATE_SETTINGS',
+ UPDATE_WORDLIST: 'UPDATE_WORDLIST',
// queries
SEARCH_ASSETS: 'SEARCH_ASSETS',
@@ -27,6 +31,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/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.`);
}
diff --git a/perms/mutationReducer.js b/perms/mutationReducer.js
index ec97fe7a2..2e8718ed1 100644
--- a/perms/mutationReducer.js
+++ b/perms/mutationReducer.js
@@ -4,33 +4,26 @@ 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', 'STAFF']);
case types.REMOVE_COMMENT_TAG:
return check(user, ['ADMIN', 'MODERATOR', 'STAFF']);
+ case types.UPDATE_USER_ROLES:
+ case types.REJECT_USERNAME:
+ case types.SET_USER_STATUS:
+ case types.SUSPEND_USER:
+ case types.SET_COMMENT_STATUS:
case types.UPDATE_CONFIG:
+ case types.UPDATE_SETTINGS:
+ case types.UPDATE_WORDLIST:
+ case types.UPDATE_ASSET_SETTINGS:
+ case types.UPDATE_ASSET_STATUS:
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/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/plugin-api/beta/client/hocs/index.js b/plugin-api/beta/client/hocs/index.js
index 1bc47868e..15c8b3544 100644
--- a/plugin-api/beta/client/hocs/index.js
+++ b/plugin-api/beta/client/hocs/index.js
@@ -5,3 +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,
+ withStopIgnoringUser,
+} from 'coral-framework/graphql/mutations';
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.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"
]
}
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..960e66bb4
--- /dev/null
+++ b/plugins/talk-plugin-author-menu/client/actions.js
@@ -0,0 +1,19 @@
+import {SET_CONTENT_SLOT, RESET_CONTENT_SLOT, OPEN_MENU, CLOSE_MENU} from './constants';
+
+export const setContentSlot = (slot) => ({
+ type: SET_CONTENT_SLOT,
+ 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/components/AuthorName.css b/plugins/talk-plugin-author-menu/client/components/AuthorName.css
new file mode 100644
index 000000000..d9a8bdd71
--- /dev/null
+++ b/plugins/talk-plugin-author-menu/client/components/AuthorName.css
@@ -0,0 +1,16 @@
+.root {
+ display: inline-block;
+ position: relative;
+}
+
+.button {
+ composes: buttonReset from "coral-framework/styles/reset.css";
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+.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..191f93490
--- /dev/null
+++ b/plugins/talk-plugin-author-menu/client/components/AuthorName.js
@@ -0,0 +1,31 @@
+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 (
+
+