Implement Timestamp

Merge branch 'next' into timestamp

Fix test and translations

Merge branch 'timestamp' of github.com:coralproject/talk into timestamp

* 'timestamp' of github.com:coralproject/talk:
  Fix test and translations
  Implement Timestamp
This commit is contained in:
Belén Curcio
2018-07-10 13:34:27 -03:00
parent 0784e7dd63
commit 805492931b
42 changed files with 734 additions and 285 deletions
@@ -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<TalkContext>({} as any);
@@ -27,7 +32,9 @@ export const TalkContextProvider: StatelessComponent<{
}> = ({ value, children }) => (
<Provider value={value}>
<LocalizationProvider messages={value.localeMessages}>
{children}
<UIContext.Provider value={{ timeagoFormatter: value.timeagoFormatter }}>
{children}
</UIContext.Provider>
</LocalizationProvider>
</Provider>
);
@@ -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<void>);
}
/**
* 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 (
<Localized
id="framework-timeago"
$value={value}
$unit={unit}
$suffix={ourSuffix}
>
<span>now</span>
</Localized>
);
};
/**
* `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.
@@ -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(<Comment {...props} />);
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(<Comment {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -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<CommentProps> = props => {
<Typography className={styles.author} gutterBottom>
{props.author && props.author.username}
</Typography>
<Timestamp date={props.createdAt} />
<Typography>{props.body}</Typography>
</div>
);
@@ -0,0 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders username and body 1`] = `
<div
className="root"
>
<withPropsOnChange(Typography)
className="author"
gutterBottom={true}
>
Marvin
</withPropsOnChange(Typography)>
<withPropsOnChange(Timestamp)
date="1995-12-17T06:24:00.000Z"
/>
<withPropsOnChange(Typography)>
Woof
</withPropsOnChange(Typography)>
</div>
`;
exports[`renders with gutterBottom 1`] = `
<div
className="root gutterBottom"
>
<withPropsOnChange(Typography)
className="author"
gutterBottom={true}
>
Marvin
</withPropsOnChange(Typography)>
<withPropsOnChange(Timestamp)
date="1995-12-17T06:24:00.000Z"
/>
<withPropsOnChange(Typography)>
Woof
</withPropsOnChange(Typography)>
</div>
`;
@@ -20,6 +20,7 @@ const enhanced = withFragmentContainer<{ data: Data }>(
author {
username
}
createdAt
body
}
`
+10 -33
View File
@@ -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"
]
}
"include": ["./**/*", "../../types/**/*.d.ts"],
"exclude": ["node_modules"]
}
@@ -0,0 +1,4 @@
.root {
composes: body1 from "talk-ui/shared/typography.css";
background-color: transparent;
}
@@ -0,0 +1,15 @@
---
name: Timestamp
menu: UI Kit
---
import { Playground } from 'docz'
import Timestamp from './Timestamp'
# Timestamp
## Basic usage
<Playground>
<Timestamp date={`2018-07-04T12:14:14.564Z`} />
</Playground>
@@ -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(<Timestamp {...props} />).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(
<UIContext.Provider value={context}>
<Timestamp {...props} />
</UIContext.Provider>
).toJSON();
expect(tree).toMatchSnapshot();
});
@@ -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<InnerProps> {
public render() {
const { date, classes, live, className } = this.props;
return (
<UIContext.Consumer>
{({ timeagoFormatter }) => (
<TimeAgo
date={date}
className={cn(className, classes.root)}
live={live}
formatter={timeagoFormatter || defaultFormatter}
/>
)}
</UIContext.Consumer>
);
}
}
const enhanced = withStyles(styles)(Timestamp);
export type TimestampProps = PropTypesOf<typeof enhanced>;
export default enhanced;
@@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`uses default formatter 1`] = `
<time
className="root"
dateTime="2108-12-17T06:24:00.000Z"
title="2108-12-17T06:24:00.000Z"
>
2108-12-17T06:24:00.000Z
</time>
`;
exports[`uses formatter from context 1`] = `
<time
className="root"
dateTime="2108-12-17T06:24:00.000Z"
title="2108-12-17T06:24:00.000Z"
>
My Formatter
</time>
`;
@@ -0,0 +1,2 @@
export * from "./Timestamp";
export { default } from "./Timestamp";
@@ -0,0 +1,10 @@
import React from "react";
import { Formatter } from "react-timeago";
export interface UIContext {
timeagoFormatter: Formatter;
}
const UIContext = React.createContext<UIContext>({} as any);
export default UIContext;
@@ -0,0 +1 @@
export { default } from "./UIContext";
+2
View File
@@ -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";
+8 -4
View File
@@ -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 {
}
+1 -1
View File
@@ -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));
@@ -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) =>
@@ -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.
"""
+38
View File
@@ -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
}
+45
View File
@@ -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
}
}
View File
+42
View File
@@ -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
}
}
+6 -19
View File
@@ -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"
]
}
"include": ["./**/*"],
"exclude": ["node_modules", "./core/client"]
}
+55
View File
@@ -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<any>;
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<TimeAgoProps>;
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;
}