diff --git a/src/core/client/framework/types.ts b/src/core/client/framework/types.ts index 60a609441..c064c3fa4 100644 --- a/src/core/client/framework/types.ts +++ b/src/core/client/framework/types.ts @@ -1,2 +1,3 @@ // TODO: (@cvle) Extract useful common types into its own package. export { Omit, Overwrite, PropTypesOf } from "talk-ui/types"; +export { DeepPartial } from "talk-common/types"; diff --git a/src/core/client/stream/containers/StoryClosedTimeoutContainer.tsx b/src/core/client/stream/containers/StoryClosedTimeoutContainer.tsx new file mode 100644 index 000000000..731ead9c9 --- /dev/null +++ b/src/core/client/stream/containers/StoryClosedTimeoutContainer.tsx @@ -0,0 +1,67 @@ +import React from "react"; + +import { graphql, withFragmentContainer } from "talk-framework/lib/relay"; + +import { StoryClosedTimeoutContainer_story as StoryData } from "talk-stream/__generated__/StoryClosedTimeoutContainer_story.graphql"; +import { + SetStoryClosedMutation, + withSetStoryClosedMutation, +} from "talk-stream/mutations"; + +interface Props { + story: StoryData; + setStoryClosed: SetStoryClosedMutation; +} + +function createTimeout(callback: () => void, closedAt: string) { + const diff = new Date(closedAt).valueOf() - Date.now(); + if (diff > 0) { + return setTimeout(callback, diff); + } + return null; +} + +class StoryClosedTimeoutContainer extends React.Component { + private timer: any = null; + + constructor(props: Props) { + super(props); + if (props.story.closedAt) { + this.timer = createTimeout(this.handleClose, this.props.story.closedAt); + } + } + + public componentWillReceiveProps(nextProps: Props) { + if (nextProps.story.closedAt !== this.props.story.closedAt) { + clearTimeout(this.timer); + this.timer = createTimeout(this.handleClose, nextProps.story.closedAt); + } + } + + public componentWillUnmount() { + clearTimeout(this.timer); + this.timer = null; + } + + private handleClose = () => { + this.timer = null; + this.props.setStoryClosed({ storyID: this.props.story.id, isClosed: true }); + }; + + public render() { + return null; + } +} + +const enhanced = withSetStoryClosedMutation( + withFragmentContainer({ + story: graphql` + fragment StoryClosedTimeoutContainer_story on Story { + id + closedAt + } + `, + })(StoryClosedTimeoutContainer) +); + +export default enhanced; diff --git a/src/core/client/stream/fetches/RefreshSettingsQuery.ts b/src/core/client/stream/fetches/RefreshSettingsQuery.ts index 25473b3a7..708fbaaee 100644 --- a/src/core/client/stream/fetches/RefreshSettingsQuery.ts +++ b/src/core/client/stream/fetches/RefreshSettingsQuery.ts @@ -4,19 +4,27 @@ import { Environment } from "relay-runtime"; import { createFetchContainer, fetchQuery } from "talk-framework/lib/relay"; import { RefreshSettingsQuery as QueryTypes } from "talk-stream/__generated__/RefreshSettingsQuery.graphql"; +export type RefreshSettingsVariables = QueryTypes["variables"]; + const query = graphql` - query RefreshSettingsQuery { + query RefreshSettingsQuery($storyID: ID!) { settings { ...StreamContainer_settings } + # We also refrech story props that are + # dependent on the settings. + story(id: $storyID) { + closedAt + isClosed + } } `; -function fetch(environment: Environment) { +function fetch(environment: Environment, variables: RefreshSettingsVariables) { return fetchQuery( environment, query, - {}, + variables, { force: true } ); } @@ -26,6 +34,6 @@ export const withRefreshSettingsFetch = createFetchContainer( fetch ); -export type RefreshSettingsFetch = () => Promise< - QueryTypes["response"]["settings"] ->; +export type RefreshSettingsFetch = ( + variables: RefreshSettingsVariables +) => Promise; diff --git a/src/core/client/stream/mutations/SetStoryClosedMutation.ts b/src/core/client/stream/mutations/SetStoryClosedMutation.ts new file mode 100644 index 000000000..8bd839bad --- /dev/null +++ b/src/core/client/stream/mutations/SetStoryClosedMutation.ts @@ -0,0 +1,27 @@ +import { commitLocalUpdate, Environment } from "relay-runtime"; + +import { createMutationContainer } from "talk-framework/lib/relay"; + +export interface SetStoryClosedInput { + storyID: string; + isClosed: boolean; +} + +export type SetStoryClosedMutation = ( + input: SetStoryClosedInput +) => Promise; + +export async function commit( + environment: Environment, + input: SetStoryClosedInput +) { + return commitLocalUpdate(environment, store => { + const record = store.get(input.storyID)!; + record.setValue(input.isClosed, "isClosed"); + }); +} + +export const withSetStoryClosedMutation = createMutationContainer( + "setStoryClosed", + commit +); diff --git a/src/core/client/stream/mutations/index.ts b/src/core/client/stream/mutations/index.ts index 3e2950242..5688c1bf6 100644 --- a/src/core/client/stream/mutations/index.ts +++ b/src/core/client/stream/mutations/index.ts @@ -50,3 +50,8 @@ export { withRemoveCommentReactionMutation, RemoveCommentReactionMutation, } from "./RemoveCommentReactionMutation"; +export { + SetStoryClosedInput, + withSetStoryClosedMutation, + SetStoryClosedMutation, +} from "./SetStoryClosedMutation"; diff --git a/src/core/client/stream/tabs/comments/components/Comment/ReplyButton.tsx b/src/core/client/stream/tabs/comments/components/Comment/ReplyButton.tsx index cf8ddd25b..e9ef02f57 100644 --- a/src/core/client/stream/tabs/comments/components/Comment/ReplyButton.tsx +++ b/src/core/client/stream/tabs/comments/components/Comment/ReplyButton.tsx @@ -7,6 +7,7 @@ interface Props { id?: string; onClick?: EventHandler>; active?: boolean; + disabled?: boolean; } const ReplyButton: StatelessComponent = props => ( @@ -16,6 +17,7 @@ const ReplyButton: StatelessComponent = props => ( variant="ghost" size="small" active={props.active} + disabled={props.disabled} > reply diff --git a/src/core/client/stream/tabs/comments/components/PostCommentForm.tsx b/src/core/client/stream/tabs/comments/components/PostCommentForm.tsx index 805b1f8af..01c673a29 100644 --- a/src/core/client/stream/tabs/comments/components/PostCommentForm.tsx +++ b/src/core/client/stream/tabs/comments/components/PostCommentForm.tsx @@ -23,6 +23,8 @@ export interface PostCommentFormProps { initialValues?: FormProps; min: number | null; max: number | null; + disabled?: boolean; + disabledMessage?: React.ReactNode; } const PostCommentForm: StatelessComponent = props => ( @@ -64,26 +66,38 @@ const PostCommentForm: StatelessComponent = props => ( } value={input.value} placeholder="Post a comment" - disabled={submitting} + disabled={submitting || props.disabled} /> - {meta.touched && - (meta.error || - (meta.submitError && !meta.dirtySinceLastSubmit)) && ( - - {meta.error || meta.submitError} - - )} - {submitError && ( - - {submitError} - - )} - {props.max && ( - + {props.disabled ? ( + <> + {props.disabledMessage && ( + + {props.disabledMessage} + + )} + + ) : ( + <> + {meta.touched && + (meta.error || + (meta.submitError && !meta.dirtySinceLastSubmit)) && ( + + {meta.error || meta.submitError} + + )} + {submitError && ( + + {submitError} + + )} + {props.max && ( + + )} + )} @@ -91,7 +105,7 @@ const PostCommentForm: StatelessComponent = props => ( -
- - -
- - -
-
- -
-
- A dialog for reporting comments -
-
-
-
- - - - - -
-
-
-
- -
- - -   - - - Replying to: - - Markus - - -
-
-
- -
-
- - - -
-
-
-
-
-
- - -
-
- -
- -`; - exports[`post a reply: open reply form 1`] = `
+ +
+
- - -
-
-
Community Guidelines + className="Markdown-root" + dangerouslySetInnerHTML={ + Object { + "__html": "

Community Guidelines

", - } } - /> -
+ } + /> +
+