diff --git a/.circleci/config.yml b/.circleci/config.yml index 908210e13..bd7493f6b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -43,6 +43,20 @@ jobs: name: Perform linting command: npm run lint + # unit_tests will run the unit tests. + unit_tests: + <<: *job_defaults + steps: + - checkout + - attach_workspace: + at: ~/coralproject/talk + - run: + name: Compile schemas and types + command: npm run compile + - run: + name: Perform testing + command: npm run test + # build will build the static assets and server typescript files. build: <<: *job_defaults @@ -74,6 +88,9 @@ workflows: - lint: requires: - npm_dependencies + - unit_tests: + requires: + - npm_dependencies - build: requires: - npm_dependencies diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..3c3629e64 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +node_modules diff --git a/.vscode/settings.json b/.vscode/settings.json index 195b73de5..4dd5d1b72 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,12 +9,10 @@ "**/.hg": true, "**/CVS": true, "**/.DS_Store": true, - "node_modules": true, - "dist": true, - ".vscode": true, - "package-lock.json": true + ".vs": true }, + "tslint.exclude": "**/node_modules/**", "tslint.autoFixOnSave": true, "tslint.jsEnable": true, "typescript.tsdk": "node_modules/typescript/lib" -} \ No newline at end of file +} diff --git a/config/jest.config.js b/config/jest.config.js index 682a2c8a4..ed9b641ae 100644 --- a/config/jest.config.js +++ b/config/jest.config.js @@ -8,7 +8,7 @@ module.exports = { setupFiles: ["/config/polyfills.js"], testMatch: [ "**/__tests__/**/*.{js,jsx,mjs,ts,tsx}", - "**/*.(spec|test).{js,jsx,mjs,ts,tsx}", + "**/*.spec.{js,jsx,mjs,ts,tsx}", ], testEnvironment: "node", testURL: "http://localhost", @@ -22,7 +22,12 @@ module.exports = { "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|ts|tsx)$", ], moduleNameMapper: { - "^react-native$": "react-native-web", + "^talk-admin/(.*)$": "/src/core/client/admin/$1", + "^talk-ui/(.*)$": "/src/core/client/ui/$1", + "^talk-stream/(.*)$": "/src/core/client/stream/$1", + "^talk-framework/(.*)$": "/src/core/client/framework/$1", + "^talk-common/(.*)$": "/src/core/common/$1", + "^talk-server/(.*)$": "/src/core/server/$1", }, moduleFileExtensions: [ "web.js", diff --git a/config/jest/cssTransform.js b/config/jest/cssTransform.js index 606cc276b..90b6bfa63 100644 --- a/config/jest/cssTransform.js +++ b/config/jest/cssTransform.js @@ -1,14 +1,94 @@ "use strict"; -// This is a custom Jest transformer turning style imports into empty objects. -// http://facebook.github.io/jest/docs/en/webpack.html +// Adapted version of https://github.com/Connormiha/jest-css-modules-transform +// Copyright https://github.com/Connormiha/jest-css-modules-transform/graphs/contributors +// This oututs `module.exports = cssObject` instead of `module.exports = { default: cssObject }`; + +const { sep: pathSep, resolve } = require("path"); +const postcss = require("postcss"); +const postcssNested = postcss([require("postcss-nested")]); + +const REG_EXP_NAME_BREAK_CHAR = /[\s,.{/#[:]/; + +const getCSSSelectors = (css, path) => { + const end = css.length; + let i = 0; + let char; + let bracketsCount = 0; + const result = {}; + + while (i < end) { + if (i === -1) { + throw Error(`Parse error ${path}`); + } + if (css.indexOf("/*", i) === i) { + i = css.indexOf("*/", i + 2); + // Unclosed comment. Break to avoid infinity loop + if (i === -1) { + // Don't parse, but save collected result + return result; + } + continue; + } + char = css[i]; + if (char === "{") { + bracketsCount++; + i++; + continue; + } + if (char === "}") { + bracketsCount--; + i++; + continue; + } + if (char === '"' || char === "'") { + do { + i = css.indexOf(char, i + 1); + // Syntax error since this line. Don't parse, but save collected result + if (i === -1) { + return result; + } + } while (css[i - 1] === "\\"); + i++; + continue; + } + if (bracketsCount > 0) { + i++; + continue; + } + if (char === "." || char === "#") { + i++; + const startWord = i; + while (!REG_EXP_NAME_BREAK_CHAR.test(css[i])) { + i++; + } + const word = css.slice(startWord, i); + result[word] = word; + continue; + } + if (css.indexOf("@keyframes", i) === i) { + i += 10; + while (REG_EXP_NAME_BREAK_CHAR.test(css[i])) { + i++; + } + const startWord = i; + while (!REG_EXP_NAME_BREAK_CHAR.test(css[i])) { + i++; + } + const word = css.slice(startWord, i); + result[word] = word; + continue; + } + i++; + } + return result; +}; module.exports = { - process() { - return "module.exports = {};"; - }, - getCacheKey() { - // The output is always the same. - return "cssTransform"; + process(src, path, config) { + const filename = path.slice(path.lastIndexOf(pathSep) + 1); + const textCSS = postcssNested.process(src).css; + const exprt = JSON.stringify(getCSSSelectors(textCSS, path)); + return `module.exports = ${exprt}`; }, }; diff --git a/config/nodemon/relay-stream.json b/config/nodemon/relay-stream.json deleted file mode 100644 index dc9556823..000000000 --- a/config/nodemon/relay-stream.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "exec": "npm-run-all compile:relay-stream", - "ext": "ts,tsx,graphql", - "watch": [ - "./src/core/client/stream", - "./src/core/**/*.graphql" - ] -} diff --git a/config/nodemon/server.json b/config/nodemon/server.json deleted file mode 100644 index 08472086e..000000000 --- a/config/nodemon/server.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "exec": "npm run start:development", - "ext": "ts,graphql", - "watch": [ - "./src" - ], - "ignore": [ - "./src/client" - ] -} diff --git a/config/webpack.config.dev.js b/config/webpack.config.dev.js index 80872c5a4..64ff0eeba 100644 --- a/config/webpack.config.dev.js +++ b/config/webpack.config.dev.js @@ -180,7 +180,7 @@ module.exports = { // All available locales can be loadable on demand. // To restrict available locales set: - // availableLocales: ["en-US"] + // availableLocales: ["en-US"], }, }, ], diff --git a/package-lock.json b/package-lock.json index b3622a48d..ee9b43566 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1628,13 +1628,17 @@ } }, "@types/react-relay": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@types/react-relay/-/react-relay-1.3.6.tgz", - "integrity": "sha512-X0qv3nGlE4TStFLLKxgj+6MgHZEExB1N/RDcVcyCauAojV5byD0c6VhuqAluYaTKaL2FBuxdtDL405IhIIjEbQ==", + "version": "github:coralproject/patched#ba4c8d01bb737e5f073534b32d870294e39cc5a8", + "from": "github:coralproject/patched#types/react-relay", + "dev": true + }, + "@types/react-test-renderer": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-16.0.1.tgz", + "integrity": "sha512-kmNh8g67Ck/y/vp6KX+4JTJXiTGLZBylNhu+R7sm7zcvsrnIGVO6J1zew5inVg428j9f8yHpl68RcYOZXVborQ==", "dev": true, "requires": { - "@types/react": "*", - "@types/relay-runtime": "*" + "@types/react": "*" } }, "@types/recompose": { @@ -7635,17 +7639,17 @@ } }, "expect": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-23.2.0.tgz", - "integrity": "sha1-U6fhNeNv4n51hnsReP8IqqzCsN0=", + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-23.3.0.tgz", + "integrity": "sha1-7LBRrcvcQKxNtXbBYGfxL9sTzGE=", "dev": true, "requires": { "ansi-styles": "^3.2.0", "jest-diff": "^23.2.0", "jest-get-type": "^22.1.0", "jest-matcher-utils": "^23.2.0", - "jest-message-util": "^23.2.0", - "jest-regex-util": "^23.0.0" + "jest-message-util": "^23.3.0", + "jest-regex-util": "^23.3.0" }, "dependencies": { "ansi-styles": { @@ -9820,12 +9824,6 @@ "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", "dev": true }, - "ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", - "dev": true - }, "immutable": { "version": "3.7.6", "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz", @@ -10841,13 +10839,13 @@ "dev": true }, "jest": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-23.2.0.tgz", - "integrity": "sha1-govzGgltRdzwaCTR6gMBOve8/CA=", + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-23.3.0.tgz", + "integrity": "sha1-E1XNeS84zyD7pNoC3dt8oU2UhLU=", "dev": true, "requires": { "import-local": "^1.0.0", - "jest-cli": "^23.2.0" + "jest-cli": "^23.3.0" }, "dependencies": { "ansi-regex": { @@ -10871,9 +10869,9 @@ } }, "jest-cli": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-23.2.0.tgz", - "integrity": "sha1-O1Q6PaUUXdiTeTEBcoI3n8aWxFs=", + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-23.3.0.tgz", + "integrity": "sha1-MH6b53M0Q7eJqCedaUBU0FGp5eI=", "dev": true, "requires": { "ansi-escapes": "^3.0.0", @@ -10888,18 +10886,18 @@ "istanbul-lib-instrument": "^1.10.1", "istanbul-lib-source-maps": "^1.2.4", "jest-changed-files": "^23.2.0", - "jest-config": "^23.2.0", - "jest-environment-jsdom": "^23.2.0", + "jest-config": "^23.3.0", + "jest-environment-jsdom": "^23.3.0", "jest-get-type": "^22.1.0", "jest-haste-map": "^23.2.0", - "jest-message-util": "^23.2.0", - "jest-regex-util": "^23.0.0", - "jest-resolve-dependencies": "^23.2.0", - "jest-runner": "^23.2.0", - "jest-runtime": "^23.2.0", - "jest-snapshot": "^23.2.0", - "jest-util": "^23.2.0", - "jest-validate": "^23.2.0", + "jest-message-util": "^23.3.0", + "jest-regex-util": "^23.3.0", + "jest-resolve-dependencies": "^23.3.0", + "jest-runner": "^23.3.0", + "jest-runtime": "^23.3.0", + "jest-snapshot": "^23.3.0", + "jest-util": "^23.3.0", + "jest-validate": "^23.3.0", "jest-watcher": "^23.2.0", "jest-worker": "^23.2.0", "micromatch": "^3.1.10", @@ -10944,23 +10942,23 @@ } }, "jest-config": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-23.2.0.tgz", - "integrity": "sha1-0vtVb9WioZw561bROdzKXa0qHIg=", + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-23.3.0.tgz", + "integrity": "sha1-u01Ttw+VAPr933GNImq7U7E7gyM=", "dev": true, "requires": { "babel-core": "^6.0.0", "babel-jest": "^23.2.0", "chalk": "^2.0.1", "glob": "^7.1.1", - "jest-environment-jsdom": "^23.2.0", - "jest-environment-node": "^23.2.0", + "jest-environment-jsdom": "^23.3.0", + "jest-environment-node": "^23.3.0", "jest-get-type": "^22.1.0", - "jest-jasmine2": "^23.2.0", - "jest-regex-util": "^23.0.0", + "jest-jasmine2": "^23.3.0", + "jest-regex-util": "^23.3.0", "jest-resolve": "^23.2.0", - "jest-util": "^23.2.0", - "jest-validate": "^23.2.0", + "jest-util": "^23.3.0", + "jest-validate": "^23.3.0", "pretty-format": "^23.2.0" }, "dependencies": { @@ -11009,24 +11007,24 @@ } }, "jest-environment-jsdom": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-23.2.0.tgz", - "integrity": "sha1-NjRgOgipdbDKimWDIPVqVKjgRVg=", + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-23.3.0.tgz", + "integrity": "sha1-GQRX+RyeYVRUxBhgVgZdtu16Tio=", "dev": true, "requires": { "jest-mock": "^23.2.0", - "jest-util": "^23.2.0", + "jest-util": "^23.3.0", "jsdom": "^11.5.1" } }, "jest-environment-node": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-23.2.0.tgz", - "integrity": "sha1-tv5BNy44IJO7bz2b32wcTsClDxg=", + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-23.3.0.tgz", + "integrity": "sha1-Ho3yHIR6pdA7dlc/DcFvzeUDTDI=", "dev": true, "requires": { "jest-mock": "^23.2.0", - "jest-util": "^23.2.0" + "jest-util": "^23.3.0" } }, "jest-get-type": { @@ -11062,21 +11060,21 @@ } }, "jest-jasmine2": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-23.2.0.tgz", - "integrity": "sha1-qmcM2x5NX47HdMlN2l4QX+M9i7Q=", + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-23.3.0.tgz", + "integrity": "sha1-qHBrqsI8ihMNWqjvVGSp1JCW0bU=", "dev": true, "requires": { "chalk": "^2.0.1", "co": "^4.6.0", - "expect": "^23.2.0", + "expect": "^23.3.0", "is-generator-fn": "^1.0.0", "jest-diff": "^23.2.0", "jest-each": "^23.2.0", "jest-matcher-utils": "^23.2.0", - "jest-message-util": "^23.2.0", - "jest-snapshot": "^23.2.0", - "jest-util": "^23.2.0", + "jest-message-util": "^23.3.0", + "jest-snapshot": "^23.3.0", + "jest-util": "^23.3.0", "pretty-format": "^23.2.0" } }, @@ -11101,9 +11099,9 @@ } }, "jest-message-util": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-23.2.0.tgz", - "integrity": "sha1-WR6BSP/2nPibBBSAnHIXVuvv50Q=", + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-23.3.0.tgz", + "integrity": "sha1-vAexHOxpcftd2d4t+2DrwiFQwWA=", "dev": true, "requires": { "@babel/code-frame": "^7.0.0-beta.35", @@ -11120,9 +11118,9 @@ "dev": true }, "jest-regex-util": { - "version": "23.0.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-23.0.0.tgz", - "integrity": "sha1-3Vwf3gxG9DcTFM8Q96dRoj9Oj3Y=", + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-23.3.0.tgz", + "integrity": "sha1-X4ZylUfCeFxAAs6qj4Sf6MpHG8U=", "dev": true }, "jest-resolve": { @@ -11137,31 +11135,31 @@ } }, "jest-resolve-dependencies": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-23.2.0.tgz", - "integrity": "sha1-bfjVcJxkBmOc0H9Uv/B04BtcBFg=", + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-23.3.0.tgz", + "integrity": "sha1-hETTsLEoi4CGTYgB/1C0Sk1pXR0=", "dev": true, "requires": { - "jest-regex-util": "^23.0.0", - "jest-snapshot": "^23.2.0" + "jest-regex-util": "^23.3.0", + "jest-snapshot": "^23.3.0" } }, "jest-runner": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-23.2.0.tgz", - "integrity": "sha1-DZGWfqgvcrDHBZEJJghtIFXOda8=", + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-23.3.0.tgz", + "integrity": "sha1-BMfkWKYXUBpIddsNf/vg48vUO/s=", "dev": true, "requires": { "exit": "^0.1.2", "graceful-fs": "^4.1.11", - "jest-config": "^23.2.0", + "jest-config": "^23.3.0", "jest-docblock": "^23.2.0", "jest-haste-map": "^23.2.0", - "jest-jasmine2": "^23.2.0", + "jest-jasmine2": "^23.3.0", "jest-leak-detector": "^23.2.0", - "jest-message-util": "^23.2.0", - "jest-runtime": "^23.2.0", - "jest-util": "^23.2.0", + "jest-message-util": "^23.3.0", + "jest-runtime": "^23.3.0", + "jest-util": "^23.3.0", "jest-worker": "^23.2.0", "source-map-support": "^0.5.6", "throat": "^4.0.0" @@ -11179,9 +11177,9 @@ } }, "jest-runtime": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-23.2.0.tgz", - "integrity": "sha1-YtywF2ahxMZGltwJAgnnbOGq3Lw=", + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-23.3.0.tgz", + "integrity": "sha1-SGWqtM7/gvnOxjNf164UIswd598=", "dev": true, "requires": { "babel-core": "^6.0.0", @@ -11191,14 +11189,14 @@ "exit": "^0.1.2", "fast-json-stable-stringify": "^2.0.0", "graceful-fs": "^4.1.11", - "jest-config": "^23.2.0", + "jest-config": "^23.3.0", "jest-haste-map": "^23.2.0", - "jest-message-util": "^23.2.0", - "jest-regex-util": "^23.0.0", + "jest-message-util": "^23.3.0", + "jest-regex-util": "^23.3.0", "jest-resolve": "^23.2.0", - "jest-snapshot": "^23.2.0", - "jest-util": "^23.2.0", - "jest-validate": "^23.2.0", + "jest-snapshot": "^23.3.0", + "jest-util": "^23.3.0", + "jest-validate": "^23.3.0", "micromatch": "^3.1.10", "realpath-native": "^1.0.0", "slash": "^1.0.0", @@ -11214,30 +11212,35 @@ "dev": true }, "jest-snapshot": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-23.2.0.tgz", - "integrity": "sha1-x6PQFxd7utYMillYac+QqHguan4=", + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-23.3.0.tgz", + "integrity": "sha1-/E6fgeRUMtEFB+J/ULzmD0TYFCQ=", "dev": true, "requires": { + "babel-traverse": "^6.0.0", + "babel-types": "^6.0.0", "chalk": "^2.0.1", "jest-diff": "^23.2.0", "jest-matcher-utils": "^23.2.0", + "jest-message-util": "^23.3.0", + "jest-resolve": "^23.2.0", "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", - "pretty-format": "^23.2.0" + "pretty-format": "^23.2.0", + "semver": "^5.5.0" } }, "jest-util": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-23.2.0.tgz", - "integrity": "sha1-YrdwdXaW2W4JSgS48cNzylClqy4=", + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-23.3.0.tgz", + "integrity": "sha1-efNbsMMBAO9hHZY+5riPjthzqB0=", "dev": true, "requires": { "callsites": "^2.0.0", "chalk": "^2.0.1", "graceful-fs": "^4.1.11", "is-ci": "^1.0.10", - "jest-message-util": "^23.2.0", + "jest-message-util": "^23.3.0", "mkdirp": "^0.5.1", "slash": "^1.0.0", "source-map": "^0.6.0" @@ -11252,9 +11255,9 @@ } }, "jest-validate": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.2.0.tgz", - "integrity": "sha1-Z8i5CeEa8XAXZSOIlMZ6wykbGV4=", + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.3.0.tgz", + "integrity": "sha1-1Jvqaq2YwwrNLLtUJDR5igzBP3Y=", "dev": true, "requires": { "chalk": "^2.0.1", @@ -12790,50 +12793,6 @@ "which": "^1.3.0" } }, - "nodemon": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.17.5.tgz", - "integrity": "sha512-FG2mWJU1Y58a9ktgMJ/RZpsiPz3b7ge77t/okZHEa4NbrlXGKZ8s1A6Q+C7+JPXohAfcPALRwvxcAn8S874pmw==", - "dev": true, - "requires": { - "chokidar": "^2.0.2", - "debug": "^3.1.0", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.0.4", - "pstree.remy": "^1.1.0", - "semver": "^5.5.0", - "supports-color": "^5.2.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.2", - "update-notifier": "^2.3.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "nopt": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", @@ -15881,13 +15840,13 @@ "dev": true }, "prompts": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-0.1.9.tgz", - "integrity": "sha512-RMRvwAUDVUMhP/z3YfDW6igMwT0UnL+w3XCUUNxxHjgwJnVEdHWYJVjM7hQMPub8HCk12xZYAqWlbgLBnqebwg==", + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-0.1.10.tgz", + "integrity": "sha512-/MPwms6+g/m6fvXZlQyOL4m4ziDim2+Wc6CdWVjp+nVCkzEkK2N4rR74m/bbGf+dkta+/SBpo1FfES8Wgrk/Fw==", "dev": true, "requires": { - "clorox": "^1.0.1", - "sisteransi": "^0.1.0" + "clorox": "^1.0.3", + "sisteransi": "^0.1.1" } }, "prop-types": { @@ -15936,15 +15895,6 @@ "integrity": "sha512-+AqO1Ae+N/4r7Rvchrdm432afjT9hqJRyBN3DQv9At0tPz4hIFSGKbq64fN9dVoCow4oggIIax5/iONx0r9hZw==", "dev": true }, - "pstree.remy": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.0.tgz", - "integrity": "sha512-q5I5vLRMVtdWa8n/3UEzZX7Lfghzrg9eG2IKk2ENLSofKRCXVqMvMUHxCKgXNaqH/8ebhBxrqftHWnyTFweJ5Q==", - "dev": true, - "requires": { - "ps-tree": "^1.1.0" - } - }, "public-encrypt": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", @@ -16419,6 +16369,12 @@ "shallowequal": "^1.0.2" } }, + "react-is": { + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.4.1.tgz", + "integrity": "sha512-xpb0PpALlFWNw/q13A+1aHeyJyLYCg0/cCHPUA43zYluZuIPHaHL3k8OBsTgQtxqW0FhyDEMvi8fZ/+7+r4OSQ==", + "dev": true + }, "react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", @@ -16519,6 +16475,23 @@ } } }, + "react-test-renderer": { + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.4.1.tgz", + "integrity": "sha512-wyyiPxRZOTpKnNIgUBOB6xPLTpIzwcQMIURhZvzUqZzezvHjaGNsDPBhMac5fIY3Jf5NuKxoGvV64zDSOECPPQ==", + "dev": true, + "requires": { + "fbjs": "^0.8.16", + "object-assign": "^4.1.1", + "prop-types": "^15.6.0", + "react-is": "^16.4.1" + } + }, + "react-timeago": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/react-timeago/-/react-timeago-4.1.9.tgz", + "integrity": "sha512-MKucv9nU65BOPqIrClAFxqvpGCC4RdRpqp0P1YIb7C3yT6TQVdcoOlr0k4TDHvLQhbkwd3nbTxiDQMa3iDlZxg==" + }, "read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -16568,9 +16541,9 @@ } }, "realpath-native": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.0.0.tgz", - "integrity": "sha512-XJtlRJ9jf0E1H1SLeJyQ9PGzQD7S65h1pRXEcAeK48doKOnKxcgPeNohJvD5u/2sI9J1oke6E8bZHS/fmW1UiQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.0.1.tgz", + "integrity": "sha512-W14EcXuqUvKP8dkWkD7B95iMy77lpMnlFXbbk409bQtNCbeu0kvRE5reo+yIZ3JXxg6frbGsz2DLQ39lrCB40g==", "dev": true, "requires": { "util.promisify": "^1.0.0" @@ -17814,9 +17787,9 @@ } }, "sisteransi": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-0.1.0.tgz", - "integrity": "sha512-kHXcIr0Z9FM6d7pwFDDIMQKGndIEtIF1oBSMXWtItpx4mrH1jhANVNT35GVekBekXl6J+5i7lJMIGq3Gm7pIdA==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-0.1.1.tgz", + "integrity": "sha512-PmGOd02bM9YO5ifxpw36nrNMBTptEtfRl4qUYl9SndkolplkrZZOW7PGHjrZL53QvMVj9nQ+TKqUnRsw4tJa4g==", "dev": true }, "slash": { @@ -18801,15 +18774,6 @@ "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=", "dev": true }, - "touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "dev": true, - "requires": { - "nopt": "~1.0.10" - } - }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", @@ -19684,15 +19648,6 @@ "integrity": "sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==", "dev": true }, - "undefsafe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", - "integrity": "sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY=", - "dev": true, - "requires": { - "debug": "^2.2.0" - } - }, "unherit": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.1.tgz", diff --git a/package.json b/package.json index 5f615b41f..96eed95dc 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,11 @@ "scripts": { "start": "node dist/index.js", "test": "node scripts/test.js --env=jsdom", - "build": "npm-run-all --parallel compile:* && npm-run-all --parallel build:*", + "build": "npm-run-all compile --parallel build:*", "build:client": "node ./scripts/build.js", "build:server": "tsc -p ./src/tsconfig.json", "watch": "NODE_ENV=development ts-node ./scripts/watcher/bin/watcher.ts ./config/watcher.ts", + "compile": "npm-run-all --parallel compile:*", "compile:css-types": "tcm src/core/client/", "compile:relay-stream": "relay-compiler --src ./src/core/client/stream --schema $(ts-node ./scripts/schemaPath.ts tenant) --language typescript --artifactDirectory ./src/core/client/stream/__generated__ --no-watchman", "start:development": "NODE_ENV=development ts-node --project ./src/tsconfig.json -r tsconfig-paths/register ./src/index.ts", @@ -44,6 +45,7 @@ "mongodb": "^3.0.10", "passport": "^0.4.0", "performance-now": "^2.1.0", + "react-timeago": "^4.1.9", "subscriptions-transport-ws": "^0.9.11", "uuid": "^3.2.1" }, @@ -72,7 +74,8 @@ "@types/passport": "^0.4.5", "@types/query-string": "^6.1.0", "@types/react-dom": "^16.0.6", - "@types/react-relay": "^1.3.6", + "@types/react-relay": "github:coralproject/patched#types/react-relay", + "@types/react-test-renderer": "^16.0.1", "@types/recompose": "^0.26.1", "@types/relay-runtime": "github:coralproject/patched#types/relay-runtime", "@types/uuid": "^3.4.3", @@ -100,9 +103,8 @@ "fluent-react": "^0.7.0", "graphql-playground-middleware-express": "^1.7.0", "html-webpack-plugin": "^3.2.0", - "jest": "^23.2.0", + "jest": "^23.3.0", "loader-utils": "^1.1.0", - "nodemon": "^1.17.5", "npm-run-all": "^4.1.3", "postcss-flexbugs-fixes": "^3.3.1", "postcss-font-magician": "^2.2.1", @@ -116,6 +118,7 @@ "react-dom": "^16.4.0", "react-final-form": "^3.6.0", "react-relay": "github:coralproject/patched#react-relay", + "react-test-renderer": "^16.4.1", "recompose": "^0.27.1", "relay-compiler": "github:coralproject/patched#relay-compiler", "relay-compiler-language-typescript": "github:coralproject/patched#relay-compiler-language-typescript", @@ -141,4 +144,4 @@ "webpack-hot-client": "^4.0.3", "webpack-manifest-plugin": "^2.0.3" } -} \ No newline at end of file +} diff --git a/scripts/test.js b/scripts/test.js index 1ac08fd2e..f08b18de6 100644 --- a/scripts/test.js +++ b/scripts/test.js @@ -19,11 +19,11 @@ const paths = require("../config/paths"); const jest = require("jest"); let argv = process.argv.slice(2); +argv.push("--config", paths.appJestConfig); // Watch unless on CI or in coverage mode if (!process.env.CI && argv.indexOf("--coverage") < 0) { argv.push("--watch"); - argv.push("--config", paths.appJestConfig); } jest.run(argv); diff --git a/scripts/watcher/CommandExecutor.ts b/scripts/watcher/CommandExecutor.ts index 60cf15e6a..fddba0b8b 100644 --- a/scripts/watcher/CommandExecutor.ts +++ b/scripts/watcher/CommandExecutor.ts @@ -5,13 +5,13 @@ import { Executor } from "./types"; interface CommandExecutorOptions { args?: ReadonlyArray; - // If true, allow spawning multiple processes. + /** If true, allow spawning multiple processes. */ spawnMutiple?: boolean; - // Specify the period in which the process is started at max once. + /** Specify the period in which the process is started at max once. */ debounce?: number | false; - // If true, will run command upon initialization. + /** If true, will run command upon initialization. */ runOnInit?: boolean; } diff --git a/scripts/watcher/LongRunningExecutor.ts b/scripts/watcher/LongRunningExecutor.ts index cffe6f7db..7019c1fb3 100644 --- a/scripts/watcher/LongRunningExecutor.ts +++ b/scripts/watcher/LongRunningExecutor.ts @@ -6,7 +6,7 @@ import { Executor } from "./types"; interface LongRunningExecutorOptions { args?: ReadonlyArray; - // Specify the period in which the process is restarted at max once. + /** Specify the period in which the process is restarted at max once. */ debounce?: number; } diff --git a/scripts/watcher/bin/watcher.ts b/scripts/watcher/bin/watcher.ts index 32209c064..a3a50e6c3 100644 --- a/scripts/watcher/bin/watcher.ts +++ b/scripts/watcher/bin/watcher.ts @@ -4,16 +4,24 @@ import program from "commander"; import path from "path"; import watch from "../"; +function list(val: string) { + return val.split(","); +} + program .version("0.1.0") .usage("") + .option("-o, --only ", "only run the specified watcher", list) .arguments("") .description("Run watchers defined in ") - .action(configFile => { + .action((configFile, cmd) => { + const { only = [] } = cmd; + let config: any = require(path.resolve(configFile)); if (config.__esModule) { config = config.default; } - watch(config); + + watch(config, { only }); }) .parse(process.argv); diff --git a/scripts/watcher/types.ts b/scripts/watcher/types.ts index f78daee24..15bd6090b 100644 --- a/scripts/watcher/types.ts +++ b/scripts/watcher/types.ts @@ -17,6 +17,10 @@ export interface Executor { execute(filePath: string): void; } +export interface Options { + only?: string[]; +} + export interface Config { rootDir?: string; backend?: Watcher; diff --git a/scripts/watcher/watch.ts b/scripts/watcher/watch.ts index 572f7107b..8a3feac75 100644 --- a/scripts/watcher/watch.ts +++ b/scripts/watcher/watch.ts @@ -2,7 +2,7 @@ import Joi from "joi"; import path from "path"; import ChokidarWatcher from "./ChokidarWatcher"; -import { Config, configSchema, WatchConfig, Watcher } from "./types"; +import { Config, configSchema, Options, WatchConfig, Watcher } from "./types"; // Polyfill the asyncIterator symbol. if (Symbol.asyncIterator === undefined) { @@ -43,9 +43,22 @@ function setupCleanup(config: Config) { ); } -export default async function watch(config: Config) { +function filterOnly(config: Config, only: string[]) { + for (const key of Object.keys(config.watchers)) { + if (only.indexOf(key) === -1) { + // tslint:disable-next-line:no-console + console.log(`Disabled watcher "${key}"`); + delete config.watchers[key]; + } + } +} + +export default async function watch(config: Config, options?: Options) { Joi.assert(config, configSchema); const watcher = config.backend || new ChokidarWatcher(); + if (options && options.only && options.only.length > 0) { + filterOnly(config, options.only); + } setupCleanup(config); for (const key of Object.keys(config.watchers)) { // tslint:disable-next-line:no-console diff --git a/src/core/client/framework/lib/bootstrap/TalkContext.tsx b/src/core/client/framework/lib/bootstrap/TalkContext.tsx index 93bc84c43..cfa8c50c5 100644 --- a/src/core/client/framework/lib/bootstrap/TalkContext.tsx +++ b/src/core/client/framework/lib/bootstrap/TalkContext.tsx @@ -1,7 +1,9 @@ import { LocalizationProvider } from "fluent-react/compat"; import { MessageContext } from "fluent/compat"; import React, { StatelessComponent } from "react"; +import { Formatter } from "react-timeago"; import { Environment } from "relay-runtime"; +import { UIContext } from "talk-ui/components"; export interface TalkContext { // relayEnvironment for our relay framework. @@ -9,6 +11,9 @@ export interface TalkContext { // localMessages for our i18n framework. localeMessages: MessageContext[]; + + // formatter for timeago. + timeagoFormatter: Formatter; } const { Provider, Consumer } = React.createContext({} as any); @@ -27,7 +32,9 @@ export const TalkContextProvider: StatelessComponent<{ }> = ({ value, children }) => ( - {children} + + {children} + ); diff --git a/src/core/client/framework/lib/bootstrap/createContext.ts b/src/core/client/framework/lib/bootstrap/createContext.tsx similarity index 71% rename from src/core/client/framework/lib/bootstrap/createContext.ts rename to src/core/client/framework/lib/bootstrap/createContext.tsx index 764d09214..6043eacb9 100644 --- a/src/core/client/framework/lib/bootstrap/createContext.ts +++ b/src/core/client/framework/lib/bootstrap/createContext.tsx @@ -1,4 +1,7 @@ +import { Localized } from "fluent-react/compat"; import { noop } from "lodash"; +import React from "react"; +import { Formatter } from "react-timeago"; import { Environment, Network, RecordSource, Store } from "relay-runtime"; import { generateMessages, LocalesData, negotiateLanguages } from "../i18n"; @@ -16,6 +19,25 @@ interface CreateContextArguments { init?: ((context: TalkContext) => void | Promise); } +/** + * timeagoFormatter integrates timeago into our translation + * framework. It gets injected into the UIContext. + */ +export const timeagoFormatter: Formatter = (value, unit, suffix) => { + // We use 'in' instead of 'from now' for language consistency + const ourSuffix = suffix === "from now" ? "in" : suffix; + return ( + + now + + ); +}; + /** * `createContext` manages the dependencies of our framework * and returns a `TalkContext` that can be passed to the @@ -46,6 +68,7 @@ export default async function createContext({ const context = { relayEnvironment, localeMessages, + timeagoFormatter, }; // Run custom initializations. diff --git a/src/core/client/stream/components/Comment.spec.tsx b/src/core/client/stream/components/Comment.spec.tsx new file mode 100644 index 000000000..9e9284c4e --- /dev/null +++ b/src/core/client/stream/components/Comment.spec.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { createRenderer } from "react-test-renderer/shallow"; +import Comment from "./Comment"; + +it("renders username and body", () => { + const props = { + author: { + username: "Marvin", + }, + body: "Woof", + createdAt: new Date("December 17, 1995 03:24:00").toISOString(), + }; + const renderer = createRenderer(); + renderer.render(); + expect(renderer.getRenderOutput()).toMatchSnapshot(); +}); + +it("renders with gutterBottom", () => { + const props = { + author: { + username: "Marvin", + }, + body: "Woof", + createdAt: new Date("December 17, 1995 03:24:00").toISOString(), + gutterBottom: true, + }; + const renderer = createRenderer(); + renderer.render(); + expect(renderer.getRenderOutput()).toMatchSnapshot(); +}); diff --git a/src/core/client/stream/components/Comment.tsx b/src/core/client/stream/components/Comment.tsx index c1b3f9303..e6620529b 100644 --- a/src/core/client/stream/components/Comment.tsx +++ b/src/core/client/stream/components/Comment.tsx @@ -2,7 +2,7 @@ import cn from "classnames"; import React from "react"; import { StatelessComponent } from "react"; -import { Typography } from "talk-ui/components"; +import { Timestamp, Typography } from "talk-ui/components"; import * as styles from "./Comment.css"; @@ -12,6 +12,7 @@ export interface CommentProps { username: string; } | null; body: string | null; + createdAt: string; gutterBottom?: boolean; } @@ -24,6 +25,7 @@ const Comment: StatelessComponent = props => { {props.author && props.author.username} + {props.body} ); diff --git a/src/core/client/stream/components/__snapshots__/Comment.spec.tsx.snap b/src/core/client/stream/components/__snapshots__/Comment.spec.tsx.snap new file mode 100644 index 000000000..9380fd3d4 --- /dev/null +++ b/src/core/client/stream/components/__snapshots__/Comment.spec.tsx.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders username and body 1`] = ` +
+ + Marvin + + + + Woof + +
+`; + +exports[`renders with gutterBottom 1`] = ` +
+ + Marvin + + + + Woof + +
+`; diff --git a/src/core/client/stream/containers/CommentContainer.tsx b/src/core/client/stream/containers/CommentContainer.tsx index 9a4d03cd8..7b3f66093 100644 --- a/src/core/client/stream/containers/CommentContainer.tsx +++ b/src/core/client/stream/containers/CommentContainer.tsx @@ -20,6 +20,7 @@ const enhanced = withFragmentContainer<{ data: Data }>( author { username } + createdAt body } ` diff --git a/src/core/client/tsconfig.json b/src/core/client/tsconfig.json index ffb2142b6..e8fb8eeab 100644 --- a/src/core/client/tsconfig.json +++ b/src/core/client/tsconfig.json @@ -5,40 +5,17 @@ "module": "esnext", "jsx": "preserve", "allowJs": false, - "lib": [ - "dom", - "es7", - "scripthost", - "es2015", - "esnext.asynciterable" - ], + "lib": ["dom", "es7", "scripthost", "es2015", "esnext.asynciterable"], "baseUrl": "./", "paths": { - "talk-admin/*": [ - "./admin/*" - ], - "talk-stream/*": [ - "./stream/*" - ], - "talk-framework/*": [ - "./framework/*" - ], - "talk-ui/*": [ - "./ui/*" - ], - "talk-common/*": [ - "../common/*" - ], - "talk-locales/*": [ - "../../locales/*" - ] + "talk-admin/*": ["./admin/*"], + "talk-stream/*": ["./stream/*"], + "talk-framework/*": ["./framework/*"], + "talk-ui/*": ["./ui/*"], + "talk-common/*": ["../common/*"], + "talk-locales/*": ["../../locales/*"] } }, - "include": [ - "./**/*", - "../../types/**/*.d.ts" - ], - "exclude": [ - "node_modules" - ] -} \ No newline at end of file + "include": ["./**/*", "../../types/**/*.d.ts"], + "exclude": ["node_modules"] +} diff --git a/src/core/client/ui/components/Timestamp/Timestamp.css b/src/core/client/ui/components/Timestamp/Timestamp.css new file mode 100644 index 000000000..3faa5e162 --- /dev/null +++ b/src/core/client/ui/components/Timestamp/Timestamp.css @@ -0,0 +1,4 @@ +.root { + composes: body1 from "talk-ui/shared/typography.css"; + background-color: transparent; +} diff --git a/src/core/client/ui/components/Timestamp/Timestamp.mdx b/src/core/client/ui/components/Timestamp/Timestamp.mdx new file mode 100644 index 000000000..07c205ece --- /dev/null +++ b/src/core/client/ui/components/Timestamp/Timestamp.mdx @@ -0,0 +1,15 @@ +--- +name: Timestamp +menu: UI Kit +--- + +import { Playground } from 'docz' +import Timestamp from './Timestamp' + +# Timestamp + +## Basic usage + + + + diff --git a/src/core/client/ui/components/Timestamp/Timestamp.spec.tsx b/src/core/client/ui/components/Timestamp/Timestamp.spec.tsx new file mode 100644 index 000000000..17043020c --- /dev/null +++ b/src/core/client/ui/components/Timestamp/Timestamp.spec.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { create } from "react-test-renderer"; + +import UIContext from "../UIContext"; +import Timestamp from "./Timestamp"; + +it("uses default formatter", () => { + const props = { + date: new Date("December 17, 2108 03:24:00").toISOString(), + }; + const tree = create().toJSON(); + + expect(tree).toMatchSnapshot(); +}); + +it("uses formatter from context", () => { + const context: any = { + timeagoFormatter: () => "My Formatter", + }; + const props = { + date: new Date("December 17, 2108 03:24:00").toISOString(), + }; + const tree = create( + + + + ).toJSON(); + + expect(tree).toMatchSnapshot(); +}); diff --git a/src/core/client/ui/components/Timestamp/Timestamp.tsx b/src/core/client/ui/components/Timestamp/Timestamp.tsx new file mode 100644 index 000000000..004092b11 --- /dev/null +++ b/src/core/client/ui/components/Timestamp/Timestamp.tsx @@ -0,0 +1,41 @@ +import cn from "classnames"; +import React from "react"; +import TimeAgo, { Formatter } from "react-timeago"; + +import { UIContext } from "talk-ui/components"; +import { withStyles } from "talk-ui/hocs"; +import { PropTypesOf } from "talk-ui/types"; + +import * as styles from "./Timestamp.css"; + +interface InnerProps { + date: string; + live?: boolean; + classes: typeof styles; + className?: string; +} + +const defaultFormatter: Formatter = (value, unit, suffix, timestamp: string) => + new Date(timestamp).toISOString(); + +class Timestamp extends React.Component { + public render() { + const { date, classes, live, className } = this.props; + return ( + + {({ timeagoFormatter }) => ( + + )} + + ); + } +} + +const enhanced = withStyles(styles)(Timestamp); +export type TimestampProps = PropTypesOf; +export default enhanced; diff --git a/src/core/client/ui/components/Timestamp/__snapshots__/Timestamp.spec.tsx.snap b/src/core/client/ui/components/Timestamp/__snapshots__/Timestamp.spec.tsx.snap new file mode 100644 index 000000000..fb4af9b41 --- /dev/null +++ b/src/core/client/ui/components/Timestamp/__snapshots__/Timestamp.spec.tsx.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`uses default formatter 1`] = ` + +`; + +exports[`uses formatter from context 1`] = ` + +`; diff --git a/src/core/client/ui/components/Timestamp/index.ts b/src/core/client/ui/components/Timestamp/index.ts new file mode 100644 index 000000000..880bc3790 --- /dev/null +++ b/src/core/client/ui/components/Timestamp/index.ts @@ -0,0 +1,2 @@ +export * from "./Timestamp"; +export { default } from "./Timestamp"; diff --git a/src/core/client/ui/components/UIContext/UIContext.ts b/src/core/client/ui/components/UIContext/UIContext.ts new file mode 100644 index 000000000..b3f4fd338 --- /dev/null +++ b/src/core/client/ui/components/UIContext/UIContext.ts @@ -0,0 +1,10 @@ +import React from "react"; +import { Formatter } from "react-timeago"; + +export interface UIContext { + timeagoFormatter: Formatter; +} + +const UIContext = React.createContext({} as any); + +export default UIContext; diff --git a/src/core/client/ui/components/UIContext/index.ts b/src/core/client/ui/components/UIContext/index.ts new file mode 100644 index 000000000..fb7f47676 --- /dev/null +++ b/src/core/client/ui/components/UIContext/index.ts @@ -0,0 +1 @@ +export { default } from "./UIContext"; diff --git a/src/core/client/ui/components/index.ts b/src/core/client/ui/components/index.ts index 54643cb2a..24a30ff07 100644 --- a/src/core/client/ui/components/index.ts +++ b/src/core/client/ui/components/index.ts @@ -2,3 +2,5 @@ export { default as BaseButton } from "./BaseButton"; export { default as Button } from "./Button"; export { default as Center } from "./Center"; export { default as Typography } from "./Typography"; +export { default as Timestamp } from "./Timestamp"; +export { default as UIContext } from "./UIContext"; diff --git a/src/core/client/ui/shared/typography.css b/src/core/client/ui/shared/typography.css index 649460503..b64007081 100644 --- a/src/core/client/ui/shared/typography.css +++ b/src/core/client/ui/shared/typography.css @@ -33,11 +33,14 @@ color: $palette-text-primary; } -.subtitle1 {} +.subtitle1 { +} -.subtitle2 {} +.subtitle2 { +} -.body2 {} +.body2 { +} .body1 { font-size: calc(16rem / $rem-base); @@ -56,4 +59,5 @@ letter-spacing: calc(0.57em / 16); } -.overline {} +.overline { +} diff --git a/src/core/server/app/router.ts b/src/core/server/app/router.ts index fd481ee5c..eca3bbbf2 100644 --- a/src/core/server/app/router.ts +++ b/src/core/server/app/router.ts @@ -43,7 +43,7 @@ async function createTenantRouter(opts: AppOptions) { async function createAPIRouter(opts: AppOptions) { // Create a router. const router = express.Router(); - + // Configure the tenant routes. router.use("/tenant", await createTenantRouter(opts)); diff --git a/src/core/server/graph/tenant/resolvers/comment.ts b/src/core/server/graph/tenant/resolvers/comment.ts index 22f72b5e5..d4033e0cb 100644 --- a/src/core/server/graph/tenant/resolvers/comment.ts +++ b/src/core/server/graph/tenant/resolvers/comment.ts @@ -2,6 +2,8 @@ import Context from "talk-server/graph/tenant/context"; import { Comment, ConnectionInput } from "talk-server/models/comment"; export default { + createdAt: async (comment: Comment, _: any, ctx: Context) => + comment.created_at, author: async (comment: Comment, _: any, ctx: Context) => ctx.loaders.Users.user.load(comment.author_id), replies: async (comment: Comment, input: ConnectionInput, ctx: Context) => diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index eb060b6a4..2585b9b19 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -202,6 +202,11 @@ type Comment { """ body: String + """ + createdAt is the date in which the comment was created. + """ + createdAt: Time + """ author is the User that authored the Comment. """ diff --git a/src/locales/de/framework.ftl b/src/locales/de/framework.ftl index fbb6cb340..de2d31278 100644 --- a/src/locales/de/framework.ftl +++ b/src/locales/de/framework.ftl @@ -3,3 +3,41 @@ ### among different targets. framework-validation-required = Dies ist ein Pflichtpfeld. + +framework-timeago = + { $suffix -> + [ago] vor + [in] in + } + { $value } + { $unit -> + [second] { $value -> + [1] Sekunde + *[other] Sekunden + } + [minuto] { $value -> + [1] Minute + *[other] Minuten + } + [hour] { $value -> + [0] Stunde + *[other] Stunden + } + [day] { $value -> + [1] Tag + *[other] Tage + } + [week] { $value -> + [1] Woche + *[other] Wochen + } + [month] { $value -> + [1] Monat + *[other] Monate + } + [year] { $value -> + [1] Jahr + *[other] Jahre + } + *[other] unknown unit + } diff --git a/src/locales/en-US/framework.ftl b/src/locales/en-US/framework.ftl index d50f28b07..d6a9fafdd 100644 --- a/src/locales/en-US/framework.ftl +++ b/src/locales/en-US/framework.ftl @@ -3,3 +3,48 @@ ### among different targets. framework-validation-required = This field is required. + +framework-timeago-time = + { $value } + { $unit -> + [second] { $value -> + [1] second + *[other] seconds + } + [minute] { $value -> + [1] minute + *[other] minutes + } + [hour] { $value -> + [0] hour + *[other] hours + } + [day] { $value -> + [1] day + *[other] days + } + [week] { $value -> + [1] week + *[other] weeks + } + [month] { $value -> + [1] month + *[other] months + } + [year] { $value -> + [1] year + *[other] years + } + *[other] unknown unit + } + +framework-timeago = + { $value -> + [0] now + *[other] + { $suffix -> + [ago] {framework-timeago-time} ago + [in] in {framework-timeago-time} + *[other] unknown suffix + } + } diff --git a/src/locales/es/common.ftl b/src/locales/es/common.ftl new file mode 100644 index 000000000..e69de29bb diff --git a/src/locales/es/framework.ftl b/src/locales/es/framework.ftl index 9cfdf012f..2b95d944c 100644 --- a/src/locales/es/framework.ftl +++ b/src/locales/es/framework.ftl @@ -2,3 +2,45 @@ ### All keys must start with `framework` because this file is shared ### among different targets. +framework-timeago = + { $value -> + [0] ahora + *[other] + { $suffix -> + [ago] hace + [in] en + *[other] unknown suffix + } + { $value } + { $unit -> + [second] { $value -> + [1] segundo + *[other] segundos + } + [minute] { $value -> + [1] minuto + *[other] minutos + } + [hour] { $value -> + [0] hora + *[other] horas + } + [day] { $value -> + [1] dia + *[other] dias + } + [week] { $value -> + [1] semana + *[other] semanas + } + [month] { $value -> + [1] mes + *[other] meses + } + [year] { $value -> + [1] año + *[other] años + } + *[other] unknown unit + } + } diff --git a/src/tsconfig.json b/src/tsconfig.json index eddf98d29..857892f4d 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -9,26 +9,13 @@ "outDir": "../dist", // See https://github.com/prismagraphql/graphql-request/issues/26 for why we // have to include "dom" here. - "lib": [ - "es6", - "esnext.asynciterable", - "dom" - ], + "lib": ["es6", "esnext.asynciterable", "dom"], "baseUrl": "./", "paths": { - "talk-server/*": [ - "./core/server/*" - ], - "talk-common/*": [ - "./core/common/*" - ] + "talk-server/*": ["./core/server/*"], + "talk-common/*": ["./core/common/*"] } }, - "include": [ - "./**/*" - ], - "exclude": [ - "node_modules", - "./core/client" - ] -} \ No newline at end of file + "include": ["./**/*"], + "exclude": ["node_modules", "./core/client"] +} diff --git a/src/types/react-timeago.d.ts b/src/types/react-timeago.d.ts new file mode 100644 index 000000000..431bbf88e --- /dev/null +++ b/src/types/react-timeago.d.ts @@ -0,0 +1,55 @@ +declare module "react-timeago" { + import React from "react"; + + export type Formatter = ( + value: number, + unit: "second" | "minute" | "hour" | "day" | "week" | "month" | "year", + suffix: "ago" | "from now", + epochMiliseconds: string + ) => string | React.ReactElement; + + export interface LocaleDefinition { + prefixAgo?: string; + prefixFromNow?: string; + suffixAgo?: string; + suffixFromNow?: string; + second?: string; + seconds?: string; + minute?: string; + minutes?: string; + hour?: string; + hours?: string; + day?: string; + days?: string; + week?: string; + weeks?: string; + month?: string; + months?: string; + year?: string; + years?: string; + wordSeparator?: string; + numbers?: number[]; + } + + export interface TimeAgoProps { + date: string; + live?: boolean; + className: string; + formatter?: Formatter; + } + + const TimeAgo: React.ComponentType; + export default TimeAgo; +} + +declare module "react-timeago/lib/formatters/buildFormatter" { + import { Formatter, LocaleDefinition } from "react-timeago"; + function buildFormatter(localeInput: LocaleDefinition): Formatter; + export default buildFormatter; +} + +declare module "react-timeago/lib/language-strings/*" { + import { LocaleDefinition } from "react-timeago"; + const localeStrings: LocaleDefinition; + export default localeStrings; +}