Merge branch 'next' into profile-mobile

This commit is contained in:
Wyatt Johnson
2018-10-16 18:23:28 +00:00
committed by GitHub
188 changed files with 12902 additions and 6343 deletions
+37 -8
View File
@@ -20,7 +20,12 @@ jobs:
- attach_workspace:
at: ~/coralproject/talk
- restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }}
keys:
# Find a cache corresponding to this specific package-lock.json
# checksum when this file is changed, this key will fail.
- v1-dependency-cache-{{ checksum "package-lock.json" }}
# Find the most recently generated cache used from any branch
- v1-dependency-cache-
- run:
name: Update NPM
command: sudo npm update -g npm
@@ -29,11 +34,11 @@ jobs:
command: npm audit
- run:
name: Install dependencies
command: npm install
command: npm ci
- save_cache:
key: dependency-cache-{{ checksum "package-lock.json" }}
key: v1-dependency-cache-{{ checksum "package-lock.json" }}
paths:
- ./node_modules
- ~/.npm
- persist_to_workspace:
root: .
paths: node_modules
@@ -85,20 +90,36 @@ jobs:
at: ~/coralproject/talk
- restore_cache:
keys:
- build-cache-{{ .Branch }}-{{ .Revision }}
- build-cache-{{ .Branch }}-
- build-cache-
- v1-build-cache-{{ .Branch }}-{{ .Revision }}
- v1-build-cache-{{ .Branch }}-
- v1-build-cache-
- run:
name: Build
command: npm run build
- save_cache:
key: build-cache-{{ .Branch }}-{{ .Revision }}
key: v1-build-cache-{{ .Branch }}-{{ .Revision }}
paths:
- ./dist
- persist_to_workspace:
root: .
paths: dist
# release_docker will build and push the Docker image.
release_docker:
<<: *job_defaults
steps:
- checkout
- setup_remote_docker
- run:
name: Login
command: docker login -u $DOCKER_USER -p $DOCKER_PASS
- run:
name: Build
command: docker build -t coralproject/talk:next --build-arg REVISION_HASH=${CIRCLE_SHA1} .
- run:
name: Push
command: docker push coralproject/talk:next
workflows:
version: 2
build-and-test:
@@ -113,3 +134,11 @@ workflows:
- build:
requires:
- npm_dependencies
- release_docker:
requires:
- lint
- unit_tests
- build
filters:
branches:
only: next
+1 -2
View File
@@ -13,5 +13,4 @@ coverage
*.DS_STORE
*.css.d.ts
__generated__
__generated__
+3 -2
View File
@@ -1,6 +1,6 @@
FROM node:8-alpine
# Install installation dependancies.
# Install build dependancies.
RUN apk --no-cache add git
# Create app directory.
@@ -36,6 +36,7 @@ ENV NODE_ENV production
# Store the current git revision.
ARG REVISION_HASH
ENV REVISION_HASH=${REVISION_HASH}
RUN mkdir dist/core/common/__generated__ && \
echo "{\"revision\": \"${REVISION_HASH}\"}" > dist/core/common/__generated__/revision.json
CMD ["npm", "run", "start"]
+20
View File
@@ -0,0 +1,20 @@
/**
* This is a project wide babel configuration.
* https://babeljs.io/docs/en/config-files#project-wide-configuration
*
* We use this file to apply babel configuration to packages in `node_modules`
* for testing with jest.
*/
module.exports = {
env: {
test: {
presets: [
[
"@babel/env",
{ targets: "last 2 versions, ie 11", modules: "commonjs" },
],
"@babel/react",
],
},
},
};
+2 -1
View File
@@ -16,6 +16,7 @@ module.exports = {
testEnvironment: "node",
testURL: "http://localhost",
transform: {
"^.+\\.jsx?$": "<rootDir>/node_modules/babel-jest",
"^.+\\.tsx?$": "<rootDir>/node_modules/ts-jest",
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
"^.+\\.ftl$": "<rootDir>/config/jest/contentTransform.js",
@@ -23,7 +24,7 @@ module.exports = {
"<rootDir>/config/jest/fileTransform.js",
},
transformIgnorePatterns: [
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|ts|tsx)$",
"[/\\\\]node_modules[/\\\\](?!(fluent|react-relay-network-modern)[/\\\\]).+\\.(js|jsx|mjs|ts|tsx)$",
],
moduleNameMapper: {
"^talk-admin/(.*)$": "<rootDir>/src/core/client/admin/$1",
+19
View File
@@ -65,6 +65,23 @@ const config: Config = {
runOnInit: true,
}),
},
compileRelayInstall: {
paths: [
"core/client/install/**/*.ts",
"core/client/install/**/*.tsx",
"core/client/install/**/*.graphql",
"core/server/**/*.graphql",
],
ignore: [
"core/**/*.d.ts",
"core/**/*.graphql.ts",
"**/test/**/*",
"core/**/*.spec.*",
],
executor: new CommandExecutor("npm run compile:relay-install", {
runOnInit: true,
}),
},
compileCSSTypes: {
paths: ["**/*.css"],
executor: new CommandExecutor("npm run compile:css-types", {
@@ -94,6 +111,7 @@ const config: Config = {
"compileCSSTypes",
"compileRelayStream",
"compileRelayAuth",
"compileRelayInstall",
"compileRelayAdmin",
"compileSchema",
],
@@ -103,6 +121,7 @@ const config: Config = {
"compileCSSTypes",
"compileRelayStream",
"compileRelayAuth",
"compileRelayInstall",
],
},
};
+684 -16
View File
@@ -1563,6 +1563,15 @@
"integrity": "sha512-LAQ1d4OPfSJ/BMbI2DuizmYrrkD9JMaTdi2hQTlI53lQ4kRQPyZQRS4CYQ7O66bnBBnP/oYdRxbk++X0xuFU6A==",
"dev": true
},
"@samverschueren/stream-to-observable": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz",
"integrity": "sha512-MI4Xx6LHs4Webyvi6EbspgyAb4D2Q2VtnCQ1blOJcoLS6mVa8lNN2rkIy1CVxfTUpoyIbCTkXES1rLXztFD1lg==",
"dev": true,
"requires": {
"any-observable": "^0.3.0"
}
},
"@shellscape/koa-send": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@shellscape/koa-send/-/koa-send-4.1.3.tgz",
@@ -1675,6 +1684,15 @@
"@types/node": "*"
}
},
"@types/bunyan-prettystream": {
"version": "0.1.31",
"resolved": "https://registry.npmjs.org/@types/bunyan-prettystream/-/bunyan-prettystream-0.1.31.tgz",
"integrity": "sha512-NE7fq2ZcX7OSMK+VhTNJkVEHlo+hm0uVXpuLeH1ifGm52Qwuo/kLD2GHo7UcEXMFu3duKver/AFo8C4TME93zw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/case-sensitive-paths-webpack-plugin": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@types/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.1.2.tgz",
@@ -1754,6 +1772,15 @@
"integrity": "sha512-p+gNRe4RPjpl1lTBUomFJ42P8ymArH/P93DFJ0iY873BJ4ZmogcKc6TbHgZQmtQMsy3jxcAo0HcTjidXwo8uKg==",
"dev": true
},
"@types/cors": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.4.tgz",
"integrity": "sha512-ipZjBVsm2tF/n8qFGOuGBkUij9X9ZswVi9G3bx/6dz7POpVa6gVHcj1wsX/LVEn9MMF41fxK/PnZPPoTD1UFPw==",
"dev": true,
"requires": {
"@types/express": "*"
}
},
"@types/cross-spawn": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.0.tgz",
@@ -2905,6 +2932,12 @@
"integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=",
"dev": true
},
"any-observable": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz",
"integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==",
"dev": true
},
"any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
@@ -3587,9 +3620,9 @@
}
},
"babel-jest": {
"version": "23.2.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-23.2.0.tgz",
"integrity": "sha1-FKnWo/QSLf6mBp03CFrfJqU6Tbo=",
"version": "23.6.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-23.6.0.tgz",
"integrity": "sha512-lqKGG6LYXYu+DQh/slrQ8nxXQkEkhugdXsU6St7GmhVS7Ilc/22ArwqXNJrf0QaOBjZB0360qZMwXqDYQHXaew==",
"dev": true,
"requires": {
"babel-plugin-istanbul": "^4.1.6",
@@ -5714,6 +5747,11 @@
"safe-json-stringify": "~1"
}
},
"bunyan-prettystream": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/bunyan-prettystream/-/bunyan-prettystream-0.1.3.tgz",
"integrity": "sha1-bDtxMmb2rTIAfHtqsemYokU0nZg="
},
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
@@ -6165,6 +6203,44 @@
"restore-cursor": "^2.0.0"
}
},
"cli-truncate": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz",
"integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=",
"dev": true,
"requires": {
"slice-ansi": "0.0.4",
"string-width": "^1.0.1"
},
"dependencies": {
"is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true,
"requires": {
"number-is-nan": "^1.0.0"
}
},
"slice-ansi": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz",
"integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=",
"dev": true
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
"strip-ansi": "^3.0.0"
}
}
}
},
"cli-width": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
@@ -6671,6 +6747,13 @@
"moment": "2.22.2",
"validator": "7.2.0",
"yargs-parser": "10.0.0"
},
"dependencies": {
"validator": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-7.2.0.tgz",
"integrity": "sha512-c8NGTUYeBEcUIGeMppmNVKHE7wwfm3mYbNZxV+c5mlv9fDHI7Ad3p07qfNrn/CvpdkK2k61fOLRO2sTEhgQXmg=="
}
}
},
"cookie": {
@@ -6787,6 +6870,15 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cors": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.4.tgz",
"integrity": "sha1-K9OB8usgECAQXNUOpZ2mMJBpRoY=",
"requires": {
"object-assign": "^4",
"vary": "^1"
}
},
"cosmiconfig": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-3.1.0.tgz",
@@ -7608,6 +7700,12 @@
"resolved": "https://registry.npmjs.org/dataloader/-/dataloader-1.4.0.tgz",
"integrity": "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw=="
},
"date-fns": {
"version": "1.29.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.29.0.tgz",
"integrity": "sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw==",
"dev": true
},
"date-now": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
@@ -7690,6 +7788,12 @@
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
},
"dedent": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
"integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=",
"dev": true
},
"deep-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
@@ -9135,6 +9239,12 @@
"integrity": "sha1-dDi3b5K0G5GfP73TUPvQdX2s3fc=",
"dev": true
},
"elegant-spinner": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz",
"integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=",
"dev": true
},
"elliptic": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz",
@@ -9606,6 +9716,12 @@
"integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
"dev": true
},
"exit-hook": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz",
"integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=",
"dev": true
},
"expand-brackets": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
@@ -10158,6 +10274,12 @@
"integrity": "sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ=",
"dev": true
},
"find-parent-dir": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.0.tgz",
"integrity": "sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ=",
"dev": true
},
"find-pkg": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/find-pkg/-/find-pkg-1.0.0.tgz",
@@ -11092,6 +11214,12 @@
"integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=",
"dev": true
},
"get-own-enumerable-property-symbols": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-2.0.1.tgz",
"integrity": "sha512-TtY/sbOemiMKPRUDDanGCSgBYe7Mf0vbRsWnBZ+9yghpZ1MvcpSpuZFjHdEeY/LZjZy0vdLjS77L6HosisFiug==",
"dev": true
},
"get-port": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz",
@@ -11515,14 +11643,6 @@
}
}
},
"graphql-playground-middleware-express": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/graphql-playground-middleware-express/-/graphql-playground-middleware-express-1.7.2.tgz",
"integrity": "sha512-JvKsVOR/U5QguBtEvTt0ozQ49uh1C6cW8O1xR6krQpJZIxjLYqpgusLUddTiVkka6Q/A4/AXBohY85jPudxYDg==",
"requires": {
"graphql-playground-html": "1.6.0"
}
},
"graphql-redis-subscriptions": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/graphql-redis-subscriptions/-/graphql-redis-subscriptions-1.5.0.tgz",
@@ -12461,6 +12581,163 @@
"decamelize": "^1.0.0"
}
},
"husky": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/husky/-/husky-1.1.0.tgz",
"integrity": "sha512-jnUD0PK3xGLB5Jc3f3UEwl8qOZeLd0WiWABhVyHPS5R298HOccGZJMOMBSk3gFksAa1BeK9FQYYEfPNlqkfBxg==",
"dev": true,
"requires": {
"cosmiconfig": "^5.0.6",
"execa": "^0.9.0",
"find-up": "^3.0.0",
"get-stdin": "^6.0.0",
"is-ci": "^1.2.1",
"pkg-dir": "^3.0.0",
"please-upgrade-node": "^3.1.1",
"read-pkg": "^4.0.1",
"run-node": "^1.0.0",
"slash": "^2.0.0"
},
"dependencies": {
"ci-info": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz",
"integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==",
"dev": true
},
"cosmiconfig": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.0.6.tgz",
"integrity": "sha512-6DWfizHriCrFWURP1/qyhsiFvYdlJzbCzmtFWh744+KyWsJo5+kPzUZZaMRSSItoYc0pxFX7gEO7ZC1/gN/7AQ==",
"dev": true,
"requires": {
"is-directory": "^0.3.1",
"js-yaml": "^3.9.0",
"parse-json": "^4.0.0"
}
},
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
"dev": true,
"requires": {
"lru-cache": "^4.0.1",
"shebang-command": "^1.2.0",
"which": "^1.2.9"
}
},
"execa": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-0.9.0.tgz",
"integrity": "sha512-BbUMBiX4hqiHZUA5+JujIjNb6TyAlp2D5KLheMjMluwOuzcnylDL4AxZYLLn1n2AGB49eSWwyKvvEQoRpnAtmA==",
"dev": true,
"requires": {
"cross-spawn": "^5.0.1",
"get-stream": "^3.0.0",
"is-stream": "^1.1.0",
"npm-run-path": "^2.0.0",
"p-finally": "^1.0.0",
"signal-exit": "^3.0.0",
"strip-eof": "^1.0.0"
}
},
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"dev": true,
"requires": {
"locate-path": "^3.0.0"
}
},
"get-stdin": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz",
"integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==",
"dev": true
},
"is-ci": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz",
"integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==",
"dev": true,
"requires": {
"ci-info": "^1.5.0"
}
},
"locate-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"dev": true,
"requires": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
}
},
"p-limit": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz",
"integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==",
"dev": true,
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"dev": true,
"requires": {
"p-limit": "^2.0.0"
}
},
"p-try": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz",
"integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==",
"dev": true
},
"parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
"dev": true,
"requires": {
"error-ex": "^1.3.1",
"json-parse-better-errors": "^1.0.1"
}
},
"pkg-dir": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz",
"integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==",
"dev": true,
"requires": {
"find-up": "^3.0.0"
}
},
"read-pkg": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz",
"integrity": "sha1-ljYlN48+HE1IyFhytabsfV0JMjc=",
"dev": true,
"requires": {
"normalize-package-data": "^2.3.2",
"parse-json": "^4.0.0",
"pify": "^3.0.0"
}
},
"slash": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
"dev": true
}
}
},
"hyphenate-style-name": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz",
@@ -13129,6 +13406,15 @@
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
"dev": true
},
"is-observable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz",
"integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==",
"dev": true,
"requires": {
"symbol-observable": "^1.1.0"
}
},
"is-odd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz",
@@ -13223,6 +13509,12 @@
"has": "^1.0.1"
}
},
"is-regexp": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
"integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=",
"dev": true
},
"is-relative": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
@@ -15864,6 +16156,318 @@
"uc.micro": "^1.0.1"
}
},
"lint-staged": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-7.3.0.tgz",
"integrity": "sha512-AXk40M9DAiPi7f4tdJggwuKIViUplYtVj1os1MVEteW7qOkU50EOehayCfO9TsoGK24o/EsWb41yrEgfJDDjCw==",
"dev": true,
"requires": {
"chalk": "^2.3.1",
"commander": "^2.14.1",
"cosmiconfig": "^5.0.2",
"debug": "^3.1.0",
"dedent": "^0.7.0",
"execa": "^0.9.0",
"find-parent-dir": "^0.3.0",
"is-glob": "^4.0.0",
"is-windows": "^1.0.2",
"jest-validate": "^23.5.0",
"listr": "^0.14.1",
"lodash": "^4.17.5",
"log-symbols": "^2.2.0",
"micromatch": "^3.1.8",
"npm-which": "^3.0.1",
"p-map": "^1.1.1",
"path-is-inside": "^1.0.2",
"pify": "^3.0.0",
"please-upgrade-node": "^3.0.2",
"staged-git-files": "1.1.1",
"string-argv": "^0.0.2",
"stringify-object": "^3.2.2"
},
"dependencies": {
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"dev": true
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"cosmiconfig": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.0.6.tgz",
"integrity": "sha512-6DWfizHriCrFWURP1/qyhsiFvYdlJzbCzmtFWh744+KyWsJo5+kPzUZZaMRSSItoYc0pxFX7gEO7ZC1/gN/7AQ==",
"dev": true,
"requires": {
"is-directory": "^0.3.1",
"js-yaml": "^3.9.0",
"parse-json": "^4.0.0"
}
},
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
"dev": true,
"requires": {
"lru-cache": "^4.0.1",
"shebang-command": "^1.2.0",
"which": "^1.2.9"
}
},
"debug": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz",
"integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"execa": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-0.9.0.tgz",
"integrity": "sha512-BbUMBiX4hqiHZUA5+JujIjNb6TyAlp2D5KLheMjMluwOuzcnylDL4AxZYLLn1n2AGB49eSWwyKvvEQoRpnAtmA==",
"dev": true,
"requires": {
"cross-spawn": "^5.0.1",
"get-stream": "^3.0.0",
"is-stream": "^1.1.0",
"npm-run-path": "^2.0.0",
"p-finally": "^1.0.0",
"signal-exit": "^3.0.0",
"strip-eof": "^1.0.0"
}
},
"jest-validate": {
"version": "23.6.0",
"resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.6.0.tgz",
"integrity": "sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A==",
"dev": true,
"requires": {
"chalk": "^2.0.1",
"jest-get-type": "^22.1.0",
"leven": "^2.1.0",
"pretty-format": "^23.6.0"
}
},
"parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
"dev": true,
"requires": {
"error-ex": "^1.3.1",
"json-parse-better-errors": "^1.0.1"
}
},
"pretty-format": {
"version": "23.6.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz",
"integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==",
"dev": true,
"requires": {
"ansi-regex": "^3.0.0",
"ansi-styles": "^3.2.0"
}
}
}
},
"listr": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/listr/-/listr-0.14.2.tgz",
"integrity": "sha512-vmaNJ1KlGuGWShHI35X/F8r9xxS0VTHh9GejVXwSN20fG5xpq3Jh4bJbnumoT6q5EDM/8/YP1z3YMtQbFmhuXw==",
"dev": true,
"requires": {
"@samverschueren/stream-to-observable": "^0.3.0",
"is-observable": "^1.1.0",
"is-promise": "^2.1.0",
"is-stream": "^1.1.0",
"listr-silent-renderer": "^1.1.1",
"listr-update-renderer": "^0.4.0",
"listr-verbose-renderer": "^0.4.0",
"p-map": "^1.1.1",
"rxjs": "^6.1.0"
},
"dependencies": {
"rxjs": {
"version": "6.3.3",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz",
"integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==",
"dev": true,
"requires": {
"tslib": "^1.9.0"
}
}
}
},
"listr-silent-renderer": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz",
"integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=",
"dev": true
},
"listr-update-renderer": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.4.0.tgz",
"integrity": "sha1-NE2YDaLKLosUW6MFkI8yrj9MyKc=",
"dev": true,
"requires": {
"chalk": "^1.1.3",
"cli-truncate": "^0.2.1",
"elegant-spinner": "^1.0.1",
"figures": "^1.7.0",
"indent-string": "^3.0.0",
"log-symbols": "^1.0.2",
"log-update": "^1.0.2",
"strip-ansi": "^3.0.1"
},
"dependencies": {
"ansi-escapes": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz",
"integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=",
"dev": true
},
"chalk": {
"version": "1.1.3",
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true,
"requires": {
"ansi-styles": "^2.2.1",
"escape-string-regexp": "^1.0.2",
"has-ansi": "^2.0.0",
"strip-ansi": "^3.0.0",
"supports-color": "^2.0.0"
}
},
"cli-cursor": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz",
"integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=",
"dev": true,
"requires": {
"restore-cursor": "^1.0.1"
}
},
"figures": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
"integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=",
"dev": true,
"requires": {
"escape-string-regexp": "^1.0.5",
"object-assign": "^4.1.0"
}
},
"log-symbols": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz",
"integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=",
"dev": true,
"requires": {
"chalk": "^1.0.0"
}
},
"log-update": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-1.0.2.tgz",
"integrity": "sha1-GZKfZMQJPS0ucHWh2tivWcKWuNE=",
"dev": true,
"requires": {
"ansi-escapes": "^1.0.0",
"cli-cursor": "^1.0.2"
}
},
"onetime": {
"version": "1.1.0",
"resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
"dev": true
},
"restore-cursor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz",
"integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=",
"dev": true,
"requires": {
"exit-hook": "^1.0.0",
"onetime": "^1.0.0"
}
}
}
},
"listr-verbose-renderer": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz",
"integrity": "sha1-ggb0z21S3cWCfl/RSYng6WWTOjU=",
"dev": true,
"requires": {
"chalk": "^1.1.3",
"cli-cursor": "^1.0.2",
"date-fns": "^1.27.2",
"figures": "^1.7.0"
},
"dependencies": {
"chalk": {
"version": "1.1.3",
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true,
"requires": {
"ansi-styles": "^2.2.1",
"escape-string-regexp": "^1.0.2",
"has-ansi": "^2.0.0",
"strip-ansi": "^3.0.0",
"supports-color": "^2.0.0"
}
},
"cli-cursor": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz",
"integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=",
"dev": true,
"requires": {
"restore-cursor": "^1.0.1"
}
},
"figures": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
"integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=",
"dev": true,
"requires": {
"escape-string-regexp": "^1.0.5",
"object-assign": "^4.1.0"
}
},
"onetime": {
"version": "1.1.0",
"resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
"dev": true
},
"restore-cursor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz",
"integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=",
"dev": true,
"requires": {
"exit-hook": "^1.0.0",
"onetime": "^1.0.0"
}
}
}
},
"load-cfg": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/load-cfg/-/load-cfg-0.5.6.tgz",
@@ -17322,6 +17926,15 @@
"once": "^1.3.2"
}
},
"npm-path": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/npm-path/-/npm-path-2.0.4.tgz",
"integrity": "sha512-IFsj0R9C7ZdR5cP+ET342q77uSRdtWOlWpih5eC+lu29tIDbNEgDbzgVJ5UFvYHWhxDZ5TFkJafFioO0pPQjCw==",
"dev": true,
"requires": {
"which": "^1.2.10"
}
},
"npm-run-all": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.3.tgz",
@@ -17372,6 +17985,17 @@
"path-key": "^2.0.0"
}
},
"npm-which": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/npm-which/-/npm-which-3.0.1.tgz",
"integrity": "sha1-kiXybsOihcIJyuZ8OxGmtKtxQKo=",
"dev": true,
"requires": {
"commander": "^2.9.0",
"npm-path": "^2.0.2",
"which": "^1.2.10"
}
},
"nth-check": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz",
@@ -18210,6 +18834,15 @@
"find-up": "^2.1.0"
}
},
"please-upgrade-node": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.1.1.tgz",
"integrity": "sha512-KY1uHnQ2NlQHqIJQpnh/i54rKkuxCEBx+voJIS/Mvb+L2iYd2NMotwduhKTMjfC1uKoX3VXOxLjIYG66dfJTVQ==",
"dev": true,
"requires": {
"semver-compare": "^1.0.0"
}
},
"plugin-error": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz",
@@ -21362,6 +21995,11 @@
"relay-runtime": "1.7.0-rc.1"
}
},
"react-relay-network-modern": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/react-relay-network-modern/-/react-relay-network-modern-2.4.0.tgz",
"integrity": "sha512-LR/RhHcJclDCVEiwRhlRtf1iltSnbGSxS2rag+bAljMFJ0kOVSYUK3+NDPRbcHLRqbha1FuQXBVfHjjPE6jhMA=="
},
"react-responsive": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-5.0.0.tgz",
@@ -22527,6 +23165,12 @@
"is-promise": "^2.1.0"
}
},
"run-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz",
"integrity": "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==",
"dev": true
},
"run-queue": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz",
@@ -22664,6 +23308,12 @@
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
},
"semver-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
"dev": true
},
"semver-diff": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz",
@@ -23293,6 +23943,12 @@
"integrity": "sha512-to7oADIniaYwS3MhtCa/sQhrxidCCQiF/qp4/m5iN3ipf0Y7Xlri0f6eG29r08aL7JYl8n32AF3Q5GYBZ7K8vw==",
"dev": true
},
"staged-git-files": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/staged-git-files/-/staged-git-files-1.1.1.tgz",
"integrity": "sha512-H89UNKr1rQJvI1c/PIR3kiAMBV23yvR7LItZiV74HWZwzt7f3YHuujJ9nJZlt58WlFox7XQsOahexwk7nTe69A==",
"dev": true
},
"state-toggle": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.1.tgz",
@@ -23397,6 +24053,12 @@
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
},
"string-argv": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.0.2.tgz",
"integrity": "sha1-2sMECGkMIfPDYwo/86BYd73L1zY=",
"dev": true
},
"string-length": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz",
@@ -23482,6 +24144,17 @@
"is-hexadecimal": "^1.0.0"
}
},
"stringify-object": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.2.2.tgz",
"integrity": "sha512-O696NF21oLiDy8PhpWu8AEqoZHw++QW6mUv0UvKZe8gWSdSvMXkiLufK7OmnP27Dro4GU5kb9U7JIO0mBuCRQg==",
"dev": true,
"requires": {
"get-own-enumerable-property-symbols": "^2.0.1",
"is-obj": "^1.0.1",
"is-regexp": "^1.0.0"
}
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
@@ -25305,11 +25978,6 @@
"spdx-expression-parse": "^3.0.0"
}
},
"validator": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-7.2.0.tgz",
"integrity": "sha512-c8NGTUYeBEcUIGeMppmNVKHE7wwfm3mYbNZxV+c5mlv9fDHI7Ad3p07qfNrn/CvpdkK2k61fOLRO2sTEhgQXmg=="
},
"value-equal": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz",
+33 -2
View File
@@ -1,6 +1,18 @@
{
"name": "@coralproject/talk",
"version": "5.0.0",
"author": "The Coral Project",
"homepage": "https://coralproject.net/",
"repository": {
"type": "git",
"url": "git://github.com/coralproject/talk.git"
},
"engines": {
"node": ">=8.9.0",
"npm": ">=6.4.1"
},
"bugs": "https://github.com/coralproject/talk/issues",
"contributors": "https://github.com/coralproject/talk/graphs/contributors",
"description": "A better commenting experience from Mozilla, The Washington Post, and The New York Times.",
"scripts": {
"build": "npm-run-all compile --parallel build:*",
@@ -10,6 +22,7 @@
"compile:css-types": "tcm src/core/client/",
"compile:relay-stream": "ts-node ./scripts/compileRelay --src ./src/core/client/stream --schema tenant",
"compile:relay-auth": "ts-node ./scripts/compileRelay --src ./src/core/client/auth --schema tenant",
"compile:relay-install": "ts-node ./scripts/compileRelay --src ./src/core/client/install --schema tenant",
"compile:relay-admin": "ts-node ./scripts/compileRelay --src ./src/core/client/admin --schema tenant",
"compile:schema": "node ./scripts/generateSchemaTypes.js",
"docz": "docz",
@@ -30,7 +43,6 @@
"tscheck:scripts": "tsc --project ./tsconfig.json --noEmit",
"watch": "NODE_ENV=development ts-node ./scripts/watcher/bin/watcher.ts --config ./config/watcher.ts"
},
"author": "",
"license": "Apache-2.0",
"dependencies": {
"akismet-api": "^4.2.0",
@@ -38,9 +50,11 @@
"bcryptjs": "^2.4.3",
"bull": "^3.4.4",
"bunyan": "^1.8.12",
"bunyan-prettystream": "^0.1.3",
"cheerio": "^1.0.0-rc.2",
"consolidate": "0.14.0",
"convict": "^4.3.1",
"cors": "^2.8.4",
"dataloader": "^1.4.0",
"dotenv": "^6.0.0",
"dotenv-expand": "^4.2.0",
@@ -53,7 +67,7 @@
"graphql": "^0.13.2",
"graphql-config": "^2.0.1",
"graphql-fields": "^1.1.0",
"graphql-playground-middleware-express": "^1.7.2",
"graphql-playground-html": "^1.6.0",
"graphql-redis-subscriptions": "^1.5.0",
"graphql-tools": "^3.0.5",
"html-to-text": "^4.0.0",
@@ -80,6 +94,7 @@
"passport-strategy": "^1.0.0",
"performance-now": "^2.1.0",
"permit": "^0.2.4",
"react-relay-network-modern": "^2.4.0",
"striptags": "^3.1.1",
"subscriptions-transport-ws": "^0.9.12",
"tlds": "^1.203.1",
@@ -96,6 +111,7 @@
"@types/bcryptjs": "^2.4.1",
"@types/bull": "^3.3.16",
"@types/bunyan": "^1.8.4",
"@types/bunyan-prettystream": "^0.1.31",
"@types/case-sensitive-paths-webpack-plugin": "^2.1.2",
"@types/cheerio": "^0.22.8",
"@types/chokidar": "^1.7.5",
@@ -104,6 +120,7 @@
"@types/compression-webpack-plugin": "^0.4.2",
"@types/consolidate": "0.0.34",
"@types/convict": "^4.2.0",
"@types/cors": "^2.8.4",
"@types/cross-spawn": "^6.0.0",
"@types/dompurify": "0.0.31",
"@types/dotenv": "^4.0.3",
@@ -154,6 +171,7 @@
"@types/ws": "^5.1.2",
"autoprefixer": "^8.6.5",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^23.6.0",
"babel-loader": "^8.0.0-beta",
"babel-plugin-module-resolver": "^3.1.1",
"babel-plugin-relay": "^1.7.0-rc.1",
@@ -189,11 +207,13 @@
"gulp-sourcemaps": "^2.6.4",
"gulp-typescript": "^5.0.0-alpha.3",
"html-webpack-plugin": "^3.2.0",
"husky": "^1.1.0",
"jest": "^23.4.1",
"jest-junit": "^5.1.0",
"jest-localstorage-mock": "^2.2.0",
"jest-mock-console": "^0.4.0",
"jsdom": "^11.11.0",
"lint-staged": "^7.3.0",
"loader-utils": "^1.1.0",
"material-design-icons": "^3.0.1",
"mini-css-extract-plugin": "^0.4.1",
@@ -255,5 +275,16 @@
"webpack-hot-client": "^4.1.1",
"webpack-manifest-plugin": "^2.0.3",
"whatwg-fetch": "^2.0.4"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,ts,tsx}": [
"tslint --fix",
"git add"
]
}
}
+51
View File
@@ -9,6 +9,7 @@ import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
import UglifyJsPlugin from "uglifyjs-webpack-plugin";
import webpack, { Configuration } from "webpack";
import ManifestPlugin from "webpack-manifest-plugin";
import PublicURIWebpackPlugin from "./plugins/PublicURIWebpackPlugin";
import paths from "./paths";
@@ -39,6 +40,13 @@ export default function createWebpackConfig({
const isProduction = env.NODE_ENV === "production";
/**
* ifProduction will only include the nodes if we're in production mode.
*/
const ifProduction = isProduction
? <T extends {}>(...nodes: T[]) => nodes
: <T extends {}>(...nodes: T[]) => [];
const htmlWebpackConfig: Options = {
minify: isProduction && {
removeComments: true,
@@ -255,6 +263,22 @@ export default function createWebpackConfig({
},
],
},
{
test: paths.appInstallLocalesTemplate,
use: [
// This is the locales loader that loads available locales
// from a particular target.
{
loader: "locales-loader",
options: {
...localesOptions,
// Target specifies the prefix for fluent files to be loaded.
// ${target}-xyz.ftl and ${†arget}.ftl are loaded into the locales.
target: "install",
},
},
],
},
// Loader for our fluent files.
{
test: /\.ftl$/,
@@ -422,19 +446,29 @@ export default function createWebpackConfig({
stream: [
// We ship polyfills by default
paths.appPolyfill,
...ifProduction(paths.appPublicPath),
...devServerEntries,
paths.appStreamIndex,
],
auth: [
// We ship polyfills by default
paths.appPolyfill,
...ifProduction(paths.appPublicPath),
...devServerEntries,
paths.appAuthIndex,
// Remove deactivated entries.
],
install: [
// We ship polyfills by default
paths.appPolyfill,
...ifProduction(paths.appPublicPath),
...devServerEntries,
paths.appInstallIndex,
],
admin: [
// We ship polyfills by default
paths.appPolyfill,
...ifProduction(paths.appPublicPath),
...devServerEntries,
paths.appAdminIndex,
],
@@ -457,6 +491,14 @@ export default function createWebpackConfig({
inject: "body",
...htmlWebpackConfig,
}),
// Generates an `install.html` file with the <script> injected.
new HtmlWebpackPlugin({
filename: "install.html",
template: paths.appInstallHTML,
chunks: ["install"],
inject: "body",
...htmlWebpackConfig,
}),
// Generates an `admin.html` file with the <script> injected.
new HtmlWebpackPlugin({
filename: "admin.html",
@@ -465,6 +507,15 @@ export default function createWebpackConfig({
inject: "body",
...htmlWebpackConfig,
}),
...ifProduction(
// Inject the pieces we need here to resolve all the now relative url's
// against the CDN if it's provided. It will inject the following into
// the configuration blob on the page.
new PublicURIWebpackPlugin(
"{{ staticURI | dump | safe }}",
"{{ staticURI }}"
)
),
// Makes some environment variables available in index.html.
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
+5
View File
@@ -17,6 +17,7 @@ export default {
appSrc: resolveSrc("."),
appTsconfig: resolveSrc("core/client/tsconfig.json"),
appPolyfill: resolveSrc("core/build/polyfills.js"),
appPublicPath: resolveSrc("core/build/publicPath.js"),
appLocales: resolveSrc("locales"),
appThemeVariables: resolveSrc("core/client/ui/theme/variables.ts"),
appThemeVariablesCSS: resolveSrc("core/client/ui/theme/variables.css"),
@@ -29,6 +30,10 @@ export default {
appAuthLocalesTemplate: resolveSrc("core/client/auth/locales.ts"),
appAuthIndex: resolveSrc("core/client/auth/index.tsx"),
appInstallHTML: resolveSrc("core/client/install/index.html"),
appInstallLocalesTemplate: resolveSrc("core/client/install/locales.ts"),
appInstallIndex: resolveSrc("core/client/install/index.tsx"),
appAdminHTML: resolveSrc("core/client/admin/index.html"),
appAdminLocalesTemplate: resolveSrc("core/client/admin/locales.ts"),
appAdminIndex: resolveSrc("core/client/admin/index.tsx"),
@@ -0,0 +1,60 @@
import { Hooks } from "html-webpack-plugin";
import { Compiler, Plugin } from "webpack";
export default class PublicURIWebpackPlugin implements Plugin {
private configTemplate: string;
private prefixTemplate: string;
constructor(configTemplate: string, prefixTemplate: string) {
this.configTemplate = configTemplate;
this.prefixTemplate = prefixTemplate;
}
private prefixAttribute(attr: string | boolean) {
if (!attr || typeof attr !== "string" || !attr.startsWith("/")) {
return attr;
}
return this.prefixTemplate + attr;
}
private prefixTag = (tag: {
tagName: string;
attributes: Record<string, string | boolean>;
}) => {
switch (tag.tagName) {
case "link":
tag.attributes.href = this.prefixAttribute(tag.attributes.href);
break;
case "script":
tag.attributes.src = this.prefixAttribute(tag.attributes.src);
break;
}
};
public apply = (compiler: Compiler) => {
compiler.hooks.compilation.tap("CDNWebpackPlugin", compilation => {
(compilation.hooks as Hooks).htmlWebpackPluginAlterAssetTags.tapAsync(
"CDNWebpackPlugin",
(htmlPluginData, cb) => {
// Prefix all the asset's url's with the template.
htmlPluginData.head.forEach(this.prefixTag);
htmlPluginData.body.forEach(this.prefixTag);
// Insert the public path reference.
htmlPluginData.body.unshift({
tagName: "script",
attributes: {
type: "application/json",
id: "config",
},
innerHTML: this.configTemplate,
voidTag: false,
});
return cb(null, htmlPluginData);
}
);
});
};
}
+3
View File
@@ -0,0 +1,3 @@
__webpack_public_path__ = JSON.parse(
document.getElementById("config").innerText
);
+9 -2
View File
@@ -15,7 +15,13 @@ import PymControl, {
defaultPymControlFactory,
PymControlFactory,
} from "./PymControl";
import { ensureEndSlash } from "./utils";
import { ensureNoEndSlash } from "./utils";
// This is importing the url helper from the framework using a relative path
// import because the ts paths are not configured to use the framework for this
// target.
// TODO: (wyattjoh) replace with import from framework when we include it in the config.
import urls from "../framework/helpers/urls";
export interface StreamEmbedConfig {
assetID?: string;
@@ -110,7 +116,8 @@ export class StreamEmbed {
assetURL: this.config.assetURL,
commentID: this.config.commentID,
});
const url = `${ensureEndSlash(this.config.rootURL)}stream.html${
const url = `${ensureNoEndSlash(this.config.rootURL)}${urls.embed.stream}${
query ? `?${query}` : ""
}`;
this.pymControl = this.pymControlFactory({
@@ -4,7 +4,7 @@ exports[`should pass correct values to pymControl 1`] = `
Object {
"id": "container-id",
"title": "StreamEmbed",
"url": "http://localhost/stream.html?assetID=asset-id&assetURL=asset-url&commentID=comment-id",
"url": "http://localhost/embed/stream?assetID=asset-id&assetURL=asset-url&commentID=comment-id",
}
`;
@@ -12,6 +12,6 @@ exports[`should pass default values to pymControl 1`] = `
Object {
"id": "container-id",
"title": "StreamEmbed",
"url": "http://localhost/stream.html",
"url": "http://localhost/embed/stream",
}
`;
@@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Basic integration test should render iframe 1`] = `"<iframe src=\\"http://localhost/stream.html?assetURL=http%3A%2F%2Flocalhost%2F&amp;initialWidth=0&amp;childId=basic-integration-test-id&amp;parentTitle=&amp;parentUrl=http%3A%2F%2Flocalhost%2F\\" width=\\"100%\\" scrolling=\\"no\\" marginheight=\\"0\\" frameborder=\\"0\\" title=\\"Talk Embed Stream\\" id=\\"basic-integration-test-id_iframe\\" name=\\"basic-integration-test-id_iframe\\" style=\\"width: 1px; min-width: 100%;\\"></iframe>"`;
exports[`Basic integration test should render iframe 1`] = `"<iframe src=\\"http://localhost/embed/stream?assetURL=http%3A%2F%2Flocalhost%2F&amp;initialWidth=0&amp;childId=basic-integration-test-id&amp;parentTitle=&amp;parentUrl=http%3A%2F%2Flocalhost%2F\\" width=\\"100%\\" scrolling=\\"no\\" marginheight=\\"0\\" frameborder=\\"0\\" title=\\"Talk Embed Stream\\" id=\\"basic-integration-test-id_iframe\\" name=\\"basic-integration-test-id_iframe\\" style=\\"width: 1px; min-width: 100%;\\"></iframe>"`;
exports[`Basic integration test should use canonical link 1`] = `"<iframe src=\\"http://localhost/stream.html?assetURL=http%3A%2F%2Flocalhost%2Fcanonical&amp;initialWidth=0&amp;childId=basic-integration-test-id&amp;parentTitle=&amp;parentUrl=http%3A%2F%2Flocalhost%2F\\" width=\\"100%\\" scrolling=\\"no\\" marginheight=\\"0\\" frameborder=\\"0\\" title=\\"Talk Embed Stream\\" name=\\"basic-integration-test-id_iframe\\" style=\\"width: 1px; min-width: 100%;\\"></iframe>"`;
exports[`Basic integration test should use canonical link 1`] = `"<iframe src=\\"http://localhost/embed/stream?assetURL=http%3A%2F%2Flocalhost%2Fcanonical&amp;initialWidth=0&amp;childId=basic-integration-test-id&amp;parentTitle=&amp;parentUrl=http%3A%2F%2Flocalhost%2F\\" width=\\"100%\\" scrolling=\\"no\\" marginheight=\\"0\\" frameborder=\\"0\\" title=\\"Talk Embed Stream\\" name=\\"basic-integration-test-id_iframe\\" style=\\"width: 1px; min-width: 100%;\\"></iframe>"`;
+1 -1
View File
@@ -2,7 +2,7 @@
"extends": "../tsconfig.json",
"compilerOptions": {
"lib": ["dom", "es5"],
"types": ["jest"],
"types": ["jest", "node"],
"paths": {}
},
"include": [
@@ -0,0 +1,11 @@
import ensureNoEndSlash from "./ensureNoEndSlash";
it("should remove the slash from the end", () => {
const path = ensureNoEndSlash("/test/");
expect(path).toBe("/test");
});
it("should not change a string if there is no ending slash", () => {
const path = ensureNoEndSlash("/test");
expect(path).toBe("/test");
});
@@ -0,0 +1,3 @@
export default function ensureEndSlash(p: string) {
return p.replace(/\/$/, "");
}
+1
View File
@@ -1,5 +1,6 @@
export { default as buildURL } from "./buildURL";
export { default as ensureEndSlash } from "./ensureEndSlash";
export { default as ensureNoEndSlash } from "./ensureNoEndSlash";
export { default as startsWith } from "./startsWith";
export { default as prefixStorage } from "./prefixStorage";
export { default as parseHashQuery } from "./parseHashQuery";
@@ -1,2 +1,3 @@
export { default as getMe } from "./getMe";
export { default as getURLWithCommentID } from "./getURLWithCommentID";
export { default as urls } from "./urls";
@@ -0,0 +1,15 @@
export default (process.env.NODE_ENV !== "development"
? {
admin: "/admin",
embed: {
stream: "/embed/stream",
auth: "/embed/auth",
},
}
: {
admin: "/admin.html",
embed: {
stream: "/stream.html",
auth: "/auth.html",
},
});
@@ -4,7 +4,7 @@ import { noop } from "lodash";
import { Child as PymChild } from "pym.js";
import React, { Component, ComponentType } from "react";
import { Formatter } from "react-timeago";
import { Environment, Network, RecordSource, Store } from "relay-runtime";
import { Environment, RecordSource, Store } from "relay-runtime";
import uuid from "uuid/v4";
import { getBrowserInfo } from "talk-framework/lib/browserInfo";
@@ -21,7 +21,7 @@ import { RestClient } from "talk-framework/lib/rest";
import { ClickFarAwayRegister } from "talk-ui/components/ClickOutside";
import { generateBundles, LocalesData, negotiateLanguages } from "../i18n";
import { createFetch, TokenGetter } from "../network";
import { createNetwork, TokenGetter } from "../network";
import { PostMessageService } from "../postMessage";
import { TalkContext, TalkContextProvider } from "./TalkContext";
@@ -97,7 +97,7 @@ function createRelayEnvironment() {
return "";
};
const environment = new Environment({
network: Network.create(createFetch(tokenGetter)),
network: createNetwork(tokenGetter),
store: new Store(source),
});
return { environment, tokenGetter };
@@ -1,25 +0,0 @@
export interface GraphQLErrorItem {
message: string;
locations: Array<{
line: number;
column: number;
}>;
}
/**
* Graphql wraps graphql errors at the network layer.
*/
export default class GraphQLError extends Error {
// Original error.
public readonly origin: GraphQLErrorItem[];
constructor(origin: GraphQLErrorItem[]) {
super(origin.map(o => o.message).join(" "));
// Maintains proper stack trace for where our error was thrown.
if (Error.captureStackTrace) {
Error.captureStackTrace(this, GraphQLError);
}
this.origin = origin;
}
}
@@ -1,6 +1,2 @@
export { default as NetworkError } from "./networkError";
export { default as UnknownServerError } from "./unknownServerError";
export { default as BadUserInputError } from "./badUserInputError";
export { default as GraphQLError } from "./graphqlError";
export * from "./graphqlError";
@@ -1,18 +0,0 @@
/**
* NetworkError wraps errors at the network layer.
*/
export default class NetworkError extends Error {
// Original error.
public readonly origin: Error;
constructor(origin: Error) {
// Pass remaining arguments (including vendor specific ones) to parent constructor.
super(origin.message);
// Maintains proper stack trace for where our error was thrown.
if (Error.captureStackTrace) {
Error.captureStackTrace(this, NetworkError);
}
this.origin = origin;
}
}
@@ -0,0 +1,10 @@
import { FluentNumber, FluentType } from "fluent/compat";
import { FluentShortNumber } from "../types";
export default function SHORT_NUMBER([t]: [FluentType]) {
if (!(t instanceof FluentNumber)) {
throw new Error(`Invalid argument for SHORT_NUMBER ${t.valueOf()}`);
}
return new FluentShortNumber(t.valueOf());
}
@@ -0,0 +1 @@
export { default as SHORT_NUMBER } from "./SHORT_NUMBER";
@@ -1,50 +1,8 @@
import "fluent-intl-polyfill/compat";
import { negotiateLanguages as negotiate } from "fluent-langneg/compat";
import { FluentBundle } from "fluent/compat";
export interface BundledLocales {
[locale: string]: string;
}
export interface LoadableLocales {
[locale: string]: (() => Promise<string>);
}
/**
* This type describes the shape of the generated code from our `locales-loader`.
* Please check `./src/loaders` and the webpack config for more information.
*/
export interface LocalesData {
readonly defaultLocale: string;
readonly fallbackLocale: string;
readonly availableLocales: ReadonlyArray<string>;
readonly bundled: BundledLocales;
readonly loadables: LoadableLocales;
}
/**
* negotiateLanguages accepts `userLocales` which usually comes from
* `navigator.languages` and the locales `data` as generated by
* the `locales-loader` and returns an array of matching languages.
*/
export function negotiateLanguages(
userLocales: ReadonlyArray<string>,
data: LocalesData
) {
// Choose locale that is best for the user.
const languages = negotiate(userLocales, data.availableLocales, {
defaultLocale: data.defaultLocale,
strategy: "lookup",
});
if (data.fallbackLocale && languages[0] !== data.fallbackLocale) {
// Use default locale as fallback in case we have
// missing keys.
languages.push(data.fallbackLocale);
}
return languages;
}
import * as functions from "./functions";
import { LocalesData } from "./locales";
// Don't warn in production.
let decorateWarnMissing = (bundle: FluentBundle) => bundle;
@@ -81,14 +39,14 @@ if (process.env.NODE_ENV !== "production") {
*
* Use it in conjunction with `negotiateLanguages`.
*/
export async function generateBundles(
export default async function generateBundles(
locales: ReadonlyArray<string>,
data: LocalesData
): Promise<FluentBundle[]> {
const promises = [];
for (const locale of locales) {
const bundle = new FluentBundle(locale);
const bundle = new FluentBundle(locale, { functions });
if (locale in data.bundled) {
bundle.addMessages(data.bundled[locale]);
promises.push(decorateWarnMissing(bundle));
@@ -0,0 +1,3 @@
export { default as generateBundles } from "./generateBundles";
export { default as negotiateLanguages } from "./negotiateLanguages";
export { BundledLocales, LoadableLocales, LocalesData } from "./locales";
@@ -0,0 +1,19 @@
export interface BundledLocales {
[locale: string]: string;
}
export interface LoadableLocales {
[locale: string]: (() => Promise<string>);
}
/**
* This type describes the shape of the generated code from our `locales-loader`.
* Please check `./src/loaders` and the webpack config for more information.
*/
export interface LocalesData {
readonly defaultLocale: string;
readonly fallbackLocale: string;
readonly availableLocales: ReadonlyArray<string>;
readonly bundled: BundledLocales;
readonly loadables: LoadableLocales;
}
@@ -0,0 +1,27 @@
import { negotiateLanguages as negotiate } from "fluent-langneg/compat";
import { LocalesData } from "./locales";
/**
* negotiateLanguages accepts `userLocales` which usually comes from
* `navigator.languages` and the locales `data` as generated by
* the `locales-loader` and returns an array of matching languages.
*/
export default function negotiateLanguages(
userLocales: ReadonlyArray<string>,
data: LocalesData
) {
// Choose locale that is best for the user.
const languages = negotiate(userLocales, data.availableLocales, {
defaultLocale: data.defaultLocale,
strategy: "lookup",
});
if (data.fallbackLocale && languages[0] !== data.fallbackLocale) {
// Use default locale as fallback in case we have
// missing keys.
languages.push(data.fallbackLocale);
}
return languages;
}
@@ -0,0 +1,33 @@
import { toPairs } from "lodash";
import { getShortNumberCode, validateFormat } from "./FluentShortNumber";
describe("getShortNumberCode", () => {
it("returns correct value", () => {
const cases = {
123: "100",
4322: "1000",
33223: "10000",
};
toPairs(cases).forEach(([i, o]) => {
expect(getShortNumberCode(parseFloat(i))).toBe(o);
});
});
});
describe("validateFormat", () => {
it("returns correct value", () => {
const cases = {
"0k": true,
"0kilo": true,
"0.0": false,
"0": false,
"0.": false,
"0.0k": true,
"000.0k": true,
"000M": true,
};
toPairs(cases).forEach(([i, o]) => {
expect(validateFormat(i)).toBe(o);
});
});
});
@@ -0,0 +1,81 @@
import { FluentBundle, FluentNumber, FluentType } from "fluent/compat";
const formatRegExp = /^(0+|0+\.0+)[^\d\.]+$/;
export function validateFormat(fmt: string) {
return formatRegExp.test(fmt);
}
export function getShortNumberCode(n: number) {
let code = "1";
while (n >= 10) {
n /= 10;
code += "0";
}
return code;
}
function formatShortNumber(n: number, format: string, bundle: FluentBundle) {
const lastIndexOf0 = format.lastIndexOf("0");
const unit = format.substr(lastIndexOf0 + 1);
const rest = format.substr(0, lastIndexOf0 + 1);
const splitted = rest.split(".");
const digits = splitted[0].length;
const fractalDigits = (splitted.length > 1 && splitted[1].length) || 0;
const threshold = Math.pow(10, digits);
while (n > threshold) {
n /= 10;
}
const formattedNumber = new FluentNumber(n, {
maximumFractionDigits: fractalDigits,
}).toString(bundle);
return `${formattedNumber}${unit}`;
}
export default class FluentShortNumber extends FluentNumber {
constructor(value: any, opts?: any) {
super(value, opts);
}
public toString(bundle: FluentBundle) {
if (this.value < 1000) {
return super.toString(bundle);
}
const key = `framework-shortNumber-${getShortNumberCode(this.value)}`;
const fmt = bundle.getMessage(key);
// Handle message not found.
if (!fmt) {
const message = `Missing translation key for ${key} for languages ${bundle.locales.toString()}`;
if (process.env.NODE_ENV === "production") {
// tslint:disable-next-line:no-console
console.warn(message);
} else {
throw new Error(message);
}
return super.toString(bundle);
}
// Check for invalid message.
if (!validateFormat(fmt)) {
const message = `Invalid Short Number Format ${fmt}`;
if (process.env.NODE_ENV === "production") {
// tslint:disable-next-line:no-console
console.warn(message);
} else {
throw new Error(message);
}
return super.toString(bundle);
}
return formatShortNumber(this.value, fmt, bundle);
}
public match(bundle: FluentBundle, other: FluentType) {
if (other instanceof FluentShortNumber) {
return this.value === other.valueOf;
}
return false;
}
}
@@ -0,0 +1 @@
export { default as FluentShortNumber } from "./FluentShortNumber";
@@ -53,3 +53,9 @@ export const PASSWORDS_DO_NOT_MATCH = () => (
<span>Passwords do not match. Try again.</span>
</Localized>
);
export const INVALID_URL = () => (
<Localized id="framework-validation-invalidURL">
<span>Invalid URL</span>
</Localized>
);
@@ -0,0 +1,40 @@
import {
authMiddleware,
batchMiddleware,
cacheMiddleware,
RelayNetworkLayer,
retryMiddleware,
urlMiddleware,
} from "react-relay-network-modern/es";
import customErrorMiddleware from "./customErrorMiddleware";
export type TokenGetter = () => string;
const graphqlURL = "/api/tenant/graphql";
export default function createNetwork(tokenGetter: TokenGetter) {
return new RelayNetworkLayer([
customErrorMiddleware,
cacheMiddleware({
size: 100, // max 100 requests
ttl: 900000, // 15 minutes
}),
urlMiddleware({
url: req => Promise.resolve(graphqlURL),
}),
batchMiddleware({
batchUrl: (requestMap: any) => Promise.resolve(graphqlURL),
batchTimeout: 10,
}),
retryMiddleware({
fetchTimeout: 15000,
retryDelays: (attempt: number) => Math.pow(2, attempt + 4) * 100,
// or simple array [3200, 6400, 12800, 25600, 51200, 102400, 204800, 409600],
statusCodes: [500, 503, 504],
}),
authMiddleware({
token: tokenGetter,
}),
]);
}
@@ -0,0 +1,31 @@
import { Middleware } from "react-relay-network-modern/es";
import { BadUserInputError, UnknownServerError } from "../errors";
function getError(errors: Error[]): Error | null {
if (errors.length > 1 || !(errors[0] as any).extensions) {
// Multiple errors are GraphQL errors.
// TODO: (cvle) Is this assumption correct?
// No extensions == GraphQL error.
// TODO: (cvle) harmonize with server.
return null;
}
const err = errors[0];
if ((err as any).code === "BAD_USER_INPUT") {
return new BadUserInputError((err as any).extensions);
}
return new UnknownServerError(err.message, (err as any).extensions);
}
const customErrorMiddleware: Middleware = next => async req => {
const res = await next(req);
if (req.isMutation() && res.errors) {
// Extract custom error.
const error = getError(res.errors);
if (error) {
throw error;
}
}
return res;
};
export default customErrorMiddleware;
@@ -1,72 +0,0 @@
import { FetchFunction } from "relay-runtime";
import {
BadUserInputError,
GraphQLError,
NetworkError,
UnknownServerError,
} from "../errors";
// Normalize errors.
function getError(errors: Error[]): Error {
if (errors.length > 1) {
// Multiple errors are GraphQL errors.
// TODO: (cvle) Is this assumption correct?
return new GraphQLError(errors as any);
}
const err = errors[0] as Error;
if ((err as any).extensions) {
if ((err as any).code === "BAD_USER_INPUT") {
return new BadUserInputError((err as any).extensions);
}
return new UnknownServerError(err.message, (err as any).extensions);
}
// No extensions == GraphQL error.
// TODO: (cvle) harmonize with server.
return new GraphQLError(errors as any);
}
export type TokenGetter = () => string;
type CreateFetch = (token?: TokenGetter) => FetchFunction;
/**
* createFetch returns a simple implementation of the `FetchFunction`
* required by Relay. It'll return a `NetworkError` on failure.
*/
const createFetch: CreateFetch = tokenGetter => async (
operation,
variables
) => {
const token = tokenGetter && tokenGetter();
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
try {
const response = await fetch("/api/tenant/graphql", {
method: "POST",
headers,
body: JSON.stringify({
query: operation.text,
variables,
}),
});
if (response.status >= 500) {
throw new Error(`${response.status} ${response.statusText}`);
}
const data = await response.json();
if (data.errors) {
throw getError(data.errors);
}
return data;
} catch (err) {
if (err instanceof TypeError) {
throw new NetworkError(err);
}
throw err;
}
};
export default createFetch;
@@ -1 +1 @@
export { default as createFetch, TokenGetter } from "./fetchQuery";
export { default as createNetwork, TokenGetter } from "./createNetwork";
@@ -2,6 +2,7 @@ import { ReactNode } from "react";
import {
INVALID_CHARACTERS,
INVALID_EMAIL,
INVALID_URL,
PASSWORD_TOO_SHORT,
PASSWORDS_DO_NOT_MATCH,
USERNAME_TOO_LONG,
@@ -56,6 +57,17 @@ export const validateUsernameCharacters = createValidator(
INVALID_CHARACTERS()
);
/**
* validateURL is a Validator that checks that the URL only contains valid characters.
*/
export const validateURL = createValidator(
v =>
/^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/.test(
v
),
INVALID_URL()
);
/**
* validateUsernameMinLength is a Validator that checks that the username has a min length of characters
*/
+1
View File
@@ -1,3 +1,4 @@
export { default as signIn, SignInInput } from "./signIn";
export { default as signUp, SignUpInput } from "./signUp";
export { default as signOut } from "./signOut";
export { default as install, InstallInput } from "./install";
+22
View File
@@ -0,0 +1,22 @@
import { RestClient } from "../lib/rest";
export interface InstallInput {
tenant: {
organizationName: string;
organizationContactEmail: string;
organizationURL: string;
domains: string[];
};
user: {
username: string;
password: string;
email: string;
};
}
export default function install(rest: RestClient, input: InstallInput) {
return rest.fetch("/tenant/install", {
method: "POST",
body: input,
});
}
@@ -1,4 +1,8 @@
import "fluent-intl-polyfill/compat";
import { FluentBundle } from "fluent/compat";
import * as functions from "talk-framework/lib/i18n/functions";
import fs from "fs";
import path from "path";
@@ -34,7 +38,7 @@ function createFluentBundle(
target: string,
pathToLocale: string
): FluentBundle {
const bundle = new FluentBundle("en-US");
const bundle = new FluentBundle("en-US", { functions });
const files = fs.readdirSync(pathToLocale);
const prefixes = commonPrefixes.concat(target);
files.forEach(f => {
@@ -0,0 +1,48 @@
export function denormalizeComment(comment: any, parents: any[] = []) {
const replyNodes =
(comment.replies &&
comment.replies.edges.map((edge: any) =>
denormalizeComment(edge, [...parents, comment])
)) ||
[];
const repliesPageInfo = (comment.replies && comment.replies.pageInfo) || {
endCursor: null,
hasNextPage: false,
};
return {
...comment,
replies: { edges: replyNodes, pageInfo: repliesPageInfo },
replyCount: replyNodes.length,
parentCount: parents.length,
parents: {
edges: parents,
pageInfo: { startCursor: null, hasPreviousPage: false },
},
};
}
export function denormalizeComments(commentList: any[]) {
return commentList.map(c => denormalizeComment(c));
}
export function denormalizeAsset(asset: any) {
const commentNodes =
(asset.comments &&
asset.comments.edges.map((edge: any) => denormalizeComment(edge))) ||
[];
const commentsPageInfo = (asset.comments && asset.comments.pageInfo) || {
endCursor: null,
hasNextPage: false,
};
return {
...asset,
comments: { edges: commentNodes, pageInfo: commentsPageInfo },
commentCounts: {
totalVisible: commentNodes.length,
},
};
}
export function denormalizeAssets(assetList: any[]) {
return assetList.map(a => denormalizeAsset(a));
}
@@ -9,3 +9,4 @@ export {
NoFragmentRefs,
} from "./removeFragmentRefs";
export { default as createUUIDGenerator } from "./createUUIDGenerator";
export * from "./denormalize";
+10
View File
@@ -0,0 +1,10 @@
const path = require("path");
module.exports = {
extends: "../.babelrc.js",
plugins: [
[
"babel-plugin-relay",
{ artifactDirectory: path.resolve(__dirname, "./__generated__") },
],
],
};
@@ -0,0 +1,25 @@
/* Here we add global stylings for body and document */
:global {
body {
margin: 0;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
background-color: var(--palette-background-light);
color: var(--palette-text-primary);
}
}
.root {
width: 100%;
box-sizing: border-box;
}
.container {
padding: var(--spacing-unit) var(--spacing-unit) calc(2 * var(--spacing-unit))
var(--spacing-unit);
max-width: 1080px;
margin: 0 auto;
}
@@ -0,0 +1,9 @@
import { shallow } from "enzyme";
import React from "react";
import App from "./App";
it("renders correctly", () => {
const wrapper = shallow(<App />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,20 @@
import React, { Component } from "react";
import * as styles from "./App.css";
import WizardContainer from "../containers/WizardContainer";
import MainBar from "./MainBar";
class App extends Component {
public render() {
return (
<div className={styles.root}>
<MainBar />
<div className={styles.container}>
<WizardContainer />
</div>
</div>
);
}
}
export default App;
@@ -0,0 +1,20 @@
.root {
padding: calc(4 * var(--spacing-unit)) 0;
}
.headline {
font-size: 1.2rem;
}
.headlineMain {
font-weight: bold;
font-size: 1.5rem;
}
.subHeadline {
font-size: 1.5rem;
}
.subHeadlineMain {
font-size: 2rem;
}
@@ -0,0 +1,42 @@
import cn from "classnames";
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Flex, Typography } from "talk-ui/components";
import * as styles from "./Header.css";
interface HeaderProps {
main?: boolean;
}
const Header: StatelessComponent<HeaderProps> = ({ main }) => {
return (
<Flex
alignItems="center"
justifyContent="center"
direction="column"
className={styles.root}
>
<Typography
className={cn(styles.headline, {
[styles.headlineMain]: main,
})}
>
The Coral Project
</Typography>
<Localized id="install-header-title">
<Typography
className={cn(styles.subHeadline, {
[styles.subHeadlineMain]: main,
})}
variant="heading1"
>
Talk Installation Wizard
</Typography>
</Localized>
</Flex>
);
};
export default Header;
@@ -0,0 +1,26 @@
.root {
width: 100%;
border-bottom: 2px solid var(--palette-grey-main);
padding: var(--spacing-unit);
box-sizing: border-box;
background: var(--palette-text-primary);
background: linear-gradient(
to right,
var(--palette-text-primary) 0%,
var(--palette-grey-main) 100%
);
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14),
0 1px 10px 0 rgba(0, 0, 0, 0.12);
}
.container {
max-width: 1080px;
margin: 0 auto;
display: flex;
justify-content: flex-start;
box-sizing: border-box;
}
.title {
color: var(--palette-text-light);
}
@@ -0,0 +1,19 @@
import React from "react";
import { Typography } from "talk-ui/components";
import * as styles from "./MainBar.css";
const MainBar = () => {
return (
<div className={styles.root}>
<div className={styles.container}>
<Typography variant="heading1" className={styles.title}>
Talk
</Typography>
</div>
</div>
);
};
export default MainBar;
@@ -0,0 +1,13 @@
.root {
max-width: 600px;
margin: 0 auto;
}
.section {
max-width: 400px;
margin: 0 auto;
}
.stepBar {
padding: var(--spacing-unit) 0 calc(6 * var(--spacing-unit));
}
@@ -0,0 +1,57 @@
import cn from "classnames";
import { Localized } from "fluent-react/compat";
import React, { Component, ReactNode } from "react";
import { Step, StepBar } from "talk-ui/components";
import { WizardProps } from "../components/Wizard";
import Header from "./Header";
import * as styles from "./Wizard.css";
export interface WizardProps {
currentStep: number;
className?: string;
children: ReactNode;
}
class Wizard extends Component<WizardProps> {
public render() {
const { children, currentStep, className } = this.props;
const wizardChildren = React.Children.toArray(children);
const wizardChildrenToRender = wizardChildren.filter(
(_, i) => i === currentStep
);
return (
<div className={cn(className, styles.root)}>
<Header main={currentStep === 0} />
{currentStep !== 0 &&
currentStep !== wizardChildren.length - 1 && (
<StepBar currentStep={currentStep - 1} className={styles.stepBar}>
<Step hidden>Start</Step>
<Step>
<Localized id="install-createYourAccount-stepTitle">
<span>Create Admin Account</span>
</Localized>
</Step>
<Step>
<Localized id="install-addOrganization-stepTitle">
<span>Add Organization Details</span>
</Localized>
</Step>
<Step>
<Localized id="install-permittedDomains-stepTitle">
<span>Add Permitted Domains</span>
</Localized>
</Step>
<Step hidden>Finish</Step>
</StepBar>
)}
<section className={styles.section}>{wizardChildrenToRender}</section>
</div>
);
}
}
export default Wizard;
@@ -0,0 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
className="App-root"
>
<MainBar />
<div
className="App-container"
>
<withContext(createMutationContainer(WizardContainer)) />
</div>
</div>
`;
@@ -0,0 +1,124 @@
import React, { Component } from "react";
import Wizard from "../components/Wizard";
import { InstallMutation, withInstallMutation } from "../mutations";
import FinalStep from "../steps/components/FinalStep";
import InitialStep from "../steps/components/InitialStep";
import AddOrganizationContainer from "../steps/containers/AddOrganizationContainer";
import CreateYourAccountContainer from "../steps/containers/CreateYourAccountContainer";
import PermittedDomainsContainer from "../steps/containers/PermittedDomainsContainer";
import { InstallInput } from "talk-framework/rest";
interface FormData {
organizationName: string;
organizationContactEmail: string;
organizationURL: string;
email: string;
username: string;
password: string;
confirmPassword: string;
domains: string[];
}
interface WizardContainerState {
step: number;
data: FormData;
}
function shapeFinalData(data: FormData): InstallInput {
const {
organizationName,
organizationContactEmail,
organizationURL,
domains,
username,
password,
email,
} = data;
return {
tenant: {
organizationName,
organizationContactEmail,
organizationURL,
domains,
},
user: {
username,
password,
email,
},
};
}
interface Props {
install: InstallMutation;
}
class WizardContainer extends Component<Props, WizardContainerState> {
public state = {
step: 0,
data: {
organizationContactEmail: "",
organizationName: "",
organizationURL: "",
email: "",
username: "",
password: "",
confirmPassword: "",
domains: [],
},
};
private handleSaveData = (newData: FormData) => {
this.setState(({ data }) => ({
data: { ...data, ...newData },
}));
};
private handleGoToNextStep = () =>
this.setState(({ step }) => ({
step: step + 1,
}));
private handleGoToPreviousStep = () =>
this.setState(({ step }) => ({
step: step - 1,
}));
private handleInstall = async (newData: Partial<FormData>) => {
return await this.props.install(
shapeFinalData({ ...this.state.data, ...newData })
);
};
public render() {
return (
<Wizard currentStep={this.state.step}>
<InitialStep onGoToNextStep={this.handleGoToNextStep} />
<CreateYourAccountContainer
data={this.state.data}
onSaveData={this.handleSaveData}
onGoToNextStep={this.handleGoToNextStep}
onGoToPreviousStep={this.handleGoToPreviousStep}
/>
<AddOrganizationContainer
data={this.state.data}
onSaveData={this.handleSaveData}
onGoToNextStep={this.handleGoToNextStep}
onGoToPreviousStep={this.handleGoToPreviousStep}
/>
<PermittedDomainsContainer
data={this.state.data}
onGoToNextStep={this.handleGoToNextStep}
onGoToPreviousStep={this.handleGoToPreviousStep}
onInstall={this.handleInstall}
/>
<FinalStep />
</Wizard>
);
}
}
export default withInstallMutation(WizardContainer);
+15
View File
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Talk - Install</title>
<meta charset="utf-8">
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no">
</head>
<body>
<div id="app"></div>
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
import React from "react";
import { StatelessComponent } from "react";
import ReactDOM from "react-dom";
import { createManaged } from "talk-framework/lib/bootstrap";
import App from "./components/App";
import localesData from "./locales";
async function main() {
const ManagedTalkContextProvider = await createManaged({
localesData,
userLocales: navigator.languages,
});
const Index: StatelessComponent = () => (
<ManagedTalkContextProvider>
<App />
</ManagedTalkContextProvider>
);
ReactDOM.render(<Index />, document.getElementById("app"));
}
main();
+9
View File
@@ -0,0 +1,9 @@
/**
* The actual content of this file is being generated by our `locales-loader`.
* Please check `./src/loaders` and the webpack config for more information.
*
* This file only represents the types that gets exported.
*/
import { LocalesData } from "talk-framework/lib/i18n";
export default {} as LocalesData;
@@ -0,0 +1,21 @@
import { Environment } from "relay-runtime";
import { TalkContext } from "talk-framework/lib/bootstrap";
import { createMutationContainer } from "talk-framework/lib/relay";
import { install, InstallInput } from "talk-framework/rest";
export type InstallMutation = (input: InstallInput) => Promise<void>;
export async function commit(
environment: Environment,
input: InstallInput,
{ rest }: TalkContext
) {
try {
await install(rest, input);
} catch (err) {
throw err;
}
}
export const withInstallMutation = createMutationContainer("install", commit);
@@ -0,0 +1 @@
export { withInstallMutation, InstallMutation } from "./InstallMutation";
@@ -0,0 +1,204 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field, Form } from "react-final-form";
import { OnSubmit } from "talk-framework/lib/form";
import {
composeValidators,
required,
validateEmail,
validateURL,
} from "talk-framework/lib/validation";
import {
CallOut,
Flex,
FormField,
HorizontalGutter,
InputDescription,
InputLabel,
TextField,
Typography,
ValidationMessage,
} from "talk-ui/components";
import BackButton from "./BackButton";
import NextButton from "./NextButton";
interface FormProps {
organizationName: string;
organizationContactEmail: string;
organizationURL: string;
}
export interface AddOrganizationForm {
onSubmit: OnSubmit<FormProps>;
onGoToPreviousStep: () => void;
data: FormProps;
}
const AddOrganization: StatelessComponent<AddOrganizationForm> = props => {
return (
<Form
onSubmit={props.onSubmit}
initialValues={{
organizationName: props.data.organizationName,
organizationContactEmail: props.data.organizationContactEmail,
organizationURL: props.data.organizationURL,
}}
>
{({ handleSubmit, submitting, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<HorizontalGutter size="double">
<Localized id="install-addOrganization-title">
<Typography variant="heading1" align="center">
Add Organization
</Typography>
</Localized>
<Localized id="install-addOrganization-description">
<Typography variant="bodyCopy" align="center">
Please tell us the name of your organization. This will appear
in emails when inviting new team members.
</Typography>
</Localized>
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<Field
name="organizationName"
validate={composeValidators(required)}
>
{({ input, meta }) => (
<FormField>
<Localized id="install-addOrganization-orgName">
<InputLabel>Organization Name</InputLabel>
</Localized>
<Localized
id="install-addOrganization-orgNameTextField"
attrs={{ placeholder: true }}
>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Organization Name"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={submitting}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
<Field
name="organizationContactEmail"
validate={composeValidators(required, validateEmail)}
>
{({ input, meta }) => (
<FormField>
<Localized id="install-addOrganization-orgEmail">
<InputLabel>Organization Contact Email</InputLabel>
</Localized>
<Localized
id="install-addOrganization-orgEmailTextField"
attrs={{ placeholder: true }}
>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Organization Contact Email"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={submitting}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
<Field
name="organizationURL"
validate={composeValidators(required, validateURL)}
>
{({ input, meta }) => (
<FormField>
<Localized id="install-addOrganization-orgURL">
<InputLabel>Organization URL</InputLabel>
</Localized>
<Localized
id="install-addOrganization-orgURLDescription"
strong={<strong />}
>
<InputDescription>
{/* Related: https://github.com/prettier/prettier/issues/2347 */}
Be sure to include <strong>{"http://"}</strong> or{" "}
<strong>{"https://"}</strong> in your URL
</InputDescription>
</Localized>
<Localized
id="install-addOrganization-orgURLTextField"
attrs={{ placeholder: true }}
>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Organization URL"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={submitting}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
<Flex direction="row-reverse" itemGutter>
<NextButton submitting={submitting} />
<BackButton
submitting={submitting}
onGoToPreviousStep={props.onGoToPreviousStep}
/>
</Flex>
</HorizontalGutter>
</form>
)}
</Form>
);
};
export default AddOrganization;
@@ -0,0 +1,30 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Button } from "talk-ui/components";
export interface BackButtonProps {
submitting: boolean;
onGoToPreviousStep: () => void;
}
const BackButton: StatelessComponent<BackButtonProps> = ({
submitting,
onGoToPreviousStep,
}) => {
return (
<Localized id="install-backButton-back">
<Button
onClick={onGoToPreviousStep}
variant="filled"
color="regular"
size="large"
disabled={submitting}
>
Back
</Button>
</Localized>
);
};
export default BackButton;
@@ -0,0 +1,235 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field, Form } from "react-final-form";
import { OnSubmit } from "talk-framework/lib/form";
import {
composeValidators,
required,
validateEmail,
validateEqualPasswords,
validatePassword,
validateUsername,
} from "talk-framework/lib/validation";
import {
CallOut,
Flex,
FormField,
HorizontalGutter,
InputDescription,
InputLabel,
TextField,
Typography,
ValidationMessage,
} from "talk-ui/components";
import NextButton from "./NextButton";
interface FormProps {
email: string;
username: string;
password: string;
confirmPassword: string;
}
export interface CreateYourAccountForm {
onSubmit: OnSubmit<FormProps>;
onGoToPreviousStep: () => void;
data: FormProps;
}
const CreateYourAccount: StatelessComponent<CreateYourAccountForm> = props => {
return (
<Form
onSubmit={props.onSubmit}
initialValues={{
email: props.data.email,
username: props.data.username,
}}
>
{({ handleSubmit, submitting, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<HorizontalGutter size="double">
<Localized id="install-createYourAccount-title">
<Typography variant="heading1" align="center">
Create an Administrator Account
</Typography>
</Localized>
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<Field
name="email"
validate={composeValidators(required, validateEmail)}
>
{({ input, meta }) => (
<FormField>
<Localized id="install-createYourAccount-email">
<InputLabel>Email</InputLabel>
</Localized>
<Localized
id="install-createYourAccount-emailTextField"
attrs={{ placeholder: true }}
>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Email"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={submitting}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
<Field
name="username"
validate={composeValidators(required, validateUsername)}
>
{({ input, meta }) => (
<FormField>
<Localized id="install-createYourAccount-username">
<InputLabel>Username</InputLabel>
</Localized>
<Localized id="install-createYourAccount-usernameDescription">
<InputDescription>
A unique identifier displayed on your comments. You may
use _ and .
</InputDescription>
</Localized>
<Localized
id="install-createYourAccount-usernameTextField"
attrs={{ placeholder: true }}
>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Username"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={submitting}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
<Field
name="password"
validate={composeValidators(required, validatePassword)}
>
{({ input, meta }) => (
<FormField>
<Localized id="install-createYourAccount-password">
<InputLabel>Password</InputLabel>
</Localized>
<Localized id="install-createYourAccount-passwordDescription">
<InputDescription>
Must be at least 8 characters
</InputDescription>
</Localized>
<Localized
id="install-createYourAccount-passwordTextField"
attrs={{ placeholder: true }}
>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Password"
type="password"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={submitting}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
<Field
name="confirmPassword"
validate={composeValidators(required, validateEqualPasswords)}
>
{({ input, meta }) => (
<FormField>
<Localized id="install-createYourAccount-confirmPassword">
<InputLabel>Confirm Password</InputLabel>
</Localized>
<Localized
id="install-createYourAccount-confirmPasswordTextField"
attrs={{ placeholder: true }}
>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Confirm Password"
type="password"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={submitting}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
<Flex direction="row-reverse">
<NextButton submitting={submitting} />
</Flex>
</HorizontalGutter>
</form>
)}
</Form>
);
};
export default CreateYourAccount;
@@ -0,0 +1,41 @@
import { Localized } from "fluent-react/compat";
import React, { Component } from "react";
import { urls } from "talk-framework/helpers";
import { Button, Flex, Typography } from "talk-ui/components";
class FinalStep extends Component {
public render() {
return (
<Flex direction="column" justifyContent="center" itemGutter="double">
<Localized id="install-finalStep-description">
<Typography variant="bodyCopy">
Thanks for installing Talk! We sent an email to verify your email
address. While you finish setting up the account, you can start
engaging with your readers now.
</Typography>
</Localized>
<Flex direction="row" itemGutter justifyContent="center">
<Localized id="install-finalStep-goToTheDocs">
<Button
anchor
color="regular"
variant="filled"
href="https://docs.coralproject.net"
target="_blank"
>
Go to the Docs
</Button>
</Localized>
<Localized id="install-finalStep-goToAdmin">
<Button anchor color="primary" variant="filled" href={urls.admin}>
Go to Admin
</Button>
</Localized>
</Flex>
</Flex>
);
}
}
export default FinalStep;
@@ -0,0 +1,38 @@
import { Localized } from "fluent-react/compat";
import React, { Component } from "react";
import { Button, Flex, Typography } from "talk-ui/components";
interface InitialStepProps {
onGoToNextStep: () => void;
}
class InitialStep extends Component<InitialStepProps> {
public render() {
return (
<Flex direction="column" justifyContent="center" itemGutter="double">
<Localized id="install-initialStep-copy">
<Typography variant="bodyCopy">
The remainder of the Talk installation will take about ten minutes.
Once you complete the following three steps, you will have a free
installation and provision Mongo and Redis.
</Typography>
</Localized>
<Flex justifyContent="center">
<Localized id="install-initialStep-getStarted">
<Button
onClick={this.props.onGoToNextStep}
color="primary"
variant="filled"
fullWidth={false}
>
Get Started
</Button>
</Localized>
</Flex>
</Flex>
);
}
}
export default InitialStep;
@@ -0,0 +1,27 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Button, ButtonIcon } from "talk-ui/components";
export interface NextButtonProps {
submitting: boolean;
}
const NextButton: StatelessComponent<NextButtonProps> = props => {
return (
<Button
variant="filled"
color="primary"
size="large"
type="submit"
disabled={props.submitting}
>
<Localized id="install-nextButton-next">
<span>Next</span>
</Localized>
<ButtonIcon>arrow_forward</ButtonIcon>
</Button>
);
};
export default NextButton;
@@ -0,0 +1,126 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field, Form } from "react-final-form";
import { OnSubmit } from "talk-framework/lib/form";
import {
Button,
CallOut,
Flex,
FormField,
HorizontalGutter,
InputDescription,
InputLabel,
TextField,
Typography,
ValidationMessage,
} from "talk-ui/components";
import BackButton from "./BackButton";
interface FormProps {
domains: string;
}
export interface PermittedDomainsForm {
onSubmit: OnSubmit<FormProps>;
onGoToPreviousStep: () => void;
data: {
domains: string[];
};
}
const PermittedDomains: StatelessComponent<PermittedDomainsForm> = props => {
return (
<Form
onSubmit={props.onSubmit}
initialValues={{
domains: props.data.domains.join(","),
}}
>
{({ handleSubmit, submitting, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<HorizontalGutter size="double">
<Localized id="install-permittedDomains-title">
<Typography variant="heading1" align="center">
Permitted Domains
</Typography>
</Localized>
<Localized id="install-permittedDomains-description">
<Typography variant="bodyCopy" align="center">
Enter the domains you would like to permit for Talk, e.g. your
local, staging and production environments (ex. localhost:3000,
staging.domain.com, domain.com).
</Typography>
</Localized>
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<Field name="domains">
{({ input, meta }) => (
<FormField>
<Localized id="install-permittedDomains-permittedDomains">
<InputLabel>Permitted Domains</InputLabel>
</Localized>
<Localized id="install-permittedDomains-permittedDomainsDescription">
<InputDescription>
Insert domains separated by comma
</InputDescription>
</Localized>
<Localized
id="install-permittedDomains-permittedDomainsTextField"
attrs={{ placeholder: true }}
>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
placeholder="Domains"
color={
meta.touched && (meta.error || meta.submitError)
? "error"
: "regular"
}
disabled={submitting}
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
<Flex direction="row-reverse" itemGutter>
<Localized id="install-permittedDomains-finishInstall">
<Button
variant="filled"
color="primary"
size="large"
type="submit"
disabled={submitting}
>
Finish Install
</Button>
</Localized>
<BackButton
submitting={submitting}
onGoToPreviousStep={props.onGoToPreviousStep}
/>
</Flex>
</HorizontalGutter>
</form>
)}
</Form>
);
};
export default PermittedDomains;
@@ -0,0 +1,39 @@
import { FORM_ERROR } from "final-form";
import React, { Component } from "react";
import { PropTypesOf } from "talk-ui/types";
import AddOrganization, {
AddOrganizationForm,
} from "../components/AddOrganization";
interface AddOrganizationContainerProps {
onGoToNextStep: () => void;
onGoToPreviousStep: () => void;
data: PropTypesOf<typeof AddOrganization>["data"];
onSaveData: (newData: PropTypesOf<typeof AddOrganization>["data"]) => void;
}
class AddOrganizationContainer extends Component<
AddOrganizationContainerProps
> {
private onSubmit: AddOrganizationForm["onSubmit"] = async (input, form) => {
try {
this.props.onSaveData(input);
return this.props.onGoToNextStep();
} catch (error) {
return { [FORM_ERROR]: error.message };
}
};
public render() {
return (
<AddOrganization
data={this.props.data}
onSubmit={this.onSubmit}
onGoToPreviousStep={this.props.onGoToPreviousStep}
/>
);
}
}
export default AddOrganizationContainer;
@@ -0,0 +1,39 @@
import { FORM_ERROR } from "final-form";
import React, { Component } from "react";
import { PropTypesOf } from "talk-ui/types";
import CreateYourAccount, {
CreateYourAccountForm,
} from "../components/CreateYourAccount";
interface CreateYourAccountContainerProps {
onGoToNextStep: () => void;
onGoToPreviousStep: () => void;
data: PropTypesOf<typeof CreateYourAccount>["data"];
onSaveData: (newData: PropTypesOf<typeof CreateYourAccount>["data"]) => void;
}
class CreateYourAccountContainer extends Component<
CreateYourAccountContainerProps
> {
private onSubmit: CreateYourAccountForm["onSubmit"] = async (input, form) => {
try {
this.props.onSaveData(input);
return this.props.onGoToNextStep();
} catch (error) {
return { [FORM_ERROR]: error.message };
}
};
public render() {
return (
<CreateYourAccount
data={this.props.data}
onSubmit={this.onSubmit}
onGoToPreviousStep={this.props.onGoToPreviousStep}
/>
);
}
}
export default CreateYourAccountContainer;
@@ -0,0 +1,42 @@
import { FORM_ERROR } from "final-form";
import React, { Component } from "react";
import { PropTypesOf } from "talk-ui/types";
import PermittedDomains, {
PermittedDomainsForm,
} from "../components/PermittedDomains";
interface PermittedDomainsContainerProps {
onGoToNextStep: () => void;
onGoToPreviousStep: () => void;
data: PropTypesOf<typeof PermittedDomains>["data"];
onInstall: (
newData: PropTypesOf<typeof PermittedDomains>["data"]
) => Promise<void>;
}
class PermittedDomainsContainer extends Component<
PermittedDomainsContainerProps
> {
private onSubmit: PermittedDomainsForm["onSubmit"] = async (input, form) => {
try {
const domains = input.domains.split(",");
await this.props.onInstall({ domains });
return this.props.onGoToNextStep();
} catch (error) {
return { [FORM_ERROR]: error.message };
}
};
public render() {
return (
<PermittedDomains
data={this.props.data}
onSubmit={this.onSubmit}
onGoToPreviousStep={this.props.onGoToPreviousStep}
/>
);
}
}
export default PermittedDomainsContainer;
+1 -1
View File
@@ -2,7 +2,7 @@
:global {
body {
margin: 0;
padding: 2px 8px;
padding: 0;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
@@ -18,6 +18,7 @@ import {
} from "talk-stream/mutations";
import { Popup } from "talk-ui/components";
import { urls } from "talk-framework/helpers";
import UserBoxAuthenticated from "../components/UserBoxAuthenticated";
interface InnerProps {
@@ -57,7 +58,7 @@ export class UserBoxContainer extends Component<InnerProps> {
return (
<>
<Popup
href={`/auth.html?view=${view}`}
href={`${urls.embed.auth}?view=${view}`}
title="Talk Auth"
features="menubar=0,resizable=0,width=350,height=395,top=200,left=500"
open={open}
@@ -5,7 +5,7 @@ exports[`renders correctly 1`] = `
<Popup
features="menubar=0,resizable=0,width=350,height=395,top=200,left=500"
focus={false}
href="/auth.html?view=SIGN_IN"
href="/embed/auth?view=SIGN_IN"
onBlur={[Function]}
onClose={[Function]}
onFocus={[Function]}
@@ -1,4 +1,5 @@
import { shallow } from "enzyme";
import qs from "query-string";
import React from "react";
import { Environment, RecordSource } from "relay-runtime";
@@ -10,12 +11,20 @@ import { OnPymSetCommentID } from "./OnPymSetCommentID";
let relayEnvironment: Environment;
const source: RecordSource = new RecordSource();
const previousLocation = location.toString();
const previousState = window.history.state;
beforeAll(() => {
relayEnvironment = createRelayEnvironment({
source,
});
});
afterEach(() => {
// As history will change after the listener triggers, reset this to before.
window.history.replaceState(previousState, document.title, previousLocation);
});
it("Sets comment id", () => {
const id = "comment1-id";
const props = {
@@ -29,6 +38,7 @@ it("Sets comment id", () => {
};
shallow(<OnPymSetCommentID {...props} />);
expect(source.get(LOCAL_ID)!.commentID).toEqual(id);
expect(qs.parse(location.search).commentID).toEqual(id);
});
it("Sets comment id to null when empty", () => {
@@ -44,4 +54,5 @@ it("Sets comment id to null when empty", () => {
};
shallow(<OnPymSetCommentID {...props} />);
expect(source.get(LOCAL_ID)!.commentID).toEqual(null);
expect(qs.parse(location.search).commentID).toBeUndefined();
});
@@ -3,6 +3,7 @@ import { Component } from "react";
import { commitLocalUpdate } from "react-relay";
import { Environment } from "relay-runtime";
import { getURLWithCommentID } from "talk-framework/helpers";
import { withContext } from "talk-framework/lib/bootstrap";
import { LOCAL_ID } from "talk-framework/lib/relay";
@@ -21,6 +22,15 @@ export class OnPymSetCommentID extends Component<Props> {
const id = raw || null;
if (s.get(LOCAL_ID)!.getValue("commentID") !== id) {
s.get(LOCAL_ID)!.setValue(id, "commentID");
// Change iframe url, this is important
// because it is used to cleanly initialized
// a user session.
window.history.replaceState(
window.history.state,
document.title,
getURLWithCommentID(location.href, id || undefined)
);
}
});
});
@@ -24,17 +24,19 @@ function sharedUpdater(
store: RecordSourceSelectorProxy,
input: CreateCommentInput
) {
updateAsset(store, input);
if (input.local) {
localUpdate(store, input);
} else {
update(store, input);
}
updateProfile(store, input);
}
/**
* update integrates new comment into the CommentConnection.
*/
function update(store: RecordSourceSelectorProxy, input: CreateCommentInput) {
function updateAsset(
store: RecordSourceSelectorProxy,
input: CreateCommentInput
) {
// Updating Comment Count
const asset = store.get(input.assetID);
if (asset) {
@@ -45,7 +47,12 @@ function update(store: RecordSourceSelectorProxy, input: CreateCommentInput) {
record.setValue(currentCount + 1, "totalVisible");
}
}
}
/**
* update integrates new comment into the CommentConnection.
*/
function update(store: RecordSourceSelectorProxy, input: CreateCommentInput) {
// Get the payload returned from the server.
const payload = store.getRootField("createComment")!;
@@ -109,6 +116,26 @@ function localUpdate(
}
}
/**
* updateProfile integrates new comment into the profile.
*/
function updateProfile(
store: RecordSourceSelectorProxy,
input: CreateCommentInput
) {
// Get the payload returned from the server.
const payload = store.getRootField("createComment")!;
// Get the edge of the newly created comment.
const newEdge = payload.getLinkedRecord("edge")!;
const newComment = newEdge.getLinkedRecord("node");
// TODO: update profile comments connection after we
// integrated pagination.
// tslint:disable-next-line:no-unused-expression
newComment;
}
const mutation = graphql`
mutation CreateCommentMutation($input: CreateCommentInput!) {
createComment(input: $input) {
@@ -1,3 +1,4 @@
import qs from "query-string";
import { Environment, RecordSource } from "relay-runtime";
import sinon from "sinon";
@@ -16,10 +17,19 @@ beforeAll(() => {
});
});
const previousLocation = location.toString();
const previousState = window.history.state;
afterEach(() => {
// As history will change after the listener triggers, reset this to before.
window.history.replaceState(previousState, document.title, previousLocation);
});
it("Sets comment id", () => {
const id = "comment1-id";
commit(environment, { id }, {} as any);
expect(source.get(LOCAL_ID)!.commentID).toEqual(id);
expect(qs.parse(location.search).commentID).toEqual(id);
});
it("Should call setCommentID in pym", async () => {
@@ -50,5 +60,6 @@ it("Should call setCommentID in pym with empty id", async () => {
commit(environment, { id: null }, context as any);
await timeout();
expect(source.get(LOCAL_ID)!.commentID).toEqual(null);
expect(qs.parse(location.search).commentID).toBeUndefined();
context.pym.sendMessage.verify();
});
@@ -1,5 +1,6 @@
import { commitLocalUpdate, Environment } from "relay-runtime";
import { getURLWithCommentID } from "talk-framework/helpers";
import { TalkContext } from "talk-framework/lib/bootstrap";
import { createMutationContainer } from "talk-framework/lib/relay";
import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer";
@@ -19,6 +20,16 @@ export async function commit(
const record = store.get(LOCAL_ID)!;
record.setValue(input.id, "commentID");
record.setValue("COMMENTS", "activeTab");
// Change iframe url, this is important
// because it is used to cleanly initialized
// a user session.
window.history.replaceState(
window.history.state,
document.title,
getURLWithCommentID(location.href, input.id || undefined)
);
if (pym) {
// This sets the comment id on the parent url.
pym.sendMessage("setCommentID", input.id || "");
@@ -1,5 +1,9 @@
.root {
}
.highlight {
background-color: var(--palette-primary-lightest);
padding: var(--spacing-unit);
}
.topBar {
margin-bottom: calc(0.5 * var(--spacing-unit));
}
@@ -8,9 +8,7 @@ import Comment from "./Comment";
it("renders username and body", () => {
const props: PropTypesOf<typeof Comment> = {
id: "comment-id",
author: {
username: "Marvin",
},
username: "Marvin",
body: "Woof",
createdAt: "1995-12-17T03:24:00.000Z",
topBarRight: "topBarRight",
@@ -1,3 +1,4 @@
import cn from "classnames";
import React, { StatelessComponent } from "react";
import HTMLContent from "talk-stream/components/HTMLContent";
@@ -12,19 +13,21 @@ import Username from "./Username";
export interface CommentProps {
id?: string;
className?: string;
author: {
username: string | null;
} | null;
username: string | null;
body: string | null;
createdAt: string;
topBarRight?: React.ReactNode;
footer?: React.ReactNode;
showEditedMarker?: boolean;
highlight?: boolean;
}
const Comment: StatelessComponent<CommentProps> = props => {
return (
<div role="article" className={styles.root}>
<div
role="article"
className={cn(styles.root, { [styles.highlight]: props.highlight })}
>
<Flex
className={styles.topBar}
direction="row"
@@ -32,10 +35,7 @@ const Comment: StatelessComponent<CommentProps> = props => {
id={props.id}
>
<TopBarLeft>
{props.author &&
props.author.username && (
<Username>{props.author.username}</Username>
)}
{props.username && <Username>{props.username}</Username>}
<Flex direction="row" alignItems="baseline" itemGutter>
<Timestamp>{props.createdAt}</Timestamp>
{props.showEditedMarker && <EditedMarker />}
@@ -8,9 +8,7 @@ import IndentedComment from "./IndentedComment";
it("renders correctly", () => {
const props: PropTypesOf<typeof IndentedComment> = {
indentLevel: 1,
author: {
username: "Marvin",
},
username: "Marvin",
body: "Woof",
createdAt: "1995-12-17T03:24:00.000Z",
};
@@ -0,0 +1,28 @@
import React, { StatelessComponent } from "react";
import Timestamp from "talk-stream/components/Timestamp";
import { Flex } from "talk-ui/components";
import TopBarLeft from "./TopBarLeft";
import Username from "./Username";
export interface RootParentProps {
id?: string;
username: string | null;
createdAt: string;
}
const RootParent: StatelessComponent<RootParentProps> = props => {
return (
<Flex direction="row" justifyContent="space-between" id={props.id}>
<TopBarLeft>
{props.username && <Username>{props.username}</Username>}
<Flex direction="row" alignItems="baseline" itemGutter>
<Timestamp>{props.createdAt}</Timestamp>
</Flex>
</TopBarLeft>
</Flex>
);
};
export default RootParent;
@@ -6,13 +6,9 @@ exports[`renders correctly 1`] = `
level={1}
>
<Comment
author={
Object {
"username": "Marvin",
}
}
body="Woof"
createdAt="1995-12-17T03:24:00.000Z"
username="Marvin"
/>
</Indent>
`;
@@ -3,3 +3,4 @@ export { default as TopBarLeft } from "./TopBarLeft";
export { default as Username } from "./Username";
export { default as ButtonsBar } from "./ButtonsBar";
export { default as ShowConversationLink } from "./ShowConversationLink";
export { default as RootParent } from "./RootParent";
@@ -0,0 +1,7 @@
.root {
}
.loadMore {
padding-left: var(--spacing-unit);
padding-bottom: var(--spacing-unit);
}
@@ -0,0 +1,70 @@
import { shallow } from "enzyme";
import { noop } from "lodash";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import { removeFragmentRefs } from "talk-framework/testHelpers";
import ConversationThread from "./ConversationThread";
const ConversationThreadN = removeFragmentRefs(ConversationThread);
describe("with 2 remaining parent comments", () => {
it("renders correctly", () => {
const props: PropTypesOf<typeof ConversationThreadN> = {
className: "root",
me: {},
asset: {},
settings: {},
comment: {},
disableLoadMore: false,
loadMore: noop,
remaining: 2,
parents: [],
rootParent: {
id: "root-parent",
createdAt: "1995-12-17T03:24:00.000Z",
username: "parentAuthor",
},
};
const wrapper = shallow(<ConversationThreadN {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders with disabled load more", () => {
const props: PropTypesOf<typeof ConversationThreadN> = {
className: "root",
me: {},
asset: {},
settings: {},
comment: {},
disableLoadMore: true,
loadMore: noop,
remaining: 2,
parents: [],
rootParent: {
id: "root-parent",
createdAt: "1995-12-17T03:24:00.000Z",
username: "parentAuthor",
},
};
const wrapper = shallow(<ConversationThreadN {...props} />);
expect(wrapper).toMatchSnapshot();
});
});
it("renders with no parent comments", () => {
const props: PropTypesOf<typeof ConversationThreadN> = {
className: "root",
me: {},
asset: {},
settings: {},
comment: {},
disableLoadMore: false,
loadMore: noop,
remaining: 0,
parents: [],
rootParent: null,
};
const wrapper = shallow(<ConversationThreadN {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,122 @@
import cn from "classnames";
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { Button, Flex, HorizontalGutter } from "talk-ui/components";
import CommentContainer from "../containers/CommentContainer";
import LocalReplyListContainer from "../containers/LocalReplyListContainer";
import { RootParent } from "./Comment";
import * as styles from "./ConversationThread.css";
import Counter from "./Counter";
import { Circle, Line } from "./Timeline";
export interface ConversationThreadProps {
className?: string;
me: PropTypesOf<typeof CommentContainer>["me"] &
(PropTypesOf<typeof LocalReplyListContainer>["me"] | null);
asset: PropTypesOf<typeof CommentContainer>["asset"] &
PropTypesOf<typeof LocalReplyListContainer>["asset"];
settings: PropTypesOf<typeof CommentContainer>["settings"] &
PropTypesOf<typeof LocalReplyListContainer>["settings"];
comment: PropTypesOf<typeof CommentContainer>["comment"];
disableLoadMore: boolean;
loadMore: () => void;
remaining: number;
parents: Array<
{ id: string } & PropTypesOf<typeof CommentContainer>["comment"] &
PropTypesOf<typeof LocalReplyListContainer>["comment"]
>;
rootParent: {
id: string;
createdAt: string;
username: string | null;
} | null;
}
const ConversationThread: StatelessComponent<
ConversationThreadProps
> = props => {
if (props.remaining === 0 && props.parents.length === 0) {
return (
<div className={cn(props.className, styles.root)}>
<CommentContainer
comment={props.comment}
asset={props.asset}
settings={props.settings}
me={props.me}
highlight
/>
</div>
);
}
return (
<div className={cn(props.className, styles.root)}>
<HorizontalGutter container={<Line dotted />}>
{props.rootParent && (
<Circle>
<RootParent
id={props.rootParent.id}
username={props.rootParent.username}
createdAt={props.rootParent.createdAt}
/>
</Circle>
)}
{props.remaining > 0 && (
<Circle hollow className={styles.loadMore}>
<Flex alignItems="center" itemGutter="half">
<Localized
id="comments-conversationThread-showHiddenComments"
$count={props.remaining}
>
<Button
onClick={props.loadMore}
disabled={props.disableLoadMore}
id="comments-conversationThread-showHiddenComments"
variant="underlined"
>
Show hidden comments
</Button>
</Localized>
{props.remaining > 1 && <Counter>{props.remaining}</Counter>}
</Flex>
</Circle>
)}
</HorizontalGutter>
<HorizontalGutter container={Line}>
{props.parents.map((parent, i) => (
<Circle key={parent.id} hollow={!!props.remaining || i > 0}>
<CommentContainer
comment={parent}
asset={props.asset}
me={props.me}
settings={props.settings}
localReply
/>
{props.me && (
<LocalReplyListContainer
asset={props.asset}
me={props.me}
settings={props.settings}
comment={parent}
indentLevel={1}
/>
)}
</Circle>
))}
<Circle end>
<CommentContainer
comment={props.comment}
asset={props.asset}
settings={props.settings}
me={props.me}
highlight
/>
</Circle>
</HorizontalGutter>
</div>
);
};
export default ConversationThread;
@@ -0,0 +1,8 @@
.root {
composes: subTabCounter from "talk-ui/shared/typography.css";
background-color: var(--palette-grey-dark);
color: var(--palette-text-light);
border-radius: 1px;
padding: 2px 3px 3px 3px;
}
@@ -0,0 +1,9 @@
import { shallow } from "enzyme";
import React from "react";
import Counter from "./Counter";
it("renders correctly", () => {
const wrapper = shallow(<Counter>20</Counter>);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,16 @@
import cn from "classnames";
import React, { StatelessComponent } from "react";
import * as styles from "./Counter.css";
interface Props {
className?: string;
}
const Counter: StatelessComponent<Props> = props => {
return (
<span className={cn(styles.root, props.className)}>{props.children}</span>
);
};
export default Counter;
@@ -2,6 +2,23 @@
width: 100%;
}
.button {
margin-bottom: calc(2 * var(--spacing-unit));
.replyList {
padding-left: calc(2 * var(--spacing-unit));
}
.title1 {
font-size: calc(14rem / var(--rem-base));
font-weight: var(--font-weight-medium);
font-family: var(--font-family-sans-serif);
line-height: calc(20em / 16);
letter-spacing: calc(0.2em / 16);
color: var(--palette-text-primary);
}
.title2 {
font-size: calc(18rem / var(--rem-base));
font-weight: var(--font-weight-medium);
font-family: var(--font-family-sans-serif);
line-height: calc(20em / 16);
letter-spacing: calc(0.2em / 16);
color: var(--palette-text-primary);
}
@@ -0,0 +1,36 @@
import { shallow } from "enzyme";
import { noop } from "lodash";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import { removeFragmentRefs } from "talk-framework/testHelpers";
import PermalinkView from "./PermalinkView";
const PermalinkViewN = removeFragmentRefs(PermalinkView);
it("renders correctly", () => {
const props: PropTypesOf<typeof PermalinkViewN> = {
me: {},
asset: {},
settings: {},
comment: {},
showAllCommentsHref: "http://localhost/link",
onShowAllComments: noop,
};
const wrapper = shallow(<PermalinkViewN {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders comment not found", () => {
const props: PropTypesOf<typeof PermalinkViewN> = {
me: {},
asset: {},
settings: {},
comment: null,
showAllCommentsHref: "http://localhost/link",
onShowAllComments: noop,
};
const wrapper = shallow(<PermalinkViewN {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -2,16 +2,25 @@ import { Localized } from "fluent-react/compat";
import React, { MouseEvent, StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { Button, Typography } from "talk-ui/components";
import { Button, Flex, HorizontalGutter, Typography } from "talk-ui/components";
import CommentContainer from "../containers/CommentContainer";
import UserBoxContainer from "../../../containers/UserBoxContainer";
import ConversationThreadContainer from "../containers/ConversationThreadContainer";
import ReplyListContainer from "../containers/ReplyListContainer";
import * as styles from "./PermalinkView.css";
export interface PermalinkViewProps {
me: PropTypesOf<typeof CommentContainer>["me"];
asset: PropTypesOf<typeof CommentContainer>["asset"];
settings: PropTypesOf<typeof CommentContainer>["settings"];
comment: PropTypesOf<typeof CommentContainer>["comment"] | null;
me: PropTypesOf<typeof ConversationThreadContainer>["me"] &
PropTypesOf<typeof ReplyListContainer>["me"] &
PropTypesOf<typeof UserBoxContainer>["me"];
asset: PropTypesOf<typeof ConversationThreadContainer>["asset"] &
PropTypesOf<typeof ReplyListContainer>["asset"];
comment:
| PropTypesOf<typeof ConversationThreadContainer>["comment"] &
PropTypesOf<typeof ReplyListContainer>["comment"]
| null;
settings: PropTypesOf<typeof ConversationThreadContainer>["settings"] &
PropTypesOf<typeof ReplyListContainer>["settings"];
showAllCommentsHref: string | null;
onShowAllComments: (e: MouseEvent<any>) => void;
}
@@ -25,38 +34,57 @@ const PermalinkView: StatelessComponent<PermalinkViewProps> = ({
me,
}) => {
return (
<div className={styles.root}>
{showAllCommentsHref && (
<Localized id="comments-permalinkView-showAllComments">
<Button
id="talk-comments-permalinkView-showAllComments"
variant="outlined"
color="primary"
onClick={onShowAllComments}
className={styles.button}
href={showAllCommentsHref}
target="_parent"
fullWidth
anchor
>
Show all Comments
</Button>
<HorizontalGutter className={styles.root} size="double">
<UserBoxContainer me={me} />
<Flex alignItems="center" justifyContent="center" direction="column">
<Localized id="comments-permalinkView-currentViewing">
<Typography className={styles.title1}>
You are currently viewing a
</Typography>
</Localized>
)}
<Localized id="comments-permalinkView-singleConversation">
<Typography className={styles.title2}>SINGLE CONVERSATION</Typography>
</Localized>
{showAllCommentsHref && (
<Localized id="comments-permalinkView-viewFullDiscussion">
<Button
id="talk-comments-permalinkView-viewFullDiscussion"
variant="underlined"
color="primary"
onClick={onShowAllComments}
href={showAllCommentsHref}
target="_parent"
anchor
>
View Full Discussion
</Button>
</Localized>
)}
</Flex>
{!comment && (
<Localized id="comments-permalinkView-commentNotFound">
<Typography>Comment not found</Typography>
</Localized>
)}
{comment && (
<CommentContainer
settings={settings}
me={me}
comment={comment}
asset={asset}
/>
<HorizontalGutter>
<ConversationThreadContainer
me={me}
comment={comment}
asset={asset}
settings={settings}
/>
<div className={styles.replyList}>
<ReplyListContainer
me={me}
comment={comment}
asset={asset}
settings={settings}
/>
</div>
</HorizontalGutter>
)}
</div>
</HorizontalGutter>
);
};
@@ -2,6 +2,10 @@
composes: root from "talk-stream/shared/htmlContent.css";
}
.toolbar {
background-color: var(--palette-common-white);
}
.placeholder {
composes: placeholder from "talk-ui/shared/typography.css";
margin: var(--spacing-unit) 0 0 calc(1px + var(--spacing-unit));
@@ -95,6 +95,7 @@ const RTE: StatelessComponent<RTEProps> = props => {
className={className}
contentClassName={styles.content}
placeholderClassName={styles.placeholder}
toolbarClassName={styles.toolbar}
onChange={onChange}
value={value || defaultValue}
disabled={disabled}
@@ -0,0 +1,31 @@
.circleContainer {
position: relative;
}
.circleSubContainer {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
left: calc(-1 * var(--spacing-unit) - var(--spacing-unit) - 1px);
background-color: var(--palette-common-white);
width: calc(2 * var(--spacing-unit));
height: 23px;
color: var(--palette-grey-main);
}
.end {
align-items: flex-start;
padding-top: 8px;
height: 100%;
@media (min-width: $breakpoints-xs) {
padding-top: 6px;
}
}
.circle {
width: var(--spacing-unit);
height: var(--spacing-unit);
fill: currentColor;
stroke: currentColor;
}
@@ -0,0 +1,16 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Circle from "./Circle";
it("renders correctly", () => {
const props: PropTypesOf<typeof Circle> = {
className: "root",
hollow: true,
end: true,
};
const wrapper = shallow(<Circle {...props} />);
expect(wrapper).toMatchSnapshot();
});

Some files were not shown because too many files have changed in this diff Show More