mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf04b4c2a6 | ||
|
|
9022532525 | ||
|
|
0fa27ae41b | ||
|
|
0065875f12 | ||
|
|
ba08447d5e | ||
|
|
92c72f7041 | ||
|
|
08a9788bcc | ||
|
|
3e0241b523 | ||
|
|
fe8cb90a1c | ||
|
|
74366bec55 | ||
|
|
ed92f4916d | ||
|
|
ceb96dba75 | ||
|
|
f73597d7d1 | ||
|
|
79e0da2c2f | ||
|
|
1f69bb2f14 | ||
|
|
d0ab2ef8c5 | ||
|
|
1624787004 | ||
|
|
8ea88f9b96 | ||
|
|
45873398dd | ||
|
|
739d5b6611 | ||
|
|
192c3486af | ||
|
|
0e9621c499 | ||
|
|
5b92f7e968 | ||
|
|
c9a0ab8848 | ||
|
|
e180cf87d7 | ||
|
|
de976a5397 | ||
|
|
257830a6c5 | ||
|
|
518056f382 | ||
|
|
84936ca35c | ||
|
|
4194b08a12 | ||
|
|
98e6a3ccc7 | ||
|
|
64f5171ff2 | ||
|
|
6711f09a79 | ||
|
|
8966a8201b | ||
|
|
7fc0e2e489 | ||
|
|
7c3c510bbc | ||
|
|
94ece10284 | ||
|
|
ef01f427de | ||
|
|
5db096942a | ||
|
|
4645ed4cd7 | ||
|
|
7fd524cd6a | ||
|
|
3cb5d0ff9f | ||
|
|
44216221a4 | ||
|
|
0f9b2ba114 | ||
|
|
071666e8b8 | ||
|
|
2a9a5099dc | ||
|
|
d0f51d8815 | ||
|
|
b228725370 | ||
|
|
58f2f71743 | ||
|
|
8fd266deeb | ||
|
|
1b73a5fcd7 | ||
|
|
9a58673545 | ||
|
|
2dc63d3a27 | ||
|
|
1503b735e5 | ||
|
|
6db72db837 | ||
|
|
18b274a6a9 | ||
|
|
78cfb4cac8 |
+16
-9
@@ -1,8 +1,8 @@
|
||||
# job_environment will setup the environment for any job being executed.
|
||||
job_environment: &job_environment
|
||||
NODE_ENV: test
|
||||
WEBPACK_MAX_CORES: 4
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
NODE_ENV: "test"
|
||||
WEBPACK_MAX_CORES: "4"
|
||||
NODE_OPTIONS: "--max-old-space-size=8192"
|
||||
|
||||
# job_defaults applies all the defaults for each job.
|
||||
job_defaults: &job_defaults
|
||||
@@ -13,7 +13,7 @@ job_defaults: &job_defaults
|
||||
environment:
|
||||
<<: *job_environment
|
||||
|
||||
version: 2
|
||||
version: 2.1
|
||||
jobs:
|
||||
# npm_dependencies will install the dependencies used by all other steps.
|
||||
npm_dependencies:
|
||||
@@ -49,7 +49,8 @@ jobs:
|
||||
- ~/.npm
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths: node_modules
|
||||
paths:
|
||||
- node_modules
|
||||
|
||||
# lint will perform file linting.
|
||||
lint:
|
||||
@@ -69,13 +70,16 @@ jobs:
|
||||
command: |
|
||||
npm run doctoc
|
||||
git diff --exit-code
|
||||
- run:
|
||||
name: Lint Versions
|
||||
command: npx @coralproject/package-version-lint
|
||||
|
||||
# unit_tests will run the unit tests.
|
||||
unit_tests:
|
||||
<<: *job_defaults
|
||||
environment:
|
||||
<<: *job_environment
|
||||
CI: true
|
||||
CI: "true"
|
||||
JEST_JUNIT_OUTPUT: "reports/junit/js-test-results.xml"
|
||||
steps:
|
||||
- checkout
|
||||
@@ -112,14 +116,15 @@ jobs:
|
||||
no_output_timeout: 30m
|
||||
- run:
|
||||
name: Verify Bundle Size
|
||||
command: npx bundlesize
|
||||
command: npx bundlesize2 --enable-github-checks
|
||||
- save_cache:
|
||||
key: v1-build-cache-{{ .Branch }}-{{ .Revision }}
|
||||
paths:
|
||||
- ./dist
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths: dist
|
||||
paths:
|
||||
- dist
|
||||
|
||||
# docker_tests will test that the docker build process completes.
|
||||
docker_tests:
|
||||
@@ -141,6 +146,9 @@ jobs:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Verify release version
|
||||
command: npx @coralproject/package-version-lint --expect ${CIRCLE_TAG/#v}
|
||||
- setup_remote_docker
|
||||
- deploy:
|
||||
name: Deploy the code
|
||||
@@ -167,7 +175,6 @@ filter_develop: &filter_develop
|
||||
- next
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build-test:
|
||||
jobs:
|
||||
- docker_tests:
|
||||
|
||||
+55
-48
@@ -1,10 +1,11 @@
|
||||
const typescriptEslintRecommended = require('@typescript-eslint/eslint-plugin/dist/configs/eslint-recommended').default.overrides[0];
|
||||
const typescriptRecommended = require('@typescript-eslint/eslint-plugin/dist/configs/recommended.json');
|
||||
const typescriptRecommendedTypeChecking = require('@typescript-eslint/eslint-plugin/dist/configs/recommended-requiring-type-checking.json');
|
||||
const typescriptEslintPrettier = require('eslint-config-prettier/@typescript-eslint');
|
||||
const react = require('eslint-plugin-react').configs.recommended;
|
||||
const jsxA11y = require('eslint-plugin-jsx-a11y').configs.recommended;
|
||||
const reactPrettier = require('eslint-config-prettier/react');
|
||||
const typescriptEslintRecommended = require("@typescript-eslint/eslint-plugin/dist/configs/eslint-recommended")
|
||||
.default.overrides[0];
|
||||
const typescriptRecommended = require("@typescript-eslint/eslint-plugin/dist/configs/recommended.json");
|
||||
const typescriptRecommendedTypeChecking = require("@typescript-eslint/eslint-plugin/dist/configs/recommended-requiring-type-checking.json");
|
||||
const typescriptEslintPrettier = require("eslint-config-prettier/@typescript-eslint");
|
||||
const react = require("eslint-plugin-react").configs.recommended;
|
||||
const jsxA11y = require("eslint-plugin-jsx-a11y").configs.recommended;
|
||||
const reactPrettier = require("eslint-config-prettier/react");
|
||||
|
||||
const typescriptOverrides = {
|
||||
files: ["*.ts", "*.tsx"],
|
||||
@@ -24,7 +25,7 @@ const typescriptOverrides = {
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
}
|
||||
},
|
||||
},
|
||||
rules: Object.assign(
|
||||
typescriptEslintRecommended.rules,
|
||||
@@ -36,7 +37,10 @@ const typescriptOverrides = {
|
||||
{
|
||||
"@typescript-eslint/adjacent-overload-signatures": "error",
|
||||
// TODO: (cvle) change `readonly` param to `array-simple` when upgraded typescript.
|
||||
"@typescript-eslint/array-type": ["error", { "default": "array-simple", "readonly": "generic"}],
|
||||
"@typescript-eslint/array-type": [
|
||||
"error",
|
||||
{ default: "array-simple", readonly: "generic" },
|
||||
],
|
||||
"@typescript-eslint/ban-types": "error",
|
||||
"@typescript-eslint/camelcase": "off",
|
||||
"@typescript-eslint/consistent-type-assertions": "error",
|
||||
@@ -46,22 +50,25 @@ const typescriptOverrides = {
|
||||
"@typescript-eslint/explicit-member-accessibility": [
|
||||
"error",
|
||||
{
|
||||
"overrides": {
|
||||
"constructors": "off",
|
||||
overrides: {
|
||||
constructors: "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/indent": "off",
|
||||
"@typescript-eslint/interface-name-prefix": "error",
|
||||
"@typescript-eslint/member-delimiter-style": "off",
|
||||
"@typescript-eslint/no-empty-function": "error",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-empty-interface": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-misused-new": "error",
|
||||
"@typescript-eslint/no-namespace": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-parameter-properties": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", {"args": "none", "ignoreRestSiblings": true}],
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{ args: "none", ignoreRestSiblings: true },
|
||||
],
|
||||
"@typescript-eslint/no-use-before-define": "off", // TODO: (cvle) Should be on?
|
||||
"@typescript-eslint/no-use-before-declare": "off",
|
||||
"@typescript-eslint/no-var-requires": "error",
|
||||
@@ -77,6 +84,7 @@ const typescriptOverrides = {
|
||||
"react/display-name": "error",
|
||||
"react/prop-types": "off",
|
||||
"react/no-unescaped-entities": "off",
|
||||
"no-empty-function": "off",
|
||||
}
|
||||
),
|
||||
};
|
||||
@@ -84,31 +92,36 @@ const typescriptOverrides = {
|
||||
let typescriptTypeCheckingOverrides = {
|
||||
files: ["*.ts", "*.tsx"],
|
||||
parserOptions: {
|
||||
project: ["tsconfig.json", "./src/tsconfig.json", "./src/core/client/tsconfig.json"],
|
||||
project: [
|
||||
"./tsconfig.json",
|
||||
"./src/tsconfig.json",
|
||||
"./src/core/client/tsconfig.json",
|
||||
],
|
||||
// TODO: (cvle) this is a workaround, see: https://github.com/typescript-eslint/typescript-eslint/issues/1091.
|
||||
createDefaultProgram: true,
|
||||
},
|
||||
rules: Object.assign(
|
||||
typescriptRecommendedTypeChecking.rules,
|
||||
{
|
||||
"@typescript-eslint/tslint/config": ["error", {
|
||||
"rules": {
|
||||
rules: Object.assign(typescriptRecommendedTypeChecking.rules, {
|
||||
"@typescript-eslint/tslint/config": [
|
||||
"error",
|
||||
{
|
||||
rules: {
|
||||
"ordered-imports": {
|
||||
"options": {
|
||||
"import-sources-order": "case-insensitive",
|
||||
options: {
|
||||
// Legacy sorting until this is fixed: https://github.com/SoominHan/import-sorter/issues/60
|
||||
"import-sources-order": "case-insensitive-legacy",
|
||||
"module-source-path": "full",
|
||||
"named-imports-order": "case-insensitive",
|
||||
"named-imports-order": "case-insensitive-legacy",
|
||||
},
|
||||
},
|
||||
},
|
||||
}],
|
||||
// 28.11.19: (cvle) Disabled because behavior of regexp.exec seems different than str.match?
|
||||
"@typescript-eslint/prefer-regexp-exec": "off",
|
||||
"@typescript-eslint/require-await": "off",
|
||||
"@typescript-eslint/no-misused-promises": "off",
|
||||
"@typescript-eslint/unbound-method": "off", // 10.10.19: (cvle) seems to give false positive.
|
||||
}
|
||||
),
|
||||
},
|
||||
],
|
||||
// 28.11.19: (cvle) Disabled because behavior of regexp.exec seems different than str.match?
|
||||
"@typescript-eslint/prefer-regexp-exec": "off",
|
||||
"@typescript-eslint/require-await": "off",
|
||||
"@typescript-eslint/no-misused-promises": "off",
|
||||
"@typescript-eslint/unbound-method": "off", // 10.10.19: (cvle) seems to give false positive.
|
||||
}),
|
||||
};
|
||||
|
||||
const jestOverrides = {
|
||||
@@ -117,8 +130,8 @@ const jestOverrides = {
|
||||
},
|
||||
files: ["test/**/*.ts", "test/**/*.tsx"],
|
||||
globals: {
|
||||
"expectAndFail": "readonly",
|
||||
"fail": "readonly",
|
||||
expectAndFail: "readonly",
|
||||
fail: "readonly",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -142,22 +155,19 @@ module.exports = {
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
parserOptions: {
|
||||
"ecmaVersion": 2018,
|
||||
ecmaVersion: 2018,
|
||||
},
|
||||
rules: {
|
||||
"arrow-body-style": "off",
|
||||
"arrow-parens": [
|
||||
"off",
|
||||
"as-needed",
|
||||
],
|
||||
"camelcase": "off",
|
||||
"complexity": "off",
|
||||
"arrow-parens": ["off", "as-needed"],
|
||||
camelcase: "off",
|
||||
complexity: "off",
|
||||
"constructor-super": "error",
|
||||
"spaced-comment": ["error", "always"],
|
||||
"curly": "error",
|
||||
curly: "error",
|
||||
"dot-notation": "error",
|
||||
"eol-last": "off",
|
||||
"eqeqeq": "error",
|
||||
eqeqeq: "error",
|
||||
"guard-for-in": "error",
|
||||
"jsdoc/require-jsdoc": "off",
|
||||
"jsdoc/require-returns": "off",
|
||||
@@ -165,10 +175,7 @@ module.exports = {
|
||||
"jsdoc/require-param-type": "off",
|
||||
"jsdoc/require-returns-type": "off",
|
||||
"linebreak-style": "off",
|
||||
"max-classes-per-file": [
|
||||
"error",
|
||||
1,
|
||||
],
|
||||
"max-classes-per-file": ["error", 1],
|
||||
"member-ordering": "off",
|
||||
"new-parens": "off",
|
||||
"newline-per-chained-call": "off",
|
||||
@@ -185,7 +192,7 @@ module.exports = {
|
||||
"no-irregular-whitespace": "off",
|
||||
"no-multiple-empty-lines": "off",
|
||||
"no-new-wrappers": "error",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-prototype-builtins": "error",
|
||||
"no-shadow": "error",
|
||||
"no-throw-literal": "error",
|
||||
"no-undef": "off",
|
||||
@@ -193,14 +200,14 @@ module.exports = {
|
||||
"no-unsafe-finally": "error",
|
||||
"no-unused-expressions": "error",
|
||||
"no-unused-labels": "error",
|
||||
"no-unused-vars": ["error", {"args": "none", "ignoreRestSiblings": true}],
|
||||
"no-unused-vars": ["error", { args: "none", ignoreRestSiblings: true }],
|
||||
"no-var": "error",
|
||||
"object-shorthand": "error",
|
||||
"one-var": "off",
|
||||
"prefer-arrow-callback": "off",
|
||||
"prefer-const": "error",
|
||||
"quote-props": "off",
|
||||
"radix": "error",
|
||||
radix: "error",
|
||||
"require-atomic-updates": "off",
|
||||
"space-before-function-paren": "off",
|
||||
"sort-imports": "off",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
Vendored
+1
-1
@@ -54,5 +54,5 @@
|
||||
"search.exclude": {
|
||||
"package-lock.json": true
|
||||
},
|
||||
"debug.node.autoAttach": "on"
|
||||
"debug.node.autoAttach": "off",
|
||||
}
|
||||
|
||||
@@ -129,6 +129,7 @@ createComment.error
|
||||
- <a href="#showSharePopover">showSharePopover</a>
|
||||
- <a href="#showUserPopover">showUserPopover</a>
|
||||
- <a href="#signOut">signOut</a>
|
||||
- <a href="#signedIn">signedIn</a>
|
||||
- <a href="#unfeatureComment">unfeatureComment</a>
|
||||
- <a href="#updateNotificationSettings">updateNotificationSettings</a>
|
||||
- <a href="#updateStorySettings">updateStorySettings</a>
|
||||
@@ -538,6 +539,7 @@ createComment.error
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="signedIn">**signedIn**</a>: This event is emitted when the viewer signed in (not applicable for SSO).
|
||||
- <a id="unfeatureComment">**unfeatureComment.success**, **unfeatureComment.error**</a>: This event is emitted when the viewer unfeatures a comment.
|
||||
```ts
|
||||
{
|
||||
|
||||
@@ -174,6 +174,16 @@ also supports comments in specific languages. When the language is supported in
|
||||
Coral and supported by the Perspective API, the language should be added to the
|
||||
language map in `src/core/server/services/comments/pipeline/phases/toxic.ts`.
|
||||
|
||||
To assist with the translation process, we have a script that is based on the
|
||||
work by @cristiandean in https://github.com/coralproject/talk/pull/2949 that
|
||||
will detect missing, new, or changed translation keys for the specified
|
||||
language. You can use this with:
|
||||
|
||||
```sh
|
||||
# usage: ./scripts/i18n/validate.ts <locale>
|
||||
./scripts/i18n/validate.ts pt-BR
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Documentation that is publicly shown on
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
# External Moderation Phases Guide
|
||||
|
||||
This document is in reference to external moderation phases emitted by Coral.
|
||||
You can configure external moderation phases on your installation of Coral by
|
||||
visiting `/admin/configure/moderation/phases`.
|
||||
|
||||
Once you've configured a external moderation phase in Coral, you will start to
|
||||
receive moderation requests in the form of a
|
||||
[External Moderation Requests](#external-moderation-request) at the provided
|
||||
callback URL. These will be in the form of `POST` requests with a `JSON`
|
||||
payload.
|
||||
|
||||
When a comment is created or edited, it will be processed by moderation phases in
|
||||
a predefined order. Any external moderation phase is run last, and only if all
|
||||
other moderation phases before it do not return a status. The current set of
|
||||
moderation phases is listed in order [here](https://github.com/coralproject/talk/blob/master/src/core/server/services/comments/pipeline/phases/index.ts).
|
||||
|
||||
Once you have received a moderation request, you must respond within the
|
||||
provided timeout else the phase will be skipped and it will continue. It is
|
||||
strongly recommended to [verify the request signature](#request-signing).
|
||||
|
||||
The external moderation phase must respond with one of the following:
|
||||
|
||||
1. Do not moderate the comment, and return a 204 without a body.
|
||||
2. Perform a moderation action and return a 200 with a [External Moderation Response](#external-moderation-response)
|
||||
as a `JSON` encoded body containing the operations you want to perform on the
|
||||
comment.
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table of Contents
|
||||
|
||||
- [Request Signing](#request-signing)
|
||||
- [Schema](#schema)
|
||||
- [External Moderation Request](#external-moderation-request)
|
||||
- [External Moderation Response](#external-moderation-response)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Request Signing
|
||||
|
||||
Requests sent by Coral for external moderation phases use the same process as
|
||||
those used by webhooks. Refer to the [webhooks documentation](WEBHOOKS.md#webhook-signing)
|
||||
for instructions on how to verify signatures sent by Coral.
|
||||
|
||||
## Schema
|
||||
|
||||
### External Moderation Request
|
||||
|
||||
```ts
|
||||
interface ExternalModerationRequest {
|
||||
/**
|
||||
* action refers to the specific operation being performed. If `NEW`, this
|
||||
* is referring to a new comment being created. If `EDIT`, then this refers to
|
||||
* an operation involving an edit operation on an existing Comment.
|
||||
*/
|
||||
action: "NEW" | "EDIT";
|
||||
|
||||
/**
|
||||
* comment refers to the actual Comment data for the Comment being
|
||||
* created/edited.
|
||||
*/
|
||||
comment: {
|
||||
/**
|
||||
* body refers to the actual body text of the Comment being created/edited.
|
||||
*/
|
||||
body: string;
|
||||
|
||||
/**
|
||||
* parentID is the identifier for the parent comment (if this Comment is a
|
||||
* reply, null otherwise).
|
||||
*/
|
||||
parentID: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* author refers to the User that is creating/editing the Comment.
|
||||
*/
|
||||
author: {
|
||||
/**
|
||||
* id is the identifier for this User.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* role refers to the role of this User.
|
||||
*/
|
||||
role: "COMMENTER" | "STAFF" | "MODERATOR" | "ADMIN";
|
||||
};
|
||||
|
||||
/**
|
||||
* story refers to the Story being commented on.
|
||||
*/
|
||||
story: {
|
||||
/**
|
||||
* id is the identifier for this Story.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* url is the URL for this Story.
|
||||
*/
|
||||
url: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* site refers to the Site that the story being commented on belongs to.
|
||||
*/
|
||||
site: {
|
||||
/**
|
||||
* id is the identifier for this Site.
|
||||
*/
|
||||
id: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* tenantID is the identifer of the Tenant that this Comment is being
|
||||
* created/edited on.
|
||||
*/
|
||||
tenantID: string;
|
||||
|
||||
/**
|
||||
* tenantDomain is the domain that is associated with this Tenant that this
|
||||
* Comment is being created/edited on.
|
||||
*/
|
||||
tenantDomain: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
New comment on a story:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "NEW",
|
||||
"comment": {
|
||||
"body": "Here's a comment!",
|
||||
"parentID": null
|
||||
},
|
||||
"author": {
|
||||
"id": "baf4e943-3594-4fcc-b2ba-3e8de7a76352",
|
||||
"role": "COMMENTER"
|
||||
},
|
||||
"story": {
|
||||
"id": "245b3856-b0a0-4d2f-a6bb-58c71f18d6a6",
|
||||
"url": "http://localhost:1313/posts/a-story-url/"
|
||||
},
|
||||
"site": {
|
||||
"id": "a4bede88-2d2c-4424-bc18-4322a9e285a6"
|
||||
},
|
||||
"tenantID": "19ba5794-7eeb-4d46-a81b-c00c61672501",
|
||||
"tenantDomain": "localhost"
|
||||
}
|
||||
```
|
||||
|
||||
New reply on a comment on a story:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "NEW",
|
||||
"comment": {
|
||||
"body": "Here's a reply!",
|
||||
"parentID": "d79b787f-f406-49a0-a179-72e3652e54be"
|
||||
},
|
||||
"author": {
|
||||
"id": "baf4e943-3594-4fcc-b2ba-3e8de7a76352",
|
||||
"role": "COMMENTER"
|
||||
},
|
||||
"story": {
|
||||
"id": "245b3856-b0a0-4d2f-a6bb-58c71f18d6a6",
|
||||
"url": "http://localhost:1313/posts/a-story-url/"
|
||||
},
|
||||
"site": {
|
||||
"id": "a4bede88-2d2c-4424-bc18-4322a9e285a6"
|
||||
},
|
||||
"tenantID": "19ba5794-7eeb-4d46-a81b-c00c61672501",
|
||||
"tenantDomain": "localhost"
|
||||
}
|
||||
```
|
||||
|
||||
### External Moderation Response
|
||||
|
||||
```ts
|
||||
interface ExternalModerationResponse {
|
||||
/**
|
||||
* actions is an optional list of any flags to be added to this Comment.
|
||||
*/
|
||||
actions?: Array<{
|
||||
actionType: "FLAG";
|
||||
reason: "COMMENT_DETECTED_TOXIC" | "COMMENT_DETECTED_SPAM";
|
||||
}>;
|
||||
|
||||
/**
|
||||
* tags are any listed tags that should be added to the comment.
|
||||
*/
|
||||
tags?: Array<"FEATURED" | "STAFF">;
|
||||
|
||||
/**
|
||||
* status when provided decides and terminates the moderation process by
|
||||
* setting the status of the comment.
|
||||
*/
|
||||
status?: "NONE" | "APPROVED" | "REJECTED" | "PREMOD" | "SYSTEM_WITHHELD";
|
||||
}
|
||||
```
|
||||
|
||||
#### Examples
|
||||
|
||||
Add a flag to a comment and do not set a status:
|
||||
|
||||
```json
|
||||
{
|
||||
"actions": [{ "actionType": "FLAG", "reason": "COMMENT_DETECTED_TOXIC" }]
|
||||
}
|
||||
```
|
||||
|
||||
Reject a comment:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "REJECTED"
|
||||
}
|
||||
```
|
||||
|
||||
Feature a comment and do not set a status:
|
||||
|
||||
```json
|
||||
{
|
||||
"tags": ["FEATURED"]
|
||||
}
|
||||
```
|
||||
|
||||
Approve a comment and mark it as featured:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "APPROVED",
|
||||
"tags": ["FEATURED"]
|
||||
}
|
||||
```
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright 2019 Vox Media, Inc
|
||||
Copyright 2020 Vox Media, Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -5,9 +5,9 @@ how moderation, comment display, and conversation function, creating the
|
||||
opportunity for safer, smarter discussions around your work.
|
||||
[Read more about Coral here](https://coralproject.net/talk).
|
||||
|
||||
Built with <3 by The Coral Project, a part of [Vox Media](https://product.voxmedia.com/).
|
||||
Built with <3 by the Coral team, a part of [Vox Media](https://product.voxmedia.com/).
|
||||
|
||||
Preview Coral easily by running Coral via a Heroku App:
|
||||
Preview Coral easily by running it via a Heroku App:
|
||||
|
||||
[](https://heroku.com/deploy?template=https://github.com/coralproject/talk)
|
||||
|
||||
@@ -28,13 +28,11 @@ You can get started with Coral using our [Documentation](https://docs.coralproje
|
||||
|
||||
## Pre-Launch Guide
|
||||
|
||||
You’ve installed Talk on your server, and you’re preparing to launch it on your site. The real community work starts now, before you go live. You have a unique opportunity pre-launch to set your community up for success. Read our [Talk Community Guide](https://coralproject.net/blog/youve-installed-talk-now-what/).
|
||||
You’ve installed Coral on your server, and you’re preparing to launch it on your site. The real community work starts now, before you go live. You have a unique opportunity pre-launch to set your community up for success. Read our [Community Guides](https://guides.coralproject.net/start-here/) to learn more.
|
||||
|
||||
## More Resources
|
||||
|
||||
- [Our Blog](https://coralproject.net/blog)
|
||||
- [Community Guides for Journalism](https://guides.coralproject.net/)
|
||||
- [More About Us](https://coralproject.net/)
|
||||
- [Learn more about Coral and sign up for a webinar](https://coralproject.net/)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+15
@@ -174,6 +174,16 @@ function when comparing signatures.
|
||||
* date when this event was created.
|
||||
*/
|
||||
createdAt: string;
|
||||
|
||||
/**
|
||||
* tenantID is the ID of the Tenant that this event originated at.
|
||||
*/
|
||||
tenantID: string;
|
||||
|
||||
/**
|
||||
* tenantDomain is the domain that is associated with this Tenant that this event originated at.
|
||||
*/
|
||||
tenantDomain: string;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -199,6 +209,11 @@ function when comparing signatures.
|
||||
* storyURL is the URL of the newly created Story.
|
||||
*/
|
||||
storyURL: string;
|
||||
|
||||
/**
|
||||
* siteID is the Site that the newly created Story was created on.
|
||||
*/
|
||||
siteID: string;
|
||||
}
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ const plugins =
|
||||
|
||||
const environment =
|
||||
process.env.WEBPACK === "true"
|
||||
? { targets: "IE 11", modules: false }
|
||||
? { modules: false }
|
||||
: { targets: { node: "current" }, modules: "commonjs" };
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -40,11 +40,9 @@ module.exports = {
|
||||
snapshotSerializers: ["enzyme-to-json/serializer"],
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
useBabelrc: true,
|
||||
tsConfigFile: path.resolve(
|
||||
__dirname,
|
||||
"../../src/core/client/tsconfig.json"
|
||||
),
|
||||
babelConfig: true,
|
||||
tsConfig: path.resolve(__dirname, "../../src/core/client/tsconfig.json"),
|
||||
},
|
||||
},
|
||||
preset: "ts-jest/presets/js-with-babel",
|
||||
};
|
||||
|
||||
@@ -21,8 +21,9 @@ module.exports = {
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
useBabelrc: true,
|
||||
tsConfigFile: path.resolve(__dirname, "../../src/tsconfig.json"),
|
||||
babelConfig: true,
|
||||
tsConfig: path.resolve(__dirname, "../../src/tsconfig.json"),
|
||||
},
|
||||
},
|
||||
preset: "ts-jest",
|
||||
};
|
||||
|
||||
@@ -1,105 +1,10 @@
|
||||
// Apply all the configuration provided in the .env file.
|
||||
require("dotenv").config();
|
||||
|
||||
const fs = require("fs");
|
||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||
const extensions = [".ts", ".tsx", ".js"];
|
||||
// TODO: There is some weird issue with including paths.ts here
|
||||
const postCSSConfigPath = "./src/core/build/postcss.config";
|
||||
const appDirectory = fs.realpathSync(process.cwd());
|
||||
|
||||
const styleLoader = {
|
||||
loader: require.resolve("style-loader"),
|
||||
options: {
|
||||
sourceMap: true,
|
||||
hmr: true,
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
title: "Coral 5.0",
|
||||
source: "./src",
|
||||
typescript: true,
|
||||
title: "Coral 5",
|
||||
src: "./src",
|
||||
host: process.env.HOST || "0.0.0.0",
|
||||
port: parseInt(process.env.DOCZ_PORT, 10) || 3030,
|
||||
codeSandbox: false, // Too large to create code sandboxes..
|
||||
modifyBundlerConfig: config => {
|
||||
config.entry.app.push(
|
||||
`${appDirectory}/src/core/client/ui/theme/variables.css.ts`
|
||||
);
|
||||
config.module.rules.push({
|
||||
test: /\.css\.ts$/,
|
||||
use: [
|
||||
styleLoader,
|
||||
{
|
||||
loader: require.resolve("css-loader"),
|
||||
options: {
|
||||
modules: true,
|
||||
importLoaders: 2,
|
||||
localIdentName: "[name]-[local]-[hash:base64:5]",
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve("postcss-loader"),
|
||||
options: {
|
||||
config: {
|
||||
path: postCSSConfigPath,
|
||||
},
|
||||
parser: "postcss-js",
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve("babel-loader"),
|
||||
options: {
|
||||
configFile: false,
|
||||
babelrc: false,
|
||||
presets: [
|
||||
"@babel/typescript",
|
||||
[
|
||||
"@babel/env",
|
||||
{ targets: { node: "current" }, modules: "commonjs" },
|
||||
],
|
||||
],
|
||||
// This is a feature of `babel-loader` for webpack (not Babel itself).
|
||||
// It enables caching results in ./node_modules/.cache/babel-loader/
|
||||
// directory for faster rebuilds.
|
||||
cacheDirectory: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
config.module.rules.push({
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
styleLoader,
|
||||
{
|
||||
loader: require.resolve("css-loader"),
|
||||
options: {
|
||||
modules: true,
|
||||
importLoaders: 1,
|
||||
localIdentName: "[name]-[local]-[hash:base64:5]",
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve("postcss-loader"),
|
||||
options: {
|
||||
config: {
|
||||
path: postCSSConfigPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
config.resolve.plugins = [
|
||||
new TsconfigPathsPlugin({
|
||||
extensions,
|
||||
// TODO: There is some weird issue with including paths.ts here
|
||||
configFile: "./src/core/client/tsconfig.json",
|
||||
}),
|
||||
];
|
||||
// fs.writeFileSync(path.resolve(__dirname, "tmp"), stringify(config, null, 2));
|
||||
return config;
|
||||
},
|
||||
files: "**/*.mdx",
|
||||
};
|
||||
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||
|
||||
/** Path to postCSSConfig */
|
||||
const postCSSConfigPath = path.resolve(
|
||||
__dirname,
|
||||
"../src/core/build/postcss.config"
|
||||
);
|
||||
const rootDir = path.resolve(__dirname, "../");
|
||||
const srcDir = path.resolve(rootDir, "./src");
|
||||
const appTsconfig = path.resolve(rootDir, "./src/core/client/tsconfig.json");
|
||||
|
||||
const CSS_PATTERN = /\.css$/;
|
||||
const MODULE_CSS_PATTERN = /\.module\.css$/;
|
||||
|
||||
// Define `RegExp.toJSON` so that we can stringify RegExp.
|
||||
Object.defineProperty(RegExp.prototype, "toJSON", {
|
||||
value: RegExp.prototype.toString,
|
||||
});
|
||||
|
||||
const isCssRules = (rule) =>
|
||||
rule.test &&
|
||||
(rule.test.toString() === CSS_PATTERN.toString() ||
|
||||
rule.test.toString() === MODULE_CSS_PATTERN.toString());
|
||||
|
||||
const findCssRules = (config) =>
|
||||
config.module.rules.find(
|
||||
(rule) => Array.isArray(rule.oneOf) && rule.oneOf.every(isCssRules)
|
||||
);
|
||||
|
||||
exports.onCreateWebpackConfig = ({
|
||||
stage,
|
||||
rules,
|
||||
loaders,
|
||||
plugins,
|
||||
actions,
|
||||
getConfig,
|
||||
}) => {
|
||||
// Get webpack config.
|
||||
const config = getConfig();
|
||||
|
||||
if (stage === "develop") {
|
||||
config.entry.commons.push(
|
||||
// Add our global css variables file.
|
||||
`${srcDir}/core/client/ui/theme/variables.css.ts`
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: (cvle) couldn't get build to work...
|
||||
if (stage === "build-javascript") {
|
||||
config.entry.app = [
|
||||
config.entry.app,
|
||||
`${appDir}/core/client/ui/theme/variables.css.ts`,
|
||||
];
|
||||
}
|
||||
*/
|
||||
|
||||
// Find the gatsby CSS rules.
|
||||
const cssRules = findCssRules(config);
|
||||
// Exclude them from our src dir because they are incomaptible with our
|
||||
// CSS rules.
|
||||
cssRules.exclude = srcDir;
|
||||
// Add .tx .tsx to modules
|
||||
config.resolve.extensions.push(".ts", ".tsx");
|
||||
actions.replaceWebpackConfig(config);
|
||||
|
||||
// Write out webpack config to .docz folder.
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, "webpack-" + stage),
|
||||
JSON.stringify(config, {}, 2)
|
||||
);
|
||||
|
||||
// Turn on sourceMap during develop.
|
||||
const sourceMap = stage.startsWith("develop");
|
||||
|
||||
// CSS loaders to prepend.
|
||||
const prependCSSLoaders = [];
|
||||
if (stage === "develop") {
|
||||
prependCSSLoaders.push(loaders.style());
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: (cvle) couldn't get build to work...
|
||||
if (stage === "build-javascript") {
|
||||
moreLoaders.push(loaders.style());
|
||||
}
|
||||
*/
|
||||
|
||||
actions.setWebpackConfig({
|
||||
resolve: {
|
||||
plugins: [
|
||||
// Resolve our custom paths.
|
||||
new TsconfigPathsPlugin({
|
||||
extensions: [".ts", ".tsx", ".js"],
|
||||
configFile: path.resolve(rootDir, "./src/core/client/tsconfig.json"),
|
||||
}),
|
||||
],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
include: srcDir,
|
||||
oneOf: [
|
||||
{
|
||||
test: /\.css\.ts$/,
|
||||
use: [
|
||||
...prependCSSLoaders,
|
||||
{
|
||||
loader: require.resolve("css-loader"),
|
||||
options: {
|
||||
modules: {
|
||||
localIdentName: "[name]-[local]-[hash:base64:5]",
|
||||
},
|
||||
importLoaders: 2,
|
||||
sourceMap,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve("postcss-loader"),
|
||||
options: {
|
||||
config: {
|
||||
path: postCSSConfigPath,
|
||||
},
|
||||
parser: "postcss-js",
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve("babel-loader"),
|
||||
options: {
|
||||
configFile: false,
|
||||
babelrc: false,
|
||||
presets: [
|
||||
"@babel/typescript",
|
||||
[
|
||||
"@babel/env",
|
||||
{ targets: { node: "current" }, modules: "commonjs" },
|
||||
],
|
||||
],
|
||||
// This is a feature of `babel-loader` for webpack (not Babel itself).
|
||||
// It enables caching results in ./node_modules/.cache/babel-loader/
|
||||
// directory for faster rebuilds.
|
||||
cacheDirectory: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
...prependCSSLoaders,
|
||||
{
|
||||
loader: require.resolve("css-loader"),
|
||||
options: {
|
||||
modules: {
|
||||
localIdentName: "[name]-[local]-[hash:base64:5]",
|
||||
},
|
||||
importLoaders: 1,
|
||||
sourceMap,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve("postcss-loader"),
|
||||
options: {
|
||||
config: {
|
||||
path: postCSSConfigPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve("babel-loader"),
|
||||
options: {
|
||||
root: rootDir,
|
||||
// This is a feature of `babel-loader` for webpack (not Babel itself).
|
||||
// It enables caching results in ./node_modules/.cache/babel-loader/
|
||||
// directory for faster rebuilds.
|
||||
cacheDirectory: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve("ts-loader"),
|
||||
options: {
|
||||
configFile: appTsconfig,
|
||||
compilerOptions: {
|
||||
target: "es2015",
|
||||
module: "esnext",
|
||||
jsx: "preserve",
|
||||
noEmit: false,
|
||||
},
|
||||
transpileOnly: true,
|
||||
// Overwrites the behavior of `include` and `exclude` to only
|
||||
// include files that are actually being imported and which
|
||||
// are necessary to compile the bundle.
|
||||
onlyCompileBundledFiles: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// Write out processed webpack config to .docz folder.
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, "webpack-" + stage + "-processed"),
|
||||
JSON.stringify(getConfig(), {}, 2)
|
||||
);
|
||||
};
|
||||
Generated
+25715
-14291
File diff suppressed because it is too large
Load Diff
+270
-250
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"name": "@coralproject/talk",
|
||||
"version": "6.0.0",
|
||||
"version": "6.2.3",
|
||||
"author": "The Coral Project",
|
||||
"homepage": "https://coralproject.net/",
|
||||
"sideEffects": [
|
||||
"*.css.ts",
|
||||
"*.css"
|
||||
],
|
||||
"repository": {
|
||||
@@ -18,7 +19,7 @@
|
||||
"contributors": [
|
||||
"https://github.com/coralproject/talk/graphs/contributors"
|
||||
],
|
||||
"description": "A better commenting experience from Mozilla, The Washington Post, and The New York Times.",
|
||||
"description": "A better commenting experience from Vox Media.",
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production npm-run-all generate-persist --parallel build:client build:server",
|
||||
"build:development": "NODE_ENV=development npm-run-all generate --parallel build:client build:server",
|
||||
@@ -26,7 +27,7 @@
|
||||
"build:server": "gulp server",
|
||||
"migration:create": "ts-node --transpile-only ./scripts/migration/create.ts",
|
||||
"docs:events": "ts-node ./scripts/generateEventDocs.ts ./src/core/client/stream/events.ts ./CLIENT_EVENTS.md",
|
||||
"doctoc": "doctoc --maxlevel=3 --title '## Table of Contents' README.md CLIENT_EVENTS.md CONTRIBUTING.md WEBHOOKS.md",
|
||||
"doctoc": "doctoc --maxlevel=3 --title '## Table of Contents' README.md CLIENT_EVENTS.md CONTRIBUTING.md WEBHOOKS.md EXTERNAL_MODERATION_PHASES.md",
|
||||
"generate": "npm-run-all generate:css-types generate:schema generate:relay",
|
||||
"generate-persist": "npm-run-all generate:css-types generate:schema generate:relay-persist",
|
||||
"generate:css-types": "tcm src/core/client/",
|
||||
@@ -58,336 +59,345 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@coralproject/bunyan-prettystream": "^0.1.4",
|
||||
"@fluent/bundle": "^0.14.0",
|
||||
"@emotion/core": "^10.0.28",
|
||||
"@fluent/bundle": "^0.15.0",
|
||||
"@fluent/dom": "^0.6.0",
|
||||
"@metascraper/helpers": "^5.7.21",
|
||||
"@hapi/joi": "^17.1.1",
|
||||
"@metascraper/helpers": "^5.11.6",
|
||||
"abort-controller": "^3.0.0",
|
||||
"akismet-api": "^4.2.0",
|
||||
"apollo-server-express": "^2.8.1",
|
||||
"archiver": "^3.0.3",
|
||||
"akismet-api": "^5.0.0",
|
||||
"apollo-server-express": "^2.11.0",
|
||||
"archiver": "^3.1.1",
|
||||
"basic-auth": "^2.0.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bull": "^3.8.1",
|
||||
"bull": "^3.13.0",
|
||||
"bunyan": "^1.8.12",
|
||||
"cheerio": "^1.0.0-rc.2",
|
||||
"cheerio": "^1.0.0-rc.3",
|
||||
"consolidate": "0.14.0",
|
||||
"content-security-policy-builder": "^2.0.0",
|
||||
"convict": "^4.3.1",
|
||||
"content-security-policy-builder": "^2.1.0",
|
||||
"convict": "^5.2.0",
|
||||
"cookie": "^0.4.0",
|
||||
"cookie-parser": "^1.4.4",
|
||||
"cors": "^2.8.4",
|
||||
"cron": "^1.7.1",
|
||||
"csv-stringify": "^5.3.0",
|
||||
"dataloader": "^1.4.0",
|
||||
"dompurify": "^2.0.7",
|
||||
"dotenv": "^6.0.0",
|
||||
"dotenv-expand": "^4.2.0",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^1.8.2",
|
||||
"csv-stringify": "^5.3.6",
|
||||
"dataloader": "1.4.0",
|
||||
"dompurify": "^2.0.8",
|
||||
"dotenv": "^8.2.0",
|
||||
"env-rewrite": "^1.0.2",
|
||||
"express": "^4.16.3",
|
||||
"express": "^4.17.1",
|
||||
"express-enforces-ssl": "^1.1.0",
|
||||
"express-static-gzip": "^0.3.2",
|
||||
"fs-extra": "^6.0.1",
|
||||
"graphql": "^0.13.2",
|
||||
"graphql-config": "^2.0.1",
|
||||
"graphql-extensions": "^0.2.1",
|
||||
"graphql-fields": "^1.1.0",
|
||||
"express-static-gzip": "^2.0.6",
|
||||
"fs-extra": "^8.1.0",
|
||||
"graphql": "^14.6.0",
|
||||
"graphql-config": "^2.2.2",
|
||||
"graphql-extensions": "^0.11.0",
|
||||
"graphql-fields": "2.0.3",
|
||||
"graphql-import": "^0.7.1",
|
||||
"graphql-playground-html": "^1.6.0",
|
||||
"graphql-redis-subscriptions": "^2.1.0",
|
||||
"graphql-playground-html": "^1.6.13",
|
||||
"graphql-redis-subscriptions": "^2.2.1",
|
||||
"graphql-subscriptions": "^1.1.0",
|
||||
"graphql-tools": "^3.0.5",
|
||||
"helmet": "^3.21.2",
|
||||
"html-minifier": "^3.5.21",
|
||||
"html-to-text": "^4.0.0",
|
||||
"ioredis": "^4.9.0",
|
||||
"joi": "^13.4.0",
|
||||
"jsdom": "^15.0.0",
|
||||
"graphql-tools": "^4.0.7",
|
||||
"helmet": "^3.22.0",
|
||||
"html-minifier": "^4.0.0",
|
||||
"html-to-text": "^5.1.1",
|
||||
"ioredis": "^4.16.1",
|
||||
"jsdom": "^16.2.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"juice": "^5.2.0",
|
||||
"jwks-rsa": "^1.6.0",
|
||||
"keymaster": "^1.6.2",
|
||||
"linkifyjs": "^2.1.8",
|
||||
"juice": "^6.0.0",
|
||||
"jwks-rsa": "^1.7.0",
|
||||
"linkifyjs": "^2.1.9",
|
||||
"lodash": "^4.17.15",
|
||||
"long-settimeout": "^1.0.1",
|
||||
"lru-cache": "^5.1.1",
|
||||
"luxon": "^1.12.0",
|
||||
"metascraper-author": "^5.8.0",
|
||||
"metascraper-description": "^5.8.0",
|
||||
"metascraper-image": "^5.8.0",
|
||||
"metascraper-title": "^5.8.0",
|
||||
"mongodb": "^3.2.7",
|
||||
"luxon": "^1.22.2",
|
||||
"metascraper-author": "^5.11.6",
|
||||
"metascraper-description": "^5.11.6",
|
||||
"metascraper-image": "^5.11.6",
|
||||
"metascraper-title": "^5.11.6",
|
||||
"mongodb": "^3.5.5",
|
||||
"mongodb-core": "^3.2.7",
|
||||
"ms": "^2.1.1",
|
||||
"ms": "^2.1.2",
|
||||
"node-fetch": "^2.6.0",
|
||||
"nodemailer": "^4.6.7",
|
||||
"nunjucks": "^3.1.3",
|
||||
"nodemailer": "^6.4.6",
|
||||
"nunjucks": "^3.2.1",
|
||||
"on-finished": "^2.3.0",
|
||||
"passport": "^0.4.1",
|
||||
"passport-facebook": "^3.0.0",
|
||||
"passport-google-oauth2": "^0.2.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-oauth2": "^1.4.0",
|
||||
"passport-oauth2": "^1.5.0",
|
||||
"passport-strategy": "^1.0.0",
|
||||
"performance-now": "^2.1.0",
|
||||
"permit": "^0.2.4",
|
||||
"prom-client": "^11.3.0",
|
||||
"prom-client": "^12.0.0",
|
||||
"proxy-agent": "^3.1.1",
|
||||
"querystringify": "^2.1.1",
|
||||
"react-helmet": "^5.2.1",
|
||||
"source-map-support": "^0.5.12",
|
||||
"stack-utils": "^1.0.2",
|
||||
"source-map-support": "^0.5.16",
|
||||
"stack-utils": "^2.0.1",
|
||||
"striptags": "^3.1.1",
|
||||
"throng": "^4.0.0",
|
||||
"tsscmp": "^1.0.6",
|
||||
"url-regex": "^5.0.0",
|
||||
"uuid": "^3.3.3",
|
||||
"verror": "^1.10.0"
|
||||
"uuid": "^7.0.3",
|
||||
"verror": "^1.10.0",
|
||||
"xregexp": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.8.3",
|
||||
"@babel/preset-env": "^7.8.3",
|
||||
"@babel/preset-react": "^7.8.3",
|
||||
"@babel/preset-typescript": "^7.8.3",
|
||||
"@babel/runtime-corejs3": "^7.8.3",
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/preset-env": "^7.9.0",
|
||||
"@babel/preset-react": "^7.9.4",
|
||||
"@babel/preset-typescript": "^7.9.0",
|
||||
"@babel/runtime-corejs3": "^7.9.2",
|
||||
"@coralproject/npm-run-all": "^4.1.5",
|
||||
"@coralproject/rte": "^0.11.1",
|
||||
"@fluent/react": "^0.10.0",
|
||||
"@fluent/react": "^0.11.1",
|
||||
"@intervolga/optimize-cssnano-plugin": "^1.0.6",
|
||||
"@types/agent-base": "^4.2.0",
|
||||
"@types/archiver": "^3.0.0",
|
||||
"@types/basic-auth": "^1.1.2",
|
||||
"@types/bcryptjs": "^2.4.1",
|
||||
"@types/bull": "^3.5.12",
|
||||
"@types/bunyan": "^1.8.4",
|
||||
"@types/case-sensitive-paths-webpack-plugin": "^2.1.2",
|
||||
"@types/cheerio": "^0.22.8",
|
||||
"@types/classnames": "^2.2.7",
|
||||
"@types/commander": "^2.12.2",
|
||||
"@types/archiver": "^3.1.0",
|
||||
"@types/basic-auth": "^1.1.3",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/bull": "^3.12.1",
|
||||
"@types/bunyan": "^1.8.6",
|
||||
"@types/case-sensitive-paths-webpack-plugin": "^2.1.4",
|
||||
"@types/cheerio": "^0.22.17",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/common-tags": "^1.8.0",
|
||||
"@types/compression-webpack-plugin": "^2.0.0",
|
||||
"@types/consolidate": "0.0.34",
|
||||
"@types/convict": "^4.2.0",
|
||||
"@types/compression-webpack-plugin": "^2.0.1",
|
||||
"@types/consolidate": "0.14.0",
|
||||
"@types/convict": "^4.2.1",
|
||||
"@types/cookie": "^0.3.3",
|
||||
"@types/cookie-parser": "^1.4.1",
|
||||
"@types/cors": "^2.8.4",
|
||||
"@types/cron": "^1.7.1",
|
||||
"@types/cross-spawn": "^6.0.0",
|
||||
"@types/dompurify": "0.0.33",
|
||||
"@types/dotenv": "^4.0.3",
|
||||
"@types/enzyme": "^3.1.15",
|
||||
"@types/enzyme-adapter-react-16": "^1.0.3",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/cors": "^2.8.6",
|
||||
"@types/cron": "^1.7.2",
|
||||
"@types/cross-spawn": "^6.0.1",
|
||||
"@types/dompurify": "^2.0.1",
|
||||
"@types/enzyme": "^3.10.5",
|
||||
"@types/enzyme-adapter-react-16": "^1.0.6",
|
||||
"@types/eventemitter2": "^4.1.0",
|
||||
"@types/express": "^4.16.0",
|
||||
"@types/express": "^4.17.4",
|
||||
"@types/express-enforces-ssl": "^1.1.1",
|
||||
"@types/flat": "0.0.28",
|
||||
"@types/fs-extra": "^5.0.4",
|
||||
"@types/graphql": "^0.13.3",
|
||||
"@types/helmet": "0.0.45",
|
||||
"@types/html-minifier": "^3.5.2",
|
||||
"@types/express-serve-static-core": "^4.17.3",
|
||||
"@types/flat": "^5.0.0",
|
||||
"@types/fs-extra": "^8.1.0",
|
||||
"@types/hapi__joi": "^16.0.12",
|
||||
"@types/helmet": "^0.0.45",
|
||||
"@types/html-minifier": "^3.5.3",
|
||||
"@types/html-minifier-terser": "^5.0.0",
|
||||
"@types/html-to-text": "^1.4.31",
|
||||
"@types/html-webpack-plugin": "^3.2.0",
|
||||
"@types/ioredis": "^4.0.10",
|
||||
"@types/jest": "^24.0.23",
|
||||
"@types/html-webpack-plugin": "^3.2.2",
|
||||
"@types/ioredis": "^4.14.9",
|
||||
"@types/jest": "^25.1.4",
|
||||
"@types/jest-axe": "^3.2.1",
|
||||
"@types/joi": "^13.0.8",
|
||||
"@types/jsdom": "^12.2.3",
|
||||
"@types/jsonwebtoken": "^8.3.5",
|
||||
"@types/linkifyjs": "^2.1.1",
|
||||
"@types/lodash": "^4.14.118",
|
||||
"@types/jsdom": "^16.2.0",
|
||||
"@types/jsonwebtoken": "^8.3.8",
|
||||
"@types/linkifyjs": "^2.1.3",
|
||||
"@types/lodash": "^4.14.149",
|
||||
"@types/lru-cache": "^5.1.0",
|
||||
"@types/luxon": "^1.12.0",
|
||||
"@types/marked": "^0.6.0",
|
||||
"@types/mini-css-extract-plugin": "^0.2.0",
|
||||
"@types/mongodb": "^3.1.22",
|
||||
"@types/ms": "^0.7.30",
|
||||
"@types/node": "^10.5.2",
|
||||
"@types/node-fetch": "^2.5.3",
|
||||
"@types/nodemailer": "^4.6.2",
|
||||
"@types/nunjucks": "^3.1.1",
|
||||
"@types/object-diff": "0.0.0",
|
||||
"@types/luxon": "^1.22.0",
|
||||
"@types/marked": "^0.7.3",
|
||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
||||
"@types/mongodb": "3.1.22",
|
||||
"@types/ms": "^0.7.31",
|
||||
"@types/node": "^12.12.34",
|
||||
"@types/node-fetch": "^2.5.5",
|
||||
"@types/nodemailer": "^6.4.0",
|
||||
"@types/nunjucks": "^3.1.3",
|
||||
"@types/on-finished": "^2.3.1",
|
||||
"@types/passport": "^1.0.2",
|
||||
"@types/passport-facebook": "^2.1.8",
|
||||
"@types/passport": "^1.0.3",
|
||||
"@types/passport-facebook": "^2.1.9",
|
||||
"@types/passport-local": "^1.0.33",
|
||||
"@types/passport-oauth2": "^1.4.5",
|
||||
"@types/passport-strategy": "^0.2.33",
|
||||
"@types/passport-oauth2": "^1.4.8",
|
||||
"@types/passport-strategy": "^0.2.35",
|
||||
"@types/permit": "^0.2.1",
|
||||
"@types/prettier": "^1.19.0",
|
||||
"@types/prop-types": "^15.5.8",
|
||||
"@types/react": "^16.8.15",
|
||||
"@types/prettier": "^1.19.1",
|
||||
"@types/react": "^16.9.31",
|
||||
"@types/react-axe": "^3.1.0",
|
||||
"@types/react-copy-to-clipboard": "^4.2.6",
|
||||
"@types/react-dom": "^16.8.4",
|
||||
"@types/react-helmet": "^5.0.10",
|
||||
"@types/react-relay": "^5.0.1",
|
||||
"@types/react-responsive": "^3.0.1",
|
||||
"@types/react-test-renderer": "^16.8.1",
|
||||
"@types/react-transition-group": "^2.0.14",
|
||||
"@types/recompose": "^0.26.5",
|
||||
"@types/relay-runtime": "^5.0.5",
|
||||
"@types/react-copy-to-clipboard": "^4.3.0",
|
||||
"@types/react-dom": "^16.9.6",
|
||||
"@types/react-helmet": "^5.0.15",
|
||||
"@types/react-relay": "^7.0.3",
|
||||
"@types/react-responsive": "^8.0.2",
|
||||
"@types/react-test-renderer": "^16.9.2",
|
||||
"@types/react-transition-group": "^4.2.4",
|
||||
"@types/recompose": "^0.30.7",
|
||||
"@types/relay-runtime": "^8.0.7",
|
||||
"@types/sane": "^2.0.0",
|
||||
"@types/shallow-equals": "^1.0.0",
|
||||
"@types/simplemde": "^1.11.7",
|
||||
"@types/sinon": "^7.0.11",
|
||||
"@types/source-map-support": "^0.5.0",
|
||||
"@types/sinon": "^7.5.2",
|
||||
"@types/source-map-support": "^0.5.1",
|
||||
"@types/stack-trace": "0.0.29",
|
||||
"@types/stack-utils": "^1.0.1",
|
||||
"@types/throng": "^4.0.2",
|
||||
"@types/uuid": "^3.4.4",
|
||||
"@types/uuid": "^7.0.2",
|
||||
"@types/verror": "^1.10.3",
|
||||
"@types/vinyl": "^2.0.2",
|
||||
"@types/webpack": "^4.4.31",
|
||||
"@types/vinyl": "^2.0.4",
|
||||
"@types/webpack": "^4.41.10",
|
||||
"@types/webpack-assets-manifest": "^3.0.0",
|
||||
"@types/webpack-bundle-analyzer": "^2.13.1",
|
||||
"@types/webpack-dev-server": "^3.1.5",
|
||||
"@types/ws": "^5.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "2.3.3",
|
||||
"@typescript-eslint/eslint-plugin-tslint": "2.3.3",
|
||||
"@typescript-eslint/parser": "2.3.3",
|
||||
"acorn": "^6.1.1",
|
||||
"ansi-styles": "^3.2.0",
|
||||
"autoprefixer": "^9.5.1",
|
||||
"@types/webpack-bundle-analyzer": "^2.13.3",
|
||||
"@types/webpack-dev-server": "^3.10.1",
|
||||
"@types/ws": "^7.2.3",
|
||||
"@types/xregexp": "^4.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
||||
"@typescript-eslint/eslint-plugin-tslint": "^2.26.0",
|
||||
"@typescript-eslint/parser": "^2.26.0",
|
||||
"autoprefixer": "^9.7.5",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-jest": "^24.9.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-jest": "^25.2.4",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
"babel-plugin-module-resolver": "^3.2.0",
|
||||
"babel-plugin-relay": "^5.0.0",
|
||||
"babel-plugin-module-resolver": "^4.0.0",
|
||||
"babel-plugin-relay": "^9.0.0",
|
||||
"babel-plugin-use-lodash-es": "^0.2.0",
|
||||
"bowser": "^1.9.4",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.2.0",
|
||||
"chalk": "^2.4.2",
|
||||
"chokidar": "^3.0.0",
|
||||
"bowser": "^2.9.0",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.3.0",
|
||||
"chalk": "^3.0.0",
|
||||
"chokidar": "^3.3.1",
|
||||
"classnames": "^2.2.6",
|
||||
"commander": "^2.20.0",
|
||||
"comment-json": "^1.1.3",
|
||||
"commander": "^5.0.0",
|
||||
"comment-json": "^3.0.2",
|
||||
"common-tags": "^1.8.0",
|
||||
"compression-webpack-plugin": "^2.0.0",
|
||||
"copy-webpack-plugin": "^5.0.3",
|
||||
"compression-webpack-plugin": "^3.1.0",
|
||||
"core-js": "^3.6.4",
|
||||
"cross-spawn": "^6.0.5",
|
||||
"css-loader": "^1.0.1",
|
||||
"css-vars-ponyfill": "^2.1.2",
|
||||
"del": "^4.1.1",
|
||||
"cross-spawn": "^7.0.1",
|
||||
"css-loader": "^3.4.2",
|
||||
"css-vars-ponyfill": "^2.2.1",
|
||||
"del": "^5.1.0",
|
||||
"doctoc": "^1.4.0",
|
||||
"docz": "^0.13.7",
|
||||
"docz-theme-default": "^0.13.7",
|
||||
"enzyme": "^3.9.0",
|
||||
"enzyme-adapter-react-16": "^1.12.1",
|
||||
"enzyme-to-json": "^3.3.5",
|
||||
"eslint": "^6.5.1",
|
||||
"eslint-config-prettier": "^6.3.0",
|
||||
"eslint-plugin-jsdoc": "^15.9.7",
|
||||
"docz": "^v2.3.0-alpha.14",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.2",
|
||||
"enzyme-to-json": "^3.4.4",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-prettier": "^6.10.1",
|
||||
"eslint-plugin-jsdoc": "^22.1.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-prettier": "^3.1.1",
|
||||
"eslint-plugin-react": "^7.15.1",
|
||||
"eventemitter2": "^5.0.1",
|
||||
"farce": "^0.2.6",
|
||||
"eslint-plugin-prettier": "^3.1.2",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"eventemitter2": "^6.3.1",
|
||||
"farce": "^0.2.8",
|
||||
"final-form": "4.18.6",
|
||||
"final-form-arrays": "1.1.2",
|
||||
"flat": "^4.1.0",
|
||||
"final-form-arrays": "^3.0.2",
|
||||
"flat": "^5.0.0",
|
||||
"fluent-intl-polyfill": "^0.1.0",
|
||||
"fork-ts-checker-webpack-plugin": "^1.5.0",
|
||||
"found": "^0.4.9",
|
||||
"found-relay": "^0.4.3",
|
||||
"graphql-schema-linter": "^0.2.0",
|
||||
"graphql-schema-typescript": "^1.2.9",
|
||||
"fork-ts-checker-webpack-plugin": "^4.1.2",
|
||||
"found": "^0.4.10",
|
||||
"found-relay": "^0.7.0",
|
||||
"graphql-schema-linter": "^0.2.4",
|
||||
"graphql-schema-typescript": "^1.3.2",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-babel": "^8.0.0",
|
||||
"gulp-cli": "^2.2.0",
|
||||
"gulp-sourcemaps": "^2.6.5",
|
||||
"gulp-typescript": "^5.0.1",
|
||||
"html-webpack-plugin": "^4.0.0-beta.11",
|
||||
"husky": "^2.2.0",
|
||||
"intersection-observer": "^0.6.0",
|
||||
"jest": "^24.8.0",
|
||||
"jest-axe": "^3.2.0",
|
||||
"jest-junit": "^6.4.0",
|
||||
"gulp-typescript": "^6.0.0-alpha.1",
|
||||
"html-webpack-plugin": "^4.0.4",
|
||||
"husky": "^4.2.3",
|
||||
"intersection-observer": "^0.7.0",
|
||||
"jest": "^25.2.4",
|
||||
"jest-axe": "^3.4.0",
|
||||
"jest-junit": "^10.0.0",
|
||||
"jest-localstorage-mock": "^2.4.0",
|
||||
"jest-mock-console": "^1.0.0",
|
||||
"lint-staged": "^8.1.6",
|
||||
"loader-utils": "^1.2.3",
|
||||
"lodash-es": "^4.17.14",
|
||||
"marked": "^0.7.0",
|
||||
"keymaster": "^1.6.2",
|
||||
"lint-staged": "^10.1.1",
|
||||
"loader-utils": "^2.0.0",
|
||||
"lodash-es": "^4.17.15",
|
||||
"marked": "^0.8.2",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"mini-css-extract-plugin": "^0.6.0",
|
||||
"object-diff": "0.0.4",
|
||||
"postcss-advanced-variables": "^3.0.0",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"postcss-advanced-variables": "^3.0.1",
|
||||
"postcss-calc-function": "^1.1.0",
|
||||
"postcss-css-variables": "^0.11.0",
|
||||
"postcss-flexbugs-fixes": "^4.1.0",
|
||||
"postcss-font-magician": "^2.2.1",
|
||||
"postcss-import": "^11.1.0",
|
||||
"postcss-js": "^2.0.1",
|
||||
"postcss-flexbugs-fixes": "^4.2.0",
|
||||
"postcss-font-magician": "^2.3.1",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-js": "^2.0.3",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"postcss-mixins": "^6.2.1",
|
||||
"postcss-nested": "^4.1.1",
|
||||
"postcss-mixins": "^6.2.3",
|
||||
"postcss-nested": "^4.2.1",
|
||||
"postcss-prepend-imports": "^1.0.1",
|
||||
"postcss-preset-env": "^6.5.0",
|
||||
"prettier": "^1.18.2",
|
||||
"prop-types": "^15.6.2",
|
||||
"proxy-polyfill": "^0.3.0",
|
||||
"pstree.remy": "^1.1.6",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"prettier": "^2.0.2",
|
||||
"proxy-polyfill": "^0.3.1",
|
||||
"pstree.remy": "^1.1.7",
|
||||
"pym.js": "^1.3.2",
|
||||
"raw-loader": "^0.5.1",
|
||||
"react": "^16.9.0",
|
||||
"react-axe": "^3.3.0",
|
||||
"react-copy-to-clipboard": "^5.0.1",
|
||||
"react-dev-utils": "^9.0.0",
|
||||
"react-dom": "^16.9.0",
|
||||
"react-error-overlay": "^5.1.6",
|
||||
"raw-loader": "^4.0.0",
|
||||
"react": "^16.13.1",
|
||||
"react-axe": "^3.4.1",
|
||||
"react-copy-to-clipboard": "^5.0.2",
|
||||
"react-dev-utils": "^10.2.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-error-overlay": "^6.0.7",
|
||||
"react-final-form": "6.3.0",
|
||||
"react-final-form-arrays": "3.1.0",
|
||||
"react-popper": "^1.3.2",
|
||||
"react-relay": "^5.0.0",
|
||||
"react-relay-network-modern": "^4.0.4",
|
||||
"react-responsive": "^7.0.0",
|
||||
"react-test-renderer": "^16.9.0",
|
||||
"react-timeago": "^4.1.9",
|
||||
"react-transition-group": "^2.9.0",
|
||||
"react-with-state-props": "^2.0.4",
|
||||
"recompose": "0.27.1",
|
||||
"regenerator-runtime": "^0.13.3",
|
||||
"relay-compiler": "^5.0.0",
|
||||
"relay-compiler-language-typescript": "^4.1.0",
|
||||
"react-final-form-arrays": "^3.1.1",
|
||||
"react-helmet": "^5.2.1",
|
||||
"react-popper": "^1.3.7",
|
||||
"react-relay": "^9.0.0",
|
||||
"react-relay-network-modern": "^4.6.1",
|
||||
"react-responsive": "^8.0.3",
|
||||
"react-test-renderer": "^16.13.1",
|
||||
"react-timeago": "^4.4.0",
|
||||
"react-transition-group": "^4.3.0",
|
||||
"recompose": "^0.30.0",
|
||||
"regenerator-runtime": "^0.13.5",
|
||||
"relay-compiler": "^9.0.0",
|
||||
"relay-compiler-language-typescript": "^12.0.0",
|
||||
"relay-local-schema": "^0.8.0",
|
||||
"relay-runtime": "^5.0.0",
|
||||
"relay-runtime": "^9.0.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"sane": "^4.1.0",
|
||||
"scheduler": "^0.15.0",
|
||||
"shallow-equals": "^1.0.0",
|
||||
"simplemde": "^1.11.2",
|
||||
"simulant": "^0.2.2",
|
||||
"sinon": "^7.3.2",
|
||||
"sockjs-client": "^1.3.0",
|
||||
"stack-trace": "0.0.10",
|
||||
"strip-ansi": "^5.2.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"sinon": "^9.0.1",
|
||||
"sockjs-client": "^1.4.0",
|
||||
"stack-trace": "^0.0.10",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"style-loader": "^1.1.3",
|
||||
"subscriptions-transport-ws": "^0.9.16",
|
||||
"terser-webpack-plugin": "^1.2.3",
|
||||
"thread-loader": "^2.1.2",
|
||||
"terser-webpack-plugin": "^2.3.5",
|
||||
"thread-loader": "^2.1.3",
|
||||
"timekeeper": "^2.2.0",
|
||||
"ts-jest": "<23.10.0",
|
||||
"ts-loader": "^6.0.0",
|
||||
"ts-node": "^8.1.0",
|
||||
"ts-node-dev": "^1.0.0-pre.37",
|
||||
"tsconfig-paths": "^3.8.0",
|
||||
"ts-jest": "^25.3.0",
|
||||
"ts-loader": "^6.2.2",
|
||||
"ts-node": "^8.8.1",
|
||||
"ts-node-dev": "^1.0.0-pre.44",
|
||||
"tsconfig-paths": "^3.9.0",
|
||||
"tsconfig-paths-webpack-plugin": "^3.2.0",
|
||||
"tslint": "^5.20.0",
|
||||
"typed-css-modules": "^0.4.2",
|
||||
"typeface-manuale": "^0.0.71",
|
||||
"typeface-nunito": "0.0.72",
|
||||
"typeface-open-sans": "0.0.75",
|
||||
"typeface-source-sans-pro": "^0.0.54",
|
||||
"typescript": "3.3.4000",
|
||||
"typescript-snapshots-plugin": "^1.6.0",
|
||||
"wait-for-expect": "^1.1.1",
|
||||
"webpack": "^4.41.5",
|
||||
"tslint": "^6.1.0",
|
||||
"typed-css-modules": "^0.6.3",
|
||||
"typeface-manuale": "^1.1.4",
|
||||
"typeface-nunito": "^1.1.3",
|
||||
"typeface-open-sans": "^0.0.75",
|
||||
"typeface-source-sans-pro": "^1.1.5",
|
||||
"typescript": "^3.8.3",
|
||||
"typescript-snapshots-plugin": "^1.7.0",
|
||||
"wait-for-expect": "^1.3.0",
|
||||
"webpack": "^4.42.1",
|
||||
"webpack-assets-manifest": "^3.1.1",
|
||||
"webpack-bundle-analyzer": "^3.3.2",
|
||||
"webpack-cli": "^3.3.2",
|
||||
"webpack-dev-server": "3.2.1",
|
||||
"webpack-bundle-analyzer": "^3.6.1",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-dev-server": "^3.10.3",
|
||||
"whatwg-fetch": "^3.0.0"
|
||||
},
|
||||
"dependencies-pins-documentation": {
|
||||
"wait-for-expect@1.x.x": [
|
||||
"Newer versions breaks the use of jest fake timers"
|
||||
],
|
||||
"consolidate@14.0.0": [
|
||||
"Newer versions has problems with using nunjucks.",
|
||||
"Update when the following issue is resolved:",
|
||||
"https://github.com/tj/consolidate.js/issues/244"
|
||||
],
|
||||
"final-form@4.18.6": [
|
||||
"Newer versions has problems with resetting forms.",
|
||||
"Update when the following issue is resolved:",
|
||||
"https://github.com/final-form/final-form/issues/317"
|
||||
],
|
||||
"react-final-form@6.3.0": [
|
||||
"Newer versions has problems with radio and checkbox fields.",
|
||||
"Update when the following issue is resolved:",
|
||||
"https://github.com/final-form/react-final-form/issues/683"
|
||||
]
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "FAST_LINT=true lint-staged"
|
||||
@@ -403,20 +413,30 @@
|
||||
"{src/core/client/stream/events.ts,scripts/generateEventDocs.ts,CLIENT_EVENTS.md}": [
|
||||
"npm run docs:events -- --verify"
|
||||
],
|
||||
"{README,CLIENT_EVENTS,CONTRIBUTING,WEBHOOKS}.md": [
|
||||
"npm run doctoc",
|
||||
"git add"
|
||||
"{README,CLIENT_EVENTS,CONTRIBUTING,WEBHOOKS,EXTERNAL_MODERATION_PHASES}.md": [
|
||||
"npm run doctoc"
|
||||
]
|
||||
},
|
||||
"bundlesize": [
|
||||
{
|
||||
"path": "./dist/static/assets/js/embed.js",
|
||||
"maxSize": "15 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/static/assets/js/count.js",
|
||||
"maxSize": "2 kB"
|
||||
}
|
||||
],
|
||||
"graphql-schema-linter": {
|
||||
"rules": [
|
||||
"types-are-capitalized"
|
||||
]
|
||||
}
|
||||
},
|
||||
"browsers": [
|
||||
">1%",
|
||||
"last 4 versions",
|
||||
"Firefox ESR",
|
||||
"IE 11",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -27,14 +27,14 @@ const forkTsCheckerWebpackPlugin = require("react-dev-utils/ForkTsCheckerWebpack
|
||||
const isInteractive = false;
|
||||
|
||||
function prepareUrls(protocol, host, port) {
|
||||
const formatUrl = hostname =>
|
||||
const formatUrl = (hostname) =>
|
||||
url.format({
|
||||
protocol,
|
||||
hostname,
|
||||
port,
|
||||
pathname: "/",
|
||||
});
|
||||
const prettyPrintUrl = hostname =>
|
||||
const prettyPrintUrl = (hostname) =>
|
||||
url.format({
|
||||
protocol,
|
||||
hostname,
|
||||
@@ -144,12 +144,12 @@ function createCompiler({
|
||||
const tsMessagesPromises = [];
|
||||
|
||||
if (useTypeScript) {
|
||||
compiler.compilers.forEach(singleCompiler => {
|
||||
compiler.compilers.forEach((singleCompiler) => {
|
||||
let tsMessagesPromise;
|
||||
let tsMessagesResolver;
|
||||
singleCompiler.hooks.beforeCompile.tap("beforeCompile", () => {
|
||||
tsMessagesPromise = new Promise(resolve => {
|
||||
tsMessagesResolver = msgs => resolve(msgs);
|
||||
tsMessagesPromise = new Promise((resolve) => {
|
||||
tsMessagesResolver = (msgs) => resolve(msgs);
|
||||
});
|
||||
tsMessagesPromises.push(tsMessagesPromise);
|
||||
});
|
||||
@@ -170,13 +170,15 @@ function createCompiler({
|
||||
(diagnostics, lints) => {
|
||||
console.log("RECEIVED");
|
||||
const allMsgs = [...diagnostics, ...lints];
|
||||
const format = message =>
|
||||
const format = (message) =>
|
||||
`${message.file}\n${typescriptFormatter(message, true)}`;
|
||||
|
||||
tsMessagesResolver({
|
||||
errors: allMsgs.filter(msg => msg.severity === "error").map(format),
|
||||
errors: allMsgs
|
||||
.filter((msg) => msg.severity === "error")
|
||||
.map(format),
|
||||
warnings: allMsgs
|
||||
.filter(msg => msg.severity === "warning")
|
||||
.filter((msg) => msg.severity === "warning")
|
||||
.map(format),
|
||||
});
|
||||
}
|
||||
@@ -186,7 +188,7 @@ function createCompiler({
|
||||
|
||||
// "done" event fires when Webpack has finished recompiling the bundle.
|
||||
// Whether or not you have warnings or errors, you will get this event.
|
||||
compiler.hooks.done.tap("done", async stats => {
|
||||
compiler.hooks.done.tap("done", async (stats) => {
|
||||
if (isInteractive) {
|
||||
clearConsole();
|
||||
}
|
||||
@@ -203,7 +205,7 @@ function createCompiler({
|
||||
...config.stats,
|
||||
};
|
||||
if (Array.isArray(config)) {
|
||||
statOptions.children = config.map(c => c.stats || {});
|
||||
statOptions.children = config.map((c) => c.stats || {});
|
||||
}
|
||||
const statsData = stats.toJson(statOptions);
|
||||
|
||||
@@ -220,7 +222,7 @@ function createCompiler({
|
||||
const results = await Promise.all(tsMessagesPromises);
|
||||
clearTimeout(delayedMsg);
|
||||
|
||||
results.forEach(msgs => {
|
||||
results.forEach((msgs) => {
|
||||
statsData.errors.push(...msgs.errors);
|
||||
statsData.warnings.push(...msgs.warnings);
|
||||
|
||||
|
||||
+4
-2
@@ -31,7 +31,7 @@ const isProduction = process.env.NODE_ENV === "production";
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on("unhandledRejection", err => {
|
||||
process.on("unhandledRejection", (err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
@@ -117,7 +117,9 @@ function build(previousFileSizes: any) {
|
||||
return reject(err);
|
||||
}
|
||||
const messages = formatWebpackMessages(
|
||||
stats.toJson({ children: webpackConfig.map(c => c.stats || {}) as any })
|
||||
stats.toJson({
|
||||
children: webpackConfig.map((c) => c.stats || {}) as any,
|
||||
})
|
||||
);
|
||||
if (messages.errors.length) {
|
||||
// Only keep the first error. Others are often indicative
|
||||
|
||||
@@ -104,7 +104,7 @@ function gatherEntries(
|
||||
docs: ts.displayPartsToString(symbol.getDocumentationComment(checker)),
|
||||
type: type.getSymbol()!.getName() as DocEntry["type"],
|
||||
};
|
||||
typeNode.forEachChild(ch => {
|
||||
typeNode.forEachChild((ch) => {
|
||||
if (ts.isTypeLiteralNode(ch)) {
|
||||
const text = printer.printNode(
|
||||
ts.EmitHint.Unspecified,
|
||||
@@ -179,13 +179,13 @@ function emitDocs(markdownFile: string, entries: DocEntry[], verify = false) {
|
||||
const summary = stripIndent`
|
||||
- ${entries
|
||||
.map(
|
||||
e => `<a href="#${getEventName(e.name)}">${getEventName(e.name)}</a>`
|
||||
(e) => `<a href="#${getEventName(e.name)}">${getEventName(e.name)}</a>`
|
||||
)
|
||||
.join("\n - ")}
|
||||
`;
|
||||
const list = entries
|
||||
.map(
|
||||
e =>
|
||||
(e) =>
|
||||
codeBlock`
|
||||
- ${
|
||||
e.type === "ViewerEvent"
|
||||
|
||||
@@ -47,7 +47,7 @@ async function main() {
|
||||
}
|
||||
|
||||
// Create the types for this file.
|
||||
const types = await generateTSTypesAsString(schema, {
|
||||
const types = await generateTSTypesAsString(schema, file.fileName, {
|
||||
tabSpaces: 2,
|
||||
typePrefix: "GQL",
|
||||
strictNulls: false,
|
||||
@@ -66,13 +66,13 @@ if (require.main === module) {
|
||||
// Only run the main module on file load if this is the main module (we're
|
||||
// executing this file directly).
|
||||
main()
|
||||
.then(files => {
|
||||
.then((files) => {
|
||||
for (const { fileName } of files) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Generated ${fileName}`);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
Executable
+153
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env ./node_modules/.bin/tsnd
|
||||
|
||||
//
|
||||
// Based on the script written by @cristiandean
|
||||
//
|
||||
// Source: https://gist.github.com/cristiandean/8196bc2b965f9acf9ad8f9e4530011c1
|
||||
//
|
||||
|
||||
import { FluentResource } from "@fluent/bundle";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const PROJECT_ROOT = path.resolve(__dirname, "..", "..");
|
||||
|
||||
const LOCALE_DIRECTORIES = [
|
||||
path.join(PROJECT_ROOT, "src/locales"),
|
||||
path.join(PROJECT_ROOT, "src/core/server/locales"),
|
||||
];
|
||||
|
||||
const wrap = (color: string) => (msg: string) => {
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`${color}%s\x1b[0m`, msg);
|
||||
};
|
||||
|
||||
const log = {
|
||||
red: wrap("\x1b[31m"),
|
||||
green: wrap("\x1b[32m"),
|
||||
plain: wrap("\x1b[0m"),
|
||||
};
|
||||
|
||||
interface LocaleFileListing {
|
||||
locale: string;
|
||||
files: LocaleFile[];
|
||||
}
|
||||
|
||||
interface LocaleFile {
|
||||
fileName: string;
|
||||
directory: string;
|
||||
filePath: string;
|
||||
keys: Set<string>;
|
||||
}
|
||||
|
||||
function loadLocale(locale: string) {
|
||||
const listing: LocaleFileListing = { locale, files: [] };
|
||||
|
||||
for (const directory of LOCALE_DIRECTORIES) {
|
||||
// Resolve the directory with the language code, this should be a folder
|
||||
// containing language files for the specified context.
|
||||
const localeDirectory = path.join(directory, locale);
|
||||
if (!fs.existsSync(localeDirectory)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The folder exists, list all files in this directory and begin loading
|
||||
// them.
|
||||
const localeFiles = fs.readdirSync(localeDirectory);
|
||||
for (const fileName of localeFiles) {
|
||||
const filePath = path.join(localeDirectory, fileName);
|
||||
|
||||
// Load the file.
|
||||
const source = fs.readFileSync(filePath, "utf-8").toString();
|
||||
|
||||
// Create the resource based on the file.
|
||||
const resource = new FluentResource(source);
|
||||
|
||||
// Iterate over the ids to create the set of ID's.
|
||||
const keys = new Set<string>();
|
||||
|
||||
for (const entry of resource.body) {
|
||||
if (entry.id.startsWith("-")) {
|
||||
// Identifiers starting with a `-` define terms, these are not used
|
||||
// for this as we're only comparing messages.
|
||||
continue;
|
||||
}
|
||||
|
||||
keys.add(entry.id);
|
||||
}
|
||||
|
||||
// Add the new file.
|
||||
listing.files.push({ fileName, directory, filePath, keys });
|
||||
}
|
||||
}
|
||||
|
||||
return listing;
|
||||
}
|
||||
|
||||
function diffSet(a: Set<string>, b: Set<string>): Set<string> {
|
||||
const n = new Set(a);
|
||||
for (const e of b) {
|
||||
n.delete(e);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function prefixDiffSet(prefix: string, a: Set<string>, b: Set<string>) {
|
||||
return [...diffSet(a, b).values()]
|
||||
.map((value) => `${prefix}${value}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function diff(from: LocaleFileListing, to: LocaleFileListing) {
|
||||
for (const f of from.files) {
|
||||
// Log the header information for this file.
|
||||
log.plain(`* From: ${f.filePath}`);
|
||||
|
||||
// Find the associated "to" file.
|
||||
const t = to.files.find(
|
||||
(file) => file.fileName === f.fileName && file.directory === f.directory
|
||||
);
|
||||
if (!t) {
|
||||
log.red(
|
||||
`* To: ${path.join(f.directory, to.locale, f.fileName)} (missing)`
|
||||
);
|
||||
log.plain("-".repeat(60));
|
||||
continue;
|
||||
}
|
||||
|
||||
log.plain(`* To: ${t.filePath}`);
|
||||
|
||||
log.green(prefixDiffSet(" + ", f.keys, t.keys));
|
||||
log.red(prefixDiffSet(" - ", t.keys, f.keys));
|
||||
|
||||
log.plain("-".repeat(60));
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
try {
|
||||
if (process.argv.length < 3) {
|
||||
throw new Error("usage: ./scripts/i18n/missingTranslations.ts <locale>");
|
||||
}
|
||||
|
||||
// Load the target translation.
|
||||
const to = loadLocale(process.argv[2]);
|
||||
|
||||
// Load the base language (US English).
|
||||
const from = loadLocale("en-US");
|
||||
|
||||
// Report the difference in key existence.
|
||||
diff(from, to);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
+1
-1
@@ -26,7 +26,7 @@ process.env.NODE_ENV = "development";
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on("unhandledRejection", err => {
|
||||
process.on("unhandledRejection", (err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ process.env.NODE_ENV = "test";
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on("unhandledRejection", err => {
|
||||
process.on("unhandledRejection", (err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import chokidar from "chokidar";
|
||||
import path from "path";
|
||||
|
||||
import { Watcher, WatchOptions } from "./types";
|
||||
|
||||
function prependRootDir(
|
||||
|
||||
@@ -30,7 +30,7 @@ const cmd = program
|
||||
.description("Run watchers defined in <configFile>")
|
||||
.parse(process.argv);
|
||||
|
||||
run(cmd.args, cmd.opts()).catch(err => {
|
||||
run(cmd.args, cmd.opts()).catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Joi from "joi";
|
||||
import Joi from "@hapi/joi";
|
||||
|
||||
export interface WatchOptions {
|
||||
ignore?: ReadonlyArray<string>;
|
||||
@@ -44,23 +44,13 @@ export const configSchema = Joi.object({
|
||||
watchers: Joi.object().pattern(
|
||||
/.*/,
|
||||
Joi.object({
|
||||
paths: Joi.array()
|
||||
.items(Joi.string())
|
||||
.unique(),
|
||||
ignore: Joi.array()
|
||||
.items(Joi.string())
|
||||
.unique()
|
||||
.optional(),
|
||||
paths: Joi.array().items(Joi.string()).unique(),
|
||||
ignore: Joi.array().items(Joi.string()).unique().optional(),
|
||||
executor: Joi.object(),
|
||||
})
|
||||
),
|
||||
defaultSet: Joi.string().optional(),
|
||||
sets: Joi.object()
|
||||
.pattern(
|
||||
/.*/,
|
||||
Joi.array()
|
||||
.items(Joi.string())
|
||||
.unique()
|
||||
)
|
||||
.pattern(/.*/, Joi.array().items(Joi.string()).unique())
|
||||
.optional(),
|
||||
}).with("defaultSet", "sets");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Joi from "@hapi/joi";
|
||||
import chalk from "chalk";
|
||||
import Joi from "joi";
|
||||
import { pickBy } from "lodash";
|
||||
|
||||
import SaneWatcher from "./SaneWatcher";
|
||||
@@ -28,7 +28,7 @@ async function beginWatch(
|
||||
}
|
||||
|
||||
function setupCleanup(watcher: Watcher, config: Config) {
|
||||
["SIGINT", "SIGTERM"].forEach(signal =>
|
||||
["SIGINT", "SIGTERM"].forEach((signal) =>
|
||||
process.once(signal as any, async () => {
|
||||
const cleanups = [];
|
||||
if (watcher.onCleanup) {
|
||||
@@ -50,7 +50,7 @@ function resolveSets(
|
||||
value: ReadonlyArray<string>
|
||||
) {
|
||||
const resolved: string[] = [];
|
||||
value.forEach(v => {
|
||||
value.forEach((v) => {
|
||||
if (v in sets) {
|
||||
resolved.push(...sets[v]);
|
||||
return;
|
||||
@@ -66,12 +66,12 @@ function filterOnly(
|
||||
sets?: Record<string, ReadonlyArray<string>>
|
||||
): Config["watchers"] {
|
||||
const resolved = sets ? resolveSets(sets, only) : only;
|
||||
const unknown = resolved.filter(r => !(r in watchers));
|
||||
const unknown = resolved.filter((r) => !(r in watchers));
|
||||
if (unknown.length) {
|
||||
throw new Error(`Watcher Configuration or Set for ${unknown} not found`);
|
||||
}
|
||||
return pickBy(watchers, (value, key) => {
|
||||
if (resolved.indexOf(key) === -1) {
|
||||
if (!resolved.includes(key)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.grey(`Disabled watcher "${key}"`));
|
||||
return false;
|
||||
@@ -101,7 +101,7 @@ export default async function watch(config: Config, options: Options = {}) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.cyanBright(`Start watcher "${key}"`));
|
||||
const watcherConfig = watchersConfigs[key];
|
||||
beginWatch(watcher, key, watcherConfig, rootDir).catch(err => {
|
||||
beginWatch(watcher, key, watcherConfig, rootDir).catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
|
||||
@@ -7,7 +7,6 @@ import HtmlWebpackPlugin from "html-webpack-plugin";
|
||||
import { identity } from "lodash";
|
||||
import MiniCssExtractPlugin from "mini-css-extract-plugin";
|
||||
import path from "path";
|
||||
import typescriptFormatter from "react-dev-utils/typescriptFormatter";
|
||||
import WatchMissingNodeModulesPlugin from "react-dev-utils/WatchMissingNodeModulesPlugin";
|
||||
import TerserPlugin from "terser-webpack-plugin";
|
||||
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
|
||||
@@ -82,10 +81,6 @@ export default function createWebpackConfig(
|
||||
|
||||
const styleLoader = {
|
||||
loader: require.resolve("style-loader"),
|
||||
options: {
|
||||
sourceMap: !disableSourcemaps,
|
||||
hmr: watch,
|
||||
},
|
||||
};
|
||||
|
||||
const localesOptions = {
|
||||
@@ -367,9 +362,10 @@ export default function createWebpackConfig(
|
||||
{
|
||||
loader: require.resolve("css-loader"),
|
||||
options: {
|
||||
modules: true,
|
||||
modules: {
|
||||
localIdentName: "[name]-[local]-[contenthash]",
|
||||
},
|
||||
importLoaders: 2,
|
||||
localIdentName: "[name]-[local]-[contenthash]",
|
||||
sourceMap: !disableSourcemaps,
|
||||
},
|
||||
},
|
||||
@@ -471,9 +467,10 @@ export default function createWebpackConfig(
|
||||
{
|
||||
loader: require.resolve("css-loader"),
|
||||
options: {
|
||||
modules: true,
|
||||
modules: {
|
||||
localIdentName: "[name]-[local]-[hash:base64:5]",
|
||||
},
|
||||
importLoaders: 1,
|
||||
localIdentName: "[name]-[local]-[hash:base64:5]",
|
||||
sourceMap: !disableSourcemaps,
|
||||
},
|
||||
},
|
||||
@@ -525,11 +522,6 @@ export default function createWebpackConfig(
|
||||
useTypescriptIncrementalApi: false,
|
||||
checkSyntacticErrors: true,
|
||||
tsconfig: paths.appTsconfig,
|
||||
watch: paths.appSrc,
|
||||
// TODO: (cvle) ForkTsCheckerWebpackPlugin are currently not working, so we resort to default reporting.
|
||||
silent: false,
|
||||
// The formatter is normally invoked directly in WebpackDevServerUtils during development
|
||||
formatter: typescriptFormatter,
|
||||
})
|
||||
),
|
||||
// Makes some environment variables available to the JS code, for example:
|
||||
|
||||
@@ -40,7 +40,7 @@ function getFiles(target, pathToLocale, context) {
|
||||
|
||||
const files = fs.readdirSync(pathToLocale);
|
||||
|
||||
files.forEach(f => {
|
||||
files.forEach((f) => {
|
||||
if (commonFiles.includes(f)) {
|
||||
common.push(f);
|
||||
return;
|
||||
@@ -62,12 +62,12 @@ function generateTarget(target, context) {
|
||||
locales,
|
||||
bundled,
|
||||
} = context;
|
||||
const getLocalePath = locale => path.join(pathToLocales, locale);
|
||||
const getLocaleFiles = memoize(locale =>
|
||||
const getLocalePath = (locale) => path.join(pathToLocales, locale);
|
||||
const getLocaleFiles = memoize((locale) =>
|
||||
getFiles(target, getLocalePath(locale), context)
|
||||
);
|
||||
|
||||
const loadables = locales.filter(locale => !bundled.includes(locale));
|
||||
const loadables = locales.filter((locale) => !bundled.includes(locale));
|
||||
|
||||
return `
|
||||
var ret = {
|
||||
@@ -81,22 +81,22 @@ function generateTarget(target, context) {
|
||||
// Bundled locales are directly available in the main bundle.
|
||||
${bundled
|
||||
.map(
|
||||
locale => `
|
||||
(locale) => `
|
||||
{
|
||||
var suffixes = ${JSON.stringify(getLocaleFiles(locale).suffixes)};
|
||||
var contents = [];
|
||||
${getLocaleFiles(locale)
|
||||
.common.map(
|
||||
file => `
|
||||
(file) => `
|
||||
contents.push(require(${JSON.stringify(
|
||||
path.join(getLocalePath(locale), file).replace(/\\/g, "/")
|
||||
)}));
|
||||
)}).default);
|
||||
`
|
||||
)
|
||||
.join("\n")}
|
||||
contents = contents.concat(suffixes.map(function(suffix) { return require("${path
|
||||
.join(getLocalePath(locale), target)
|
||||
.replace(/\\/g, "/")}" + suffix); }));
|
||||
.replace(/\\/g, "/")}" + suffix).default; }));
|
||||
ret.bundled[${JSON.stringify(locale)}] = contents.join("\\n");
|
||||
}
|
||||
`
|
||||
@@ -106,13 +106,13 @@ function generateTarget(target, context) {
|
||||
// Loadables are in a separate bundle, that can be easily loaded.
|
||||
${loadables
|
||||
.map(
|
||||
locale => `
|
||||
(locale) => `
|
||||
ret.loadables[${JSON.stringify(locale)}] = function() {
|
||||
var suffixes = ${JSON.stringify(getLocaleFiles(locale).suffixes)};
|
||||
var promises = [];
|
||||
${getLocaleFiles(locale)
|
||||
.common.map(
|
||||
file => `
|
||||
(file) => `
|
||||
promises.push(
|
||||
import(
|
||||
/* webpackChunkName: ${JSON.stringify(
|
||||
@@ -147,7 +147,7 @@ function generateTarget(target, context) {
|
||||
`;
|
||||
}
|
||||
|
||||
module.exports = function(source) {
|
||||
module.exports = function (source) {
|
||||
const options = Object.assign(
|
||||
{},
|
||||
DEFAULT_QUERY_VALUES,
|
||||
@@ -165,7 +165,7 @@ module.exports = function(source) {
|
||||
|
||||
let locales = fs.readdirSync(pathToLocales);
|
||||
if (availableLocales) {
|
||||
availableLocales.forEach(locale => {
|
||||
availableLocales.forEach((locale) => {
|
||||
if (!locales.includes(locale)) {
|
||||
throw new Error(`locale ${fallbackLocale} not available`);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const postcssAdvancedVariables = require("postcss-advanced-variables");
|
||||
delete require.cache[paths.appThemeVariables];
|
||||
const variables = require(paths.appThemeVariables).default;
|
||||
const flatKebabVariables = mapKeys(
|
||||
mapValues(flat(variables, { delimiter: "-" }), v => v.toString()),
|
||||
mapValues(flat(variables, { delimiter: "-" }), (v) => v.toString()),
|
||||
(_, k) => kebabCase(k)
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ const mediaQueryVariables = mapValues(
|
||||
// with the smallest screen and gradually add styling for the
|
||||
// next bigger screen. This is realized using `min-width` without
|
||||
// ever using `max-width`.
|
||||
v => `${Number.parseInt(v, 10) + 1}px`
|
||||
(v) => `${Number.parseInt(v, 10) + 1}px`
|
||||
);
|
||||
|
||||
module.exports = {
|
||||
@@ -63,12 +63,6 @@ module.exports = {
|
||||
postcssFlexbugsFixes,
|
||||
// Vendor prefixing.
|
||||
autoprefixer({
|
||||
browsers: [
|
||||
">1%",
|
||||
"last 4 versions",
|
||||
"Firefox ESR",
|
||||
"not ie < 9", // React doesn't support IE8 anyway
|
||||
],
|
||||
flexbox: "no-2009",
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { AuthState } from "coral-framework/lib/auth";
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import { initLocalBaseState } from "coral-framework/lib/relay";
|
||||
|
||||
@@ -8,7 +9,8 @@ import { initLocalBaseState } from "coral-framework/lib/relay";
|
||||
*/
|
||||
export default async function initLocalState(
|
||||
environment: Environment,
|
||||
context: CoralContext
|
||||
context: CoralContext,
|
||||
auth?: AuthState
|
||||
) {
|
||||
await initLocalBaseState(environment, context);
|
||||
initLocalBaseState(environment, context, auth);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ interface Props {
|
||||
onGotoComment?: React.EventHandler<React.MouseEvent>;
|
||||
}
|
||||
|
||||
const ApprovedComment: FunctionComponent<Props> = props => (
|
||||
const ApprovedComment: FunctionComponent<Props> = (props) => (
|
||||
<DecisionItem icon={<ApprovedIcon />}>
|
||||
<Localized
|
||||
id="decisionHistory-approvedCommentBy"
|
||||
|
||||
@@ -19,13 +19,13 @@ interface Props {
|
||||
onClosePopover: () => void;
|
||||
}
|
||||
|
||||
const DecisionHistory: FunctionComponent<Props> = props => (
|
||||
const DecisionHistory: FunctionComponent<Props> = (props) => (
|
||||
<div data-testid="decisionHistory-container">
|
||||
<Title />
|
||||
<Main>
|
||||
<DecisionList>
|
||||
{props.actions.length === 0 && <Empty />}
|
||||
{props.actions.map(action => (
|
||||
{props.actions.map((action) => (
|
||||
<DecisionHistoryItemContainer
|
||||
key={action.id}
|
||||
action={action}
|
||||
|
||||
@@ -23,7 +23,7 @@ export class DecisionHistoryContainer extends React.Component<
|
||||
|
||||
public render() {
|
||||
const actions = this.props.viewer.commentModerationActionHistory.edges.map(
|
||||
edge => edge.node
|
||||
(edge) => edge.node
|
||||
);
|
||||
return (
|
||||
<DecisionHistory
|
||||
@@ -43,7 +43,7 @@ export class DecisionHistoryContainer extends React.Component<
|
||||
this.setState({ disableLoadMore: true });
|
||||
this.props.relay.loadMore(
|
||||
10, // Fetch the next 10 feed items
|
||||
error => {
|
||||
(error) => {
|
||||
this.setState({ disableLoadMore: false });
|
||||
if (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { Component } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { graphql, QueryRenderer } from "coral-framework/lib/relay";
|
||||
import { QueryRenderer } from "coral-framework/lib/relay";
|
||||
|
||||
import { DecisionHistoryQuery as QueryTypes } from "coral-admin/__generated__/DecisionHistoryQuery.graphql";
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const DecisionItem: FunctionComponent<Props> = props => (
|
||||
const DecisionItem: FunctionComponent<Props> = (props) => (
|
||||
<li className={styles.root}>
|
||||
<Flex>
|
||||
<div className={styles.leftCol}>{props.icon}</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { FunctionComponent } from "react";
|
||||
|
||||
import styles from "./DecisionList.css";
|
||||
|
||||
const DecisionList: FunctionComponent = props => (
|
||||
const DecisionList: FunctionComponent = (props) => (
|
||||
<ul className={styles.root}>{props.children}</ul>
|
||||
);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Footer: FunctionComponent<Props> = props => (
|
||||
const Footer: FunctionComponent<Props> = (props) => (
|
||||
<Flex className={styles.root} alignItems="baseline">
|
||||
{props.children}
|
||||
</Flex>
|
||||
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
onClick?: React.EventHandler<React.MouseEvent>;
|
||||
}
|
||||
|
||||
const GoToCommentLink: FunctionComponent<Props> = props => {
|
||||
const GoToCommentLink: FunctionComponent<Props> = (props) => {
|
||||
return (
|
||||
<Link
|
||||
as={TextLink}
|
||||
|
||||
@@ -6,7 +6,7 @@ interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Info: FunctionComponent<Props> = props => (
|
||||
const Info: FunctionComponent<Props> = (props) => (
|
||||
<span className={styles.root}>{props.children}</span>
|
||||
);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { FunctionComponent } from "react";
|
||||
|
||||
import styles from "./Main.css";
|
||||
|
||||
const Main: FunctionComponent = props => (
|
||||
const Main: FunctionComponent = (props) => (
|
||||
<div className={styles.root}>{props.children}</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ interface Props {
|
||||
onGotoComment?: React.EventHandler<React.MouseEvent>;
|
||||
}
|
||||
|
||||
const RejectedComment: FunctionComponent<Props> = props => (
|
||||
const RejectedComment: FunctionComponent<Props> = (props) => (
|
||||
<DecisionItem icon={<RejectedIcon />}>
|
||||
<Localized
|
||||
id="decisionHistory-rejectedCommentBy"
|
||||
|
||||
@@ -10,7 +10,7 @@ interface Props {
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const ShowMoreButton: FunctionComponent<Props> = props => (
|
||||
const ShowMoreButton: FunctionComponent<Props> = (props) => (
|
||||
<Localized id="decisionHistory-showMoreButton">
|
||||
<BaseButton
|
||||
className={styles.root}
|
||||
|
||||
@@ -8,7 +8,7 @@ interface Props {
|
||||
children: string;
|
||||
}
|
||||
|
||||
const DecisionHistory: FunctionComponent<Props> = props => (
|
||||
const DecisionHistory: FunctionComponent<Props> = (props) => (
|
||||
<Timestamp className={styles.root}>{props.children}</Timestamp>
|
||||
);
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<ForwardRef(render)
|
||||
<Link
|
||||
as={[Function]}
|
||||
className="GoToCommentLink-root"
|
||||
onClick={[Function]}
|
||||
@@ -18,5 +18,5 @@ exports[`renders correctly 1`] = `
|
||||
<ForwardRef(forwardRef)>
|
||||
chevron_right
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(render)>
|
||||
</Link>
|
||||
`;
|
||||
|
||||
@@ -6,9 +6,9 @@ interface State {
|
||||
}
|
||||
|
||||
type Action =
|
||||
| {
|
||||
| ({
|
||||
type: "SET_MESSAGE";
|
||||
} & State
|
||||
} & State)
|
||||
| {
|
||||
type: "CLEAR_MESSAGE";
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { graphql } from "coral-framework/lib/relay";
|
||||
import { withRouteConfig } from "coral-framework/lib/router";
|
||||
|
||||
import { MainRouteQueryResponse } from "coral-admin/__generated__/MainRouteQuery.graphql";
|
||||
|
||||
@@ -9,7 +9,7 @@ interface Props {
|
||||
showConfigure: boolean;
|
||||
}
|
||||
|
||||
const Navigation: FunctionComponent<Props> = props => (
|
||||
const Navigation: FunctionComponent<Props> = (props) => (
|
||||
<AppBarNavigation>
|
||||
<Localized id="navigation-moderate">
|
||||
<NavigationLink to="/admin/moderate">Moderate</NavigationLink>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { Ability, can } from "coral-admin/permissions";
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import {
|
||||
SignOutMutation,
|
||||
withSignOutMutation,
|
||||
|
||||
@@ -8,7 +8,7 @@ interface Props {
|
||||
to: string | LocationDescriptor;
|
||||
}
|
||||
|
||||
const NavigationLink: FunctionComponent<Props> = props => (
|
||||
const NavigationLink: FunctionComponent<Props> = (props) => (
|
||||
<Link to={props.to} as={AppBarNavigationItem as any} activePropName="active">
|
||||
{props.children}
|
||||
</Link>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<ForwardRef(render)
|
||||
<Link
|
||||
activePropName="active"
|
||||
as={[Function]}
|
||||
to="/moderate"
|
||||
>
|
||||
link
|
||||
</ForwardRef(render)>
|
||||
</Link>
|
||||
`;
|
||||
|
||||
@@ -18,7 +18,7 @@ interface Props {
|
||||
onSignOut: React.EventHandler<React.MouseEvent>;
|
||||
}
|
||||
|
||||
const UserMenu: FunctionComponent<Props> = props => (
|
||||
const UserMenu: FunctionComponent<Props> = (props) => (
|
||||
<Localized id="userMenu-popover" attrs={{ description: true }}>
|
||||
<Popover
|
||||
id="userMenu"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import {
|
||||
SignOutMutation,
|
||||
withSignOutMutation,
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { FunctionComponent } from "react";
|
||||
|
||||
import styles from "./ButtonPadding.css";
|
||||
|
||||
const ButtonPadding: FunctionComponent = props => (
|
||||
const ButtonPadding: FunctionComponent = (props) => (
|
||||
<div className={styles.root}>{props.children}</div>
|
||||
);
|
||||
|
||||
|
||||
+9
-5
@@ -5,19 +5,19 @@ $comment-link: var(--v2-palette-primary-main);
|
||||
$comment-link-active: var(--v2-palette-primary-darkest);
|
||||
|
||||
.root {
|
||||
composes: bodyCommentV2 from "coral-ui/shared/typography.css";
|
||||
composes: bodyCommentV2 from "~coral-ui/shared/typography.css";
|
||||
font-family: var(--v2-font-family-primary);
|
||||
font-size: var(--v2-font-size-3);
|
||||
line-height: var(--v2-line-height-body-comment);
|
||||
color: $comment-content;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
& * bold,
|
||||
& * strong {
|
||||
b,
|
||||
strong {
|
||||
font-weight: var(--v2-font-weight-primary-bold);
|
||||
}
|
||||
& * italic,
|
||||
& * em {
|
||||
i,
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
blockquote {
|
||||
@@ -50,3 +50,7 @@ $comment-link-active: var(--v2-palette-primary-darkest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.highlight {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
+21
@@ -16,6 +16,7 @@ it("renders correctly", () => {
|
||||
},
|
||||
className: "custom",
|
||||
children: "Hello <b>Bob</b>, you bad guy",
|
||||
highlight: true,
|
||||
};
|
||||
const renderer = createRenderer();
|
||||
renderer.render(<CommentContent {...props} />);
|
||||
@@ -33,6 +34,26 @@ it("renders empty words correctly", () => {
|
||||
},
|
||||
className: "custom",
|
||||
children: "Hello <b>Bob</b>, you bad guy",
|
||||
highlight: true,
|
||||
};
|
||||
const renderer = createRenderer();
|
||||
renderer.render(<CommentContent {...props} />);
|
||||
expect(renderer.getRenderOutput()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders correctly even if it has consecutive banned words on comments", () => {
|
||||
const props: PropTypesOf<typeof CommentContent> = {
|
||||
phrases: {
|
||||
locale: "en-US",
|
||||
wordList: {
|
||||
suspect: ["worse"],
|
||||
banned: ["bad"],
|
||||
},
|
||||
},
|
||||
className: "custom",
|
||||
children:
|
||||
"This is a very long comment with bad words. Let's try bad and bad. Now bad bad.\nBad BAD bad.\n",
|
||||
highlight: true,
|
||||
};
|
||||
const renderer = createRenderer();
|
||||
renderer.render(<CommentContent {...props} />);
|
||||
@@ -0,0 +1,84 @@
|
||||
import cn from "classnames";
|
||||
import React, { FunctionComponent, useMemo } from "react";
|
||||
import striptags from "striptags";
|
||||
|
||||
import {
|
||||
getPhrasesRegExp,
|
||||
GetPhrasesRegExpOptions,
|
||||
markHTMLNode,
|
||||
} from "coral-admin/helpers";
|
||||
import { createPurify } from "coral-common/utils/purify";
|
||||
|
||||
import styles from "./CommentContent.css";
|
||||
|
||||
/**
|
||||
* Create a purify instance that will be used to handle HTML content.
|
||||
*/
|
||||
const purify = createPurify(window, false);
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
children: string | React.ReactElement;
|
||||
phrases: GetPhrasesRegExpOptions;
|
||||
highlight?: boolean;
|
||||
}
|
||||
|
||||
const CommentContent: FunctionComponent<Props> = ({
|
||||
phrases,
|
||||
className,
|
||||
children,
|
||||
highlight = false,
|
||||
}) => {
|
||||
// Cache the expression used via memo. This will reduce duplicate renders of
|
||||
// this comment content when the children change but the phrase configuration
|
||||
// does not change. The regExp is already cached on a deeper level
|
||||
// automatically, this is just lessening that impact further.
|
||||
const expression = useMemo(() => {
|
||||
// If we aren't in highlight mode for this comment, don't even attempt to
|
||||
// generate the expression.
|
||||
if (!highlight) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getPhrasesRegExp(phrases);
|
||||
}, [phrases, highlight]);
|
||||
|
||||
// Cache the parsed comment node. If the children cannot be parsed, this will
|
||||
// be null.
|
||||
const parsed = useMemo(() => {
|
||||
if (typeof children !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sanitize the input for display.
|
||||
let html = purify.sanitize(children);
|
||||
if (highlight) {
|
||||
html = striptags(html, ["a"]);
|
||||
}
|
||||
|
||||
// We create a Shadow DOM Tree with the HTML body content and use it as a
|
||||
// parser.
|
||||
const node = document.createElement("div");
|
||||
node.innerHTML = html;
|
||||
|
||||
// If the expression is available, then mark the nodes.
|
||||
if (expression) {
|
||||
markHTMLNode(node, expression);
|
||||
}
|
||||
|
||||
return node;
|
||||
}, [children, expression, highlight]);
|
||||
|
||||
if (parsed) {
|
||||
return (
|
||||
<div
|
||||
className={cn(className, styles.root, highlight && styles.highlight)}
|
||||
dangerouslySetInnerHTML={{ __html: parsed.innerHTML }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default CommentContent;
|
||||
+1
-1
@@ -8,7 +8,7 @@ export interface UsernameProps {
|
||||
children: string;
|
||||
}
|
||||
|
||||
const Username: FunctionComponent<UsernameProps> = props => {
|
||||
const Username: FunctionComponent<UsernameProps> = (props) => {
|
||||
return (
|
||||
<span className={cn(props.className, styles.root)}>{props.children}</span>
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
$moderateCardUsernameBackgroundHover: var(--v2-colors-grey-200);
|
||||
|
||||
.root {
|
||||
margin-right: var(--mini-unit);
|
||||
padding: var(--spacing-1);
|
||||
margin-left: calc(-1 * var(--v2-spacing-1));
|
||||
|
||||
line-height: var(--v2-line-height-reset);
|
||||
|
||||
&:hover {
|
||||
background-color: $moderateCardUsernameBackgroundHover;
|
||||
border-radius: var(--v2-round-corners);
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $moderateCardUsernameBackgroundHover;
|
||||
border-radius: var(--v2-round-corners);
|
||||
border-style: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import cn from "classnames";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { BaseButton } from "coral-ui/components/v2";
|
||||
import Username from "./Username";
|
||||
|
||||
import styles from "./UsernameButton.css";
|
||||
|
||||
interface Props {
|
||||
username: string;
|
||||
className?: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const UsernameButton: FunctionComponent<Props> = ({
|
||||
username,
|
||||
onClick,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<BaseButton onClick={onClick} className={cn(styles.root, className)}>
|
||||
<Username>{username}</Username>
|
||||
</BaseButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsernameButton;
|
||||
@@ -0,0 +1,36 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<div
|
||||
className="custom CommentContent-root CommentContent-highlight"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "<span>Hello Bob, you <mark>bad</mark> guy</span>",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`renders correctly even if it has consecutive banned words on comments 1`] = `
|
||||
<div
|
||||
className="custom CommentContent-root CommentContent-highlight"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "<span>This is a very long comment with <mark>bad</mark> words. Let's try <mark>bad</mark> and <mark>bad</mark>. Now <mark>bad</mark> bad.
|
||||
<mark>Bad</mark> BAD <mark>bad</mark>.
|
||||
</span>",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`renders empty words correctly 1`] = `
|
||||
<div
|
||||
className="custom CommentContent-root CommentContent-highlight"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "Hello Bob, you bad guy",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as CommentContent } from "./CommentContent";
|
||||
export { default as InReplyTo } from "./InReplyTo";
|
||||
export { default as Username } from "./Username";
|
||||
export { default as UsernameButton } from "./UsernameButton";
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { Modal } from "coral-ui/components";
|
||||
|
||||
import ConversationModalQuery from "./ConversationModalQuery";
|
||||
|
||||
interface ConversationModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
commentID?: string;
|
||||
onUsernameClicked: (userID: string) => void;
|
||||
}
|
||||
|
||||
const ConversationModal: FunctionComponent<ConversationModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
commentID,
|
||||
onUsernameClicked,
|
||||
}) => {
|
||||
if (!commentID) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
{({ firstFocusableRef, lastFocusableRef }) => (
|
||||
<ConversationModalQuery
|
||||
commentID={commentID}
|
||||
onClose={onClose}
|
||||
firstFocusableRef={firstFocusableRef}
|
||||
lastFocusableRef={lastFocusableRef}
|
||||
onUsernameClicked={onUsernameClicked}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConversationModal;
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
$conversationModalHighlightBackground: var(--v2-colors-teal-100);
|
||||
$conversationModalCommentText: var(--v2-colors-mono-500);
|
||||
.root {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.line {
|
||||
border-color: var(--v2-colors-grey-400);
|
||||
}
|
||||
|
||||
.adornments {
|
||||
margin-right: var(--v2-spacing-2);
|
||||
}
|
||||
|
||||
.circle {
|
||||
margin-top: var(--v2-spacing-2);
|
||||
margin-bottom: var(--v2-spacing-2);
|
||||
}
|
||||
|
||||
.highlightedCircle {
|
||||
margin-top: var(--v2-spacing-1);
|
||||
}
|
||||
|
||||
.highlighted {
|
||||
background-color: $conversationModalHighlightBackground;
|
||||
padding: var(--v2-spacing-2);
|
||||
padding-top: var(--v2-spacing-1);
|
||||
margin-bottom: var(--v2-spacing-2);
|
||||
}
|
||||
|
||||
.commentText {
|
||||
color: $conversationModalCommentText;
|
||||
}
|
||||
|
||||
.showReplies {
|
||||
padding-left: var(--v2-spacing-2);
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import cn from "classnames";
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
Timestamp,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import { ConversationModalCommentContainer_comment } from "coral-admin/__generated__/ConversationModalCommentContainer_comment.graphql";
|
||||
import { ConversationModalCommentContainer_settings } from "coral-admin/__generated__/ConversationModalCommentContainer_settings.graphql";
|
||||
|
||||
import { CommentContent, InReplyTo, UsernameButton } from "../Comment";
|
||||
import { Circle, Line } from "../Timeline";
|
||||
import ConversationModalRepliesQuery from "./ConversationModalRepliesQuery";
|
||||
|
||||
import styles from "./ConversationModalCommentContainer.css";
|
||||
|
||||
interface Props {
|
||||
comment: ConversationModalCommentContainer_comment;
|
||||
isHighlighted: boolean;
|
||||
onUsernameClick: (id?: string) => void;
|
||||
isParent?: boolean;
|
||||
isReply?: boolean;
|
||||
settings: ConversationModalCommentContainer_settings;
|
||||
}
|
||||
|
||||
const ConversationModalCommentContainer: FunctionComponent<Props> = ({
|
||||
comment,
|
||||
isHighlighted,
|
||||
isParent,
|
||||
isReply,
|
||||
onUsernameClick,
|
||||
settings,
|
||||
}) => {
|
||||
const commentAuthorClick = useCallback(() => {
|
||||
if (comment.author) {
|
||||
onUsernameClick(comment.author.id);
|
||||
}
|
||||
}, [onUsernameClick, comment.author]);
|
||||
const commentParentAuthorClick = useCallback(() => {
|
||||
if (comment.parent && comment.parent.author) {
|
||||
onUsernameClick(comment.parent.author.id);
|
||||
}
|
||||
}, [onUsernameClick, comment.parent]);
|
||||
const [showReplies, setShowReplies] = useState(false);
|
||||
const onShowReplies = useCallback(() => {
|
||||
setShowReplies(true);
|
||||
}, []);
|
||||
return (
|
||||
<HorizontalGutter>
|
||||
<Flex>
|
||||
<Flex
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
className={cn(styles.adornments, {
|
||||
[styles.highlightedCircle]: isHighlighted,
|
||||
})}
|
||||
>
|
||||
{!isReply && <Circle className={styles.circle} />}
|
||||
{(isParent || isReply) && <Line className={styles.line} />}
|
||||
</Flex>
|
||||
<HorizontalGutter
|
||||
spacing={1}
|
||||
className={cn(styles.root, {
|
||||
[styles.highlighted]: isHighlighted,
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
<Flex alignItems="center">
|
||||
{comment.author && comment.author.username && (
|
||||
<UsernameButton
|
||||
onClick={commentAuthorClick}
|
||||
username={comment.author.username}
|
||||
/>
|
||||
)}
|
||||
<Timestamp>{comment.createdAt}</Timestamp>
|
||||
</Flex>
|
||||
{comment.parent &&
|
||||
comment.parent.author &&
|
||||
comment.parent.author.username && (
|
||||
<InReplyTo onUsernameClick={commentParentAuthorClick}>
|
||||
{comment.parent.author.username}
|
||||
</InReplyTo>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{comment.body && (
|
||||
<CommentContent className={styles.commentText} phrases={settings}>
|
||||
{comment.body}
|
||||
</CommentContent>
|
||||
)}
|
||||
</div>
|
||||
</HorizontalGutter>
|
||||
</Flex>
|
||||
{isReply && comment.replyCount > 0 && (
|
||||
<div className={styles.showReplies}>
|
||||
{!showReplies && (
|
||||
<Localized id="conversation-modal-reply-show-replies">
|
||||
<Button
|
||||
color="mono"
|
||||
variant="outline"
|
||||
onClick={onShowReplies}
|
||||
fullWidth
|
||||
>
|
||||
Show replies
|
||||
</Button>
|
||||
</Localized>
|
||||
)}
|
||||
{showReplies && (
|
||||
<ConversationModalRepliesQuery
|
||||
onUsernameClicked={onUsernameClick}
|
||||
commentID={comment.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
settings: graphql`
|
||||
fragment ConversationModalCommentContainer_settings on Settings {
|
||||
locale
|
||||
wordList {
|
||||
banned
|
||||
suspect
|
||||
}
|
||||
multisite
|
||||
featureFlags
|
||||
...MarkersContainer_settings
|
||||
}
|
||||
`,
|
||||
comment: graphql`
|
||||
fragment ConversationModalCommentContainer_comment on Comment {
|
||||
id
|
||||
body
|
||||
createdAt
|
||||
author {
|
||||
username
|
||||
id
|
||||
}
|
||||
replyCount
|
||||
parent {
|
||||
author {
|
||||
username
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(ConversationModalCommentContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,10 @@
|
||||
.root {
|
||||
}
|
||||
.topCircle {
|
||||
margin-right: var(--v2-spacing-2);
|
||||
}
|
||||
|
||||
.bottomCircleContainer {
|
||||
width: 8px;
|
||||
padding-top: var(--v2-spacing-1);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql, RelayPaginationProp } from "react-relay";
|
||||
|
||||
import {
|
||||
useLoadMore,
|
||||
withPaginationContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
import {
|
||||
Button,
|
||||
Counter,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import { ConversationModalContainer_comment } from "coral-admin/__generated__/ConversationModalContainer_comment.graphql";
|
||||
import { ConversationModalContainer_settings } from "coral-admin/__generated__/ConversationModalContainer_settings.graphql";
|
||||
import { ConversationModalContainerPaginationQueryVariables } from "coral-admin/__generated__/ConversationModalContainerPaginationQuery.graphql";
|
||||
|
||||
import { Circle } from "../Timeline";
|
||||
import ConversationModalComment from "./ConversationModalCommentContainer";
|
||||
|
||||
import styles from "./ConversationModalContainer.css";
|
||||
|
||||
interface Props {
|
||||
relay: RelayPaginationProp;
|
||||
comment: ConversationModalContainer_comment;
|
||||
settings: ConversationModalContainer_settings;
|
||||
onClose: () => void;
|
||||
onUsernameClicked: (id?: string) => void;
|
||||
}
|
||||
|
||||
const ConversationModalContainer: FunctionComponent<Props> = ({
|
||||
comment,
|
||||
relay,
|
||||
settings,
|
||||
onUsernameClicked,
|
||||
}) => {
|
||||
const [loadMore] = useLoadMore(relay, 5);
|
||||
const parents = comment.parents.edges.map((edge) => edge.node);
|
||||
return (
|
||||
<HorizontalGutter className={styles.root}>
|
||||
{comment.parentCount > parents.length && (
|
||||
<div>
|
||||
<Flex alignItems="center">
|
||||
<Circle hollow={true} className={styles.topCircle} />
|
||||
<Button underline variant="text" onClick={loadMore}>
|
||||
<Localized id="conversation-modal-show-more-parents">
|
||||
<span>Show more of this conversation</span>
|
||||
</Localized>
|
||||
<Counter>{comment.parentCount}</Counter>
|
||||
</Button>
|
||||
</Flex>
|
||||
<Flex
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
className={styles.bottomCircleContainer}
|
||||
>
|
||||
<Circle color="light" size="small" />
|
||||
</Flex>
|
||||
</div>
|
||||
)}
|
||||
{parents.map((parent) => (
|
||||
<ConversationModalComment
|
||||
key={parent.id}
|
||||
isParent={true}
|
||||
comment={parent}
|
||||
onUsernameClick={onUsernameClicked}
|
||||
settings={settings}
|
||||
isHighlighted={false}
|
||||
/>
|
||||
))}
|
||||
<ConversationModalComment
|
||||
comment={comment}
|
||||
settings={settings}
|
||||
onUsernameClick={onUsernameClicked}
|
||||
isHighlighted={true}
|
||||
/>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: (cvle) If this could be autogenerated.
|
||||
type FragmentVariables = ConversationModalContainerPaginationQueryVariables;
|
||||
|
||||
const enhanced = withPaginationContainer<
|
||||
Props,
|
||||
ConversationModalContainerPaginationQueryVariables,
|
||||
FragmentVariables
|
||||
>(
|
||||
{
|
||||
settings: graphql`
|
||||
fragment ConversationModalContainer_settings on Settings {
|
||||
...ConversationModalCommentContainer_settings
|
||||
}
|
||||
`,
|
||||
comment: graphql`
|
||||
fragment ConversationModalContainer_comment on Comment
|
||||
@argumentDefinitions(
|
||||
count: { type: "Int!", defaultValue: 1 }
|
||||
cursor: { type: "Cursor" }
|
||||
) {
|
||||
id
|
||||
...ConversationModalCommentContainer_comment
|
||||
rootParent {
|
||||
id
|
||||
}
|
||||
parents(last: $count, before: $cursor)
|
||||
@connection(key: "ConversationModal_parents") {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...ConversationModalCommentContainer_comment
|
||||
}
|
||||
}
|
||||
}
|
||||
parentCount
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
direction: "backward",
|
||||
getConnectionFromProps(props) {
|
||||
return props.comment && props.comment.parents;
|
||||
},
|
||||
// This is also the default implementation of `getFragmentVariables` if it isn't provided.
|
||||
getFragmentVariables(prevVars, totalCount) {
|
||||
return {
|
||||
...prevVars,
|
||||
count: totalCount,
|
||||
};
|
||||
},
|
||||
getVariables(props, { count, cursor }) {
|
||||
return {
|
||||
count,
|
||||
cursor,
|
||||
// commentID isn't specified as an @argument for the fragment, but it should be a
|
||||
// variable available for the fragment under the query root.
|
||||
commentID: props.comment.id,
|
||||
};
|
||||
},
|
||||
query: graphql`
|
||||
# Pagination query to be fetched upon calling 'loadMore'.
|
||||
# Notice that we re-use our fragment, and the shape of this query matches our fragment spec.
|
||||
query ConversationModalContainerPaginationQuery(
|
||||
$count: Int!
|
||||
$cursor: Cursor
|
||||
$commentID: ID!
|
||||
) {
|
||||
comment(id: $commentID) {
|
||||
...ConversationModalContainer_comment
|
||||
@arguments(count: $count, cursor: $cursor)
|
||||
}
|
||||
}
|
||||
`,
|
||||
}
|
||||
)(ConversationModalContainer);
|
||||
|
||||
export default enhanced;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
$modal-header-text: var(--v2-colors-mono-900);
|
||||
|
||||
.title {
|
||||
font-size: var(--v2-font-size-4);
|
||||
font-family: var(--v2-font-family-primary);
|
||||
font-weight: var(--v2-font-weight-primary-regular);
|
||||
line-height: var(--v2-line-height-title);
|
||||
color: $modal-header-text;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.conversationTitle {
|
||||
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||
text-transform: uppercase;
|
||||
display: block;
|
||||
margin-bottom: var(--v2-spacing-2);
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import { useRouter } from "found";
|
||||
import React, { FunctionComponent, RefObject, useCallback } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { getModerationLink } from "coral-framework/helpers";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { Button, HorizontalGutter, ModalHeader } from "coral-ui/components/v2";
|
||||
|
||||
import { ConversationModalHeaderContainer_comment } from "coral-admin/__generated__/ConversationModalHeaderContainer_comment.graphql";
|
||||
|
||||
import styles from "./ConversationModalHeaderContainer.css";
|
||||
|
||||
interface Props {
|
||||
comment: ConversationModalHeaderContainer_comment;
|
||||
onClose: () => void;
|
||||
focusableRef: RefObject<any>;
|
||||
}
|
||||
|
||||
const ConversationModalHeaderContainer: FunctionComponent<Props> = ({
|
||||
comment,
|
||||
onClose,
|
||||
focusableRef,
|
||||
}) => {
|
||||
const { router } = useRouter();
|
||||
const onModerate = useCallback(() => {
|
||||
const link = getModerationLink({ storyID: comment.story.id });
|
||||
router.push(link);
|
||||
}, [comment.id]);
|
||||
return (
|
||||
<ModalHeader onClose={onClose} focusableRef={focusableRef}>
|
||||
<HorizontalGutter spacing={3}>
|
||||
<h1 className={styles.title}>
|
||||
<Localized id="conversation-modal-header-title">
|
||||
<span className={styles.conversationTitle}>Conversation on:</span>
|
||||
</Localized>
|
||||
{comment.story.metadata
|
||||
? comment.story.metadata.title
|
||||
: comment.story.url}
|
||||
</h1>
|
||||
<Localized id="conversation-modal-header-moderate-link">
|
||||
<Button variant="outline" color="mono" uppercase onClick={onModerate}>
|
||||
Moderate story
|
||||
</Button>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
</ModalHeader>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
comment: graphql`
|
||||
fragment ConversationModalHeaderContainer_comment on Comment {
|
||||
story {
|
||||
id
|
||||
metadata {
|
||||
title
|
||||
}
|
||||
url
|
||||
}
|
||||
id
|
||||
}
|
||||
`,
|
||||
})(ConversationModalHeaderContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,8 @@
|
||||
.root {
|
||||
padding: 0;
|
||||
width: 635px;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: var(--v2-spacing-4);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { QueryRenderData, QueryRenderer } from "coral-framework/lib/relay";
|
||||
import { CallOut, Card, Spinner } from "coral-ui/components/v2";
|
||||
|
||||
import { ConversationModalQuery as QueryTypes } from "coral-admin/__generated__/ConversationModalQuery.graphql";
|
||||
|
||||
import ConversationModalContainer from "./ConversationModalContainer";
|
||||
import ConversationModalHeaderContainer from "./ConversationModalHeaderContainer";
|
||||
import ConversationModalRepliesContainer from "./ConversationModalRepliesContainer";
|
||||
|
||||
import styles from "./ConversationModalQuery.css";
|
||||
|
||||
interface Props {
|
||||
commentID: string;
|
||||
onClose: () => void;
|
||||
firstFocusableRef: React.RefObject<any>;
|
||||
onUsernameClicked: (userID: string) => void;
|
||||
lastFocusableRef: React.RefObject<any>;
|
||||
}
|
||||
|
||||
const ConversationModalQuery: FunctionComponent<Props> = ({
|
||||
commentID,
|
||||
onClose,
|
||||
lastFocusableRef,
|
||||
onUsernameClicked,
|
||||
}) => {
|
||||
return (
|
||||
<QueryRenderer<QueryTypes>
|
||||
query={graphql`
|
||||
query ConversationModalQuery($commentID: ID!) {
|
||||
settings {
|
||||
...ConversationModalContainer_settings
|
||||
...ConversationModalRepliesContainer_settings
|
||||
}
|
||||
comment(id: $commentID) {
|
||||
...ConversationModalContainer_comment
|
||||
...ConversationModalRepliesContainer_comment
|
||||
...ConversationModalHeaderContainer_comment
|
||||
}
|
||||
}
|
||||
`}
|
||||
variables={{ commentID }}
|
||||
cacheConfig={{ force: true }}
|
||||
render={({ props }: QueryRenderData<QueryTypes>) => {
|
||||
if (!props) {
|
||||
return (
|
||||
<div>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!props.comment) {
|
||||
return (
|
||||
<div>
|
||||
<CallOut>
|
||||
<Localized id="conversation-modal-comment-not-found">
|
||||
Comment not found.
|
||||
</Localized>
|
||||
</CallOut>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className={styles.root}>
|
||||
<ConversationModalHeaderContainer
|
||||
onClose={onClose}
|
||||
comment={props.comment}
|
||||
focusableRef={lastFocusableRef}
|
||||
/>
|
||||
<div className={styles.body}>
|
||||
<ConversationModalContainer
|
||||
onClose={onClose}
|
||||
settings={props.settings}
|
||||
comment={props.comment}
|
||||
onUsernameClicked={onUsernameClicked}
|
||||
/>
|
||||
<ConversationModalRepliesContainer
|
||||
onClose={onClose}
|
||||
comment={props.comment}
|
||||
settings={props.settings}
|
||||
onUsernameClicked={onUsernameClicked}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConversationModalQuery;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
.comment {
|
||||
padding-left: calc(var(--v2-spacing-4));
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding-left: calc(var(--v2-spacing-4));
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { graphql, RelayPaginationProp } from "react-relay";
|
||||
|
||||
import {
|
||||
useLoadMore,
|
||||
withPaginationContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { Button, HorizontalGutter } from "coral-ui/components/v2";
|
||||
|
||||
import { ConversationModalRepliesContainer_comment } from "coral-admin/__generated__/ConversationModalRepliesContainer_comment.graphql";
|
||||
import { ConversationModalRepliesContainer_settings } from "coral-admin/__generated__/ConversationModalRepliesContainer_settings.graphql";
|
||||
import { ConversationModalRepliesContainerPaginationQueryVariables } from "coral-admin/__generated__/ConversationModalRepliesContainerPaginationQuery.graphql";
|
||||
|
||||
import ConversationModalCommentContainer from "./ConversationModalCommentContainer";
|
||||
|
||||
import styles from "./ConversationModalRepliesContainer.css";
|
||||
|
||||
interface Props {
|
||||
relay: RelayPaginationProp;
|
||||
comment: ConversationModalRepliesContainer_comment;
|
||||
settings: ConversationModalRepliesContainer_settings;
|
||||
onClose: () => void;
|
||||
onUsernameClicked: (id?: string) => void;
|
||||
}
|
||||
|
||||
const ConversationModalRepliesContainer: FunctionComponent<Props> = ({
|
||||
comment,
|
||||
relay,
|
||||
settings,
|
||||
onUsernameClicked,
|
||||
}) => {
|
||||
const [loadMore] = useLoadMore(relay, 5);
|
||||
const replies = useMemo(
|
||||
() => comment.replies.edges.map((edge) => edge.node, comment.replies),
|
||||
[comment.replies]
|
||||
);
|
||||
const [showReplies, setShowReplies] = useState(false);
|
||||
const onShowReplies = useCallback(() => {
|
||||
setShowReplies(true);
|
||||
}, []);
|
||||
return (
|
||||
<HorizontalGutter>
|
||||
{showReplies &&
|
||||
replies.map((reply) => (
|
||||
<div key={reply.id} className={styles.comment}>
|
||||
<ConversationModalCommentContainer
|
||||
key={reply.id}
|
||||
settings={settings}
|
||||
comment={reply}
|
||||
isHighlighted={false}
|
||||
isReply={true}
|
||||
onUsernameClick={onUsernameClicked}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className={styles.footer}>
|
||||
{!showReplies && comment.replyCount > 0 && (
|
||||
<Localized id="conversation-modal-replies-show">
|
||||
<Button variant="outline" fullWidth onClick={onShowReplies}>
|
||||
Show replies
|
||||
</Button>
|
||||
</Localized>
|
||||
)}
|
||||
{showReplies &&
|
||||
comment.replyCount > replies.length &&
|
||||
replies.length > 0 && (
|
||||
<Localized id="conversation-modal-replies-show-more">
|
||||
<Button variant="outline" fullWidth onClick={loadMore}>
|
||||
Show more replies
|
||||
</Button>
|
||||
</Localized>
|
||||
)}
|
||||
</div>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: (cvle) If this could be autogenerated.
|
||||
type FragmentVariables = Omit<
|
||||
ConversationModalRepliesContainerPaginationQueryVariables,
|
||||
"commentID"
|
||||
>;
|
||||
|
||||
const enhanced = withPaginationContainer<
|
||||
Props,
|
||||
ConversationModalRepliesContainerPaginationQueryVariables,
|
||||
FragmentVariables
|
||||
>(
|
||||
{
|
||||
settings: graphql`
|
||||
fragment ConversationModalRepliesContainer_settings on Settings {
|
||||
...ConversationModalCommentContainer_settings
|
||||
}
|
||||
`,
|
||||
comment: graphql`
|
||||
fragment ConversationModalRepliesContainer_comment on Comment
|
||||
@argumentDefinitions(
|
||||
count: { type: "Int!", defaultValue: 5 }
|
||||
cursor: { type: "Cursor" }
|
||||
orderBy: { type: "COMMENT_SORT!", defaultValue: CREATED_AT_ASC }
|
||||
) {
|
||||
id
|
||||
replies(first: $count, after: $cursor, orderBy: $orderBy)
|
||||
@connection(key: "ConversationModalReplies_replies") {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...ConversationModalCommentContainer_comment
|
||||
}
|
||||
}
|
||||
}
|
||||
replyCount
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
direction: "forward",
|
||||
getConnectionFromProps(props) {
|
||||
return props.comment && props.comment.replies;
|
||||
},
|
||||
// This is also the default implementation of `getFragmentVariables` if it isn't provided.
|
||||
getFragmentVariables(prevVars, totalCount) {
|
||||
return {
|
||||
...prevVars,
|
||||
count: totalCount,
|
||||
};
|
||||
},
|
||||
getVariables(props, { count, cursor }, fragmentVariables) {
|
||||
return {
|
||||
count,
|
||||
cursor,
|
||||
orderBy: fragmentVariables.orderBy,
|
||||
commentID: props.comment.id,
|
||||
};
|
||||
},
|
||||
query: graphql`
|
||||
# Pagination query to be fetched upon calling 'loadMore'.
|
||||
# Notice that we re-use our fragment, and the shape of this query matches our fragment spec.
|
||||
query ConversationModalRepliesContainerPaginationQuery(
|
||||
$count: Int!
|
||||
$cursor: Cursor
|
||||
$orderBy: COMMENT_SORT!
|
||||
$commentID: ID!
|
||||
) {
|
||||
comment(id: $commentID) {
|
||||
...ConversationModalRepliesContainer_comment
|
||||
@arguments(count: $count, cursor: $cursor, orderBy: $orderBy)
|
||||
}
|
||||
}
|
||||
`,
|
||||
}
|
||||
)(ConversationModalRepliesContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent, useMemo } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { QueryRenderData, QueryRenderer } from "coral-framework/lib/relay";
|
||||
import { CallOut, Spinner } from "coral-ui/components/v2";
|
||||
|
||||
import { ConversationModalRepliesQuery as QueryTypes } from "coral-admin/__generated__/ConversationModalRepliesQuery.graphql";
|
||||
|
||||
import ConversationModalCommentContainer from "./ConversationModalCommentContainer";
|
||||
|
||||
interface Props {
|
||||
commentID: string;
|
||||
onUsernameClicked: (userID: string) => void;
|
||||
}
|
||||
|
||||
const ConversationModalRepliesQuery: FunctionComponent<Props> = ({
|
||||
commentID,
|
||||
onUsernameClicked,
|
||||
}) => {
|
||||
return (
|
||||
<QueryRenderer<QueryTypes>
|
||||
query={graphql`
|
||||
query ConversationModalRepliesQuery($commentID: ID!) {
|
||||
settings {
|
||||
...ConversationModalCommentContainer_settings
|
||||
}
|
||||
comment(id: $commentID) {
|
||||
replies {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...ConversationModalCommentContainer_comment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
variables={{ commentID }}
|
||||
cacheConfig={{ force: true }}
|
||||
render={({ props }: QueryRenderData<QueryTypes>) => {
|
||||
if (!props) {
|
||||
return (
|
||||
<div>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!props.comment) {
|
||||
return (
|
||||
<div>
|
||||
<CallOut>
|
||||
<Localized id="conversation-modal-comment-not-found">
|
||||
Comment not found.
|
||||
</Localized>
|
||||
</CallOut>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const replies = useMemo(
|
||||
() => props.comment!.replies.edges.map((edge) => edge.node),
|
||||
[props.comment.replies]
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
{replies.map((reply) => (
|
||||
<div key={reply.id}>
|
||||
<ConversationModalCommentContainer
|
||||
key={reply.id}
|
||||
comment={reply}
|
||||
settings={props.settings}
|
||||
isHighlighted={false}
|
||||
isReply={true}
|
||||
onUsernameClick={onUsernameClicked}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConversationModalRepliesQuery;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./ConversationModal";
|
||||
@@ -7,7 +7,7 @@ import { BaseButton, Icon } from "coral-ui/components/v2";
|
||||
|
||||
import styles from "./ApproveButton.css";
|
||||
|
||||
interface Props extends PropTypesOf<typeof BaseButton> {
|
||||
interface Props extends Omit<PropTypesOf<typeof BaseButton>, "ref"> {
|
||||
invert?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,12 +34,12 @@ const BanCommentUserMutation = createMutation(
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
updater: store => {
|
||||
updater: (store) => {
|
||||
const user = store.get(input.userID);
|
||||
if (user) {
|
||||
const comments = user.getLinkedRecords("comments");
|
||||
if (comments) {
|
||||
comments.forEach(comment => {
|
||||
comments.forEach((comment) => {
|
||||
if (comment) {
|
||||
comment.setLinkedRecord(user, "author");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { Tag } from "coral-ui/components/v2";
|
||||
|
||||
import { CommentAuthorContainer_comment as CommentData } from "coral-admin/__generated__/CommentAuthorContainer_comment.graphql";
|
||||
@@ -13,16 +14,39 @@ interface Props {
|
||||
}
|
||||
|
||||
const CommentAuthorContainer: FunctionComponent<Props> = ({ comment }) => {
|
||||
if (!comment.author || !comment.author.status.ban.active) {
|
||||
if (
|
||||
!comment.author ||
|
||||
!(
|
||||
comment.author.status.ban.active ||
|
||||
comment.author.status.premod.active ||
|
||||
comment.author.status.suspension.active
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className={styles.authorStatus}>
|
||||
<Localized id="commentAuthor-status-banned">
|
||||
<Tag color="error">BANNED</Tag>
|
||||
</Localized>
|
||||
</div>
|
||||
{comment.author.status.ban.active && (
|
||||
<div className={styles.authorStatus}>
|
||||
<Localized id="commentAuthor-status-banned">
|
||||
<Tag color="error">BANNED</Tag>
|
||||
</Localized>
|
||||
</div>
|
||||
)}
|
||||
{comment.author.status.suspension.active && (
|
||||
<div className={styles.authorStatus}>
|
||||
<Localized id="commentAuthor-status-suspended">
|
||||
<Tag color="error">SUSPENDED</Tag>
|
||||
</Localized>
|
||||
</div>
|
||||
)}
|
||||
{comment.author.status.premod.active && (
|
||||
<div className={styles.authorStatus}>
|
||||
<Localized id="commentAuthor-status-premod">
|
||||
<Tag color="grey">PRE-MOD</Tag>
|
||||
</Localized>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -34,6 +58,12 @@ const enhanced = withFragmentContainer<Props>({
|
||||
id
|
||||
username
|
||||
status {
|
||||
suspension {
|
||||
active
|
||||
}
|
||||
premod {
|
||||
active
|
||||
}
|
||||
ban {
|
||||
active
|
||||
}
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import cn from "classnames";
|
||||
import React, { FunctionComponent, useMemo } from "react";
|
||||
|
||||
import { getPhrasesRegExp, GetPhrasesRegExpOptions } from "coral-admin/helpers";
|
||||
import { createPurify } from "coral-common/utils/purify";
|
||||
|
||||
import styles from "./CommentContent.css";
|
||||
|
||||
/**
|
||||
* Create a purify instance that will be used to handle HTML content.
|
||||
*/
|
||||
const purify = createPurify(window, false);
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
children: string | React.ReactElement;
|
||||
phrases: GetPhrasesRegExpOptions;
|
||||
}
|
||||
|
||||
function escapeHTML(unsafe: string) {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// markPhrasesHTML looks for `supsect` and `banned` words inside `text` given
|
||||
// the settings applied for the locale and highlights them by returning an HTML
|
||||
// string.
|
||||
function markPhrasesHTML(text: string, expression: RegExp) {
|
||||
const tokens = text.split(expression);
|
||||
if (tokens.length === 1) {
|
||||
return text;
|
||||
}
|
||||
return tokens
|
||||
.map((token, i) =>
|
||||
// Using our Regexp patterns it returns tokens arranged this way
|
||||
// [STRING_WITH_NO_MATCH, NEW_WORD_DELIMITER, MATCHED_WORD, ...].
|
||||
// This pattern repeats throughout. Next line will mark MATCHED_WORD
|
||||
// and escape all tokens.
|
||||
i % 3 === 2 ? `<mark>${escapeHTML(token)}</mark>` : escapeHTML(token)
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
// markHTMLNode manipulates the node by looking for #text nodes and adding markers
|
||||
// for `supsectWords` and `bannedWords`.
|
||||
function markHTMLNode(parentNode: Node, expression: RegExp) {
|
||||
parentNode.childNodes.forEach(node => {
|
||||
if (node.nodeName === "#text") {
|
||||
const newContent = markPhrasesHTML(node.textContent!, expression);
|
||||
if (newContent !== node.textContent) {
|
||||
const newNode = document.createElement("span");
|
||||
newNode.innerHTML = newContent;
|
||||
parentNode.replaceChild(newNode, node);
|
||||
}
|
||||
} else {
|
||||
markHTMLNode(node, expression);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const CommentContent: FunctionComponent<Props> = ({
|
||||
phrases,
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
// Cache the expression used via memo. This will reduce duplicate renders of
|
||||
// this comment content when the children change but the phrase configuration
|
||||
// does not change. The regExp is already cached on a deeper level
|
||||
// automatically, this is just lessening that impact further.
|
||||
const expression = useMemo(() => getPhrasesRegExp(phrases), [phrases]);
|
||||
|
||||
if (typeof children === "string") {
|
||||
// We create a Shadow DOM Tree with the HTML body content and
|
||||
// use it as a parser.
|
||||
const node = document.createElement("div");
|
||||
node.innerHTML = purify.sanitize(children);
|
||||
|
||||
if (expression) {
|
||||
// Then we traverse it recursively and manipulate it to highlight suspect words
|
||||
// and banned words.
|
||||
markHTMLNode(node, expression);
|
||||
}
|
||||
|
||||
// Finally we render the content of the Shadow DOM Tree
|
||||
return (
|
||||
<div
|
||||
className={cn(className, styles.root)}
|
||||
dangerouslySetInnerHTML={{ __html: node.innerHTML }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default CommentContent;
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { HorizontalGutter, Timestamp } from "coral-ui/components/v2";
|
||||
|
||||
import { CommentRevisionContainer_comment as CommentData } from "coral-admin/__generated__/CommentRevisionContainer_comment.graphql";
|
||||
import { CommentRevisionContainer_settings as SettingsData } from "coral-admin/__generated__/CommentRevisionContainer_settings.graphql";
|
||||
|
||||
import CommentContent from "./CommentContent";
|
||||
import { CommentContent } from "../Comment";
|
||||
|
||||
interface Props {
|
||||
comment: CommentData;
|
||||
@@ -22,12 +23,12 @@ const CommentRevisionContainer: FunctionComponent<Props> = ({
|
||||
{comment.revisionHistory
|
||||
.concat()
|
||||
.reverse()
|
||||
.filter(c =>
|
||||
.filter((c) =>
|
||||
comment && comment.revision && comment.revision.id
|
||||
? comment.revision.id !== c.id
|
||||
: true
|
||||
)
|
||||
.map(c => (
|
||||
.map((c) => (
|
||||
<div key={c.id}>
|
||||
<Timestamp>{c.createdAt}</Timestamp>
|
||||
<CommentContent phrases={settings}>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BaseButton } from "coral-ui/components/v2";
|
||||
|
||||
import styles from "./FeatureButton.css";
|
||||
|
||||
interface Props extends PropTypesOf<typeof BaseButton> {
|
||||
interface Props extends Omit<PropTypesOf<typeof BaseButton>, "ref"> {
|
||||
featured: boolean;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { graphql } from "react-relay";
|
||||
import { ConnectionHandler, Environment } from "relay-runtime";
|
||||
|
||||
import { getQueueConnection } from "coral-admin/helpers";
|
||||
import { SectionFilter } from "coral-common/section";
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
@@ -18,7 +19,11 @@ const FeatureCommentMutation = createMutation(
|
||||
"featureComment",
|
||||
(
|
||||
environment: Environment,
|
||||
input: MutationInput<FeatureCommentMutation> & { storyID: string },
|
||||
input: MutationInput<FeatureCommentMutation> & {
|
||||
storyID?: string | null;
|
||||
siteID?: string | null;
|
||||
section?: SectionFilter | null;
|
||||
},
|
||||
{ uuidGenerator }: CoralContext
|
||||
) =>
|
||||
commitMutationPromiseNormalized<FeatureCommentMutation>(environment, {
|
||||
@@ -57,7 +62,7 @@ const FeatureCommentMutation = createMutation(
|
||||
},
|
||||
storyID: input.storyID,
|
||||
},
|
||||
optimisticUpdater: store => {
|
||||
optimisticUpdater: (store) => {
|
||||
const comment = store.get(input.commentID)!;
|
||||
const tags = comment.getLinkedRecords("tags");
|
||||
if (tags) {
|
||||
@@ -67,14 +72,38 @@ const FeatureCommentMutation = createMutation(
|
||||
comment.setValue(GQLCOMMENT_STATUS.APPROVED, "status");
|
||||
}
|
||||
},
|
||||
updater: store => {
|
||||
updater: (store) => {
|
||||
const connections = [
|
||||
getQueueConnection(store, "PENDING", input.storyID),
|
||||
getQueueConnection(store, "REPORTED", input.storyID),
|
||||
getQueueConnection(store, "UNMODERATED", input.storyID),
|
||||
getQueueConnection(store, "REJECTED", input.storyID),
|
||||
].filter(c => c);
|
||||
connections.forEach(con =>
|
||||
getQueueConnection(
|
||||
store,
|
||||
"PENDING",
|
||||
input.storyID,
|
||||
input.siteID,
|
||||
input.section
|
||||
),
|
||||
getQueueConnection(
|
||||
store,
|
||||
"REPORTED",
|
||||
input.storyID,
|
||||
input.siteID,
|
||||
input.section
|
||||
),
|
||||
getQueueConnection(
|
||||
store,
|
||||
"UNMODERATED",
|
||||
input.storyID,
|
||||
input.siteID,
|
||||
input.section
|
||||
),
|
||||
getQueueConnection(
|
||||
store,
|
||||
"REJECTED",
|
||||
input.storyID,
|
||||
input.siteID,
|
||||
input.section
|
||||
),
|
||||
].filter((c) => c);
|
||||
connections.forEach((con) =>
|
||||
ConnectionHandler.deleteNode(con!, input.commentID)
|
||||
);
|
||||
},
|
||||
|
||||
@@ -9,8 +9,3 @@
|
||||
display: inline-block;
|
||||
padding-right: var(--spacing-2);
|
||||
}
|
||||
|
||||
.button {
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import CopyToClipboard from "react-copy-to-clipboard";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { getURLWithCommentID } from "coral-framework/helpers";
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { getLocationOrigin } from "coral-framework/utils";
|
||||
import { Button, Icon } from "coral-ui/components";
|
||||
import { Button, Icon } from "coral-ui/components/v2";
|
||||
|
||||
import { LinkDetailsContainer_comment } from "coral-admin/__generated__/LinkDetailsContainer_comment.graphql";
|
||||
import { LinkDetailsContainer_settings } from "coral-admin/__generated__/LinkDetailsContainer_settings.graphql";
|
||||
@@ -32,12 +33,7 @@ const LinkDetailsContainer: FunctionComponent<Props> = ({
|
||||
<CopyToClipboard
|
||||
text={getURLWithCommentID(comment.story.url, comment.id)}
|
||||
>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="filled"
|
||||
size="small"
|
||||
className={styles.button}
|
||||
>
|
||||
<Button>
|
||||
<Icon size="md">link</Icon>
|
||||
<Localized id="moderate-in-stream-link-copy">
|
||||
<span>In Stream</span>
|
||||
@@ -49,12 +45,7 @@ const LinkDetailsContainer: FunctionComponent<Props> = ({
|
||||
<CopyToClipboard
|
||||
text={`${getLocationOrigin()}/admin/moderate/comment/${comment.id}`}
|
||||
>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="filled"
|
||||
size="small"
|
||||
className={styles.button}
|
||||
>
|
||||
<Button>
|
||||
<Icon size="md">link</Icon>
|
||||
<Localized id="moderate-in-moderation-link-copy">
|
||||
<span>In Moderation</span>
|
||||
|
||||
@@ -13,9 +13,6 @@ it("renders all markers", () => {
|
||||
const props: PropTypesOf<typeof MarkersContainerN> = {
|
||||
comment: {
|
||||
status: "PREMOD",
|
||||
editing: {
|
||||
edited: false,
|
||||
},
|
||||
revision: {
|
||||
actionCounts: {
|
||||
flag: {
|
||||
@@ -33,11 +30,6 @@ it("renders all markers", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
perspective: {
|
||||
score: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
@@ -58,9 +50,6 @@ it("renders some markers", () => {
|
||||
const props: PropTypesOf<typeof MarkersContainerN> = {
|
||||
comment: {
|
||||
status: "PREMOD",
|
||||
editing: {
|
||||
edited: false,
|
||||
},
|
||||
revision: {
|
||||
actionCounts: {
|
||||
flag: {
|
||||
@@ -78,11 +67,6 @@ it("renders some markers", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
perspective: {
|
||||
score: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
@@ -17,26 +17,18 @@ interface MarkersContainerProps {
|
||||
settings: MarkersContainer_settings;
|
||||
}
|
||||
|
||||
function hasDetails(c: MarkersContainer_comment) {
|
||||
return c.revision
|
||||
? c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE +
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM >
|
||||
0 || c.revision.metadata.perspective
|
||||
: false;
|
||||
}
|
||||
|
||||
let keyCounter = 0;
|
||||
const markers: Array<
|
||||
(c: MarkersContainer_comment) => React.ReactElement<any> | null
|
||||
> = [
|
||||
c =>
|
||||
const markers: Array<(
|
||||
c: MarkersContainer_comment
|
||||
) => React.ReactElement<any> | null> = [
|
||||
(c) =>
|
||||
(c.status === "PREMOD" && (
|
||||
<Localized id="moderate-marker-preMod" key={keyCounter++}>
|
||||
<Marker color="pending">Pre-Mod</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c) =>
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_LINKS && (
|
||||
<Localized id="moderate-marker-link" key={keyCounter++}>
|
||||
@@ -44,7 +36,7 @@ const markers: Array<
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c) =>
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_BANNED_WORD && (
|
||||
<Localized id="moderate-marker-bannedWord" key={keyCounter++}>
|
||||
@@ -52,7 +44,7 @@ const markers: Array<
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c) =>
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_SUSPECT_WORD && (
|
||||
<Localized id="moderate-marker-suspectWord" key={keyCounter++}>
|
||||
@@ -60,7 +52,7 @@ const markers: Array<
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c) =>
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_SPAM && (
|
||||
<Localized id="moderate-marker-spamDetected" key={keyCounter++}>
|
||||
@@ -68,7 +60,7 @@ const markers: Array<
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c) =>
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_TOXIC && (
|
||||
<Localized id="moderate-marker-toxic" key={keyCounter++}>
|
||||
@@ -76,7 +68,7 @@ const markers: Array<
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c) =>
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_REPEAT_POST && (
|
||||
<Localized id="moderate-marker-repeatPost" key={keyCounter++}>
|
||||
@@ -84,7 +76,7 @@ const markers: Array<
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c) =>
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_RECENT_HISTORY && (
|
||||
<Localized id="moderate-marker-recentHistory" key={keyCounter++}>
|
||||
@@ -92,7 +84,7 @@ const markers: Array<
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c) =>
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE && (
|
||||
<Marker key={keyCounter++} color="reported">
|
||||
@@ -105,7 +97,7 @@ const markers: Array<
|
||||
</Marker>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c) =>
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM && (
|
||||
<Marker key={keyCounter++} color="reported">
|
||||
@@ -118,7 +110,7 @@ const markers: Array<
|
||||
</Marker>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c) =>
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_NEW_COMMENTER && (
|
||||
<Localized id="moderate-marker-newCommenter" key={keyCounter++}>
|
||||
@@ -128,42 +120,34 @@ const markers: Array<
|
||||
null,
|
||||
];
|
||||
|
||||
export class MarkersContainer extends React.Component<MarkersContainerProps> {
|
||||
public render() {
|
||||
const elements = markers.map(cb => cb(this.props.comment)).filter(m => m);
|
||||
const doesHaveDetails = hasDetails(this.props.comment);
|
||||
if (elements.length === 0 && !doesHaveDetails) {
|
||||
return null;
|
||||
}
|
||||
export const MarkersContainer: React.FunctionComponent<MarkersContainerProps> = (
|
||||
props
|
||||
) => {
|
||||
const elements = useMemo(
|
||||
() => markers.map((cb) => cb(props.comment)).filter((m) => m),
|
||||
[markers, props.comment]
|
||||
);
|
||||
|
||||
return (
|
||||
<Markers
|
||||
details={
|
||||
doesHaveDetails || this.props.comment.editing.edited ? (
|
||||
<ModerateCardDetailsContainer
|
||||
hasDetails={!!doesHaveDetails}
|
||||
hasRevisions={this.props.comment.editing.edited}
|
||||
onUsernameClick={this.props.onUsernameClick}
|
||||
comment={this.props.comment}
|
||||
settings={this.props.settings}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{elements}
|
||||
</Markers>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Markers
|
||||
details={
|
||||
<ModerateCardDetailsContainer
|
||||
onUsernameClick={props.onUsernameClick}
|
||||
comment={props.comment}
|
||||
settings={props.settings}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{elements}
|
||||
</Markers>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<MarkersContainerProps>({
|
||||
comment: graphql`
|
||||
fragment MarkersContainer_comment on Comment {
|
||||
...ModerateCardDetailsContainer_comment
|
||||
status
|
||||
editing {
|
||||
edited
|
||||
}
|
||||
revision {
|
||||
actionCounts {
|
||||
flag {
|
||||
@@ -181,11 +165,6 @@ const enhanced = withFragmentContainer<MarkersContainerProps>({
|
||||
}
|
||||
}
|
||||
}
|
||||
metadata {
|
||||
perspective {
|
||||
score
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -15,26 +15,6 @@ $moderateCardLinkTextColor: var(--v2-colors-teal-700);
|
||||
margin-bottom: var(--v2-spacing-4);
|
||||
}
|
||||
|
||||
.usernameButton {
|
||||
margin-right: var(--mini-unit);
|
||||
padding: var(--spacing-1);
|
||||
margin-left: calc(-1 * var(--v2-spacing-1));
|
||||
|
||||
line-height: var(--v2-line-height-reset);
|
||||
|
||||
&:hover {
|
||||
background-color: $moderateCardUsernameBackgroundHover;
|
||||
border-radius: var(--v2-round-corners);
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $moderateCardUsernameBackgroundHover;
|
||||
border-radius: var(--v2-round-corners);
|
||||
border-style: none;
|
||||
}
|
||||
}
|
||||
|
||||
.inReplyTo {
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ const baseProps: PropTypesOf<typeof ModerateCardN> = {
|
||||
onBan: noop,
|
||||
onUsernameClick: noop,
|
||||
onFocusOrClick: noop,
|
||||
onConversationClick: noop,
|
||||
showStory: false,
|
||||
moderatedBy: null,
|
||||
};
|
||||
|
||||
@@ -13,7 +13,8 @@ import { HOTKEYS } from "coral-admin/constants";
|
||||
import { GetPhrasesRegExpOptions } from "coral-admin/helpers";
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
import {
|
||||
BaseButton,
|
||||
Button,
|
||||
ButtonIcon,
|
||||
Card,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
@@ -22,14 +23,12 @@ import {
|
||||
Timestamp,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import { CommentContent, InReplyTo, UsernameButton } from "../Comment";
|
||||
import ApproveButton from "./ApproveButton";
|
||||
import CommentAuthorContainer from "./CommentAuthorContainer";
|
||||
import CommentContent from "./CommentContent";
|
||||
import FeatureButton from "./FeatureButton";
|
||||
import InReplyTo from "./InReplyTo";
|
||||
import MarkersContainer from "./MarkersContainer";
|
||||
import RejectButton from "./RejectButton";
|
||||
import Username from "./Username";
|
||||
|
||||
import styles from "./ModerateCard.css";
|
||||
|
||||
@@ -38,6 +37,7 @@ interface Props {
|
||||
username: string;
|
||||
createdAt: string;
|
||||
body: string;
|
||||
highlight?: boolean;
|
||||
inReplyTo?: {
|
||||
id: string;
|
||||
username: string | null;
|
||||
@@ -59,6 +59,7 @@ interface Props {
|
||||
onReject: () => void;
|
||||
onFeature: () => void;
|
||||
onUsernameClick: (id?: string) => void;
|
||||
onConversationClick: (() => void) | null;
|
||||
onFocusOrClick: () => void;
|
||||
mini?: boolean;
|
||||
hideUsername?: boolean;
|
||||
@@ -82,6 +83,7 @@ const ModerateCard: FunctionComponent<Props> = ({
|
||||
username,
|
||||
createdAt,
|
||||
body,
|
||||
highlight = false,
|
||||
inReplyTo,
|
||||
comment,
|
||||
settings,
|
||||
@@ -102,6 +104,7 @@ const ModerateCard: FunctionComponent<Props> = ({
|
||||
moderatedBy,
|
||||
selected,
|
||||
onFocusOrClick,
|
||||
onConversationClick,
|
||||
mini = false,
|
||||
hideUsername = false,
|
||||
deleted = false,
|
||||
@@ -190,12 +193,10 @@ const ModerateCard: FunctionComponent<Props> = ({
|
||||
<Flex alignItems="center">
|
||||
{!hideUsername && username && (
|
||||
<>
|
||||
<BaseButton
|
||||
<UsernameButton
|
||||
username={username}
|
||||
onClick={commentAuthorClick}
|
||||
className={styles.usernameButton}
|
||||
>
|
||||
<Username>{username}</Username>
|
||||
</BaseButton>
|
||||
/>
|
||||
<CommentAuthorContainer comment={comment} />
|
||||
</>
|
||||
)}
|
||||
@@ -220,20 +221,23 @@ const ModerateCard: FunctionComponent<Props> = ({
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.contentArea}>
|
||||
<CommentContent phrases={phrases} className={styles.content}>
|
||||
<CommentContent
|
||||
highlight={highlight}
|
||||
phrases={phrases}
|
||||
className={styles.content}
|
||||
>
|
||||
{commentBody}
|
||||
</CommentContent>
|
||||
<div className={styles.viewContext}>
|
||||
<Localized id="moderate-comment-viewContext">
|
||||
<TextLink
|
||||
className={styles.link}
|
||||
href={viewContextHref}
|
||||
target="_blank"
|
||||
>
|
||||
View Context
|
||||
</TextLink>
|
||||
</Localized>
|
||||
</div>
|
||||
{onConversationClick && (
|
||||
<div className={styles.viewContext}>
|
||||
<Button iconLeft variant="text" onClick={onConversationClick}>
|
||||
<ButtonIcon>question_answer</ButtonIcon>
|
||||
<Localized id="moderate-comment-viewConversation">
|
||||
<span>View conversation</span>
|
||||
</Localized>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(styles.separator, {
|
||||
[styles.ruledSeparator]: !mini,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "coral-admin/mutations";
|
||||
import FadeInTransition from "coral-framework/components/FadeInTransition";
|
||||
import { getModerationLink } from "coral-framework/helpers";
|
||||
import parseModerationOptions from "coral-framework/helpers/parseModerationOptions";
|
||||
import {
|
||||
MutationProp,
|
||||
withFragmentContainer,
|
||||
@@ -22,7 +23,6 @@ import {
|
||||
ModerateCardContainer_comment,
|
||||
} from "coral-admin/__generated__/ModerateCardContainer_comment.graphql";
|
||||
import { ModerateCardContainer_settings } from "coral-admin/__generated__/ModerateCardContainer_settings.graphql";
|
||||
import { ModerateCardContainer_viewer } from "coral-admin/__generated__/ModerateCardContainer_viewer.graphql";
|
||||
|
||||
import BanCommentUserMutation from "./BanCommentUserMutation";
|
||||
import FeatureCommentMutation from "./FeatureCommentMutation";
|
||||
@@ -32,7 +32,6 @@ import UnfeatureCommentMutation from "./UnfeatureCommentMutation";
|
||||
|
||||
interface Props {
|
||||
comment: ModerateCardContainer_comment;
|
||||
viewer: ModerateCardContainer_viewer;
|
||||
settings: ModerateCardContainer_settings;
|
||||
approveComment: MutationProp<typeof ApproveCommentMutation>;
|
||||
rejectComment: MutationProp<typeof RejectCommentMutation>;
|
||||
@@ -46,6 +45,7 @@ interface Props {
|
||||
mini?: boolean;
|
||||
hideUsername?: boolean;
|
||||
onUsernameClicked?: (userID: string) => void;
|
||||
onConversationClicked?: (commentID: string) => void;
|
||||
onSetSelected?: () => void;
|
||||
selected?: boolean;
|
||||
selectPrev?: () => void;
|
||||
@@ -65,7 +65,7 @@ function getStatus(comment: ModerateCardContainer_comment) {
|
||||
}
|
||||
|
||||
function isFeatured(comment: ModerateCardContainer_comment) {
|
||||
return comment.tags.some(t => t.code === GQLTAG.FEATURED);
|
||||
return comment.tags.some((t) => t.code === GQLTAG.FEATURED);
|
||||
}
|
||||
|
||||
const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
@@ -73,7 +73,6 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
settings,
|
||||
danglingLogic,
|
||||
showStoryInfo,
|
||||
viewer,
|
||||
match,
|
||||
router,
|
||||
approveComment,
|
||||
@@ -86,6 +85,7 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
selectPrev,
|
||||
selectNext,
|
||||
onUsernameClicked: usernameClicked,
|
||||
onConversationClicked: conversationClicked,
|
||||
onSetSelected: setSelected,
|
||||
banUser,
|
||||
loadNext,
|
||||
@@ -96,10 +96,14 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const { storyID, siteID, section } = parseModerationOptions(match);
|
||||
|
||||
await approveComment({
|
||||
commentID: comment.id,
|
||||
commentRevisionID: comment.revision.id,
|
||||
storyID: match.params.storyID,
|
||||
storyID,
|
||||
siteID,
|
||||
section,
|
||||
});
|
||||
if (loadNext) {
|
||||
loadNext();
|
||||
@@ -111,10 +115,14 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const { storyID, siteID, section } = parseModerationOptions(match);
|
||||
|
||||
await rejectComment({
|
||||
commentID: comment.id,
|
||||
commentRevisionID: comment.revision.id,
|
||||
storyID: match.params.storyID,
|
||||
storyID,
|
||||
siteID,
|
||||
section,
|
||||
});
|
||||
if (loadNext) {
|
||||
loadNext();
|
||||
@@ -126,10 +134,14 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const { storyID, siteID, section } = parseModerationOptions(match);
|
||||
|
||||
featureComment({
|
||||
commentID: comment.id,
|
||||
commentRevisionID: comment.revision.id,
|
||||
storyID: match.params.storyID,
|
||||
storyID,
|
||||
siteID,
|
||||
section,
|
||||
});
|
||||
}, [featureComment, comment, match]);
|
||||
|
||||
@@ -160,6 +172,13 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
[usernameClicked, comment]
|
||||
);
|
||||
|
||||
const onConversationClicked = useCallback(() => {
|
||||
if (!conversationClicked) {
|
||||
return;
|
||||
}
|
||||
conversationClicked(comment.id);
|
||||
}, [conversationClicked, comment]);
|
||||
|
||||
const handleModerateStory = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
router.push(getModerationLink({ storyID: comment.story.id }));
|
||||
@@ -203,6 +222,16 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
},
|
||||
[comment]
|
||||
);
|
||||
|
||||
// Only highlight comments that have been flagged for containing a banned or
|
||||
// suspect word.
|
||||
const highlight = comment.revision
|
||||
? comment.revision.actionCounts.flag.reasons.COMMENT_DETECTED_BANNED_WORD +
|
||||
comment.revision.actionCounts.flag.reasons
|
||||
.COMMENT_DETECTED_SUSPECT_WORD >
|
||||
0
|
||||
: false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FadeInTransition active={Boolean(comment.enteredLive)}>
|
||||
@@ -215,6 +244,7 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
}
|
||||
createdAt={comment.createdAt}
|
||||
body={comment.body!}
|
||||
highlight={highlight}
|
||||
inReplyTo={comment.parent && comment.parent.author}
|
||||
comment={comment}
|
||||
settings={settings}
|
||||
@@ -227,6 +257,9 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
onReject={handleReject}
|
||||
onFeature={onFeature}
|
||||
onUsernameClick={onUsernameClicked}
|
||||
onConversationClick={
|
||||
conversationClicked ? onConversationClicked : null
|
||||
}
|
||||
selected={selected}
|
||||
selectPrev={selectPrev}
|
||||
selectNext={selectNext}
|
||||
@@ -235,7 +268,6 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
moderatedBy={
|
||||
<ModeratedByContainer
|
||||
onUsernameClicked={onUsernameClicked}
|
||||
viewer={viewer}
|
||||
comment={comment}
|
||||
/>
|
||||
}
|
||||
@@ -283,6 +315,16 @@ const enhanced = withFragmentContainer<Props>({
|
||||
statusLiveUpdated
|
||||
createdAt
|
||||
body
|
||||
revision {
|
||||
actionCounts {
|
||||
flag {
|
||||
reasons {
|
||||
COMMENT_DETECTED_BANNED_WORD
|
||||
COMMENT_DETECTED_SUSPECT_WORD
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tags {
|
||||
code
|
||||
}
|
||||
@@ -332,11 +374,6 @@ const enhanced = withFragmentContainer<Props>({
|
||||
...MarkersContainer_settings
|
||||
}
|
||||
`,
|
||||
viewer: graphql`
|
||||
fragment ModerateCardContainer_viewer on User {
|
||||
...ModeratedByContainer_viewer
|
||||
}
|
||||
`,
|
||||
})(
|
||||
withRouter(
|
||||
withMutation(BanCommentUserMutation)(
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { ModerateCardDetailsContainer_comment as CommentData } from "coral-admin/__generated__/ModerateCardDetailsContainer_comment.graphql";
|
||||
import { ModerateCardDetailsContainer_settings as SettingsData } from "coral-admin/__generated__/ModerateCardDetailsContainer_settings.graphql";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import {
|
||||
Flex,
|
||||
@@ -13,6 +16,9 @@ import {
|
||||
TabBar,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import { ModerateCardDetailsContainer_comment } from "coral-admin/__generated__/ModerateCardDetailsContainer_comment.graphql";
|
||||
import { ModerateCardDetailsContainer_settings } from "coral-admin/__generated__/ModerateCardDetailsContainer_settings.graphql";
|
||||
|
||||
import CommentRevisionContainer from "./CommentRevisionContainer";
|
||||
import FlagDetailsContainer from "./FlagDetailsContainer";
|
||||
import LinkDetailsContainer from "./LinkDetailsContainer";
|
||||
@@ -20,43 +26,46 @@ import LinkDetailsContainer from "./LinkDetailsContainer";
|
||||
import styles from "./ModerateCardDetailsContainer.css";
|
||||
|
||||
interface Props {
|
||||
comment: CommentData;
|
||||
settings: SettingsData;
|
||||
comment: ModerateCardDetailsContainer_comment;
|
||||
settings: ModerateCardDetailsContainer_settings;
|
||||
onUsernameClick: (id?: string) => void;
|
||||
hasDetails: boolean;
|
||||
hasRevisions: boolean;
|
||||
}
|
||||
|
||||
type DetailsTabs = "DETAILS" | "HISTORY";
|
||||
type DetailsTabs = "INFO" | "HISTORY";
|
||||
|
||||
function hasFlagDetails(c: ModerateCardDetailsContainer_comment) {
|
||||
return c.revision
|
||||
? c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE +
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM >
|
||||
0 || c.revision.metadata.perspective
|
||||
: false;
|
||||
}
|
||||
|
||||
const ModerateCardDetailsContainer: FunctionComponent<Props> = ({
|
||||
comment,
|
||||
onUsernameClick,
|
||||
settings,
|
||||
hasDetails,
|
||||
hasRevisions,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<DetailsTabs>(
|
||||
hasDetails ? "DETAILS" : "HISTORY"
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState<DetailsTabs>("INFO");
|
||||
|
||||
const onTabClick = useCallback((id) => setActiveTab(id as DetailsTabs), [
|
||||
setActiveTab,
|
||||
]);
|
||||
|
||||
const doesHaveFlagDetails = useMemo(() => hasFlagDetails(comment), [comment]);
|
||||
const hasRevisions = comment.editing.edited;
|
||||
|
||||
return (
|
||||
<HorizontalGutter>
|
||||
<TabBar
|
||||
variant="default"
|
||||
activeTab={activeTab}
|
||||
onTabClick={id => setActiveTab(id as DetailsTabs)}
|
||||
>
|
||||
{hasDetails && (
|
||||
<Tab tabID="DETAILS" classes={styles}>
|
||||
<Flex alignItems="center" itemGutter>
|
||||
<Icon size="md">list</Icon>
|
||||
<Localized id="moderateCardDetails-tab-info">
|
||||
<span>Info</span>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</Tab>
|
||||
)}
|
||||
<TabBar variant="default" activeTab={activeTab} onTabClick={onTabClick}>
|
||||
<Tab tabID="INFO" classes={styles}>
|
||||
<Flex alignItems="center" itemGutter>
|
||||
<Icon size="md">list</Icon>
|
||||
<Localized id="moderateCardDetails-tab-info">
|
||||
<span>Info</span>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</Tab>
|
||||
{hasRevisions && (
|
||||
<Tab tabID="HISTORY" classes={styles}>
|
||||
<Flex alignItems="center" itemGutter>
|
||||
@@ -68,15 +77,19 @@ const ModerateCardDetailsContainer: FunctionComponent<Props> = ({
|
||||
</Tab>
|
||||
)}
|
||||
</TabBar>
|
||||
{activeTab === "DETAILS" && (
|
||||
{activeTab === "INFO" && (
|
||||
<>
|
||||
<LinkDetailsContainer comment={comment} settings={settings} />
|
||||
<hr />
|
||||
<FlagDetailsContainer
|
||||
comment={comment}
|
||||
settings={settings}
|
||||
onUsernameClick={onUsernameClick}
|
||||
/>
|
||||
{doesHaveFlagDetails && (
|
||||
<>
|
||||
<hr />
|
||||
<FlagDetailsContainer
|
||||
comment={comment}
|
||||
settings={settings}
|
||||
onUsernameClick={onUsernameClick}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{activeTab === "HISTORY" && (
|
||||
@@ -89,6 +102,24 @@ const ModerateCardDetailsContainer: FunctionComponent<Props> = ({
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
comment: graphql`
|
||||
fragment ModerateCardDetailsContainer_comment on Comment {
|
||||
editing {
|
||||
edited
|
||||
}
|
||||
revision {
|
||||
actionCounts {
|
||||
flag {
|
||||
reasons {
|
||||
COMMENT_REPORTED_OFFENSIVE
|
||||
COMMENT_REPORTED_SPAM
|
||||
}
|
||||
}
|
||||
}
|
||||
metadata {
|
||||
perspective {
|
||||
score
|
||||
}
|
||||
}
|
||||
}
|
||||
...FlagDetailsContainer_comment
|
||||
...CommentRevisionContainer_comment
|
||||
...LinkDetailsContainer_comment
|
||||
|
||||
@@ -24,10 +24,6 @@ it("viewer's username shows on moderation cards moderated by viewer", () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
viewer: {
|
||||
id: "viewer",
|
||||
username: "viewer",
|
||||
},
|
||||
onUsernameClicked: (id?: string | null) => {
|
||||
return;
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user