mirror of
https://github.com/wassname/talk.git
synced 2026-07-18 12:40:13 +08:00
Implement edit
This commit is contained in:
@@ -47,7 +47,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 (
|
||||
<Localized id="framework-timeago-just-now">
|
||||
<span>Just now</span>
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Localized
|
||||
id="framework-timeago"
|
||||
|
||||
@@ -6,6 +6,3 @@
|
||||
.footer:not(:empty) {
|
||||
margin-top: var(--spacing-unit);
|
||||
}
|
||||
.reply {
|
||||
margin-top: var(--spacing-unit);
|
||||
}
|
||||
|
||||
@@ -5,28 +5,37 @@ import { Flex } from "talk-ui/components";
|
||||
import * as styles from "./Comment.css";
|
||||
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;
|
||||
topBarRight?: ReactElement<any> | Array<ReactElement<any>>;
|
||||
footer?: ReactElement<any> | Array<ReactElement<any>>;
|
||||
}
|
||||
|
||||
const Comment: StatelessComponent<CommentProps> = props => {
|
||||
return (
|
||||
<div role="article" className={styles.root}>
|
||||
<TopBar className={styles.topBar}>
|
||||
{props.author &&
|
||||
props.author.username && <Username>{props.author.username}</Username>}
|
||||
<Timestamp>{props.createdAt}</Timestamp>
|
||||
</TopBar>
|
||||
<Flex
|
||||
className={styles.topBar}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<TopBarLeft>
|
||||
{props.author &&
|
||||
props.author.username && (
|
||||
<Username>{props.author.username}</Username>
|
||||
)}
|
||||
<Timestamp>{props.createdAt}</Timestamp>
|
||||
</TopBarLeft>
|
||||
<div>{props.topBarRight}</div>
|
||||
</Flex>
|
||||
<HTMLContent>{props.body || ""}</HTMLContent>
|
||||
<Flex className={styles.footer} direction="row" itemGutter="half">
|
||||
{props.footer}
|
||||
|
||||
+5
-5
@@ -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<typeof TopBar> = {
|
||||
const props: PropTypesOf<typeof TopBarLeft> = {
|
||||
children: <div>Hello World</div>,
|
||||
};
|
||||
|
||||
@@ -19,14 +19,14 @@ it("renders correctly on small screens", () => {
|
||||
|
||||
const testRenderer = TestRenderer.create(
|
||||
<UIContext.Provider value={context}>
|
||||
<TopBar {...props} />
|
||||
<TopBarLeft {...props} />
|
||||
</UIContext.Provider>
|
||||
);
|
||||
expect(testRenderer.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders correctly on big screens", () => {
|
||||
const props: PropTypesOf<typeof TopBar> = {
|
||||
const props: PropTypesOf<typeof TopBarLeft> = {
|
||||
children: <div>Hello World</div>,
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ it("renders correctly on big screens", () => {
|
||||
|
||||
const testRenderer = TestRenderer.create(
|
||||
<UIContext.Provider value={context}>
|
||||
<TopBar {...props} />
|
||||
<TopBarLeft {...props} />
|
||||
</UIContext.Provider>
|
||||
);
|
||||
expect(testRenderer.toJSON()).toMatchSnapshot();
|
||||
+3
-3
@@ -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<TopBarProps> = props => {
|
||||
const TopBarLeft: StatelessComponent<TopBarLeftProps> = props => {
|
||||
const rootClassName = cn(props.className);
|
||||
return (
|
||||
<MatchMedia gtWidth="xs">
|
||||
@@ -27,4 +27,4 @@ const TopBar: StatelessComponent<TopBarProps> = props => {
|
||||
);
|
||||
};
|
||||
|
||||
export default TopBar;
|
||||
export default TopBarLeft;
|
||||
@@ -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";
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
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,
|
||||
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<FormProps>;
|
||||
onCancel?: EventHandler<MouseEvent<any>>;
|
||||
onClose?: EventHandler<MouseEvent<any>>;
|
||||
initialValues?: FormProps;
|
||||
rteRef?: Ref<CoralRTE>;
|
||||
expired?: boolean;
|
||||
}
|
||||
|
||||
const EditCommentForm: StatelessComponent<EditCommentFormProps> = props => {
|
||||
const inputID = `comments-editCommentForm-rte-${props.id}`;
|
||||
return (
|
||||
<Form onSubmit={props.onSubmit} initialValues={props.initialValues}>
|
||||
{({ handleSubmit, submitting, hasValidationErrors, pristine }) => (
|
||||
<form
|
||||
className={props.className}
|
||||
autoComplete="off"
|
||||
onSubmit={handleSubmit}
|
||||
id={`comments-editCommentForm-form-${props.id}`}
|
||||
>
|
||||
<HorizontalGutter>
|
||||
<div>
|
||||
<TopBarLeft>
|
||||
{props.author &&
|
||||
props.author.username && (
|
||||
<Username>{props.author.username}</Username>
|
||||
)}
|
||||
<Timestamp>{props.createdAt}</Timestamp>
|
||||
</TopBarLeft>
|
||||
</div>
|
||||
<Field name="body" validate={required}>
|
||||
{({ input, meta }) => (
|
||||
<div>
|
||||
<Localized id="comments-editCommentForm-rteLabel">
|
||||
<AriaInfo component="label" htmlFor={inputID}>
|
||||
Edit comment
|
||||
</AriaInfo>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="comments-editCommentForm-rte"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<RTE
|
||||
inputId={inputID}
|
||||
onChange={({ html }) => input.onChange(html)}
|
||||
value={input.value}
|
||||
placeholder="Edit comment"
|
||||
forwardRef={props.rteRef}
|
||||
disabled={submitting || props.expired}
|
||||
/>
|
||||
</Localized>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<Typography align="right" color="error" gutterBottom>
|
||||
{meta.error || meta.submitError}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
{props.expired ? (
|
||||
<Localized id="comments-editCommentForm-editTimeExpired">
|
||||
<ValidationMessage fullWidth>
|
||||
Edit time has expired. You can no longer edit this comment.
|
||||
Why not post another one?
|
||||
</ValidationMessage>
|
||||
</Localized>
|
||||
) : (
|
||||
<ValidationMessage fullWidth>
|
||||
<Localized
|
||||
id="comments-editCommentForm-editRemainingTime"
|
||||
time={<RelativeTime date={props.editableUntil} live />}
|
||||
>
|
||||
<span>{"Edit: <time></time> remaining"}</span>
|
||||
</Localized>
|
||||
</ValidationMessage>
|
||||
)}
|
||||
<Flex direction="row" justifyContent="flex-end" itemGutter="half">
|
||||
{props.expired ? (
|
||||
<Localized id="comments-editCommentForm-close">
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={submitting}
|
||||
onClick={props.onClose}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</Localized>
|
||||
) : (
|
||||
<>
|
||||
<Localized id="comments-editCommentForm-cancel">
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={submitting}
|
||||
onClick={props.onCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Localized>
|
||||
<Localized id="comments-editCommentForm-saveChanges">
|
||||
<Button
|
||||
color="primary"
|
||||
variant="filled"
|
||||
disabled={submitting || hasValidationErrors || pristine}
|
||||
type="submit"
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</Localized>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditCommentForm;
|
||||
@@ -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,28 @@ interface InnerProps {
|
||||
|
||||
interface State {
|
||||
showReplyDialog: boolean;
|
||||
showEditDialog: boolean;
|
||||
editable: boolean;
|
||||
}
|
||||
|
||||
export class CommentContainer extends Component<InnerProps, State> {
|
||||
private uneditableTimer: any;
|
||||
|
||||
public state = {
|
||||
showReplyDialog: false,
|
||||
showEditDialog: false,
|
||||
editable: isBeforeDate(this.props.comment.editing.editableUntil),
|
||||
};
|
||||
|
||||
constructor(props: InnerProps) {
|
||||
super(props);
|
||||
this.uneditableTimer = this.updateWhenNotEditable();
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
clearTimeout(this.uneditableTimer);
|
||||
}
|
||||
|
||||
private openReplyDialog = () => {
|
||||
if (this.props.me) {
|
||||
this.setState(state => ({
|
||||
@@ -43,20 +62,68 @@ export class CommentContainer extends Component<InnerProps, State> {
|
||||
}
|
||||
};
|
||||
|
||||
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 { showReplyDialog, showEditDialog, editable } = this.state;
|
||||
if (showEditDialog) {
|
||||
return (
|
||||
<EditCommentFormContainer
|
||||
asset={asset}
|
||||
comment={comment}
|
||||
onClose={this.closeEditDialog}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Comment
|
||||
{...rest}
|
||||
{...comment}
|
||||
topBarRight={
|
||||
(editable && (
|
||||
<Localized id="comments-commentContainer-editButton">
|
||||
<Button
|
||||
color="primary"
|
||||
variant="underlined"
|
||||
onClick={this.openEditDialog}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</Localized>
|
||||
)) ||
|
||||
undefined
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<ReplyButton
|
||||
@@ -90,6 +157,7 @@ const enhanced = withShowAuthPopupMutation(
|
||||
asset: graphql`
|
||||
fragment CommentContainer_asset on Asset {
|
||||
...ReplyCommentFormContainer_asset
|
||||
...EditCommentFormContainer_asset
|
||||
}
|
||||
`,
|
||||
comment: graphql`
|
||||
@@ -100,7 +168,11 @@ const enhanced = withShowAuthPopupMutation(
|
||||
}
|
||||
body
|
||||
createdAt
|
||||
editing {
|
||||
editableUntil
|
||||
}
|
||||
...ReplyCommentFormContainer_comment
|
||||
...EditCommentFormContainer_comment
|
||||
}
|
||||
`,
|
||||
})(CommentContainer)
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { CoralRTE } from "@coralproject/rte";
|
||||
import React, { Component } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { isBeforeDate } from "talk-common/utils";
|
||||
import { withContext } from "talk-framework/lib/bootstrap";
|
||||
import { BadUserInputError } from "talk-framework/lib/errors";
|
||||
import { withFragmentContainer } from "talk-framework/lib/relay";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import { EditCommentFormContainer_asset as AssetData } from "talk-stream/__generated__/EditCommentFormContainer_asset.graphql";
|
||||
import { EditCommentFormContainer_comment as CommentData } from "talk-stream/__generated__/EditCommentFormContainer_comment.graphql";
|
||||
|
||||
import EditCommentForm, {
|
||||
EditCommentFormProps,
|
||||
} from "../components/EditCommentForm";
|
||||
import { EditCommentMutation, withEditCommentMutation } from "../mutations";
|
||||
|
||||
interface InnerProps {
|
||||
editComment: EditCommentMutation;
|
||||
comment: CommentData;
|
||||
onClose?: () => void;
|
||||
asset: AssetData;
|
||||
autofocus: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
initialValues?: EditCommentFormProps["initialValues"];
|
||||
initialized: boolean;
|
||||
expired: boolean;
|
||||
}
|
||||
|
||||
export class EditCommentFormContainer extends Component<InnerProps, State> {
|
||||
private expiredTimer: any;
|
||||
|
||||
public state: State = {
|
||||
initialized: false,
|
||||
expired: !isBeforeDate(this.props.comment.editing.editableUntil),
|
||||
};
|
||||
|
||||
constructor(props: InnerProps) {
|
||||
super(props);
|
||||
this.expiredTimer = this.updateWhenExpired();
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
clearTimeout(this.expiredTimer);
|
||||
}
|
||||
|
||||
private updateWhenExpired() {
|
||||
const ms =
|
||||
new Date(this.props.comment.editing.editableUntil).getTime() - Date.now();
|
||||
if (ms > 0) {
|
||||
return setTimeout(() => this.setState({ expired: true }), ms);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
private handleRTERef = (rte: CoralRTE | null) => {
|
||||
if (rte && this.props.autofocus) {
|
||||
rte.focus();
|
||||
}
|
||||
};
|
||||
|
||||
private handleOnCancelOrClose = () => {
|
||||
if (this.props.onClose) {
|
||||
this.props.onClose();
|
||||
}
|
||||
};
|
||||
|
||||
private handleOnSubmit: EditCommentFormProps["onSubmit"] = async (
|
||||
input,
|
||||
form
|
||||
) => {
|
||||
try {
|
||||
await this.props.editComment({
|
||||
assetID: this.props.asset.id,
|
||||
commentID: this.props.comment.id,
|
||||
body: input.body,
|
||||
});
|
||||
if (this.props.onClose) {
|
||||
this.props.onClose();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof BadUserInputError) {
|
||||
return error.invalidArgsLocalized;
|
||||
}
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(error);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<EditCommentForm
|
||||
id={this.props.comment.id}
|
||||
onSubmit={this.handleOnSubmit}
|
||||
initialValues={{ body: this.props.comment.body || "" }}
|
||||
onCancel={this.handleOnCancelOrClose}
|
||||
onClose={this.handleOnCancelOrClose}
|
||||
rteRef={this.handleRTERef}
|
||||
author={this.props.comment.author}
|
||||
createdAt={this.props.comment.createdAt}
|
||||
editableUntil={this.props.comment.editing.editableUntil}
|
||||
expired={this.state.expired}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
const enhanced = withContext(({ sessionStorage, browserInfo }) => ({
|
||||
// Disable autofocus on ios and enable for the rest.
|
||||
autofocus: !browserInfo.ios,
|
||||
}))(
|
||||
withEditCommentMutation(
|
||||
withFragmentContainer<InnerProps>({
|
||||
asset: graphql`
|
||||
fragment EditCommentFormContainer_asset on Asset {
|
||||
id
|
||||
}
|
||||
`,
|
||||
comment: graphql`
|
||||
fragment EditCommentFormContainer_comment on Comment {
|
||||
id
|
||||
body
|
||||
createdAt
|
||||
author {
|
||||
username
|
||||
}
|
||||
editing {
|
||||
editableUntil
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(EditCommentFormContainer)
|
||||
)
|
||||
);
|
||||
export type PostCommentFormContainerProps = PropTypesOf<typeof enhanced>;
|
||||
export default enhanced;
|
||||
@@ -92,6 +92,9 @@ function commit(
|
||||
username: me.username,
|
||||
},
|
||||
body: input.body,
|
||||
editing: {
|
||||
editableUntil: new Date(Date.now() + 10000),
|
||||
},
|
||||
},
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
} from "talk-framework/lib/relay";
|
||||
import { Omit } from "talk-framework/types";
|
||||
|
||||
import { EditCommentMutation as MutationTypes } from "talk-stream/__generated__/EditCommentMutation.graphql";
|
||||
|
||||
export type EditCommentInput = Omit<
|
||||
MutationTypes["variables"]["input"],
|
||||
"clientMutationId"
|
||||
>;
|
||||
|
||||
const mutation = graphql`
|
||||
mutation EditCommentMutation($input: EditCommentInput!) {
|
||||
editComment(input: $input) {
|
||||
comment {
|
||||
body
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: EditCommentInput) {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
editComment: {
|
||||
id: input.commentID,
|
||||
body: input.body,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
} as any, // TODO: (cvle) generated types should contain one for the optimistic response.
|
||||
});
|
||||
}
|
||||
|
||||
export const withEditCommentMutation = createMutationContainer(
|
||||
"editComment",
|
||||
commit
|
||||
);
|
||||
|
||||
export type EditCommentMutation = (
|
||||
input: EditCommentInput
|
||||
) => Promise<MutationTypes["response"]["editComment"]>;
|
||||
@@ -3,6 +3,11 @@ export {
|
||||
CreateCommentMutation,
|
||||
CreateCommentInput,
|
||||
} from "./CreateCommentMutation";
|
||||
export {
|
||||
withEditCommentMutation,
|
||||
EditCommentMutation,
|
||||
EditCommentInput,
|
||||
} from "./EditCommentMutation";
|
||||
export {
|
||||
withSetNetworkStatusMutation,
|
||||
SetNetworkStatusMutation,
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
.root {
|
||||
composes: bodyCopy from "talk-ui/shared/typography.css";
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ export { default as timeout } from "./timeout";
|
||||
export { default as animationFrame } from "./animationFrame";
|
||||
export { default as pascalCase } from "./pascalCase";
|
||||
export { default as oncePerFrame } from "./oncePerFrame";
|
||||
export { default as isBeforeDate } from "./isBeforeDate";
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function isBeforeDate(date: string | number | Date) {
|
||||
return new Date() < new Date(date);
|
||||
}
|
||||
@@ -6,10 +6,10 @@
|
||||
|
||||
framework-validation-required = Dies ist ein Pflichtpfeld.
|
||||
|
||||
framework-timeago-just-now = Gerade eben
|
||||
framework-timeago =
|
||||
{ $suffix ->
|
||||
[ago] vor
|
||||
[in] in
|
||||
}
|
||||
{ $value }
|
||||
{ $unit ->
|
||||
|
||||
@@ -13,6 +13,8 @@ framework-validation-invalidEmail = Please enter a valid email address.
|
||||
framework-validation-passwordTooShort = Password must contain at least {$minLength} characters.
|
||||
framework-validation-passwordsDoNotMatch = Passwords do not match. Try again.
|
||||
|
||||
framework-timeago-just-now = Just now
|
||||
|
||||
framework-timeago-time =
|
||||
{ $value }
|
||||
{ $unit ->
|
||||
@@ -48,12 +50,7 @@ framework-timeago-time =
|
||||
}
|
||||
|
||||
framework-timeago =
|
||||
{ $value ->
|
||||
[0] now
|
||||
*[other]
|
||||
{ $suffix ->
|
||||
[ago] {framework-timeago-time} ago
|
||||
[in] in {framework-timeago-time}
|
||||
*[other] unknown suffix
|
||||
}
|
||||
{ $suffix ->
|
||||
[ago] {framework-timeago-time} ago
|
||||
[noSuffix] {framework-timeago-time}
|
||||
}
|
||||
|
||||
@@ -53,4 +53,15 @@ comments-replyCommentForm-submit = Submit
|
||||
comments-replyCommentForm-cancel = Cancel
|
||||
comments-replyCommentForm-rteLabel = Write a reply
|
||||
comments-replyCommentForm-rte =
|
||||
.placeholder = { comments-postCommentForm-rteLabel }
|
||||
.placeholder = { comments-replyCommentForm-rteLabel }
|
||||
|
||||
comments-commentContainer-editButton = Edit
|
||||
|
||||
comments-editCommentForm-saveChanges = Save Changes
|
||||
comments-editCommentForm-cancel = Cancel
|
||||
comments-editCommentForm-close = Close
|
||||
comments-editCommentForm-rteLabel = Edit comment
|
||||
comments-editCommentForm-rte =
|
||||
.placeholder = { comments-editCommentForm-rteLabel }
|
||||
comments-editCommentForm-editRemainingTime = Edit: <time></time> remaining
|
||||
comments-editCommentForm-editTimeExpired = Edit time has expired. You can no longer edit this comment. Why not post another one?
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
### All keys must start with `framework` because this file is shared
|
||||
### among different targets.
|
||||
|
||||
framework-timeago-just-now = Hace poco
|
||||
framework-timeago =
|
||||
{ $value ->
|
||||
[0] ahora
|
||||
*[other]
|
||||
{ $suffix ->
|
||||
[ago] hace
|
||||
[in] en
|
||||
*[other] unknown suffix
|
||||
}
|
||||
{ $value }
|
||||
{ $unit ->
|
||||
|
||||
Vendored
+1
@@ -36,6 +36,7 @@ declare module "react-timeago" {
|
||||
live?: boolean;
|
||||
className: string;
|
||||
formatter?: Formatter;
|
||||
minPeriod?: number;
|
||||
}
|
||||
|
||||
const TimeAgo: React.ComponentType<TimeAgoProps>;
|
||||
|
||||
Reference in New Issue
Block a user