mirror of
https://github.com/wassname/talk.git
synced 2026-07-06 05:17:19 +08:00
Merge branch 'auth-views' of github.com:coralproject/talk into auth-views
This commit is contained in:
@@ -30,3 +30,39 @@
|
||||
2. Tenant RedisPubSub Publisher
|
||||
3. Management RedisPubSub Subscriber
|
||||
4. Management RedisPubSub Publisher
|
||||
|
||||
## Scripts
|
||||
|
||||
### Embed
|
||||
|
||||
Embed Script - Renders the iFrame <-- does not have a html page in production (should be on server?)
|
||||
|
||||
/dist/static/assets/embed.js /static/embed.js
|
||||
|
||||
### Stream
|
||||
|
||||
Stream - Renders the comment stream <-- data
|
||||
|
||||
/dist/static/assets/stream.<HASH>.css /static/assets/stream.<HASH>.css
|
||||
/dist/static/assets/stream.<HASH>.js /static/assets/stream.<HASH>.js
|
||||
/dist/static/stream.html /embed/stream
|
||||
|
||||
### Admin
|
||||
|
||||
Admin - Renders the Admin page <-- data
|
||||
|
||||
/dist/static/assets/admin.<HASH>.css /static/assets/admin.<HASH>.css
|
||||
/dist/static/assets/admin.<HASH>.js /static/assets/admin.<HASH>.js
|
||||
/dist/static/admin.html /admin
|
||||
|
||||
## Development Routes
|
||||
|
||||
|
||||
localhost:3000
|
||||
/ -> /admin
|
||||
/dev <-- server side html for dev/iframe integration
|
||||
|
||||
localhost:8080
|
||||
/ -> localhost:3000/dev
|
||||
/embed/stream <-- stream html (now is at /)
|
||||
/admin <-- stream html (now is not there)
|
||||
+1
-1
@@ -10,7 +10,7 @@ const config: Config = {
|
||||
watchers: {
|
||||
compileSchema: {
|
||||
paths: ["core/server/**/*.graphql"],
|
||||
executor: new CommandExecutor("npm run compile:schema", {
|
||||
executor: new CommandExecutor("npx gulp server:schema", {
|
||||
runOnInit: true,
|
||||
}),
|
||||
},
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
const path = require("path");
|
||||
const json = require("comment-json");
|
||||
const fs = require("fs");
|
||||
const del = require("del");
|
||||
const gulp = require("gulp");
|
||||
const ts = require("gulp-typescript");
|
||||
const sourcemaps = require("gulp-sourcemaps");
|
||||
const babel = require("gulp-babel");
|
||||
const generateTypescriptTypes = require("./scripts/generateSchemaTypes");
|
||||
const _ = require("lodash");
|
||||
|
||||
// Parse the tsconfig.json file (which contains comments).
|
||||
const tsconfig = json.parse(fs.readFileSync("./src/tsconfig.json").toString());
|
||||
|
||||
// Create the typescript project.
|
||||
const tsProject = ts.createProject("./src/tsconfig.json");
|
||||
|
||||
const resolveDistFolder = (...paths) => "./" + path.join("dist", ...paths);
|
||||
|
||||
gulp.task("clean", () => del([resolveDistFolder()]));
|
||||
|
||||
gulp.task("server:schema", () => generateTypescriptTypes());
|
||||
|
||||
gulp.task("server:scripts", () =>
|
||||
gulp
|
||||
.src([
|
||||
"./src/**/*.ts",
|
||||
"./src/**/.*.ts",
|
||||
// Exclude client files from this, that's for webpack.
|
||||
"!./src/core/client/**/*.ts",
|
||||
"!./src/core/client/**/.*.ts",
|
||||
])
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(tsProject())
|
||||
.pipe(
|
||||
babel({
|
||||
plugins: [
|
||||
[
|
||||
"module-resolver",
|
||||
{
|
||||
root: [resolveDistFolder()],
|
||||
alias: _.reduce(
|
||||
tsconfig.compilerOptions.paths,
|
||||
(alias, [pathGlob], prefixBlob) => ({
|
||||
...alias,
|
||||
[prefixBlob.replace(/\/\*/g, "")]: pathGlob
|
||||
.replace(/\/\*/g, "")
|
||||
.replace(/^\./g, resolveDistFolder()),
|
||||
}),
|
||||
{}
|
||||
),
|
||||
},
|
||||
],
|
||||
],
|
||||
})
|
||||
)
|
||||
.pipe(sourcemaps.write("."))
|
||||
.pipe(gulp.dest(resolveDistFolder()))
|
||||
);
|
||||
|
||||
gulp.task("server:static", () =>
|
||||
gulp
|
||||
.src(["./src/core/server/**/*", "!./src/core/server/**/*.ts"])
|
||||
.pipe(gulp.dest(resolveDistFolder() + "/core/server"))
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"server",
|
||||
gulp.series("server:schema", gulp.parallel("server:scripts", "server:static"))
|
||||
);
|
||||
|
||||
gulp.task("default", gulp.series("clean", "server"));
|
||||
Generated
+1752
-288
File diff suppressed because it is too large
Load Diff
+20
-8
@@ -5,7 +5,7 @@
|
||||
"scripts": {
|
||||
"build": "npm-run-all compile --parallel build:*",
|
||||
"build:client": "ts-node ./scripts/build.ts",
|
||||
"build:server": "tsc -p ./src/tsconfig.json",
|
||||
"build:server": "gulp server",
|
||||
"compile": "npm-run-all --parallel compile:*",
|
||||
"compile:css-types": "tcm src/core/client/",
|
||||
"compile:relay-stream": "ts-node ./scripts/compileRelay --src ./src/core/client/stream --schema tenant",
|
||||
@@ -13,7 +13,7 @@
|
||||
"compile:schema": "node ./scripts/generateSchemaTypes.js",
|
||||
"docz": "docz",
|
||||
"start": "node dist/index.js",
|
||||
"start:development": "cross-env NODE_ENV=development ts-node --project ./src/tsconfig.json -r tsconfig-paths/register ./src/index.ts",
|
||||
"start:development": "ts-node --project ./src/tsconfig.json -r tsconfig-paths/register ./src/index.ts",
|
||||
"start:webpackDevServer": "ts-node ./scripts/start.ts",
|
||||
"lint": "npm-run-all --parallel lint:*",
|
||||
"lint:server": "tslint --project ./src/tsconfig.json",
|
||||
@@ -27,7 +27,7 @@
|
||||
"tscheck:client": "tsc --project ./src/core/client/tsconfig.json --noEmit",
|
||||
"tscheck:client-embed": "tsc --project ./src/core/client/embed/tsconfig.json --noEmit",
|
||||
"tscheck:scripts": "tsc --project ./tsconfig.json --noEmit",
|
||||
"watch": "cross-env NODE_ENV=development ts-node ./scripts/watcher/bin/watcher.ts --config ./config/watcher.ts"
|
||||
"watch": "NODE_ENV=development ts-node ./scripts/watcher/bin/watcher.ts --config ./config/watcher.ts"
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
@@ -36,6 +36,7 @@
|
||||
"apollo-server-express": "^1.3.6",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bunyan": "^1.8.12",
|
||||
"consolidate": "0.14.0",
|
||||
"convict": "^4.3.1",
|
||||
"dataloader": "^1.4.0",
|
||||
"dotenv": "^6.0.0",
|
||||
@@ -56,6 +57,7 @@
|
||||
"lodash": "^4.17.10",
|
||||
"luxon": "^1.3.1",
|
||||
"mongodb": "^3.1.1",
|
||||
"nunjucks": "^3.1.3",
|
||||
"passport": "^0.4.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-oauth2": "^1.4.0",
|
||||
@@ -78,6 +80,7 @@
|
||||
"@types/chokidar": "^1.7.5",
|
||||
"@types/classnames": "^2.2.4",
|
||||
"@types/commander": "^2.12.2",
|
||||
"@types/consolidate": "0.0.34",
|
||||
"@types/convict": "^4.2.0",
|
||||
"@types/cross-spawn": "^6.0.0",
|
||||
"@types/dotenv": "^4.0.3",
|
||||
@@ -85,7 +88,6 @@
|
||||
"@types/enzyme-adapter-react-16": "^1.0.2",
|
||||
"@types/eventemitter2": "^4.1.0",
|
||||
"@types/express": "^4.16.0",
|
||||
"@types/extract-text-webpack-plugin": "^3.0.3",
|
||||
"@types/fs-extra": "^5.0.4",
|
||||
"@types/graphql": "^0.13.3",
|
||||
"@types/html-webpack-plugin": "^2.30.4",
|
||||
@@ -97,8 +99,10 @@
|
||||
"@types/linkify-it": "^2.0.3",
|
||||
"@types/lodash": "^4.14.111",
|
||||
"@types/luxon": "^0.5.3",
|
||||
"@types/mini-css-extract-plugin": "^0.2.0",
|
||||
"@types/mongodb": "^3.1.1",
|
||||
"@types/node": "^10.5.2",
|
||||
"@types/nunjucks": "^3.0.0",
|
||||
"@types/passport": "^0.4.6",
|
||||
"@types/passport-local": "^1.0.33",
|
||||
"@types/passport-oauth2": "^1.4.5",
|
||||
@@ -123,6 +127,7 @@
|
||||
"autoprefixer": "^8.6.5",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-loader": "^8.0.0-beta",
|
||||
"babel-plugin-module-resolver": "^3.1.1",
|
||||
"babel-plugin-relay": "github:coralproject/patched#babel-plugin-relay",
|
||||
"babel-preset-react-optimize": "^1.0.1",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.1.2",
|
||||
@@ -130,16 +135,16 @@
|
||||
"chokidar": "^2.0.4",
|
||||
"classnames": "^2.2.5",
|
||||
"commander": "^2.16.0",
|
||||
"comment-json": "^1.1.3",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
"cross-env": "^5.2.0",
|
||||
"cross-spawn": "^6.0.5",
|
||||
"css-loader": "^0.28.11",
|
||||
"del": "^3.0.0",
|
||||
"docz": "^0.5.8",
|
||||
"enzyme": "^3.3.0",
|
||||
"enzyme-adapter-react-16": "^1.1.1",
|
||||
"enzyme-to-json": "^3.3.4",
|
||||
"eventemitter2": "^5.0.1",
|
||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||
"final-form": "^4.8.1",
|
||||
"flat": "^4.1.0",
|
||||
"fluent": "^0.6.4",
|
||||
@@ -148,12 +153,19 @@
|
||||
"fluent-react": "^0.7.0",
|
||||
"graphql-playground-middleware-express": "^1.7.2",
|
||||
"graphql-schema-typescript": "^1.2.1",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-babel": "^8.0.0-beta.2",
|
||||
"gulp-cli": "^2.0.1",
|
||||
"gulp-sourcemaps": "^2.6.4",
|
||||
"gulp-typescript": "^5.0.0-alpha.3",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"jest": "^23.4.1",
|
||||
"jest-junit": "^5.1.0",
|
||||
"jest-localstorage-mock": "^2.2.0",
|
||||
"jsdom": "^11.11.0",
|
||||
"loader-utils": "^1.1.0",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"mini-css-extract-plugin": "^0.4.1",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"postcss-advanced-variables": "^2.3.3",
|
||||
"postcss-css-variables": "^0.9.0",
|
||||
@@ -195,8 +207,8 @@
|
||||
"ts-node": "^6.2.0",
|
||||
"tsconfig-paths": "^3.4.2",
|
||||
"tsconfig-paths-webpack-plugin": "^3.1.4",
|
||||
"tslint": "^5.10.0",
|
||||
"tslint-config-prettier": "^1.13.0",
|
||||
"tslint": "^5.11.0",
|
||||
"tslint-config-prettier": "^1.14.0",
|
||||
"tslint-loader": "^3.6.0",
|
||||
"tslint-plugin-prettier": "^1.3.0",
|
||||
"tslint-react": "^3.6.0",
|
||||
|
||||
@@ -77,14 +77,20 @@ async function main() {
|
||||
return files;
|
||||
}
|
||||
|
||||
main()
|
||||
.then(files => {
|
||||
for (const { fileName } of files) {
|
||||
module.exports = main;
|
||||
|
||||
if (require.main === module) {
|
||||
// Only run the main module on file load if this is the main module (we're
|
||||
// executing this file directly).
|
||||
main()
|
||||
.then(files => {
|
||||
for (const { fileName } of files) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(`Generated ${fileName}`);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(`Generated ${fileName}`);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(err);
|
||||
});
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import CaseSensitivePathsPlugin from "case-sensitive-paths-webpack-plugin";
|
||||
import ExtractTextPlugin from "extract-text-webpack-plugin";
|
||||
import HtmlWebpackPlugin, { Options } from "html-webpack-plugin";
|
||||
import MiniCssExtractPlugin from "mini-css-extract-plugin";
|
||||
import path from "path";
|
||||
import InterpolateHtmlPlugin from "react-dev-utils/InterpolateHtmlPlugin";
|
||||
import WatchMissingNodeModulesPlugin from "react-dev-utils/WatchMissingNodeModulesPlugin";
|
||||
@@ -8,6 +8,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 paths from "./paths";
|
||||
|
||||
interface CreateWebpackConfig {
|
||||
@@ -59,27 +60,6 @@ export default function createWebpackConfig({
|
||||
},
|
||||
};
|
||||
|
||||
const cssLoaders = [
|
||||
{
|
||||
loader: require.resolve("css-loader"),
|
||||
options: {
|
||||
modules: true,
|
||||
importLoaders: 1,
|
||||
localIdentName: "[name]-[local]-[hash:base64:5]",
|
||||
minimize: isProduction,
|
||||
sourceMap: isProduction && !disableSourcemaps,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve("postcss-loader"),
|
||||
options: {
|
||||
config: {
|
||||
path: paths.appPostCssConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const localesOptions = {
|
||||
pathToLocales: paths.appLocales,
|
||||
|
||||
@@ -127,13 +107,9 @@ export default function createWebpackConfig({
|
||||
},
|
||||
sourceMap: !disableSourcemaps,
|
||||
}),
|
||||
// Note: this won't work without ExtractTextPlugin.extract(..) in `loaders`.
|
||||
new ExtractTextPlugin({
|
||||
// We use [md5:contenthash:hex:20] instead of [contenthash:8]
|
||||
// because of this bug https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/763.
|
||||
// TODO: Repalce with mini-css-extract-plugin once it supports HMR.
|
||||
// https://github.com/webpack-contrib/mini-css-extract-plugin
|
||||
filename: "assets/css/[name].[md5:contenthash:hex:20].css",
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "assets/css/[name].[hash].css",
|
||||
chunkFilename: "assets/css/[id].[hash].css",
|
||||
}),
|
||||
]
|
||||
: [
|
||||
@@ -316,19 +292,27 @@ export default function createWebpackConfig({
|
||||
// in development "style" loader enables hot editing of CSS.
|
||||
{
|
||||
test: /\.css$/,
|
||||
loader:
|
||||
(isProduction &&
|
||||
ExtractTextPlugin.extract({
|
||||
fallback: styleLoader,
|
||||
use: cssLoaders,
|
||||
})) ||
|
||||
undefined,
|
||||
use:
|
||||
(!isProduction && [
|
||||
require.resolve("style-loader"),
|
||||
...cssLoaders,
|
||||
]) ||
|
||||
undefined,
|
||||
use: [
|
||||
isProduction ? MiniCssExtractPlugin.loader : styleLoader,
|
||||
{
|
||||
loader: require.resolve("css-loader"),
|
||||
options: {
|
||||
modules: true,
|
||||
importLoaders: 1,
|
||||
localIdentName: "[name]-[local]-[hash:base64:5]",
|
||||
minimize: isProduction,
|
||||
sourceMap: isProduction && !disableSourcemaps,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve("postcss-loader"),
|
||||
options: {
|
||||
config: {
|
||||
path: paths.appPostCssConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// "file" loader makes sure those assets get served by WebpackDevServer.
|
||||
// When you `import` an asset, you get its (virtual) filename.
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import * as React from "react";
|
||||
import { 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 * as styles from "./SignUp.css";
|
||||
|
||||
import {
|
||||
@@ -9,53 +19,158 @@ import {
|
||||
InputLabel,
|
||||
TextField,
|
||||
Typography,
|
||||
ValidationMessage,
|
||||
} from "talk-ui/components";
|
||||
|
||||
const SignUp: StatelessComponent = props => {
|
||||
interface FormProps {
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
export interface SignUpForm {
|
||||
onSubmit: OnSubmit<FormProps>;
|
||||
}
|
||||
|
||||
const SignUp: StatelessComponent<SignUpForm> = props => {
|
||||
return (
|
||||
<Flex itemGutter direction="column" className={styles.root}>
|
||||
<Flex direction="column">
|
||||
<Typography variant="heading1" align="center">
|
||||
Sign up to join the conversation
|
||||
</Typography>
|
||||
<FormField>
|
||||
<InputLabel>Email Address</InputLabel>
|
||||
<TextField />
|
||||
</FormField>
|
||||
<FormField>
|
||||
<InputLabel>Username</InputLabel>
|
||||
<Typography variant="inputDescription">
|
||||
A unique identifier displayed on your comments. You may use “_” and
|
||||
“.”
|
||||
</Typography>
|
||||
<TextField />
|
||||
</FormField>
|
||||
<FormField>
|
||||
<InputLabel>Password</InputLabel>
|
||||
<Typography>Must be at least 8 characters</Typography>
|
||||
<TextField />
|
||||
</FormField>
|
||||
<FormField>
|
||||
<InputLabel>Confirm Password</InputLabel>
|
||||
<TextField />
|
||||
</FormField>
|
||||
</Flex>
|
||||
<div className={styles.footer}>
|
||||
<Button variant="filled" color="primary" size="large" fullWidth>
|
||||
Sign up and join the conversation
|
||||
</Button>
|
||||
<Flex
|
||||
itemGutter="half"
|
||||
justifyContent="center"
|
||||
className={styles.subFooter}
|
||||
>
|
||||
<Typography>Already have an account?</Typography>
|
||||
<Button variant="underlined" size="small" color="primary">
|
||||
Sign In
|
||||
</Button>
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
<Form onSubmit={props.onSubmit}>
|
||||
{({ handleSubmit, submitting }) => (
|
||||
<form autoComplete="off" onSubmit={handleSubmit}>
|
||||
<Flex itemGutter direction="column" className={styles.root}>
|
||||
<Flex direction="column">
|
||||
<Typography variant="heading1" align="center">
|
||||
Sign up to join the conversation
|
||||
</Typography>
|
||||
|
||||
<Field
|
||||
name="email"
|
||||
validate={composeValidators(required, validateEmail)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<InputLabel>Email Address</InputLabel>
|
||||
<TextField
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
placeholder="Email Address"
|
||||
/>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<ValidationMessage>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
name="username"
|
||||
validate={composeValidators(required, validateUsername)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<InputLabel>Username</InputLabel>
|
||||
<Typography variant="inputDescription">
|
||||
A unique identifier displayed on your comments. You may
|
||||
use “_” and “.”
|
||||
</Typography>
|
||||
<TextField
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
placeholder="Username"
|
||||
/>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<ValidationMessage>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
name="password"
|
||||
validate={composeValidators(required, validatePassword)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<InputLabel>Password</InputLabel>
|
||||
<Typography variant="inputDescription">
|
||||
Must be at least 8 characters
|
||||
</Typography>
|
||||
<TextField
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
/>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<ValidationMessage>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
name="confirmPassword"
|
||||
validate={composeValidators(
|
||||
required,
|
||||
validatePassword,
|
||||
validateEqualPasswords
|
||||
)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<InputLabel>Confirm Password</InputLabel>
|
||||
<Typography variant="inputDescription">
|
||||
Must be at least 8 characters
|
||||
</Typography>
|
||||
<TextField
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
placeholder="Confirm Password"
|
||||
type="password"
|
||||
/>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<ValidationMessage>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
</Flex>
|
||||
<div className={styles.footer}>
|
||||
<Button variant="filled" color="primary" size="large" fullWidth>
|
||||
Sign up and join the conversation
|
||||
</Button>
|
||||
<Flex
|
||||
itemGutter="half"
|
||||
justifyContent="center"
|
||||
className={styles.subFooter}
|
||||
>
|
||||
<Typography>Already have an account?</Typography>
|
||||
<Button variant="underlined" size="small" color="primary">
|
||||
Sign In
|
||||
</Button>
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
import { BadUserInputError } from "talk-framework/lib/errors";
|
||||
import SignUp, { SignUpForm } from "../components/SignUp";
|
||||
|
||||
import SignUp from "../components/SignUp";
|
||||
import React, { Component } from "react";
|
||||
import { SignUpMutation, withSignUpMutation } from "../mutations";
|
||||
|
||||
const SignUpContainer: StatelessComponent = () => {
|
||||
return <SignUp />;
|
||||
};
|
||||
interface SignUpContainerProps {
|
||||
signUp: SignUpMutation;
|
||||
}
|
||||
|
||||
export default SignUpContainer;
|
||||
class SignUpContainer extends Component<SignUpContainerProps> {
|
||||
private onSubmit: SignUpForm["onSubmit"] = async (input, form) => {
|
||||
try {
|
||||
await this.props.signUp(input);
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
if (error instanceof BadUserInputError) {
|
||||
return error.invalidArgsLocalized;
|
||||
}
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(error);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
public render() {
|
||||
return <SignUp onSubmit={this.onSubmit} />;
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withSignUpMutation(SignUpContainer);
|
||||
export default enhanced;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { createMutationContainer } from "talk-framework/lib/relay";
|
||||
import { signIn, SignInInput } from "talk-framework/rest";
|
||||
|
||||
export type SignInMutation = (input: SignInInput) => Promise<void>;
|
||||
|
||||
export async function commit(
|
||||
environment: Environment,
|
||||
input: SignInInput,
|
||||
{ rest, postMessage }: TalkContext
|
||||
) {
|
||||
try {
|
||||
const result = await signIn(rest, input);
|
||||
postMessage.send("setAuthToken", result.token, window.opener);
|
||||
window.close();
|
||||
} catch (err) {
|
||||
postMessage.send("authError", err.toString(), window.opener);
|
||||
}
|
||||
}
|
||||
|
||||
export const withSignInMutation = createMutationContainer("signIn", commit);
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { createMutationContainer } from "talk-framework/lib/relay";
|
||||
import { signUp, SignUpInput } from "talk-framework/rest";
|
||||
|
||||
export type SignUpMutation = (input: SignUpInput) => Promise<void>;
|
||||
|
||||
export async function commit(
|
||||
environment: Environment,
|
||||
input: SignUpInput,
|
||||
{ rest, postMessage }: TalkContext
|
||||
) {
|
||||
try {
|
||||
const result = await signUp(rest, input);
|
||||
postMessage.send("setAuthToken", result.token, window.opener);
|
||||
window.close();
|
||||
} catch (err) {
|
||||
postMessage.send("authError", err.toString(), window.opener);
|
||||
}
|
||||
}
|
||||
|
||||
export const withSignUpMutation = createMutationContainer("signUp", commit);
|
||||
@@ -1 +1,3 @@
|
||||
export { withSetViewMutation, SetViewMutation } from "./SetViewMutation";
|
||||
export { withSignInMutation, SignInMutation } from "./SignInMutation";
|
||||
export { withSignUpMutation, SignUpMutation } from "./SignUpMutation";
|
||||
|
||||
@@ -6,6 +6,8 @@ import { MediaQueryMatchers } from "react-responsive";
|
||||
import { Formatter } from "react-timeago";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { PostMessageService } from "talk-framework/lib/postMessage";
|
||||
import { RestClient } from "talk-framework/lib/rest";
|
||||
import { UIContext } from "talk-ui/components";
|
||||
import { ClickFarAwayRegister } from "talk-ui/components/ClickOutside";
|
||||
|
||||
@@ -19,9 +21,21 @@ export interface TalkContext {
|
||||
/** formatter for timeago. */
|
||||
timeagoFormatter?: Formatter;
|
||||
|
||||
/** Session Storage */
|
||||
localStorage: Storage;
|
||||
|
||||
/** Session storage */
|
||||
sessionStorage: Storage;
|
||||
|
||||
/** media query values for testing purposes */
|
||||
mediaQueryValues?: MediaQueryMatchers;
|
||||
|
||||
/** Rest client */
|
||||
rest: RestClient;
|
||||
|
||||
/** postMessage service */
|
||||
postMessage: PostMessageService;
|
||||
|
||||
/**
|
||||
* A way to listen for clicks that are e.g. outside of the
|
||||
* current frame for `ClickOutside`
|
||||
|
||||
@@ -5,11 +5,18 @@ import { Child as PymChild } from "pym.js";
|
||||
import React from "react";
|
||||
import { Formatter } from "react-timeago";
|
||||
import { Environment, Network, RecordSource, Store } from "relay-runtime";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import {
|
||||
createLocalStorage,
|
||||
createSessionStorage,
|
||||
} from "talk-framework/lib/storage";
|
||||
|
||||
import { RestClient } from "talk-framework/lib/rest";
|
||||
import { ClickFarAwayRegister } from "talk-ui/components/ClickOutside";
|
||||
|
||||
import { generateMessages, LocalesData, negotiateLanguages } from "../i18n";
|
||||
import { fetchQuery } from "../network";
|
||||
import { createFetch, TokenGetter } from "../network";
|
||||
import { PostMessageService } from "../postMessage";
|
||||
import { TalkContext } from "./TalkContext";
|
||||
|
||||
interface CreateContextArguments {
|
||||
@@ -61,9 +68,17 @@ export default async function createContext({
|
||||
eventEmitter = new EventEmitter2({ wildcard: true }),
|
||||
}: CreateContextArguments): Promise<TalkContext> {
|
||||
// Initialize Relay.
|
||||
const source = new RecordSource();
|
||||
const tokenGetter: TokenGetter = () => {
|
||||
const localState = source.get(LOCAL_ID);
|
||||
if (localState) {
|
||||
return localState.authToken || "";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
const relayEnvironment = new Environment({
|
||||
network: Network.create(fetchQuery),
|
||||
store: new Store(new RecordSource()),
|
||||
network: Network.create(createFetch(tokenGetter)),
|
||||
store: new Store(source),
|
||||
});
|
||||
|
||||
// Listen for outside clicks.
|
||||
@@ -99,6 +114,10 @@ export default async function createContext({
|
||||
pym,
|
||||
eventEmitter,
|
||||
registerClickFarAway,
|
||||
rest: new RestClient("/api"),
|
||||
postMessage: new PostMessageService(),
|
||||
localStorage: createLocalStorage(),
|
||||
sessionStorage: createSessionStorage(),
|
||||
};
|
||||
|
||||
// Run custom initializations.
|
||||
|
||||
@@ -17,3 +17,27 @@ export const VALIDATION_TOO_SHORT = () => (
|
||||
<span>This field is too short.</span>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export const INVALID_EMAIL = () => (
|
||||
<Localized id="framework-validation-invalidEmail">
|
||||
<span>Please enter a valid email address.</span>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export const INVALID_USERNAME = () => (
|
||||
<Localized id="framework-validation-invalidUsername">
|
||||
<span>Please enter a valid username.</span>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export const INVALID_PASSWORD = () => (
|
||||
<Localized id="framework-validation-invalidUsername">
|
||||
<span>Please enter a valid password.</span>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export const PASSWORDS_DO_NOT_MATCH = () => (
|
||||
<Localized id="framework-validation-passwordsDoNotMatch">
|
||||
<span>Passwords do not match. Try again.</span>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
@@ -26,17 +26,28 @@ function getError(errors: Error[]): Error {
|
||||
return new GraphQLError(errors as any);
|
||||
}
|
||||
|
||||
export type TokenGetter = () => string;
|
||||
type CreateFetch = (token?: TokenGetter) => FetchFunction;
|
||||
|
||||
/**
|
||||
* fetchQuery is a simple implementation of the `FetchFunction`
|
||||
* createFetch returns a simple implementation of the `FetchFunction`
|
||||
* required by Relay. It'll return a `NetworkError` on failure.
|
||||
*/
|
||||
const fetchQuery: FetchFunction = async (operation, variables) => {
|
||||
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: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
query: operation.text,
|
||||
variables,
|
||||
@@ -58,4 +69,4 @@ const fetchQuery: FetchFunction = async (operation, variables) => {
|
||||
}
|
||||
};
|
||||
|
||||
export default fetchQuery;
|
||||
export default createFetch;
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as fetchQuery } from "./fetchQuery";
|
||||
export { default as createFetch, TokenGetter } from "./fetchQuery";
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { PostMessageService } from "./postMessage";
|
||||
|
||||
it("post and subscribe to a message", done => {
|
||||
const postMessage = new PostMessageService();
|
||||
const cancel = postMessage.on("test", value => {
|
||||
expect(value).toBe("value");
|
||||
done();
|
||||
cancel();
|
||||
});
|
||||
postMessage.send("test", "value", window);
|
||||
});
|
||||
|
||||
it("should support complex value", done => {
|
||||
const complex = { foo: "bar" };
|
||||
const postMessage = new PostMessageService();
|
||||
const cancel = postMessage.on("test", value => {
|
||||
expect(value).toBe(complex);
|
||||
done();
|
||||
cancel();
|
||||
});
|
||||
postMessage.send("test", complex, window);
|
||||
});
|
||||
|
||||
it("send to a different origin", done => {
|
||||
const postMessage = new PostMessageService();
|
||||
const cancelA = postMessage.on("testA", value => {
|
||||
throw new Error("Should not reach this");
|
||||
});
|
||||
const cancelB = postMessage.on("testB", value => {
|
||||
done();
|
||||
cancelA();
|
||||
cancelB();
|
||||
});
|
||||
postMessage.send("testA", "value", window, "http://i-do-not-exist.de");
|
||||
postMessage.send("testB", "value", window);
|
||||
});
|
||||
|
||||
it("should cancel", done => {
|
||||
const postMessage = new PostMessageService();
|
||||
const cancelA = postMessage.on("testA", value => {
|
||||
throw new Error("Should not reach this");
|
||||
});
|
||||
const cancelB = postMessage.on("testB", value => {
|
||||
done();
|
||||
cancelB();
|
||||
});
|
||||
cancelA();
|
||||
postMessage.send("testA", "value", window);
|
||||
postMessage.send("testB", "value", window);
|
||||
});
|
||||
|
||||
it("different scopes are isolated", done => {
|
||||
const postMessageA = new PostMessageService("scopeA");
|
||||
const postMessageB = new PostMessageService("scopeB");
|
||||
const cancelA = postMessageA.on("testA", value => {
|
||||
throw new Error("Should not reach this");
|
||||
});
|
||||
const cancelB = postMessageA.on("testB", value => {
|
||||
done();
|
||||
cancelA();
|
||||
cancelB();
|
||||
});
|
||||
postMessageB.send("testA", "value", window);
|
||||
postMessageA.send("testB", "value", window);
|
||||
});
|
||||
|
||||
it("different message names are isolated", done => {
|
||||
const postMessage = new PostMessageService();
|
||||
const cancelA = postMessage.on("testA", value => {
|
||||
expect(value).toBe("valueA");
|
||||
});
|
||||
const cancelB = postMessage.on("testB", value => {
|
||||
expect(value).toBe("valueB");
|
||||
done();
|
||||
cancelA();
|
||||
cancelB();
|
||||
});
|
||||
postMessage.send("testA", "valueA", window);
|
||||
postMessage.send("testB", "valueB", window);
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
type PostMessageHandler = (value: any, name: string) => void;
|
||||
|
||||
/**
|
||||
* Wrapper around the HTML postMessage API.
|
||||
*/
|
||||
export class PostMessageService {
|
||||
private origin: string;
|
||||
private scope: string;
|
||||
|
||||
constructor(
|
||||
scope = "talk",
|
||||
origin: string = `${location.protocol}//${location.host}`
|
||||
) {
|
||||
this.origin = origin;
|
||||
this.scope = scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message over the postMessage API
|
||||
* @param name string name of the message
|
||||
* @param value string value of the message
|
||||
* @param target Window target window, e.g. window.opener
|
||||
* @param targetOrigin string origin of target
|
||||
*/
|
||||
public send(name: string, value: any, target: Window, targetOrigin?: string) {
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Serialize the message to be sent via postMessage.
|
||||
const msg = { name, value, scope: this.scope };
|
||||
|
||||
// Send the message.
|
||||
target.postMessage(msg, targetOrigin || this.origin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to messages
|
||||
* @param name string Name of the message
|
||||
* @param handler PostMessageHandler
|
||||
*/
|
||||
public on(name: string, handler: PostMessageHandler) {
|
||||
const listener = (event: MessageEvent) => {
|
||||
if (!event.origin) {
|
||||
if (process.env.NODE_ENV !== "test") {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.warn("empty origin received in postMessage", name);
|
||||
}
|
||||
} else if (event.origin !== this.origin) {
|
||||
return;
|
||||
}
|
||||
if (event.data.scope !== this.scope) {
|
||||
return;
|
||||
}
|
||||
if (event.data.name !== name) {
|
||||
return;
|
||||
}
|
||||
handler(event.data.value, event.data.name);
|
||||
};
|
||||
// Attach the listener to the target.
|
||||
window.addEventListener("message", listener);
|
||||
return () => {
|
||||
window.removeEventListener("message", listener);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import merge from "lodash/merge";
|
||||
import { Overwrite } from "talk-framework/types";
|
||||
|
||||
const buildOptions = (inputOptions: RequestInit = {}) => {
|
||||
const defaultOptions: RequestInit = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "same-origin",
|
||||
};
|
||||
const options = merge({}, defaultOptions, inputOptions);
|
||||
if (options.method!.toLowerCase() !== "get") {
|
||||
options.body = JSON.stringify(options.body);
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
const handleResp = (res: Response) => {
|
||||
if (res.status > 399) {
|
||||
return res.json().then((err: any) => {
|
||||
// TODO: sync error handling with server.
|
||||
const message = err.message || err.error || res.status;
|
||||
const error = new Error(message);
|
||||
throw error;
|
||||
});
|
||||
} else if (res.status === 204) {
|
||||
return res.text();
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
};
|
||||
|
||||
type PartialRequestInit = Overwrite<Partial<RequestInit>, { body: any }>;
|
||||
|
||||
export class RestClient {
|
||||
public readonly uri: string;
|
||||
private tokenGetter?: () => string;
|
||||
|
||||
constructor(uri: string, tokenGetter?: () => string) {
|
||||
this.uri = uri;
|
||||
this.tokenGetter = tokenGetter;
|
||||
}
|
||||
|
||||
public fetch<T>(path: string, options: PartialRequestInit): Promise<T> {
|
||||
let opts = options;
|
||||
if (this.tokenGetter) {
|
||||
opts = merge({}, options, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.tokenGetter()}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
return fetch(`${this.uri}${path}`, buildOptions(opts)).then(handleResp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import createInMemoryStorage from "./InMemoryStorage";
|
||||
|
||||
it("should set and unset values", () => {
|
||||
const storage = createInMemoryStorage();
|
||||
storage.setItem("test", "value");
|
||||
expect(storage.getItem("test")).toBe("value");
|
||||
storage.removeItem("test");
|
||||
expect(storage.getItem("test")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return length", () => {
|
||||
const storage = createInMemoryStorage();
|
||||
storage.setItem("a", "value");
|
||||
storage.setItem("b", "value");
|
||||
storage.setItem("c", "value");
|
||||
expect(storage.length).toBe(3);
|
||||
});
|
||||
|
||||
it("should nth value", () => {
|
||||
const storage = createInMemoryStorage();
|
||||
storage.setItem("a", "a");
|
||||
storage.setItem("b", "b");
|
||||
storage.setItem("c", "c");
|
||||
expect(storage.key(2)).toBe("c");
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* InMemoryStorage is a dumb implementation of the Storage interface that will
|
||||
* not persist the data at all. It implements the Storage interface found:
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/Storage
|
||||
*/
|
||||
class InMemoryStorage implements Storage {
|
||||
private storage: Record<string, string>;
|
||||
|
||||
constructor() {
|
||||
this.storage = {};
|
||||
}
|
||||
|
||||
get length() {
|
||||
return Object.keys(this.storage).length;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.storage = {};
|
||||
}
|
||||
|
||||
public key(n: number) {
|
||||
if (this.length <= n) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.storage[Object.keys(this.storage)[n]];
|
||||
}
|
||||
|
||||
public getItem(key: string) {
|
||||
return this.storage[key];
|
||||
}
|
||||
|
||||
public setItem(key: string, value: string) {
|
||||
this.storage[key] = value;
|
||||
}
|
||||
|
||||
public removeItem(key: string) {
|
||||
delete this.storage[key];
|
||||
}
|
||||
}
|
||||
|
||||
export default function createInMemoryStorage() {
|
||||
return new InMemoryStorage();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import prefixStorage from "./prefixStorage";
|
||||
|
||||
export default function createLocalStorage(): Storage {
|
||||
return prefixStorage(window.localStorage, "talk");
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import prefixStorage from "./prefixStorage";
|
||||
|
||||
export default function createSessionStorage(): Storage {
|
||||
return prefixStorage(window.sessionStorage, "talk");
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as createInMemoryStorage } from "./InMemoryStorage";
|
||||
export { default as createLocalStorage } from "./LocalStorage";
|
||||
export { default as createSessionStorage } from "./SessionStorage";
|
||||
@@ -0,0 +1,86 @@
|
||||
import sinon from "sinon";
|
||||
import prefixStorage from "./prefixStorage";
|
||||
|
||||
it("should call clear", () => {
|
||||
const storage = {
|
||||
clear: sinon.mock().once(),
|
||||
};
|
||||
|
||||
const prefixed = prefixStorage(storage as any, "talk");
|
||||
prefixed.clear();
|
||||
storage.clear.verify();
|
||||
});
|
||||
|
||||
it("should call length", () => {
|
||||
const ret = 10;
|
||||
const storage = {
|
||||
get length() {
|
||||
return ret;
|
||||
},
|
||||
};
|
||||
|
||||
const prefixed = prefixStorage(storage as any, "talk");
|
||||
expect(prefixed.length).toBe(ret);
|
||||
});
|
||||
|
||||
it("should call key", () => {
|
||||
const ret = "value";
|
||||
const storage = {
|
||||
key: sinon
|
||||
.mock()
|
||||
.withArgs(3)
|
||||
.returns(ret),
|
||||
};
|
||||
|
||||
const prefixed = prefixStorage(storage as any, "talk");
|
||||
expect(prefixed.key(3)).toBe(ret);
|
||||
(storage.key as any).verify();
|
||||
});
|
||||
|
||||
it("should call key", () => {
|
||||
const ret = "value";
|
||||
const storage = {
|
||||
key: sinon
|
||||
.mock()
|
||||
.withArgs(3)
|
||||
.returns(ret),
|
||||
};
|
||||
|
||||
const prefixed = prefixStorage(storage as any, "talk");
|
||||
expect(prefixed.key(3)).toBe(ret);
|
||||
(storage.key as any).verify();
|
||||
});
|
||||
|
||||
it("should prefix setItem", () => {
|
||||
const storage = {
|
||||
setItem: sinon.mock().withArgs("talk:key", "value"),
|
||||
};
|
||||
|
||||
const prefixed = prefixStorage(storage as any, "talk");
|
||||
prefixed.setItem("key", "value");
|
||||
storage.setItem.verify();
|
||||
});
|
||||
|
||||
it("should prefix removeItem", () => {
|
||||
const storage = {
|
||||
removeItem: sinon.mock().withArgs("talk:key"),
|
||||
};
|
||||
|
||||
const prefixed = prefixStorage(storage as any, "talk");
|
||||
prefixed.removeItem("key");
|
||||
storage.removeItem.verify();
|
||||
});
|
||||
|
||||
it("should prefix getItem", () => {
|
||||
const ret = "value";
|
||||
const storage = {
|
||||
getItem: sinon
|
||||
.mock()
|
||||
.withArgs("talk:key")
|
||||
.returns(ret),
|
||||
};
|
||||
|
||||
const prefixed = prefixStorage(storage as any, "talk");
|
||||
expect(prefixed.getItem("key")).toBe(ret);
|
||||
(storage.getItem as any).verify();
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* PrefixedStorage decorates a Storage and prefixes keys in
|
||||
* getItem, setItem and removeItem with given prefix.
|
||||
*/
|
||||
class PrefixedStorage implements Storage {
|
||||
private storage: Storage;
|
||||
private prefix: string;
|
||||
|
||||
constructor(storage: Storage, prefix: string) {
|
||||
this.storage = storage;
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this.storage.length;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.storage.clear();
|
||||
}
|
||||
|
||||
public key(n: number) {
|
||||
return this.storage.key(n);
|
||||
}
|
||||
|
||||
public getItem(key: string) {
|
||||
return this.storage.getItem(`${this.prefix}:${key}`);
|
||||
}
|
||||
|
||||
public setItem(key: string, value: string) {
|
||||
return this.storage.setItem(`${this.prefix}:${key}`, value);
|
||||
}
|
||||
|
||||
public removeItem(key: string) {
|
||||
return this.storage.removeItem(`${this.prefix}:${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default function prefixStorage(storage: Storage, prefix: string) {
|
||||
return new PrefixedStorage(storage, prefix);
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
import { ReactNode } from "react";
|
||||
import { VALIDATION_REQUIRED } from "./messages";
|
||||
import {
|
||||
INVALID_EMAIL,
|
||||
INVALID_PASSWORD,
|
||||
INVALID_USERNAME,
|
||||
PASSWORDS_DO_NOT_MATCH,
|
||||
VALIDATION_REQUIRED,
|
||||
} from "./messages";
|
||||
|
||||
type Validator<T, V> = (v: T, values: V) => ReactNode;
|
||||
|
||||
@@ -31,3 +37,35 @@ export function composeValidators<T = any, V = any>(
|
||||
* required is a Validator that checks that the value is truthy.
|
||||
*/
|
||||
export const required = createValidator(v => !!v, VALIDATION_REQUIRED());
|
||||
|
||||
/**
|
||||
* validateEmail is a Validator that checks that the value is an email.
|
||||
*/
|
||||
export const validateEmail = createValidator(
|
||||
v => /^.+@.+\..+$/.test(v),
|
||||
INVALID_EMAIL()
|
||||
);
|
||||
|
||||
/**
|
||||
* validateUsername is a Validator that checks that the value is a valid username.
|
||||
*/
|
||||
export const validateUsername = createValidator(
|
||||
v => /^[a-zA-Z0-9_.]+$/.test(v),
|
||||
INVALID_USERNAME()
|
||||
);
|
||||
|
||||
/**
|
||||
* validateUsername is a Validator that checks that the value is a valid username.
|
||||
*/
|
||||
export const validatePassword = createValidator(
|
||||
v => /^(?=.{8,}).*$/.test(v),
|
||||
INVALID_PASSWORD()
|
||||
);
|
||||
|
||||
/**s
|
||||
* validateUsername is a Validator that checks that the value is a valid username.
|
||||
*/
|
||||
export const validateEqualPasswords = createValidator(
|
||||
(v, values) => v === values.password,
|
||||
PASSWORDS_DO_NOT_MATCH()
|
||||
);
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { commitLocalUpdate, Environment, RecordSource } from "relay-runtime";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import { createInMemoryStorage } from "talk-framework/lib/storage";
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
|
||||
import { commit } from "./SetAuthTokenMutation";
|
||||
|
||||
let environment: Environment;
|
||||
const source: RecordSource = new RecordSource();
|
||||
|
||||
beforeAll(() => {
|
||||
environment = createRelayEnvironment({
|
||||
source,
|
||||
});
|
||||
});
|
||||
|
||||
it("Sets auth token", async () => {
|
||||
const context = {
|
||||
localStorage: createInMemoryStorage(),
|
||||
};
|
||||
const authToken = "auth token";
|
||||
commit(environment, { authToken }, context as any);
|
||||
expect(source.get(LOCAL_ID)!.authToken).toEqual(authToken);
|
||||
expect(context.localStorage.getItem("authToken")).toEqual(authToken);
|
||||
});
|
||||
|
||||
it("Removes auth token from localStorage", async () => {
|
||||
const context = {
|
||||
localStorage: createInMemoryStorage(),
|
||||
};
|
||||
localStorage.setItem("authToken", "tmp");
|
||||
commit(environment, { authToken: null }, context as any);
|
||||
expect(context.localStorage.getItem("authToken")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Should call gc", async () => {
|
||||
const context = {
|
||||
localStorage: createInMemoryStorage(),
|
||||
};
|
||||
commitLocalUpdate(environment, store => {
|
||||
store.create("should-disappear", "tmp");
|
||||
});
|
||||
const authToken = null;
|
||||
expect(source.get("should-disappear")).not.toBeUndefined();
|
||||
commit(environment, { authToken }, context as any);
|
||||
await timeout();
|
||||
expect(source.get(LOCAL_ID)!.authToken).toEqual(authToken);
|
||||
expect(source.get("should-disappear")).toBeUndefined();
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { commitLocalUpdate, Environment } from "relay-runtime";
|
||||
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { createMutationContainer } from "talk-framework/lib/relay";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer";
|
||||
|
||||
export interface SetAuthTokenInput {
|
||||
authToken: string | null;
|
||||
}
|
||||
|
||||
export type SetAuthTokenMutation = (input: SetAuthTokenInput) => Promise<void>;
|
||||
|
||||
export async function commit(
|
||||
environment: Environment,
|
||||
input: SetAuthTokenInput,
|
||||
{ localStorage }: TalkContext
|
||||
) {
|
||||
return commitLocalUpdate(environment, store => {
|
||||
const record = store.get(LOCAL_ID)!;
|
||||
record.setValue(input.authToken, "authToken");
|
||||
if (input.authToken) {
|
||||
localStorage.setItem("authToken", input.authToken);
|
||||
} else {
|
||||
localStorage.removeItem("authToken");
|
||||
}
|
||||
|
||||
// Force gc to trigger.
|
||||
environment
|
||||
.retain({
|
||||
dataID: "tmp",
|
||||
node: { selections: [] },
|
||||
variables: {},
|
||||
})
|
||||
.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
export const withSetAuthTokenMutation = createMutationContainer(
|
||||
"setCommentID",
|
||||
commit
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
withSetAuthTokenMutation,
|
||||
SetAuthTokenMutation,
|
||||
SetAuthTokenInput,
|
||||
} from "./SetAuthTokenMutation";
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as signIn, SignInInput } from "./signIn";
|
||||
export { default as signUp, SignUpInput } from "./signUp";
|
||||
@@ -0,0 +1,17 @@
|
||||
import { RestClient } from "../lib/rest";
|
||||
|
||||
export interface SignInInput {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface SignInResponse {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export default function signIn(rest: RestClient, input: SignInInput) {
|
||||
return rest.fetch<SignInResponse>("/tenant/auth/local", {
|
||||
method: "POST",
|
||||
body: input,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { RestClient } from "../lib/rest";
|
||||
|
||||
export interface SignUpInput {
|
||||
username: string;
|
||||
password: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface SignUpResponse {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export default function signUp(rest: RestClient, input: SignUpInput) {
|
||||
return rest.fetch<SignUpResponse>("/tenant/auth/local/signup", {
|
||||
method: "POST",
|
||||
body: input,
|
||||
});
|
||||
}
|
||||
@@ -22,7 +22,6 @@ const App: StatelessComponent<AppProps> = props => {
|
||||
{view}
|
||||
</Flex>
|
||||
);
|
||||
// return <div className={styles.root}>{view}</div>;
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
.root {
|
||||
width: 100%;
|
||||
padding-bottom: var(--spacing-unit);
|
||||
}
|
||||
|
||||
.textarea {
|
||||
composes: bodyCopy from "talk-ui/shared/typography.css";
|
||||
|
||||
display: block;
|
||||
height: 100px;
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: var(--spacing-unit);
|
||||
padding: var(--spacing-unit);
|
||||
border-radius: var(--round-corners);
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.postButtonContainer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
&:first-child {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
flex-grow: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import { Field, Form } from "react-final-form";
|
||||
import PoweredBy from "./PoweredBy";
|
||||
import { OnSubmit } from "talk-framework/lib/form";
|
||||
import { required } from "talk-framework/lib/validation";
|
||||
import PoweredBy from "talk-stream/components/PoweredBy";
|
||||
import { Button, Typography } from "talk-ui/components";
|
||||
|
||||
import * as styles from "./PostCommentForm.css";
|
||||
|
||||
interface FormProps {
|
||||
@@ -15,6 +15,7 @@ interface FormProps {
|
||||
|
||||
export interface PostCommentFormProps {
|
||||
onSubmit: OnSubmit<FormProps>;
|
||||
signedIn: boolean;
|
||||
}
|
||||
|
||||
const PostCommentForm: StatelessComponent<PostCommentFormProps> = props => (
|
||||
@@ -29,6 +30,7 @@ const PostCommentForm: StatelessComponent<PostCommentFormProps> = props => (
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
placeholder="Post a comment"
|
||||
/>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
@@ -39,14 +41,26 @@ const PostCommentForm: StatelessComponent<PostCommentFormProps> = props => (
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
<div className={styles.postButtonContainer}>
|
||||
<PoweredBy />
|
||||
<Localized id="comments-postCommentForm-post">
|
||||
<Button color="primary" variant="filled" disabled={submitting}>
|
||||
Post
|
||||
</Button>
|
||||
</Localized>
|
||||
</div>
|
||||
{props.signedIn ? (
|
||||
<div className={styles.postButtonContainer}>
|
||||
<PoweredBy />
|
||||
<Localized id="comments-postCommentForm-submit">
|
||||
<Button color="primary" variant="filled" disabled={submitting}>
|
||||
Submit
|
||||
</Button>
|
||||
</Localized>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
color="primary"
|
||||
variant="filled"
|
||||
disabled
|
||||
fullWidth
|
||||
size="large"
|
||||
>
|
||||
Sign In and join the conversation
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { Flex, Typography } from "talk-ui/components";
|
||||
import * as styles from "./PoweredBy.css";
|
||||
|
||||
const PoweredBy: StatelessComponent = props => {
|
||||
const PoweredBy: StatelessComponent = () => {
|
||||
return (
|
||||
<Flex itemGutter="half">
|
||||
<Typography className={styles.text} variant="bodyCopy">
|
||||
Powered by
|
||||
</Typography>
|
||||
<Typography className={styles.text} variant="heading4">
|
||||
The Coral Project
|
||||
</Typography>
|
||||
<Typography variant="bodyCopy">Powered by</Typography>
|
||||
<Typography variant="heading4">The Coral Project</Typography>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.child {
|
||||
width: initial;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
|
||||
import { Button, Flex, Typography } from "talk-ui/components";
|
||||
|
||||
import MatchMedia from "talk-ui/components/MatchMedia";
|
||||
import * as styles from "./UserBoxAuthenticated.css";
|
||||
|
||||
export interface UserBoxUnauthenticatedProps {
|
||||
onSignOut: () => void;
|
||||
}
|
||||
|
||||
const UserBoxAuthenticated: StatelessComponent<
|
||||
UserBoxUnauthenticatedProps
|
||||
> = props => {
|
||||
return (
|
||||
<Flex itemGutter>
|
||||
<Flex itemGutter="half" className={styles.child}>
|
||||
<MatchMedia gteWidth="sm">
|
||||
<Localized id="comments-userBoxAuthenticated-signedInAs">
|
||||
<Typography variant="bodyCopy" component="span">
|
||||
Signed in as
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Typography variant="bodyCopyBold" component="span">
|
||||
okbel
|
||||
</Typography>
|
||||
</MatchMedia>
|
||||
</Flex>
|
||||
<Flex itemGutter="half" className={styles.child}>
|
||||
<Localized id="comments-userBoxAuthenticated-notYou">
|
||||
<Typography variant="bodyCopy" component="span">
|
||||
Not you?
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Localized id="comments-userBoxAuthenticated-signOut">
|
||||
<Button
|
||||
color="primary"
|
||||
size="small"
|
||||
variant="underlined"
|
||||
onClick={props.onSignOut}
|
||||
>
|
||||
Sign Out
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserBoxAuthenticated;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { shallow } from "enzyme";
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import UserBoxUnauthenticated from "./UserBoxUnauthenticated";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof UserBoxUnauthenticated> = {
|
||||
onSignIn: noop,
|
||||
onRegister: noop,
|
||||
};
|
||||
const wrapper = shallow(<UserBoxUnauthenticated {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -31,7 +31,7 @@ class PostCommentFormContainer extends Component<InnerProps> {
|
||||
return undefined;
|
||||
};
|
||||
public render() {
|
||||
return <PostCommentForm onSubmit={this.onSubmit} />;
|
||||
return <PostCommentForm onSubmit={this.onSubmit} signedIn={false} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "talk-stream/mutations";
|
||||
import { Popup } from "talk-ui/components";
|
||||
|
||||
// import UserBoxAuthenticated from "../components/UserBoxAuthenticated";
|
||||
import UserBoxUnauthenticated from "../components/UserBoxUnauthenticated";
|
||||
|
||||
interface InnerProps {
|
||||
@@ -44,6 +45,7 @@ export class UserBoxContainer extends Component<InnerProps> {
|
||||
onBlur={this.handleBlur}
|
||||
onClose={this.handleClose}
|
||||
/>
|
||||
{/* <UserBoxAuthenticated onSignOut={this.handleRegister} /> */}
|
||||
<UserBoxUnauthenticated
|
||||
onSignIn={this.handleSignIn}
|
||||
onRegister={this.handleRegister}
|
||||
|
||||
@@ -10,16 +10,24 @@ import {
|
||||
} from "talk-framework/lib/bootstrap";
|
||||
|
||||
import AppContainer from "./containers/AppContainer";
|
||||
import {
|
||||
onPostMessageAuthError,
|
||||
onPostMessageSetAuthToken,
|
||||
onPymSetCommentID,
|
||||
} from "./listeners";
|
||||
import { initLocalState } from "./local";
|
||||
import localesData from "./locales";
|
||||
import { withSetCommentID } from "./pym";
|
||||
|
||||
const pymFeatures = [withSetCommentID];
|
||||
const listeners = [
|
||||
onPymSetCommentID,
|
||||
onPostMessageSetAuthToken,
|
||||
onPostMessageAuthError,
|
||||
];
|
||||
|
||||
// This is called when the context is first initialized.
|
||||
async function init(context: TalkContext) {
|
||||
await initLocalState(context.relayEnvironment);
|
||||
pymFeatures.forEach(f => f(context));
|
||||
await initLocalState(context.relayEnvironment, context);
|
||||
listeners.forEach(f => f(context));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default as onPymSetCommentID } from "./onPymSetCommentID";
|
||||
export {
|
||||
default as onPostMessageSetAuthToken,
|
||||
} from "./onPostMessageSetAuthToken";
|
||||
export { default as onPostMessageAuthError } from "./onPostMessageAuthError";
|
||||
@@ -0,0 +1,11 @@
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
|
||||
export default function onPostMessageSetAuthToken({
|
||||
postMessage,
|
||||
}: TalkContext) {
|
||||
// Auth popup will use this to send back errors during login.
|
||||
postMessage!.on("authError", error => {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Environment, RecordSource } from "relay-runtime";
|
||||
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import { createInMemoryStorage } from "talk-framework/lib/storage";
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
|
||||
import onPostMessageSetAuthToken from "./onPostMessageSetAuthToken";
|
||||
|
||||
let relayEnvironment: Environment;
|
||||
const source: RecordSource = new RecordSource();
|
||||
|
||||
beforeAll(() => {
|
||||
relayEnvironment = createRelayEnvironment({
|
||||
source,
|
||||
});
|
||||
});
|
||||
|
||||
it("Sets auth token", () => {
|
||||
const token = "auth-token";
|
||||
const context = {
|
||||
postMessage: {
|
||||
on: (name: string, cb: (token: string) => void) => {
|
||||
expect(name).toBe("setAuthToken");
|
||||
cb(token);
|
||||
},
|
||||
},
|
||||
relayEnvironment,
|
||||
localStorage: createInMemoryStorage(),
|
||||
};
|
||||
onPostMessageSetAuthToken(context as any);
|
||||
expect(source.get(LOCAL_ID)!.authToken).toEqual(token);
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
|
||||
import { commit as setAuthToken } from "talk-framework/mutations/SetAuthTokenMutation";
|
||||
|
||||
export default function onPostMessageSetAuthToken(ctx: TalkContext) {
|
||||
const { relayEnvironment, postMessage } = ctx;
|
||||
// Auth popup will use this to handle a successful login.
|
||||
postMessage!.on("setAuthToken", (authToken: string) => {
|
||||
setAuthToken(relayEnvironment, { authToken }, ctx);
|
||||
});
|
||||
}
|
||||
+3
-3
@@ -3,7 +3,7 @@ import { Environment, RecordSource } from "relay-runtime";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
|
||||
import withSetCommentID from "./withSetCommentID";
|
||||
import onPymSetCommentID from "./onPymSetCommentID";
|
||||
|
||||
let relayEnvironment: Environment;
|
||||
const source: RecordSource = new RecordSource();
|
||||
@@ -25,7 +25,7 @@ it("Sets comment id", () => {
|
||||
},
|
||||
relayEnvironment,
|
||||
};
|
||||
withSetCommentID(context as any);
|
||||
onPymSetCommentID(context as any);
|
||||
expect(source.get(LOCAL_ID)!.commentID).toEqual(id);
|
||||
});
|
||||
|
||||
@@ -40,6 +40,6 @@ it("Sets comment id to null when empty", () => {
|
||||
},
|
||||
relayEnvironment,
|
||||
};
|
||||
withSetCommentID(context as any);
|
||||
onPymSetCommentID(context as any);
|
||||
expect(source.get(LOCAL_ID)!.commentID).toEqual(null);
|
||||
});
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { commitLocalUpdate } from "react-relay";
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
|
||||
export default function withSetCommentID({
|
||||
export default function onPymSetCommentID({
|
||||
relayEnvironment,
|
||||
pym,
|
||||
}: TalkContext) {
|
||||
@@ -12,6 +12,7 @@ exports[`init local state 1`] = `
|
||||
\\"client:root.local\\": {
|
||||
\\"__id\\": \\"client:root.local\\",
|
||||
\\"__typename\\": \\"Local\\",
|
||||
\\"authToken\\": \\"\\",
|
||||
\\"network\\": {
|
||||
\\"__ref\\": \\"client:root.local.network\\"
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Environment, RecordSource } from "relay-runtime";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import { createInMemoryStorage } from "talk-framework/lib/storage";
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
|
||||
import initLocalState from "./initLocalState";
|
||||
@@ -18,7 +19,7 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
it("init local state", async () => {
|
||||
initLocalState(environment);
|
||||
initLocalState(environment, { localStorage: createInMemoryStorage() } as any);
|
||||
await timeout();
|
||||
expect(JSON.stringify(source.toJSON(), null, 2)).toMatchSnapshot();
|
||||
});
|
||||
@@ -32,7 +33,7 @@ it("set assetID from query", () => {
|
||||
document.title,
|
||||
`http://localhost/?assetID=${assetID}`
|
||||
);
|
||||
initLocalState(environment);
|
||||
initLocalState(environment, { localStorage: createInMemoryStorage() } as any);
|
||||
expect(source.get(LOCAL_ID)!.assetID).toBe(assetID);
|
||||
window.history.replaceState(previousState, document.title, previousLocation);
|
||||
});
|
||||
@@ -46,7 +47,16 @@ it("set commentID from query", () => {
|
||||
document.title,
|
||||
`http://localhost/?commentID=${commentID}`
|
||||
);
|
||||
initLocalState(environment);
|
||||
initLocalState(environment, { localStorage: createInMemoryStorage() } as any);
|
||||
expect(source.get(LOCAL_ID)!.commentID).toBe(commentID);
|
||||
window.history.replaceState(previousState, document.title, previousLocation);
|
||||
});
|
||||
|
||||
it("set authToken from localStorage", () => {
|
||||
const authToken = "auth-token";
|
||||
const localStorage = createInMemoryStorage();
|
||||
localStorage.setItem("authToken", authToken);
|
||||
initLocalState(environment, { localStorage } as any);
|
||||
expect(source.get(LOCAL_ID)!.authToken).toBe(authToken);
|
||||
localStorage.removeItem("authToken");
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import qs from "query-string";
|
||||
import { commitLocalUpdate, Environment } from "relay-runtime";
|
||||
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import {
|
||||
createAndRetain,
|
||||
LOCAL_ID,
|
||||
@@ -17,14 +18,21 @@ import {
|
||||
/**
|
||||
* Initializes the local state, before we start the App.
|
||||
*/
|
||||
export default async function initLocalState(environment: Environment) {
|
||||
export default async function initLocalState(
|
||||
environment: Environment,
|
||||
{ localStorage }: TalkContext
|
||||
) {
|
||||
commitLocalUpdate(environment, s => {
|
||||
// TODO: (cvle) move local, auth token and network initialization to framework.
|
||||
const root = s.getRoot();
|
||||
|
||||
// Create the Local Record which is the Root for the client states.
|
||||
const localRecord = createAndRetain(environment, s, LOCAL_ID, LOCAL_TYPE);
|
||||
root.setLinkedRecord(localRecord, "local");
|
||||
|
||||
// Set auth token
|
||||
localRecord.setValue(localStorage.getItem("authToken") || "", "authToken");
|
||||
|
||||
// Parse query params
|
||||
const query = qs.parse(location.search);
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ type Local {
|
||||
assetURL: String
|
||||
commentID: String
|
||||
authPopup: AuthPopup!
|
||||
authToken: String
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { default as withSetCommentID } from "./withSetCommentID";
|
||||
@@ -1,3 +1,4 @@
|
||||
import "jest-localstorage-mock";
|
||||
import "./enzyme";
|
||||
import "./jsdom";
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.root {
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: FormField
|
||||
menu: UI Kit
|
||||
---
|
||||
|
||||
import { Playground, PropsTable } from 'docz'
|
||||
import FormField from './FormField'
|
||||
import { InputLabel, ValidationMessage, TextField, Typography, Flex, Button} from '../core/client/ui/components'
|
||||
|
||||
# Flex
|
||||
|
||||
`FormField` is to be used with Form Components `flexbox`.
|
||||
|
||||
## Justify content
|
||||
<Playground>
|
||||
<FormField>
|
||||
</FormField>
|
||||
</Playground>
|
||||
|
||||
## Align items
|
||||
<Playground>
|
||||
<Flex alignItems="center" itemGutter>
|
||||
<Button variant="filled">Push Me</Button>
|
||||
<Button variant="filled" size="small">Push Me</Button>
|
||||
<Button variant="filled" size="large">Push Me</Button>
|
||||
</Flex>
|
||||
</Playground>
|
||||
|
||||
## Direction
|
||||
<Playground>
|
||||
<Flex direction="column" itemGutter>
|
||||
<Button variant="filled">Push Me</Button>
|
||||
<Button variant="filled">Push Me</Button>
|
||||
<Button variant="filled">Push Me</Button>
|
||||
</Flex>
|
||||
</Playground>
|
||||
@@ -0,0 +1,43 @@
|
||||
import cn from "classnames";
|
||||
import React, { ReactNode } from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import { withStyles } from "talk-ui/hocs";
|
||||
|
||||
import * as styles from "./FormField.css";
|
||||
|
||||
interface InnerProps {
|
||||
children: ReactNode;
|
||||
classes: typeof styles;
|
||||
id?: string;
|
||||
className?: string;
|
||||
itemGutter?: boolean | "half";
|
||||
}
|
||||
|
||||
const FormField: StatelessComponent<InnerProps> = props => {
|
||||
const { classes, className, children, itemGutter, ...rest } = props;
|
||||
|
||||
// TODO (bc): Use flex component once the extra div issue is solved.
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
classes.root,
|
||||
{
|
||||
[classes.itemGutter]: itemGutter === true,
|
||||
[classes.halfItemGutter]: itemGutter === "half",
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FormField.defaultProps = {
|
||||
itemGutter: true,
|
||||
};
|
||||
|
||||
const enhanced = withStyles(styles)(FormField);
|
||||
export default enhanced;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./InputDescription";
|
||||
@@ -1,5 +1,5 @@
|
||||
.root {
|
||||
composes: textField from "talk-ui/shared/typography.css";
|
||||
composes: inputText placeholderPseudo from "talk-ui/shared/typography.css";
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: calc(0.5 * var(--spacing-unit));
|
||||
@@ -24,12 +24,3 @@
|
||||
.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
font-family: var(--font-family);
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
line-height: calc(16rem / var(--rem-base));
|
||||
font-size: calc(16rem / var(--rem-base));
|
||||
color: var(--palette-grey-lighter);
|
||||
}
|
||||
|
||||
@@ -18,5 +18,3 @@ import Flex from '../Flex'
|
||||
<TextField color="error" defaultValue="A TextField with an error" fullWidth/>
|
||||
</Flex>
|
||||
</Playground>
|
||||
|
||||
|
||||
|
||||
@@ -34,9 +34,21 @@ export interface TextFieldProps {
|
||||
*/
|
||||
placeholder?: string;
|
||||
/**
|
||||
* readOnly
|
||||
* Mark as readonly
|
||||
*/
|
||||
readOnly?: boolean;
|
||||
/**
|
||||
* Name
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* type
|
||||
*/
|
||||
type?: string;
|
||||
/**
|
||||
* onChange
|
||||
*/
|
||||
onChange?: <T>(event: any) => void;
|
||||
}
|
||||
|
||||
const TextField: StatelessComponent<TextFieldProps> = props => {
|
||||
|
||||
@@ -59,33 +59,34 @@
|
||||
.heading1 {
|
||||
font-size: calc(24rem / var(--rem-base));
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-family: "Manuale";
|
||||
font-family: var(--font-family-serif);
|
||||
line-height: calc(32em / 24);
|
||||
letter-spacing: calc(0.2em / 16);
|
||||
letter-spacing: calc(0.2em / 24);
|
||||
color: var(--palette-text-primary);
|
||||
}
|
||||
|
||||
.heading2 {
|
||||
font-size: calc(20rem / var(--rem-base));
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-family: "Manuale";
|
||||
font-family: var(--font-family-serif);
|
||||
line-height: calc(24em / 20);
|
||||
letter-spacing: calc(0.2em / 20);
|
||||
color: var(--palette-text-primary);
|
||||
}
|
||||
|
||||
.heading3 {
|
||||
font-size: calc(18rem / var(--rem-base));
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-family: "Manuale";
|
||||
font-family: var(--font-family-serif);
|
||||
line-height: calc(20em / 18);
|
||||
letter-spacing: calc(0.2em / 16);
|
||||
letter-spacing: calc(0.2em / 18);
|
||||
color: var(--palette-text-primary);
|
||||
}
|
||||
|
||||
.heading4 {
|
||||
font-size: calc(16rem / var(--rem-base));
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-family: "Manuale";
|
||||
font-family: var(--font-family-serif);
|
||||
line-height: calc(18em / 16);
|
||||
letter-spacing: calc(0.2em / 16);
|
||||
color: var(--palette-text-primary);
|
||||
@@ -94,7 +95,7 @@
|
||||
.bodyCopy {
|
||||
font-size: calc(16rem / var(--rem-base));
|
||||
font-weight: var(--font-weight-regular);
|
||||
font-family: "Source Sans Pro";
|
||||
font-family: var(--font-family-sans-serif);
|
||||
line-height: calc(18em / 16);
|
||||
letter-spacing: calc(0.2em / 16);
|
||||
color: var(--palette-text-primary);
|
||||
@@ -107,44 +108,44 @@
|
||||
|
||||
.button {
|
||||
color: var(--palette-text-secondary);
|
||||
font-family: "Source Sans Pro";
|
||||
font-family: var(--font-family-sans-serif);
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: 14px;
|
||||
line-height: calc(18em / 16);
|
||||
letter-spacing: calc(0.57em / 16);
|
||||
font-size: calc(14rem / var(--rem-base));
|
||||
line-height: calc(18em / 14);
|
||||
letter-spacing: calc(0.57em / 14);
|
||||
}
|
||||
|
||||
.buttonLarge {
|
||||
color: var(--palette-text-secondary);
|
||||
font-family: "Source Sans Pro";
|
||||
font-family: var(--font-family-sans-serif);
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: 16px;
|
||||
font-size: calc(16rem / var(--rem-base));
|
||||
line-height: calc(20em / 16);
|
||||
letter-spacing: calc(0.57em / 16);
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: var(--palette-text-secondary);
|
||||
font-family: "Source Sans Pro";
|
||||
font-family: var(--font-family-sans-serif);
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: 14px;
|
||||
line-height: calc(18em / 16);
|
||||
letter-spacing: calc(0.2em / 16);
|
||||
font-size: calc(14rem / var(--rem-base));
|
||||
line-height: calc(18em / 14);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.alertMessage {
|
||||
color: var(--palette-common-black);
|
||||
font-family: var(--font-family);
|
||||
font-family: var(--font-family-sans-serif);
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: calc(16rem / var(--rem-base));
|
||||
line-height: normal;
|
||||
letter-spacing: calc(0.2em / 16);
|
||||
line-height: calc(16em / 16);
|
||||
letter-spacing: calc(-0.1em / 16);
|
||||
}
|
||||
|
||||
.textField {
|
||||
font-size: calc(16rem / var(--rem-base));
|
||||
.inputText {
|
||||
font-weight: var(--font-weight-regular);
|
||||
font-family: var(--font-family);
|
||||
font-family: var(--font-family-sans-serif);
|
||||
font-size: calc(16rem / var(--rem-base));
|
||||
line-height: calc(18em / 16);
|
||||
letter-spacing: calc(0.2em / 16);
|
||||
color: var(--palette-text-primary);
|
||||
@@ -153,15 +154,15 @@
|
||||
.inputLabel {
|
||||
font-size: calc(18rem / var(--rem-base));
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-family: "Manuale";
|
||||
line-height: calc(18em / 16);
|
||||
letter-spacing: calc(0.2em / 16);
|
||||
font-family: var(--font-family-serif);
|
||||
line-height: calc(18em / 18);
|
||||
letter-spacing: calc(0.2em / 18);
|
||||
color: var(--palette-text-primary);
|
||||
}
|
||||
|
||||
.inputDescription {
|
||||
font-size: calc(14rem / var(--rem-base));
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-weight: var(--font-weight-regular);
|
||||
font-family: var(--font-family);
|
||||
line-height: calc(18em / 16);
|
||||
letter-spacing: calc(0.2em / 16);
|
||||
@@ -169,10 +170,19 @@
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
font-family: var(--font-family);
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
line-height: calc(16rem / var(--rem-base));
|
||||
font-family: var(--font-family-sans-serif);
|
||||
font-weight: var(--font-weight-regular);
|
||||
font-size: calc(16rem / var(--rem-base));
|
||||
line-height: calc(20em / 16);
|
||||
letter-spacing: 0;
|
||||
color: var(--palette-grey-lighter);
|
||||
}
|
||||
|
||||
.placeholderPseudo::placeholder {
|
||||
font-family: var(--font-family-sans-serif);
|
||||
font-weight: var(--font-weight-regular);
|
||||
font-size: calc(16rem / var(--rem-base));
|
||||
line-height: calc(20em / 16);
|
||||
letter-spacing: 0;
|
||||
color: var(--palette-grey-lighter);
|
||||
}
|
||||
|
||||
@@ -64,8 +64,8 @@ const variables = {
|
||||
roundCorners: "2px",
|
||||
/* Typography */
|
||||
remBase: 16,
|
||||
fontFamily: '"Source Sans Pro"',
|
||||
fontSize: 16,
|
||||
fontFamilySansSerif: '"Source Sans Pro"',
|
||||
fontFamilySerif: '"Manuale"',
|
||||
fontWeightLight: 300,
|
||||
fontWeightRegular: 400,
|
||||
fontWeightMedium: 600,
|
||||
|
||||
@@ -14,7 +14,6 @@ export interface SignupBody {
|
||||
username: string;
|
||||
password: string;
|
||||
email: string;
|
||||
displayName?: string;
|
||||
}
|
||||
|
||||
const SignupBodySchema = Joi.object().keys({
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { RequestHandler } from "express";
|
||||
|
||||
export const streamHandler: RequestHandler = (req, res) => {
|
||||
res.render("stream");
|
||||
};
|
||||
@@ -1,7 +1,10 @@
|
||||
import cons from "consolidate";
|
||||
import { Express } from "express";
|
||||
import http from "http";
|
||||
import { Redis } from "ioredis";
|
||||
import { Db } from "mongodb";
|
||||
import nunjucks from "nunjucks";
|
||||
import path from "path";
|
||||
|
||||
import { Config } from "talk-common/config";
|
||||
import { notFoundMiddleware } from "talk-server/app/middleware/notFound";
|
||||
@@ -29,6 +32,9 @@ export interface AppOptions {
|
||||
* createApp will create a Talk Express app that can be used to handle requests.
|
||||
*/
|
||||
export async function createApp(options: AppOptions): Promise<Express> {
|
||||
// Configure the application.
|
||||
configureApplication(options);
|
||||
|
||||
// Pull the parent out of the options.
|
||||
const { parent } = options;
|
||||
|
||||
@@ -46,7 +52,7 @@ export async function createApp(options: AppOptions): Promise<Express> {
|
||||
);
|
||||
|
||||
// Static Files
|
||||
parent.use(serveStatic);
|
||||
parent.use("/assets", serveStatic);
|
||||
|
||||
// Error Handling
|
||||
parent.use(notFoundMiddleware);
|
||||
@@ -70,6 +76,38 @@ export const listenAndServe = (
|
||||
const httpServer = app.listen(port, () => resolve(httpServer));
|
||||
});
|
||||
|
||||
function configureApplication(options: AppOptions) {
|
||||
const { parent } = options;
|
||||
|
||||
// Trust the first proxy in front of us, this will enable us to trust the fact
|
||||
// that SSL was terminated correctly.
|
||||
parent.set("trust proxy", 1);
|
||||
|
||||
// Setup the view config.
|
||||
setupViews(options);
|
||||
}
|
||||
|
||||
function setupViews(options: AppOptions) {
|
||||
const { parent } = options;
|
||||
|
||||
// configure the default views directory.
|
||||
const views = path.join(__dirname, "..", "..", "..", "static");
|
||||
parent.set("views", views);
|
||||
|
||||
// Reconfigure nunjucks.
|
||||
(cons.requires as any).nunjucks = nunjucks.configure(views, {
|
||||
// In development, we should enable file watch mode.
|
||||
watch: options.config.get("env") === "development",
|
||||
});
|
||||
|
||||
// assign the nunjucks engine to .njk and .html files.
|
||||
parent.engine("njk", cons.nunjucks);
|
||||
parent.engine("html", cons.nunjucks);
|
||||
|
||||
// set .html as the default extension.
|
||||
parent.set("view engine", "html");
|
||||
}
|
||||
|
||||
/**
|
||||
* attachSubscriptionHandlers attaches all the handlers to the http.Server to
|
||||
* handle websocket traffic by upgrading their http connections to websocket.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { RequestHandler } from "express";
|
||||
|
||||
export const notFoundMiddleware: RequestHandler = (req, res, next) => {
|
||||
// FIXME: (wyattjoh) send an error that won't log as crazily as this one does.
|
||||
next(new Error("not found"));
|
||||
};
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import serveStatic from "express-static-gzip";
|
||||
import path from "path";
|
||||
|
||||
export default serveStatic(path.join(__dirname, "..", "..", "dist"), {});
|
||||
const staticPath = path.resolve(
|
||||
path.join(__dirname, "..", "..", "..", "..", "static", "assets")
|
||||
);
|
||||
|
||||
export default serveStatic(staticPath, { index: false });
|
||||
|
||||
@@ -28,6 +28,9 @@ export default (options: MiddlewareOptions) => async (
|
||||
// Attach the tenant to the request.
|
||||
req.tenant = tenant;
|
||||
|
||||
// Attach the tenant to the view locals.
|
||||
res.locals.tenant = tenant;
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
|
||||
@@ -2,6 +2,7 @@ import express from "express";
|
||||
import passport from "passport";
|
||||
|
||||
import { signupHandler } from "talk-server/app/handlers/auth/local";
|
||||
import { streamHandler } from "talk-server/app/handlers/embed/stream";
|
||||
import { apiErrorHandler } from "talk-server/app/middleware/error";
|
||||
import { errorLogger } from "talk-server/app/middleware/logging";
|
||||
import { wrapAuthn } from "talk-server/app/middleware/passport";
|
||||
@@ -134,5 +135,8 @@ export async function createRouter(app: AppOptions, options: RouterOptions) {
|
||||
);
|
||||
}
|
||||
|
||||
// Handle the stream handler.
|
||||
router.get("/embed/stream", streamHandler);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
## Comments Tab
|
||||
|
||||
comments-postCommentForm-post = Posten
|
||||
comments-postCommentForm-submit = Posten
|
||||
comments-stream-loadMore = Mehr laden
|
||||
comments-replyList-showAll = Alle anzeigen
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Comments Tab
|
||||
|
||||
comments-postCommentForm-post = Post
|
||||
comments-postCommentForm-submit = Submit
|
||||
comments-stream-loadMore = Load more
|
||||
comments-replyList-showAll = Show all
|
||||
|
||||
|
||||
Reference in New Issue
Block a user