diff --git a/config/webpack.config.dev.js b/config/webpack.config.dev.js index fcbb75fe2..359d53f7b 100644 --- a/config/webpack.config.dev.js +++ b/config/webpack.config.dev.js @@ -180,7 +180,7 @@ module.exports = { // All available locales can be loadable on demand. // To restrict available locales set: - // availableLocales: ["en-US"] + // availableLocales: ["en-US"], }, }, ], @@ -247,6 +247,7 @@ module.exports = { options: { modules: true, importLoaders: 1, + localIdentName: "[name]-[local]-[hash:base64:5]", }, }, { diff --git a/package-lock.json b/package-lock.json index 1419c66a5..f4ddb8d3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9152,14 +9152,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -9174,20 +9172,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -9304,8 +9299,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -9317,7 +9311,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -9332,7 +9325,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -9444,8 +9436,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -9578,7 +9569,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -18851,6 +18841,12 @@ "react-is": "^16.4.1" } }, + "react-timeago": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/react-timeago/-/react-timeago-4.1.9.tgz", + "integrity": "sha512-MKucv9nU65BOPqIrClAFxqvpGCC4RdRpqp0P1YIb7C3yT6TQVdcoOlr0k4TDHvLQhbkwd3nbTxiDQMa3iDlZxg==", + "dev": true + }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/package.json b/package.json index be16f0846..b7e66c3d5 100644 --- a/package.json +++ b/package.json @@ -136,6 +136,7 @@ "react-relay": "github:coralproject/patched#react-relay", "react-responsive": "^4.1.0", "react-test-renderer": "^16.4.1", + "react-timeago": "^4.1.9", "recompose": "^0.27.1", "relay-compiler": "github:coralproject/patched#relay-compiler", "relay-compiler-language-typescript": "github:coralproject/patched#relay-compiler-language-typescript", diff --git a/src/core/client/framework/lib/bootstrap/TalkContext.tsx b/src/core/client/framework/lib/bootstrap/TalkContext.tsx index 93bc84c43..d14c245f5 100644 --- a/src/core/client/framework/lib/bootstrap/TalkContext.tsx +++ b/src/core/client/framework/lib/bootstrap/TalkContext.tsx @@ -1,7 +1,9 @@ import { LocalizationProvider } from "fluent-react/compat"; import { MessageContext } from "fluent/compat"; import React, { StatelessComponent } from "react"; +import { Formatter } from "react-timeago"; import { Environment } from "relay-runtime"; +import { UIContext } from "talk-ui/components"; export interface TalkContext { // relayEnvironment for our relay framework. @@ -9,6 +11,9 @@ export interface TalkContext { // localMessages for our i18n framework. localeMessages: MessageContext[]; + + // formatter for timeago. + timeagoFormatter?: Formatter; } const { Provider, Consumer } = React.createContext({} as any); @@ -27,7 +32,9 @@ export const TalkContextProvider: StatelessComponent<{ }> = ({ value, children }) => ( - {children} + + {children} + ); diff --git a/src/core/client/framework/lib/bootstrap/createContext.ts b/src/core/client/framework/lib/bootstrap/createContext.tsx similarity index 71% rename from src/core/client/framework/lib/bootstrap/createContext.ts rename to src/core/client/framework/lib/bootstrap/createContext.tsx index 764d09214..6043eacb9 100644 --- a/src/core/client/framework/lib/bootstrap/createContext.ts +++ b/src/core/client/framework/lib/bootstrap/createContext.tsx @@ -1,4 +1,7 @@ +import { Localized } from "fluent-react/compat"; import { noop } from "lodash"; +import React from "react"; +import { Formatter } from "react-timeago"; import { Environment, Network, RecordSource, Store } from "relay-runtime"; import { generateMessages, LocalesData, negotiateLanguages } from "../i18n"; @@ -16,6 +19,25 @@ interface CreateContextArguments { init?: ((context: TalkContext) => void | Promise); } +/** + * timeagoFormatter integrates timeago into our translation + * framework. It gets injected into the UIContext. + */ +export const timeagoFormatter: Formatter = (value, unit, suffix) => { + // We use 'in' instead of 'from now' for language consistency + const ourSuffix = suffix === "from now" ? "in" : suffix; + return ( + + now + + ); +}; + /** * `createContext` manages the dependencies of our framework * and returns a `TalkContext` that can be passed to the @@ -46,6 +68,7 @@ export default async function createContext({ const context = { relayEnvironment, localeMessages, + timeagoFormatter, }; // Run custom initializations. diff --git a/src/core/client/stream/components/App.css b/src/core/client/stream/components/App.css new file mode 100644 index 000000000..68c4e29c4 --- /dev/null +++ b/src/core/client/stream/components/App.css @@ -0,0 +1,14 @@ +/* Here we add global stylings for body and document */ +:global { + body { + margin: "0"; + + /* Support for all WebKit browsers. */ + -webkit-font-smoothing: antialiased; + /* Support for Firefox. */ + -moz-osx-font-smoothing: grayscale; + } +} + +.root { +} diff --git a/src/core/client/stream/components/App.spec.tsx b/src/core/client/stream/components/App.spec.tsx new file mode 100644 index 000000000..9b91a8974 --- /dev/null +++ b/src/core/client/stream/components/App.spec.tsx @@ -0,0 +1,22 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import { PropTypesOf } from "talk-framework/types"; + +import App from "./App"; + +it("renders correctly", () => { + const props: PropTypesOf = { + asset: {}, + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); + +it("renders correctly when asset is null", () => { + const props: PropTypesOf = { + asset: null, + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/stream/components/App.tsx b/src/core/client/stream/components/App.tsx index 1dd3ce496..14659585a 100644 --- a/src/core/client/stream/components/App.tsx +++ b/src/core/client/stream/components/App.tsx @@ -5,6 +5,8 @@ import { Flex } from "talk-ui/components"; import StreamContainer from "../containers/StreamContainer"; +import * as styles from "./App.css"; + export interface AppProps { asset: {} | null; } @@ -12,7 +14,7 @@ export interface AppProps { const App: StatelessComponent = props => { if (props.asset) { return ( - + ); diff --git a/src/core/client/stream/components/Comment.css b/src/core/client/stream/components/Comment.css deleted file mode 100644 index 77dc9d88d..000000000 --- a/src/core/client/stream/components/Comment.css +++ /dev/null @@ -1,10 +0,0 @@ -.root { -} - -.gutterBottom { - margin-bottom: calc(1px * var(--spacing-unit)); -} - -.topBar { - margin-bottom: calc(0.5px * var(--spacing-unit)); -} diff --git a/src/core/client/stream/components/Comment.spec.tsx b/src/core/client/stream/components/Comment/Comment.spec.tsx similarity index 53% rename from src/core/client/stream/components/Comment.spec.tsx rename to src/core/client/stream/components/Comment/Comment.spec.tsx index a73e04730..cae068012 100644 --- a/src/core/client/stream/components/Comment.spec.tsx +++ b/src/core/client/stream/components/Comment/Comment.spec.tsx @@ -1,26 +1,17 @@ import { shallow } from "enzyme"; import React from "react"; +import { PropTypesOf } from "talk-framework/types"; + import Comment from "./Comment"; it("renders username and body", () => { - const props = { + const props: PropTypesOf = { author: { username: "Marvin", }, body: "Woof", - }; - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); -}); - -it("renders with gutterBottom", () => { - const props = { - author: { - username: "Marvin", - }, - body: "Woof", - gutterBottom: true, + createdAt: "1995-12-17T03:24:00.000Z", }; const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); diff --git a/src/core/client/stream/components/Comment.tsx b/src/core/client/stream/components/Comment/Comment.tsx similarity index 59% rename from src/core/client/stream/components/Comment.tsx rename to src/core/client/stream/components/Comment/Comment.tsx index bd22c4ffc..f923e4149 100644 --- a/src/core/client/stream/components/Comment.tsx +++ b/src/core/client/stream/components/Comment/Comment.tsx @@ -1,30 +1,27 @@ -import cn from "classnames"; import React from "react"; import { StatelessComponent } from "react"; import { Typography } from "talk-ui/components"; -import * as styles from "./Comment.css"; +import Timestamp from "./Timestamp"; +import TopBar from "./TopBar"; import Username from "./Username"; export interface CommentProps { - className?: string; author: { username: string; } | null; body: string | null; - gutterBottom?: boolean; + createdAt: string; } const Comment: StatelessComponent = props => { - const rootClassName = cn(styles.root, props.className, { - [styles.gutterBottom]: props.gutterBottom, - }); return ( -
-
+
+ {props.author && {props.author.username}} -
+ {props.createdAt} + {props.body}
); diff --git a/src/core/client/stream/components/Comment/Timestamp.css b/src/core/client/stream/components/Comment/Timestamp.css new file mode 100644 index 000000000..f2d263ad4 --- /dev/null +++ b/src/core/client/stream/components/Comment/Timestamp.css @@ -0,0 +1,3 @@ +.root { + composes: timestamp from "talk-ui/shared/typography.css"; +} diff --git a/src/core/client/stream/components/Comment/Timestamp.spec.tsx b/src/core/client/stream/components/Comment/Timestamp.spec.tsx new file mode 100644 index 000000000..4a276b762 --- /dev/null +++ b/src/core/client/stream/components/Comment/Timestamp.spec.tsx @@ -0,0 +1,14 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import { PropTypesOf } from "talk-framework/types"; + +import Timestamp from "./Timestamp"; + +it("renders correctly", () => { + const props: PropTypesOf = { + children: "1995-12-17T03:24:00.000Z", + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/stream/components/Comment/Timestamp.tsx b/src/core/client/stream/components/Comment/Timestamp.tsx new file mode 100644 index 000000000..e6228416f --- /dev/null +++ b/src/core/client/stream/components/Comment/Timestamp.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { StatelessComponent } from "react"; + +import { RelativeTime } from "talk-ui/components"; + +import * as styles from "./Timestamp.css"; + +export interface TimestampProps { + children: string; +} + +const Timestamp: StatelessComponent = props => ( + +); + +export default Timestamp; diff --git a/src/core/client/stream/components/Comment/TopBar.css b/src/core/client/stream/components/Comment/TopBar.css new file mode 100644 index 000000000..dff9c8a74 --- /dev/null +++ b/src/core/client/stream/components/Comment/TopBar.css @@ -0,0 +1,3 @@ +.root { + margin-bottom: calc(0.5 * var(--spacing-unit)); +} diff --git a/src/core/client/stream/components/Comment/TopBar.spec.tsx b/src/core/client/stream/components/Comment/TopBar.spec.tsx new file mode 100644 index 000000000..5ee691f70 --- /dev/null +++ b/src/core/client/stream/components/Comment/TopBar.spec.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import TestRenderer from "react-test-renderer"; + +import { PropTypesOf } from "talk-framework/types"; +import { UIContext, UIContextProps } from "talk-ui/components"; + +import TopBar from "./TopBar"; + +it("renders correctly on small screens", () => { + const props: PropTypesOf = { + children:
Hello World
, + }; + + const context: UIContextProps = { + mediaQueryValues: { + width: 320, + }, + }; + + const testRenderer = TestRenderer.create( + + + + ); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("renders correctly on big screens", () => { + const props: PropTypesOf = { + children:
Hello World
, + }; + + const context: UIContextProps = { + mediaQueryValues: { + width: 1600, + }, + }; + + const testRenderer = TestRenderer.create( + + + + ); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); diff --git a/src/core/client/stream/components/Comment/TopBar.tsx b/src/core/client/stream/components/Comment/TopBar.tsx new file mode 100644 index 000000000..2dfe6419d --- /dev/null +++ b/src/core/client/stream/components/Comment/TopBar.tsx @@ -0,0 +1,32 @@ +import cn from "classnames"; +import React from "react"; +import { StatelessComponent } from "react"; + +import { Flex, MatchMedia } from "talk-ui/components"; + +import * as styles from "./TopBar.css"; + +export interface TopBarProps { + className?: string; + children: React.ReactNode; +} + +const TopBar: StatelessComponent = props => { + const rootClassName = cn(styles.root, props.className); + return ( + + {matches => ( + + {props.children} + + )} + + ); +}; + +export default TopBar; diff --git a/src/core/client/stream/components/Comment/Username.css b/src/core/client/stream/components/Comment/Username.css new file mode 100644 index 000000000..e2dd79a8e --- /dev/null +++ b/src/core/client/stream/components/Comment/Username.css @@ -0,0 +1,3 @@ +.root { + line-height: 1; +} diff --git a/src/core/client/stream/components/Comment/Username.spec.tsx b/src/core/client/stream/components/Comment/Username.spec.tsx new file mode 100644 index 000000000..1a10e4012 --- /dev/null +++ b/src/core/client/stream/components/Comment/Username.spec.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import TestRenderer from "react-test-renderer"; + +import { PropTypesOf } from "talk-framework/types"; +import { UIContext, UIContextProps } from "talk-ui/components"; + +import Username from "./Username"; + +it("renders correctly on small screens", () => { + const props: PropTypesOf = { + children: "Marvin", + }; + + const context: UIContextProps = { + mediaQueryValues: { + width: 320, + }, + }; + + const testRenderer = TestRenderer.create( + + + + ); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("renders correctly on big screens", () => { + const props: PropTypesOf = { + children: "Marvin", + }; + + const context: UIContextProps = { + mediaQueryValues: { + width: 1600, + }, + }; + + const testRenderer = TestRenderer.create( + + + + ); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); diff --git a/src/core/client/stream/components/Comment/Username.tsx b/src/core/client/stream/components/Comment/Username.tsx new file mode 100644 index 000000000..ada04c8ec --- /dev/null +++ b/src/core/client/stream/components/Comment/Username.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { StatelessComponent } from "react"; + +import { MatchMedia, Typography } from "talk-ui/components"; + +import * as styles from "./Username.css"; + +export interface UsernameProps { + children: string; +} + +const Username: StatelessComponent = props => { + return ( + + {matches => ( + + {props.children} + + )} + + ); +}; + +export default Username; diff --git a/src/core/client/stream/components/Comment/__snapshots__/Comment.spec.tsx.snap b/src/core/client/stream/components/Comment/__snapshots__/Comment.spec.tsx.snap new file mode 100644 index 000000000..2d047c200 --- /dev/null +++ b/src/core/client/stream/components/Comment/__snapshots__/Comment.spec.tsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders username and body 1`] = ` +
+ + + Marvin + + + 1995-12-17T03:24:00.000Z + + + + Woof + +
+`; diff --git a/src/core/client/stream/components/Comment/__snapshots__/Timestamp.spec.tsx.snap b/src/core/client/stream/components/Comment/__snapshots__/Timestamp.spec.tsx.snap new file mode 100644 index 000000000..862d6cad7 --- /dev/null +++ b/src/core/client/stream/components/Comment/__snapshots__/Timestamp.spec.tsx.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + +`; diff --git a/src/core/client/stream/components/Comment/__snapshots__/TopBar.spec.tsx.snap b/src/core/client/stream/components/Comment/__snapshots__/TopBar.spec.tsx.snap new file mode 100644 index 000000000..9253404bd --- /dev/null +++ b/src/core/client/stream/components/Comment/__snapshots__/TopBar.spec.tsx.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly on big screens 1`] = ` +
+
+ Hello World +
+
+`; + +exports[`renders correctly on small screens 1`] = ` +
+
+ Hello World +
+
+`; diff --git a/src/core/client/stream/components/Comment/__snapshots__/Username.spec.tsx.snap b/src/core/client/stream/components/Comment/__snapshots__/Username.spec.tsx.snap new file mode 100644 index 000000000..ab0e141e6 --- /dev/null +++ b/src/core/client/stream/components/Comment/__snapshots__/Username.spec.tsx.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly on big screens 1`] = ` + + Marvin + +`; + +exports[`renders correctly on small screens 1`] = ` + + Marvin + +`; diff --git a/src/core/client/stream/components/Comment/index.ts b/src/core/client/stream/components/Comment/index.ts new file mode 100644 index 000000000..173033df9 --- /dev/null +++ b/src/core/client/stream/components/Comment/index.ts @@ -0,0 +1 @@ +export { default, default as Comment, CommentProps } from "./Comment"; diff --git a/src/core/client/stream/components/Indent.css b/src/core/client/stream/components/Indent.css index 19100de68..c675b03a6 100644 --- a/src/core/client/stream/components/Indent.css +++ b/src/core/client/stream/components/Indent.css @@ -1,6 +1,6 @@ .root { border-left: 3px solid; - padding-left: calc(1px * var(--spacing-unit)); + padding-left: var(--spacing-unit); } .level0 { diff --git a/src/core/client/stream/components/Indent.spec.tsx b/src/core/client/stream/components/Indent.spec.tsx new file mode 100644 index 000000000..3108ba209 --- /dev/null +++ b/src/core/client/stream/components/Indent.spec.tsx @@ -0,0 +1,14 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import { PropTypesOf } from "talk-framework/types"; + +import Indent from "./Indent"; + +it("renders correctly", () => { + const props: PropTypesOf = { + children:
Hello World
, + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/stream/components/PostCommentForm.css b/src/core/client/stream/components/PostCommentForm.css index 2e9cdcc9e..df045285d 100644 --- a/src/core/client/stream/components/PostCommentForm.css +++ b/src/core/client/stream/components/PostCommentForm.css @@ -5,7 +5,7 @@ height: 100px; width: 100%; box-sizing: border-box; - margin-bottom: calc(1px * var(--spacing-unit)); + margin-bottom: var(--spacing-unit); } .postButtonContainer { diff --git a/src/core/client/stream/components/ReplyList.spec.tsx b/src/core/client/stream/components/ReplyList.spec.tsx index 12b673732..093d96049 100644 --- a/src/core/client/stream/components/ReplyList.spec.tsx +++ b/src/core/client/stream/components/ReplyList.spec.tsx @@ -3,10 +3,12 @@ import { noop } from "lodash"; import React from "react"; import sinon, { SinonSpy } from "sinon"; -import ReplyList, { ReplyListProps } from "./ReplyList"; +import { PropTypesOf } from "talk-framework/types"; + +import ReplyList from "./ReplyList"; it("renders correctly", () => { - const props: ReplyListProps = { + const props: PropTypesOf = { commentID: "comment-id", comments: [{ id: "comment-1" }, { id: "comment-2" }], onShowAll: noop, @@ -18,7 +20,7 @@ it("renders correctly", () => { }); describe("when there is more", () => { - const props: ReplyListProps = { + const props: PropTypesOf = { commentID: "comment-id", comments: [{ id: "comment-1" }, { id: "comment-2" }], onShowAll: sinon.spy(), diff --git a/src/core/client/stream/components/ReplyList.tsx b/src/core/client/stream/components/ReplyList.tsx index ec366771c..17ad32ed2 100644 --- a/src/core/client/stream/components/ReplyList.tsx +++ b/src/core/client/stream/components/ReplyList.tsx @@ -2,7 +2,7 @@ import { Localized } from "fluent-react/compat"; import * as React from "react"; import { StatelessComponent } from "react"; -import { Button } from "talk-ui/components"; +import { Button, Flex } from "talk-ui/components"; import CommentContainer from "../containers/CommentContainer"; import Indent from "./Indent"; @@ -18,9 +18,14 @@ export interface ReplyListProps { const ReplyList: StatelessComponent = props => { return ( -
+ {props.comments.map(comment => ( - + ))} {props.hasMore && ( @@ -37,7 +42,7 @@ const ReplyList: StatelessComponent = props => { )} -
+
); }; diff --git a/src/core/client/stream/components/Stream.spec.tsx b/src/core/client/stream/components/Stream.spec.tsx index 05d30d66e..335f8fe27 100644 --- a/src/core/client/stream/components/Stream.spec.tsx +++ b/src/core/client/stream/components/Stream.spec.tsx @@ -1,12 +1,14 @@ import { shallow } from "enzyme"; import { noop } from "lodash"; import React from "react"; -import sinon from "sinon"; +import sinon, { SinonSpy } from "sinon"; -import Stream, { StreamProps } from "./Stream"; +import { PropTypesOf } from "talk-framework/types"; + +import Stream from "./Stream"; it("renders correctly", () => { - const props: StreamProps = { + const props: PropTypesOf = { assetID: "asset-id", isClosed: false, comments: [{ id: "comment-1" }, { id: "comment-2" }], @@ -19,7 +21,7 @@ it("renders correctly", () => { }); describe("when there is more", () => { - const props = { + const props: PropTypesOf = { assetID: "asset-id", isClosed: false, comments: [{ id: "comment-1" }, { id: "comment-2" }], @@ -35,7 +37,7 @@ describe("when there is more", () => { it("calls onLoadMore", () => { wrapper.find("#talk-comments-stream-loadMore").simulate("click"); - expect(props.onLoadMore.calledOnce).toBe(true); + expect((props.onLoadMore as SinonSpy).calledOnce).toBe(true); }); const wrapperDisabledButton = shallow(); diff --git a/src/core/client/stream/components/Stream.tsx b/src/core/client/stream/components/Stream.tsx index f6d39940e..da55050f5 100644 --- a/src/core/client/stream/components/Stream.tsx +++ b/src/core/client/stream/components/Stream.tsx @@ -2,7 +2,7 @@ import { Localized } from "fluent-react/compat"; import * as React from "react"; import { StatelessComponent } from "react"; -import { Button } from "talk-ui/components"; +import { Button, Flex } from "talk-ui/components"; import CommentContainer from "../containers/CommentContainer"; import PostCommentFormContainer from "../containers/PostCommentFormContainer"; @@ -24,12 +24,18 @@ const Stream: StatelessComponent = props => {
-
+ {props.comments.map(comment => ( -
- + + -
+
))} {props.hasMore && ( @@ -46,7 +52,7 @@ const Stream: StatelessComponent = props => { )} -
+
); }; diff --git a/src/core/client/stream/components/Username.css b/src/core/client/stream/components/Username.css deleted file mode 100644 index 9cb1baa15..000000000 --- a/src/core/client/stream/components/Username.css +++ /dev/null @@ -1,3 +0,0 @@ -.root { - composes: heading4 from "talk-ui/shared/typography.css"; -} diff --git a/src/core/client/stream/components/Username.spec.tsx b/src/core/client/stream/components/Username.spec.tsx deleted file mode 100644 index c4f1c1dfe..000000000 --- a/src/core/client/stream/components/Username.spec.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { shallow } from "enzyme"; -import React from "react"; - -import Username from "./Username"; - -it("renders correctly", () => { - const props = { - children: "Marvin", - }; - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/src/core/client/stream/components/Username.tsx b/src/core/client/stream/components/Username.tsx deleted file mode 100644 index 117ebdb3d..000000000 --- a/src/core/client/stream/components/Username.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from "react"; -import { StatelessComponent } from "react"; - -import * as styles from "./Username.css"; - -export interface CommentProps { - children: string; -} - -const Username: StatelessComponent = props => { - return {props.children}; -}; - -export default Username; diff --git a/src/core/client/stream/components/__snapshots__/App.spec.tsx.snap b/src/core/client/stream/components/__snapshots__/App.spec.tsx.snap new file mode 100644 index 000000000..6a12e4e09 --- /dev/null +++ b/src/core/client/stream/components/__snapshots__/App.spec.tsx.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + + + +`; + +exports[`renders correctly when asset is null 1`] = ` +
+ Asset not found +
+`; diff --git a/src/core/client/stream/components/__snapshots__/Comment.spec.tsx.snap b/src/core/client/stream/components/__snapshots__/Comment.spec.tsx.snap deleted file mode 100644 index 9aafeaf5c..000000000 --- a/src/core/client/stream/components/__snapshots__/Comment.spec.tsx.snap +++ /dev/null @@ -1,37 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders username and body 1`] = ` -
-
- - Marvin - -
- - Woof - -
-`; - -exports[`renders with gutterBottom 1`] = ` -
-
- - Marvin - -
- - Woof - -
-`; diff --git a/src/core/client/stream/components/__snapshots__/Indent.spec.tsx.snap b/src/core/client/stream/components/__snapshots__/Indent.spec.tsx.snap new file mode 100644 index 000000000..910c819b2 --- /dev/null +++ b/src/core/client/stream/components/__snapshots__/Indent.spec.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +
+
+ Hello World +
+
+`; diff --git a/src/core/client/stream/components/__snapshots__/ReplyList.spec.tsx.snap b/src/core/client/stream/components/__snapshots__/ReplyList.spec.tsx.snap index 36fb0c110..e48482470 100644 --- a/src/core/client/stream/components/__snapshots__/ReplyList.spec.tsx.snap +++ b/src/core/client/stream/components/__snapshots__/ReplyList.spec.tsx.snap @@ -2,8 +2,10 @@ exports[`renders correctly 1`] = ` -
-
+
`; exports[`when there is more renders a load more button 1`] = ` -
-
+
`; diff --git a/src/core/client/stream/components/__snapshots__/Stream.spec.tsx.snap b/src/core/client/stream/components/__snapshots__/Stream.spec.tsx.snap index c529ea1ad..a8210fd99 100644 --- a/src/core/client/stream/components/__snapshots__/Stream.spec.tsx.snap +++ b/src/core/client/stream/components/__snapshots__/Stream.spec.tsx.snap @@ -10,12 +10,16 @@ exports[`renders correctly 1`] = ` -
-
-
-
+ -
-
+ +
`; @@ -67,12 +71,16 @@ exports[`when there is more disables load more button 1`] = ` -
-
-
-
+ -
+ @@ -125,7 +133,7 @@ exports[`when there is more disables load more button 1`] = ` Load More -
+
`; @@ -139,12 +147,16 @@ exports[`when there is more renders a load more button 1`] = ` -
-
-
-
+ -
+ @@ -197,6 +209,6 @@ exports[`when there is more renders a load more button 1`] = ` Load More -
+ `; diff --git a/src/core/client/stream/containers/AppContainer.spec.tsx b/src/core/client/stream/containers/AppContainer.spec.tsx new file mode 100644 index 000000000..17a0a676d --- /dev/null +++ b/src/core/client/stream/containers/AppContainer.spec.tsx @@ -0,0 +1,16 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import { PropTypesOf } from "talk-framework/types"; + +import { AppContainer } from "./AppContainer"; + +it("renders correctly", () => { + const props: PropTypesOf = { + data: { + asset: {}, + }, + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/stream/containers/AppContainer.tsx b/src/core/client/stream/containers/AppContainer.tsx index 8b84660ca..90eb1e1c6 100644 --- a/src/core/client/stream/containers/AppContainer.tsx +++ b/src/core/client/stream/containers/AppContainer.tsx @@ -11,7 +11,7 @@ interface InnerProps { data: Data; } -const AppContainer: StatelessComponent = props => { +export const AppContainer: StatelessComponent = props => { return ; }; diff --git a/src/core/client/stream/containers/CommentContainer.spec.tsx b/src/core/client/stream/containers/CommentContainer.spec.tsx index 94ffb7392..98612be38 100644 --- a/src/core/client/stream/containers/CommentContainer.spec.tsx +++ b/src/core/client/stream/containers/CommentContainer.spec.tsx @@ -1,15 +1,18 @@ import { shallow } from "enzyme"; import React from "react"; +import { PropTypesOf } from "talk-framework/types"; + import { CommentContainer } from "./CommentContainer"; it("renders username and body", () => { - const props = { + const props: PropTypesOf = { data: { author: { username: "Marvin", }, body: "Woof", + createdAt: "1995-12-17T03:24:00.000Z", }, }; diff --git a/src/core/client/stream/containers/CommentContainer.tsx b/src/core/client/stream/containers/CommentContainer.tsx index 9f20ecfc5..0196f6b2e 100644 --- a/src/core/client/stream/containers/CommentContainer.tsx +++ b/src/core/client/stream/containers/CommentContainer.tsx @@ -16,6 +16,7 @@ graphql` username } body + createdAt } `; diff --git a/src/core/client/stream/containers/ReplyListContainer.spec.tsx b/src/core/client/stream/containers/ReplyListContainer.spec.tsx index 75cb129ca..3a414dd51 100644 --- a/src/core/client/stream/containers/ReplyListContainer.spec.tsx +++ b/src/core/client/stream/containers/ReplyListContainer.spec.tsx @@ -8,7 +8,7 @@ import ReplyList from "../components/ReplyList"; import { ReplyListContainer } from "./ReplyListContainer"; it("renders correctly", () => { - const props: any = { + const props: PropTypesOf = { comment: { id: "comment-id", replies: { @@ -18,14 +18,14 @@ it("renders correctly", () => { relay: { hasMore: noop, isLoading: noop, - }, + } as any, }; const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); it("renders correctly when replies are null", () => { - const props: any = { + const props: PropTypesOf = { comment: { id: "comment-id", replies: null, @@ -33,7 +33,7 @@ it("renders correctly when replies are null", () => { relay: { hasMore: noop, isLoading: noop, - }, + } as any, }; const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); diff --git a/src/core/client/stream/containers/ReplyListContainer.tsx b/src/core/client/stream/containers/ReplyListContainer.tsx index d147edb1c..48db9645a 100644 --- a/src/core/client/stream/containers/ReplyListContainer.tsx +++ b/src/core/client/stream/containers/ReplyListContainer.tsx @@ -22,7 +22,10 @@ export class ReplyListContainer extends React.Component { }; public render() { - if (this.props.comment.replies === null) { + if ( + this.props.comment.replies === null || + this.props.comment.replies.edges.length === 0 + ) { return null; } const comments = this.props.comment.replies.edges.map(edge => edge.node); diff --git a/src/core/client/stream/components/__snapshots__/Username.spec.tsx.snap b/src/core/client/stream/containers/__snapshots__/AppContainer.spec.tsx.snap similarity index 60% rename from src/core/client/stream/components/__snapshots__/Username.spec.tsx.snap rename to src/core/client/stream/containers/__snapshots__/AppContainer.spec.tsx.snap index 27d624f2f..f9a5c5316 100644 --- a/src/core/client/stream/components/__snapshots__/Username.spec.tsx.snap +++ b/src/core/client/stream/containers/__snapshots__/AppContainer.spec.tsx.snap @@ -1,9 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders correctly 1`] = ` - - Marvin - + `; diff --git a/src/core/client/stream/containers/__snapshots__/CommentContainer.spec.tsx.snap b/src/core/client/stream/containers/__snapshots__/CommentContainer.spec.tsx.snap index 6ce053179..9e2476f37 100644 --- a/src/core/client/stream/containers/__snapshots__/CommentContainer.spec.tsx.snap +++ b/src/core/client/stream/containers/__snapshots__/CommentContainer.spec.tsx.snap @@ -8,5 +8,6 @@ exports[`renders username and body 1`] = ` } } body="Woof" + createdAt="1995-12-17T03:24:00.000Z" /> `; diff --git a/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap b/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap index 312540c09..b3d193c41 100644 --- a/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap +++ b/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap @@ -2,7 +2,7 @@ exports[`loads more comments 1`] = `
-
+
Markus +

-
+
Lukas +

-
+
Isabelle +

-
+
Markus +

-
+
Lukas +

-
+
Markus +

-
+
Markus +

Markus +

Lukas +

-
+
Markus +

-
+
Lukas +

-
+
Markus +

Lukas +

-
+
Markus +

Lukas +

Isabelle +

* { + margin: 0 calc(0.5 * var(--spacing-unit)) 0 0; + } + &.directionRowReverse { + & > * { + margin: 0 0 0 calc(0.5 * var(--spacing-unit)); + } + } + &.directionColumn { + & > * { + margin: 0 0 calc(0.5 * var(--spacing-unit)) 0; + } + } + &.directionColumnReverese { + & > * { + margin: calc(0.5 * var(--spacing-unit)) 0 0 0; + } + } + & > *:last-child { + margin: 0; + } +} + +.itemGutter { + & > * { + margin: 0 var(--spacing-unit) 0 0; + } + &.directionRowReverse { + & > * { + margin: 0 0 0 var(--spacing-unit); + } + } + &.directionColumn { + & > * { + margin: 0 0 var(--spacing-unit) 0; + } + } + &.directionColumnReverese { + & > * { + margin: var(--spacing-unit) 0 0 0; + } + } + & > *:last-child { + margin: 0; + } +} + .justifyFlexStart { justify-content: flex-start; } diff --git a/src/core/client/ui/components/Flex/Flex.tsx b/src/core/client/ui/components/Flex/Flex.tsx index 3dab50168..bfba06d05 100644 --- a/src/core/client/ui/components/Flex/Flex.tsx +++ b/src/core/client/ui/components/Flex/Flex.tsx @@ -7,6 +7,8 @@ import { pascalCase } from "talk-common/utils"; import * as styles from "./Flex.css"; interface InnerProps { + id?: string; + role?: string; justifyContent?: | "flex-start" | "flex-end" @@ -16,12 +18,24 @@ interface InnerProps { | "space-evenly"; alignItems?: "flex-start" | "flex-end" | "center" | "baseline" | "stretch"; direction?: "row" | "column" | "row-reverse" | "column-reverse"; + itemGutter?: boolean | "half"; + className?: string; } const Flex: StatelessComponent = props => { - const { justifyContent, alignItems, direction, ...rest } = props; + const { + className, + justifyContent, + alignItems, + direction, + itemGutter, + ...rest + } = props; - const classObject: Record = {}; + const classObject: Record = { + [styles.itemGutter]: itemGutter === true, + [styles.halfItemGutter]: itemGutter === "half", + }; if (justifyContent) { classObject[(styles as any)[`justify${pascalCase(justifyContent)}`]] = true; @@ -35,7 +49,7 @@ const Flex: StatelessComponent = props => { classObject[(styles as any)[`direction${pascalCase(direction)}`]] = true; } - const classNames: string = cn(styles.root, classObject); + const classNames: string = cn(styles.root, className, classObject); return

; }; diff --git a/src/core/client/ui/components/MatchMedia/MatchMedia.spec.tsx b/src/core/client/ui/components/MatchMedia/MatchMedia.spec.tsx index 3c4879015..1e600dd76 100644 --- a/src/core/client/ui/components/MatchMedia/MatchMedia.spec.tsx +++ b/src/core/client/ui/components/MatchMedia/MatchMedia.spec.tsx @@ -3,7 +3,7 @@ import React from "react"; import { PropTypesOf } from "talk-ui/types"; -import MatchMedia from "./MatchMedia"; +import { MatchMedia } from "./MatchMedia"; it("renders correctly", () => { const props: PropTypesOf = { diff --git a/src/core/client/ui/components/MatchMedia/MatchMedia.tsx b/src/core/client/ui/components/MatchMedia/MatchMedia.tsx index e7ca58f6a..43d900dfe 100644 --- a/src/core/client/ui/components/MatchMedia/MatchMedia.tsx +++ b/src/core/client/ui/components/MatchMedia/MatchMedia.tsx @@ -1,8 +1,9 @@ import React from "react"; import { ReactNode, StatelessComponent } from "react"; -import Responsive from "react-responsive"; +import Responsive, { MediaQueryMatchers } from "react-responsive"; import theme from "../../theme/variables"; +import UIContext from "../UIContext"; type Breakpoints = keyof typeof theme.breakpoints; @@ -20,9 +21,10 @@ interface InnerProps { print?: boolean; screen?: boolean; speech?: boolean; + values?: Partial; } -const MatchMedia: StatelessComponent = props => { +export const MatchMedia: StatelessComponent = props => { const { speech, minWidth, maxWidth, ...rest } = props; const mapped = { // TODO: Temporarily map newer speech to older aural type until @@ -34,4 +36,12 @@ const MatchMedia: StatelessComponent = props => { return ; }; -export default MatchMedia; +const MatchMediaWithContext: StatelessComponent = props => ( + + {({ mediaQueryValues }) => ( + + )} + +); + +export default MatchMediaWithContext; diff --git a/src/core/client/ui/components/RelativeTime/RelativeTime.css b/src/core/client/ui/components/RelativeTime/RelativeTime.css new file mode 100644 index 000000000..3faa5e162 --- /dev/null +++ b/src/core/client/ui/components/RelativeTime/RelativeTime.css @@ -0,0 +1,4 @@ +.root { + composes: body1 from "talk-ui/shared/typography.css"; + background-color: transparent; +} diff --git a/src/core/client/ui/components/RelativeTime/RelativeTime.mdx b/src/core/client/ui/components/RelativeTime/RelativeTime.mdx new file mode 100644 index 000000000..3122e8806 --- /dev/null +++ b/src/core/client/ui/components/RelativeTime/RelativeTime.mdx @@ -0,0 +1,17 @@ +--- +name: RelativeTime +menu: UI Kit +--- + +import { Playground } from 'docz' +import RelativeTime from './RelativeTime' + +# RelativeTime + +Renders relative time until given `date`. + +## Basic usage + + "".concat(value, " ", unit, " ", suffix)} /> + diff --git a/src/core/client/ui/components/RelativeTime/RelativeTime.spec.tsx b/src/core/client/ui/components/RelativeTime/RelativeTime.spec.tsx new file mode 100644 index 000000000..92d0c547d --- /dev/null +++ b/src/core/client/ui/components/RelativeTime/RelativeTime.spec.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { create } from "react-test-renderer"; + +import { PropTypesOf } from "talk-ui/types"; + +import UIContext from "../UIContext"; +import RelativeTime from "./RelativeTime"; + +it("uses default formatter", () => { + const props = { + date: "2018-12-17T03:24:00.000Z", + }; + const tree = create().toJSON(); + + expect(tree).toMatchSnapshot(); +}); + +it("uses formatter from context", () => { + const context: any = { + timeagoFormatter: () => "My Context Formatter", + }; + const props: PropTypesOf = { + date: "2018-12-17T03:24:00.000Z", + }; + const tree = create( + + + + ).toJSON(); + + expect(tree).toMatchSnapshot(); +}); + +it("uses formatter from props", () => { + const props = { + date: "2018-12-17T03:24:00.000Z", + formatter: () => "My Props Formatter", + }; + const tree = create().toJSON(); + + expect(tree).toMatchSnapshot(); +}); diff --git a/src/core/client/ui/components/RelativeTime/RelativeTime.tsx b/src/core/client/ui/components/RelativeTime/RelativeTime.tsx new file mode 100644 index 000000000..3a8022656 --- /dev/null +++ b/src/core/client/ui/components/RelativeTime/RelativeTime.tsx @@ -0,0 +1,42 @@ +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 "./RelativeTime.css"; + +interface InnerProps { + date: string; + live?: boolean; + classes: typeof styles; + className?: string; + formatter?: Formatter; +} + +const defaultFormatter: Formatter = (value, unit, suffix, timestamp: string) => + new Date(timestamp).toISOString(); + +class RelativeTime extends React.Component { + public render() { + const { date, classes, live, className, formatter } = this.props; + return ( + + {({ timeagoFormatter }) => ( + + )} + + ); + } +} + +const enhanced = withStyles(styles)(RelativeTime); +export type RelativeTimeProps = PropTypesOf; +export default enhanced; diff --git a/src/core/client/ui/components/RelativeTime/__snapshots__/RelativeTime.spec.tsx.snap b/src/core/client/ui/components/RelativeTime/__snapshots__/RelativeTime.spec.tsx.snap new file mode 100644 index 000000000..6d9388e87 --- /dev/null +++ b/src/core/client/ui/components/RelativeTime/__snapshots__/RelativeTime.spec.tsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`uses default formatter 1`] = ` + +`; + +exports[`uses formatter from context 1`] = ` + +`; + +exports[`uses formatter from props 1`] = ` + +`; diff --git a/src/core/client/ui/components/RelativeTime/index.ts b/src/core/client/ui/components/RelativeTime/index.ts new file mode 100644 index 000000000..8bc2dfb5e --- /dev/null +++ b/src/core/client/ui/components/RelativeTime/index.ts @@ -0,0 +1,2 @@ +export * from "./RelativeTime"; +export { default } from "./RelativeTime"; diff --git a/src/core/client/ui/components/Typography/Typography.css b/src/core/client/ui/components/Typography/Typography.css index da31711e9..a3c19a400 100644 --- a/src/core/client/ui/components/Typography/Typography.css +++ b/src/core/client/ui/components/Typography/Typography.css @@ -43,6 +43,10 @@ composes: overline from "talk-ui/shared/typography.css"; } +.timestamp { + composes: timestamp from "talk-ui/shared/typography.css"; +} + .alignLeft { text-align: left; } @@ -70,7 +74,7 @@ } .paragraph { - margin-bottom: calc(1px * var(--spacing-unit)); + margin-bottom: var(--spacing-unit); } .colorInherit { diff --git a/src/core/client/ui/components/Typography/Typography.tsx b/src/core/client/ui/components/Typography/Typography.tsx index 7292a672f..b743305e6 100644 --- a/src/core/client/ui/components/Typography/Typography.tsx +++ b/src/core/client/ui/components/Typography/Typography.tsx @@ -16,7 +16,8 @@ type Variant = | "subtitle2" | "body1" | "body2" - | "button"; + | "button" + | "timestamp"; // Based on Typography Component of Material UI. // https://github.com/mui-org/material-ui/blob/303199d39b42a321d28347d8440d69166f872f27/packages/material-ui/src/Typography/Typography.js @@ -99,6 +100,8 @@ const Typography: StatelessComponent = props => { { [classes.colorPrimary]: color === "primary", [classes.colorSecondary]: color === "secondary", + [classes.colorError]: color === "error", + [classes.colorTextSecondary]: color === "textSecondary", [classes.noWrap]: noWrap, [classes.gutterBottom]: gutterBottom, [classes.paragraph]: paragraph, @@ -129,6 +132,7 @@ Typography.defaultProps = { subtitle2: "h3", body1: "p", body2: "aside", + timestamp: "span", }, noWrap: false, paragraph: false, diff --git a/src/core/client/ui/components/UIContext/UIContext.ts b/src/core/client/ui/components/UIContext/UIContext.ts new file mode 100644 index 000000000..b8372d1f3 --- /dev/null +++ b/src/core/client/ui/components/UIContext/UIContext.ts @@ -0,0 +1,12 @@ +import React from "react"; +import { MediaQueryMatchers } from "react-responsive"; +import { Formatter } from "react-timeago"; + +export interface UIContextProps { + timeagoFormatter?: Formatter | null; + mediaQueryValues?: Partial; +} + +const UIContext = React.createContext({} as any); + +export default UIContext; diff --git a/src/core/client/ui/components/UIContext/index.ts b/src/core/client/ui/components/UIContext/index.ts new file mode 100644 index 000000000..86504f205 --- /dev/null +++ b/src/core/client/ui/components/UIContext/index.ts @@ -0,0 +1 @@ +export { default, UIContextProps } from "./UIContext"; diff --git a/src/core/client/ui/components/index.ts b/src/core/client/ui/components/index.ts index afca18af3..3d81f1aba 100644 --- a/src/core/client/ui/components/index.ts +++ b/src/core/client/ui/components/index.ts @@ -1,5 +1,7 @@ export { default as BaseButton } from "./BaseButton"; export { default as Button } from "./Button"; export { default as Typography } from "./Typography"; +export { default as RelativeTime } from "./RelativeTime"; +export { default as UIContext, UIContextProps } from "./UIContext"; export { default as Flex } from "./Flex"; export { default as MatchMedia } from "./MatchMedia"; diff --git a/src/core/client/ui/shared/typography.css b/src/core/client/ui/shared/typography.css index cd94c867d..3cb483cd2 100644 --- a/src/core/client/ui/shared/typography.css +++ b/src/core/client/ui/shared/typography.css @@ -61,3 +61,12 @@ .overline { } + +.timestamp { + color: var(--palette-text-secondary); + font-family: "Source Sans Pro"; + font-weight: var(--font-weight-medium); + font-size: 14px; + line-height: calc(18em / 16); + letter-spacing: calc(0.2em / 16); +} diff --git a/src/core/client/ui/theme/variables.css b/src/core/client/ui/theme/variables.css index 687417bf4..ad0215b77 100644 --- a/src/core/client/ui/theme/variables.css +++ b/src/core/client/ui/theme/variables.css @@ -6,11 +6,11 @@ */ :root { - --spacing-unit: var(--spacing-unit-small); + --spacing-unit: calc(1px * var(--spacing-unit-small)); } @media (min-width: $breakpoints-xs) { :root { - --spacing-unit: var(--spacing-unit-large); + --spacing-unit: calc(1px * var(--spacing-unit-large)); } } diff --git a/src/core/server/graph/tenant/resolvers/comment.ts b/src/core/server/graph/tenant/resolvers/comment.ts index 22f72b5e5..d4033e0cb 100644 --- a/src/core/server/graph/tenant/resolvers/comment.ts +++ b/src/core/server/graph/tenant/resolvers/comment.ts @@ -2,6 +2,8 @@ import Context from "talk-server/graph/tenant/context"; import { Comment, ConnectionInput } from "talk-server/models/comment"; export default { + createdAt: async (comment: Comment, _: any, ctx: Context) => + comment.created_at, author: async (comment: Comment, _: any, ctx: Context) => ctx.loaders.Users.user.load(comment.author_id), replies: async (comment: Comment, input: ConnectionInput, ctx: Context) => diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index 4d4090af1..5270f26fd 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -202,6 +202,11 @@ type Comment { """ body: String + """ + createdAt is the date in which the comment was created. + """ + createdAt: Time + """ author is the User that authored the Comment. """ diff --git a/src/locales/de/framework.ftl b/src/locales/de/framework.ftl index bd604bf67..2f5a6d472 100644 --- a/src/locales/de/framework.ftl +++ b/src/locales/de/framework.ftl @@ -5,3 +5,41 @@ ## Validation framework-validation-required = Dies ist ein Pflichtpfeld. + +framework-timeago = + { $suffix -> + [ago] vor + [in] in + } + { $value } + { $unit -> + [second] { $value -> + [1] Sekunde + *[other] Sekunden + } + [minuto] { $value -> + [1] Minute + *[other] Minuten + } + [hour] { $value -> + [0] Stunde + *[other] Stunden + } + [day] { $value -> + [1] Tag + *[other] Tage + } + [week] { $value -> + [1] Woche + *[other] Wochen + } + [month] { $value -> + [1] Monat + *[other] Monate + } + [year] { $value -> + [1] Jahr + *[other] Jahre + } + *[other] unknown unit + } diff --git a/src/locales/en-US/framework.ftl b/src/locales/en-US/framework.ftl index 1285552cd..5d88f9a7e 100644 --- a/src/locales/en-US/framework.ftl +++ b/src/locales/en-US/framework.ftl @@ -5,3 +5,48 @@ ## Validation framework-validation-required = This field is required. + +framework-timeago-time = + { $value } + { $unit -> + [second] { $value -> + [1] second + *[other] seconds + } + [minute] { $value -> + [1] minute + *[other] minutes + } + [hour] { $value -> + [0] hour + *[other] hours + } + [day] { $value -> + [1] day + *[other] days + } + [week] { $value -> + [1] week + *[other] weeks + } + [month] { $value -> + [1] month + *[other] months + } + [year] { $value -> + [1] year + *[other] years + } + *[other] unknown unit + } + +framework-timeago = + { $value -> + [0] now + *[other] + { $suffix -> + [ago] {framework-timeago-time} ago + [in] in {framework-timeago-time} + *[other] unknown suffix + } + } diff --git a/src/locales/es/framework.ftl b/src/locales/es/framework.ftl index 9cfdf012f..2b95d944c 100644 --- a/src/locales/es/framework.ftl +++ b/src/locales/es/framework.ftl @@ -2,3 +2,45 @@ ### All keys must start with `framework` because this file is shared ### among different targets. +framework-timeago = + { $value -> + [0] ahora + *[other] + { $suffix -> + [ago] hace + [in] en + *[other] unknown suffix + } + { $value } + { $unit -> + [second] { $value -> + [1] segundo + *[other] segundos + } + [minute] { $value -> + [1] minuto + *[other] minutos + } + [hour] { $value -> + [0] hora + *[other] horas + } + [day] { $value -> + [1] dia + *[other] dias + } + [week] { $value -> + [1] semana + *[other] semanas + } + [month] { $value -> + [1] mes + *[other] meses + } + [year] { $value -> + [1] año + *[other] años + } + *[other] unknown unit + } + } diff --git a/src/types/react-timeago.d.ts b/src/types/react-timeago.d.ts new file mode 100644 index 000000000..431bbf88e --- /dev/null +++ b/src/types/react-timeago.d.ts @@ -0,0 +1,55 @@ +declare module "react-timeago" { + import React from "react"; + + export type Formatter = ( + value: number, + unit: "second" | "minute" | "hour" | "day" | "week" | "month" | "year", + suffix: "ago" | "from now", + epochMiliseconds: string + ) => string | React.ReactElement; + + export interface LocaleDefinition { + prefixAgo?: string; + prefixFromNow?: string; + suffixAgo?: string; + suffixFromNow?: string; + second?: string; + seconds?: string; + minute?: string; + minutes?: string; + hour?: string; + hours?: string; + day?: string; + days?: string; + week?: string; + weeks?: string; + month?: string; + months?: string; + year?: string; + years?: string; + wordSeparator?: string; + numbers?: number[]; + } + + export interface TimeAgoProps { + date: string; + live?: boolean; + className: string; + formatter?: Formatter; + } + + const TimeAgo: React.ComponentType; + export default TimeAgo; +} + +declare module "react-timeago/lib/formatters/buildFormatter" { + import { Formatter, LocaleDefinition } from "react-timeago"; + function buildFormatter(localeInput: LocaleDefinition): Formatter; + export default buildFormatter; +} + +declare module "react-timeago/lib/language-strings/*" { + import { LocaleDefinition } from "react-timeago"; + const localeStrings: LocaleDefinition; + export default localeStrings; +}