diff --git a/src/core/client/auth/test/__snapshots__/navigation.spec.tsx.snap b/src/core/client/auth/test/__snapshots__/navigation.spec.tsx.snap
index c3c5fe4ed..47693769e 100644
--- a/src/core/client/auth/test/__snapshots__/navigation.spec.tsx.snap
+++ b/src/core/client/auth/test/__snapshots__/navigation.spec.tsx.snap
@@ -216,7 +216,7 @@ exports[`navigates to sign up form 1`] = `
Username
A unique identifier displayed on your comments. You may use “_” and “.”
@@ -239,7 +239,7 @@ exports[`navigates to sign up form 1`] = `
Password
Must be at least 8 characters
diff --git a/src/core/client/auth/test/__snapshots__/signUp.spec.tsx.snap b/src/core/client/auth/test/__snapshots__/signUp.spec.tsx.snap
index 4b0ee2dc7..5491404be 100644
--- a/src/core/client/auth/test/__snapshots__/signUp.spec.tsx.snap
+++ b/src/core/client/auth/test/__snapshots__/signUp.spec.tsx.snap
@@ -56,7 +56,7 @@ exports[`accepts correct password 1`] = `
Username
A unique identifier displayed on your comments. You may use “_” and “.”
@@ -92,7 +92,7 @@ exports[`accepts correct password 1`] = `
Password
Must be at least 8 characters
@@ -237,7 +237,7 @@ exports[`accepts correct password confirmation 1`] = `
Username
A unique identifier displayed on your comments. You may use “_” and “.”
@@ -273,7 +273,7 @@ exports[`accepts correct password confirmation 1`] = `
Password
Must be at least 8 characters
@@ -418,7 +418,7 @@ exports[`accepts valid email 1`] = `
Username
A unique identifier displayed on your comments. You may use “_” and “.”
@@ -454,7 +454,7 @@ exports[`accepts valid email 1`] = `
Password
Must be at least 8 characters
@@ -612,7 +612,7 @@ exports[`accepts valid username 1`] = `
Username
A unique identifier displayed on your comments. You may use “_” and “.”
@@ -635,7 +635,7 @@ exports[`accepts valid username 1`] = `
Password
Must be at least 8 characters
@@ -793,7 +793,7 @@ exports[`checks for invalid characters in username 1`] = `
Username
A unique identifier displayed on your comments. You may use “_” and “.”
@@ -829,7 +829,7 @@ exports[`checks for invalid characters in username 1`] = `
Password
Must be at least 8 characters
@@ -987,7 +987,7 @@ exports[`checks for invalid email 1`] = `
Username
A unique identifier displayed on your comments. You may use “_” and “.”
@@ -1023,7 +1023,7 @@ exports[`checks for invalid email 1`] = `
Password
Must be at least 8 characters
@@ -1181,7 +1181,7 @@ exports[`checks for too long username 1`] = `
Username
A unique identifier displayed on your comments. You may use “_” and “.”
@@ -1217,7 +1217,7 @@ exports[`checks for too long username 1`] = `
Password
Must be at least 8 characters
@@ -1375,7 +1375,7 @@ exports[`checks for too short password 1`] = `
Username
A unique identifier displayed on your comments. You may use “_” and “.”
@@ -1411,7 +1411,7 @@ exports[`checks for too short password 1`] = `
Password
Must be at least 8 characters
@@ -1569,7 +1569,7 @@ exports[`checks for too short username 1`] = `
Username
A unique identifier displayed on your comments. You may use “_” and “.”
@@ -1605,7 +1605,7 @@ exports[`checks for too short username 1`] = `
Password
Must be at least 8 characters
@@ -1763,7 +1763,7 @@ exports[`checks for wrong password confirmation 1`] = `
Username
A unique identifier displayed on your comments. You may use “_” and “.”
@@ -1799,7 +1799,7 @@ exports[`checks for wrong password confirmation 1`] = `
Password
Must be at least 8 characters
@@ -1944,7 +1944,7 @@ exports[`renders sign up form 1`] = `
Username
A unique identifier displayed on your comments. You may use “_” and “.”
@@ -1967,7 +1967,7 @@ exports[`renders sign up form 1`] = `
Password
Must be at least 8 characters
@@ -2099,7 +2099,7 @@ exports[`shows error when submitting empty form 1`] = `
Username
A unique identifier displayed on your comments. You may use “_” and “.”
@@ -2135,7 +2135,7 @@ exports[`shows error when submitting empty form 1`] = `
Password
Must be at least 8 characters
@@ -2280,7 +2280,7 @@ exports[`shows server error 1`] = `
Username
A unique identifier displayed on your comments. You may use “_” and “.”
@@ -2303,7 +2303,7 @@ exports[`shows server error 1`] = `
Password
Must be at least 8 characters
@@ -2427,7 +2427,7 @@ exports[`shows server error 2`] = `
Username
A unique identifier displayed on your comments. You may use “_” and “.”
@@ -2450,7 +2450,7 @@ exports[`shows server error 2`] = `
Password
Must be at least 8 characters
@@ -2569,7 +2569,7 @@ exports[`submits form successfully 1`] = `
Username
A unique identifier displayed on your comments. You may use “_” and “.”
@@ -2592,7 +2592,7 @@ exports[`submits form successfully 1`] = `
Password
Must be at least 8 characters
@@ -2711,7 +2711,7 @@ exports[`submits form successfully 2`] = `
Username
A unique identifier displayed on your comments. You may use “_” and “.”
@@ -2734,7 +2734,7 @@ exports[`submits form successfully 2`] = `
Password
Must be at least 8 characters
diff --git a/src/core/client/framework/lib/bootstrap/createManaged.tsx b/src/core/client/framework/lib/bootstrap/createManaged.tsx
index f2d86e7c8..bc1219427 100644
--- a/src/core/client/framework/lib/bootstrap/createManaged.tsx
+++ b/src/core/client/framework/lib/bootstrap/createManaged.tsx
@@ -53,7 +53,16 @@ interface CreateContextArguments {
*/
export const timeagoFormatter: Formatter = (value, unit, suffix) => {
// We use 'in' instead of 'from now' for language consistency
- const ourSuffix = suffix === "from now" ? "in" : suffix;
+ const ourSuffix = suffix === "from now" ? "noSuffix" : suffix;
+
+ if (unit === "second" && suffix === "ago") {
+ return (
+
+ Just now
+
+ );
+ }
+
return (
{
const props: PropTypesOf = {
- id: "comment-id",
author: {
username: "Marvin",
},
body: "Woof",
createdAt: "1995-12-17T03:24:00.000Z",
+ topBarRight: "topBarRight",
+ footer: "footer",
+ showEditedMarker: true,
};
const wrapper = shallow();
expect(wrapper).toMatchSnapshot();
diff --git a/src/core/client/stream/components/Comment/Comment.tsx b/src/core/client/stream/components/Comment/Comment.tsx
index bd4fc17f7..dcf77308e 100644
--- a/src/core/client/stream/components/Comment/Comment.tsx
+++ b/src/core/client/stream/components/Comment/Comment.tsx
@@ -1,32 +1,46 @@
-import React, { ReactElement, StatelessComponent } from "react";
+import React, { StatelessComponent } from "react";
import { Flex } from "talk-ui/components";
import * as styles from "./Comment.css";
+import EditedMarker from "./EditedMarker";
import HTMLContent from "./HTMLContent";
import Timestamp from "./Timestamp";
-import TopBar from "./TopBar";
+import TopBarLeft from "./TopBarLeft";
import Username from "./Username";
export interface CommentProps {
- id: string;
className?: string;
author: {
username: string | null;
} | null;
body: string | null;
createdAt: string;
- footer?: ReactElement | Array>;
+ topBarRight?: React.ReactNode;
+ footer?: React.ReactNode;
+ showEditedMarker?: boolean;
}
const Comment: StatelessComponent = props => {
return (
-
- {props.author &&
- props.author.username && {props.author.username}}
- {props.createdAt}
-
+
+
+ {props.author &&
+ props.author.username && (
+ {props.author.username}
+ )}
+
+ {props.createdAt}
+ {props.showEditedMarker && }
+
+
+ {props.topBarRight && {props.topBarRight}
}
+
{props.body || ""}
{props.footer}
diff --git a/src/core/client/stream/components/Comment/EditedMarker.css b/src/core/client/stream/components/Comment/EditedMarker.css
new file mode 100644
index 000000000..147c8656f
--- /dev/null
+++ b/src/core/client/stream/components/Comment/EditedMarker.css
@@ -0,0 +1,3 @@
+.root {
+ composes: detail from "talk-ui/shared/typography.css";
+}
diff --git a/src/core/client/stream/components/Comment/EditedMarker.spec.tsx b/src/core/client/stream/components/Comment/EditedMarker.spec.tsx
new file mode 100644
index 000000000..4017b77d1
--- /dev/null
+++ b/src/core/client/stream/components/Comment/EditedMarker.spec.tsx
@@ -0,0 +1,9 @@
+import { shallow } from "enzyme";
+import React from "react";
+
+import EditedMarker from "./EditedMarker";
+
+it("renders correctly", () => {
+ const wrapper = shallow();
+ expect(wrapper).toMatchSnapshot();
+});
diff --git a/src/core/client/stream/components/Comment/EditedMarker.tsx b/src/core/client/stream/components/Comment/EditedMarker.tsx
new file mode 100644
index 000000000..a07d0f7f2
--- /dev/null
+++ b/src/core/client/stream/components/Comment/EditedMarker.tsx
@@ -0,0 +1,16 @@
+import { Localized } from "fluent-react/compat";
+import React, { StatelessComponent } from "react";
+
+import styles from "./EditedMarker.css";
+
+const EditedMarker: StatelessComponent = () => (
+
+ (
+
+ )
+
+);
+
+export default EditedMarker;
diff --git a/src/core/client/stream/components/Comment/HTMLContent.tsx b/src/core/client/stream/components/Comment/HTMLContent.tsx
index b9d61c945..62bd62b78 100644
--- a/src/core/client/stream/components/Comment/HTMLContent.tsx
+++ b/src/core/client/stream/components/Comment/HTMLContent.tsx
@@ -1,22 +1,17 @@
-import React from "react";
-
import dompurify from "dompurify";
+import React, { StatelessComponent } from "react";
+
import styles from "./HTMLContent.css";
-interface ContentProps {
+interface HTMLContentProps {
children: string;
}
-class HTMLContent extends React.Component {
- public render() {
- const { children } = this.props;
- return (
-
- );
- }
-}
+const HTMLContent: StatelessComponent = ({ children }) => (
+
+);
export default HTMLContent;
diff --git a/src/core/client/stream/components/Comment/IndentedComment.spec.tsx b/src/core/client/stream/components/Comment/IndentedComment.spec.tsx
index c86a66ef2..be489a804 100644
--- a/src/core/client/stream/components/Comment/IndentedComment.spec.tsx
+++ b/src/core/client/stream/components/Comment/IndentedComment.spec.tsx
@@ -8,7 +8,6 @@ import IndentedComment from "./IndentedComment";
it("renders correctly", () => {
const props: PropTypesOf = {
indentLevel: 1,
- id: "comment-id",
author: {
username: "Marvin",
},
diff --git a/src/core/client/stream/components/Comment/TopBar.spec.tsx b/src/core/client/stream/components/Comment/TopBarLeft.spec.tsx
similarity index 81%
rename from src/core/client/stream/components/Comment/TopBar.spec.tsx
rename to src/core/client/stream/components/Comment/TopBarLeft.spec.tsx
index 5ee691f70..8769bb3dd 100644
--- a/src/core/client/stream/components/Comment/TopBar.spec.tsx
+++ b/src/core/client/stream/components/Comment/TopBarLeft.spec.tsx
@@ -4,10 +4,10 @@ import TestRenderer from "react-test-renderer";
import { PropTypesOf } from "talk-framework/types";
import { UIContext, UIContextProps } from "talk-ui/components";
-import TopBar from "./TopBar";
+import TopBarLeft from "./TopBarLeft";
it("renders correctly on small screens", () => {
- const props: PropTypesOf = {
+ const props: PropTypesOf = {
children: Hello World
,
};
@@ -19,14 +19,14 @@ it("renders correctly on small screens", () => {
const testRenderer = TestRenderer.create(
-
+
);
expect(testRenderer.toJSON()).toMatchSnapshot();
});
it("renders correctly on big screens", () => {
- const props: PropTypesOf = {
+ const props: PropTypesOf = {
children: Hello World
,
};
@@ -38,7 +38,7 @@ it("renders correctly on big screens", () => {
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/TopBarLeft.tsx
similarity index 81%
rename from src/core/client/stream/components/Comment/TopBar.tsx
rename to src/core/client/stream/components/Comment/TopBarLeft.tsx
index eed8aea32..5f3862020 100644
--- a/src/core/client/stream/components/Comment/TopBar.tsx
+++ b/src/core/client/stream/components/Comment/TopBarLeft.tsx
@@ -4,12 +4,12 @@ import { StatelessComponent } from "react";
import { Flex, MatchMedia } from "talk-ui/components";
-export interface TopBarProps {
+export interface TopBarLeftProps {
className?: string;
children: React.ReactNode;
}
-const TopBar: StatelessComponent = props => {
+const TopBarLeft: StatelessComponent = props => {
const rootClassName = cn(props.className);
return (
@@ -27,4 +27,4 @@ const TopBar: StatelessComponent = props => {
);
};
-export default TopBar;
+export default TopBarLeft;
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
index 246444f0d..64419b0fa 100644
--- a/src/core/client/stream/components/Comment/__snapshots__/Comment.spec.tsx.snap
+++ b/src/core/client/stream/components/Comment/__snapshots__/Comment.spec.tsx.snap
@@ -5,16 +5,30 @@ exports[`renders username and body 1`] = `
className="Comment-root"
role="article"
>
-
-
- Marvin
-
-
- 1995-12-17T03:24:00.000Z
-
-
+
+
+ Marvin
+
+
+
+ 1995-12-17T03:24:00.000Z
+
+
+
+
+
+ topBarRight
+
+
Woof
@@ -22,6 +36,8 @@ exports[`renders username and body 1`] = `
className="Comment-footer"
direction="row"
itemGutter="half"
- />
+ >
+ footer
+
`;
diff --git a/src/core/client/stream/components/Comment/__snapshots__/EditedMarker.spec.tsx.snap b/src/core/client/stream/components/Comment/__snapshots__/EditedMarker.spec.tsx.snap
new file mode 100644
index 000000000..8b957f989
--- /dev/null
+++ b/src/core/client/stream/components/Comment/__snapshots__/EditedMarker.spec.tsx.snap
@@ -0,0 +1,17 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+
+ (
+
+ )
+
+`;
diff --git a/src/core/client/stream/components/Comment/__snapshots__/IndentedComment.spec.tsx.snap b/src/core/client/stream/components/Comment/__snapshots__/IndentedComment.spec.tsx.snap
index 91fb1ec96..daaa79aca 100644
--- a/src/core/client/stream/components/Comment/__snapshots__/IndentedComment.spec.tsx.snap
+++ b/src/core/client/stream/components/Comment/__snapshots__/IndentedComment.spec.tsx.snap
@@ -13,7 +13,6 @@ exports[`renders correctly 1`] = `
}
body="Woof"
createdAt="1995-12-17T03:24:00.000Z"
- id="comment-id"
/>
`;
diff --git a/src/core/client/stream/components/Comment/__snapshots__/TopBar.spec.tsx.snap b/src/core/client/stream/components/Comment/__snapshots__/TopBarLeft.spec.tsx.snap
similarity index 100%
rename from src/core/client/stream/components/Comment/__snapshots__/TopBar.spec.tsx.snap
rename to src/core/client/stream/components/Comment/__snapshots__/TopBarLeft.spec.tsx.snap
diff --git a/src/core/client/stream/components/Comment/index.ts b/src/core/client/stream/components/Comment/index.ts
index abb1d79ae..56c2da651 100644
--- a/src/core/client/stream/components/Comment/index.ts
+++ b/src/core/client/stream/components/Comment/index.ts
@@ -1 +1,4 @@
export { default, default as IndentedComment } from "./IndentedComment";
+export { default as TopBarLeft } from "./TopBarLeft";
+export { default as Username } from "./Username";
+export { default as Timestamp } from "./Timestamp";
diff --git a/src/core/client/stream/components/EditCommentForm.tsx b/src/core/client/stream/components/EditCommentForm.tsx
new file mode 100644
index 000000000..d7d79ddd8
--- /dev/null
+++ b/src/core/client/stream/components/EditCommentForm.tsx
@@ -0,0 +1,161 @@
+import { CoralRTE } from "@coralproject/rte";
+import { Localized } from "fluent-react/compat";
+import React, {
+ EventHandler,
+ MouseEvent,
+ Ref,
+ StatelessComponent,
+} from "react";
+import { Field, Form } from "react-final-form";
+
+import { OnSubmit } from "talk-framework/lib/form";
+import { required } from "talk-framework/lib/validation";
+import {
+ AriaInfo,
+ Button,
+ Flex,
+ HorizontalGutter,
+ Message,
+ MessageIcon,
+ RelativeTime,
+ Typography,
+ ValidationMessage,
+} from "talk-ui/components";
+
+import { Timestamp, TopBarLeft, Username } from "./Comment";
+import RTE from "./RTE";
+
+interface FormProps {
+ body: string;
+}
+
+export interface EditCommentFormProps {
+ id: string;
+ className?: string;
+ author: {
+ username: string | null;
+ } | null;
+ createdAt: string;
+ editableUntil: string;
+ onSubmit: OnSubmit;
+ onCancel?: EventHandler>;
+ onClose?: EventHandler>;
+ initialValues?: FormProps;
+ rteRef?: Ref;
+ expired?: boolean;
+}
+
+const EditCommentForm: StatelessComponent = props => {
+ const inputID = `comments-editCommentForm-rte-${props.id}`;
+ return (
+
+
+ )}
+
+ {props.expired ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+
+ )}
+
+ );
+};
+
+export default EditCommentForm;
diff --git a/src/core/client/stream/components/PoweredBy.tsx b/src/core/client/stream/components/PoweredBy.tsx
index 2f1c036eb..b1ca618c4 100644
--- a/src/core/client/stream/components/PoweredBy.tsx
+++ b/src/core/client/stream/components/PoweredBy.tsx
@@ -14,11 +14,7 @@ const PoweredBy: StatelessComponent = props => {
id="comments-poweredBy"
logo={}
>
-
+
{"Powered by The Coral Project"}
diff --git a/src/core/client/stream/components/__snapshots__/PoweredBy.spec.tsx.snap b/src/core/client/stream/components/__snapshots__/PoweredBy.spec.tsx.snap
index 14ae7599d..fdee2029d 100644
--- a/src/core/client/stream/components/__snapshots__/PoweredBy.spec.tsx.snap
+++ b/src/core/client/stream/components/__snapshots__/PoweredBy.spec.tsx.snap
@@ -16,7 +16,7 @@ exports[`renders correctly 1`] = `
Powered by <logo>The Coral Project</logo>
diff --git a/src/core/client/stream/containers/CommentContainer.spec.tsx b/src/core/client/stream/containers/CommentContainer.spec.tsx
index bde3f9606..772a8df79 100644
--- a/src/core/client/stream/containers/CommentContainer.spec.tsx
+++ b/src/core/client/stream/containers/CommentContainer.spec.tsx
@@ -19,10 +19,15 @@ it("renders username and body", () => {
comment: {
id: "comment-id",
author: {
+ id: "author-id",
username: "Marvin",
},
body: "Woof",
createdAt: "1995-12-17T03:24:00.000Z",
+ editing: {
+ edited: false,
+ editableUntil: "1995-12-17T03:24:30.000Z",
+ },
pending: false,
},
indentLevel: 1,
@@ -42,10 +47,15 @@ it("renders body only", () => {
comment: {
id: "comment-id",
author: {
+ id: "author-id",
username: null,
},
body: "Woof",
createdAt: "1995-12-17T03:24:00.000Z",
+ editing: {
+ edited: false,
+ editableUntil: "1995-12-17T03:24:30.000Z",
+ },
pending: false,
},
indentLevel: 1,
diff --git a/src/core/client/stream/containers/CommentContainer.tsx b/src/core/client/stream/containers/CommentContainer.tsx
index 15d9b3eda..1cd09fd2e 100644
--- a/src/core/client/stream/containers/CommentContainer.tsx
+++ b/src/core/client/stream/containers/CommentContainer.tsx
@@ -1,6 +1,8 @@
+import { Localized } from "fluent-react/compat";
import React, { Component } from "react";
import { graphql } from "react-relay";
+import { isBeforeDate } from "talk-common/utils";
import withFragmentContainer from "talk-framework/lib/relay/withFragmentContainer";
import { PropTypesOf } from "talk-framework/types";
import { CommentContainer_asset as AssetData } from "talk-stream/__generated__/CommentContainer_asset.graphql";
@@ -11,10 +13,12 @@ import {
withShowAuthPopupMutation,
} from "talk-stream/mutations";
+import { Button } from "talk-ui/components";
import Comment from "../components/Comment";
import ReplyButton from "../components/Comment/ReplyButton";
-import ReplyCommentFormContainer from ".//ReplyCommentFormContainer";
+import EditCommentFormContainer from "./EditCommentFormContainer";
import PermalinkButtonContainer from "./PermalinkButtonContainer";
+import ReplyCommentFormContainer from "./ReplyCommentFormContainer";
interface InnerProps {
me: MeData | null;
@@ -26,13 +30,41 @@ interface InnerProps {
interface State {
showReplyDialog: boolean;
+ showEditDialog: boolean;
+ editable: boolean;
}
export class CommentContainer extends Component {
+ private uneditableTimer: any;
+
public state = {
showReplyDialog: false,
+ showEditDialog: false,
+ editable: this.isEditable(),
};
+ constructor(props: InnerProps) {
+ super(props);
+ if (this.isEditable()) {
+ this.uneditableTimer = this.updateWhenNotEditable();
+ }
+ }
+
+ public componentWillUnmount() {
+ clearTimeout(this.uneditableTimer);
+ }
+
+ private isEditable() {
+ const isMyComment = !!(
+ this.props.me &&
+ this.props.comment.author &&
+ this.props.me.id === this.props.comment.author.id
+ );
+ return (
+ isMyComment && isBeforeDate(this.props.comment.editing.editableUntil)
+ );
+ }
+
private openReplyDialog = () => {
if (this.props.me) {
this.setState(state => ({
@@ -43,21 +75,72 @@ export class CommentContainer extends Component {
}
};
+ private openEditDialog = () => {
+ if (this.props.me) {
+ this.setState(state => ({
+ showEditDialog: true,
+ }));
+ } else {
+ this.props.showAuthPopup({ view: "SIGN_IN" });
+ }
+ };
+
+ private closeEditDialog = () => {
+ this.setState(state => ({
+ showEditDialog: false,
+ }));
+ };
+
private closeReplyDialog = () => {
this.setState(state => ({
showReplyDialog: false,
}));
};
+ private updateWhenNotEditable() {
+ const ms =
+ new Date(this.props.comment.editing.editableUntil).getTime() - Date.now();
+ if (ms > 0) {
+ return setTimeout(() => this.setState({ editable: false }), ms);
+ }
+ return;
+ }
+
public render() {
- const { comment, asset, ...rest } = this.props;
- const { showReplyDialog } = this.state;
+ const { comment, asset, indentLevel } = this.props;
+ const { showReplyDialog, showEditDialog, editable } = this.state;
+ if (showEditDialog) {
+ return (
+
+ );
+ }
return (
<>