mirror of
https://github.com/wassname/talk.git
synced 2026-07-27 11:28:12 +08:00
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:
@@ -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>
|
||||
);
|
||||
|
||||
+23
@@ -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
|
||||
}
|
||||
`
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
}
|
||||
|
||||
Vendored
+55
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user