diff --git a/src/core/client/admin/mutations/AcceptCommentMutation.ts b/src/core/client/admin/mutations/AcceptCommentMutation.ts index b191eb0da..518e55061 100644 --- a/src/core/client/admin/mutations/AcceptCommentMutation.ts +++ b/src/core/client/admin/mutations/AcceptCommentMutation.ts @@ -4,16 +4,14 @@ import { ConnectionHandler, Environment } from "relay-runtime"; import { commitMutationPromiseNormalized, createMutationContainer, + MutationInput, + MutationResponsePromise, } from "talk-framework/lib/relay"; -import { Omit } from "talk-framework/types"; import { AcceptCommentMutation as MutationTypes } from "talk-admin/__generated__/AcceptCommentMutation.graphql"; import { getQueueConnection } from "talk-admin/helpers"; -export type AcceptCommentInput = Omit< - MutationTypes["variables"]["input"], - "clientMutationId" ->; +export type AcceptCommentInput = MutationInput; const mutation = graphql` mutation AcceptCommentMutation($input: AcceptCommentInput!) { @@ -79,4 +77,4 @@ export const withAcceptCommentMutation = createMutationContainer( export type AcceptCommentMutation = ( input: AcceptCommentInput -) => Promise; +) => MutationResponsePromise; diff --git a/src/core/client/admin/mutations/RegenerateSSOKeyMutation.ts b/src/core/client/admin/mutations/RegenerateSSOKeyMutation.ts index 9dba2624f..8440173ea 100644 --- a/src/core/client/admin/mutations/RegenerateSSOKeyMutation.ts +++ b/src/core/client/admin/mutations/RegenerateSSOKeyMutation.ts @@ -4,6 +4,7 @@ import { Environment } from "relay-runtime"; import { commitMutationPromiseNormalized, createMutationContainer, + MutationResponsePromise, } from "talk-framework/lib/relay"; import { RegenerateSSOKeyMutation as MutationTypes } from "talk-admin/__generated__/RegenerateSSOKeyMutation.graphql"; @@ -44,6 +45,7 @@ export const withRegenerateSSOKeyMutation = createMutationContainer( commit ); -export type RegenerateSSOKeyMutation = () => Promise< - MutationTypes["response"]["regenerateSSOKey"] +export type RegenerateSSOKeyMutation = () => MutationResponsePromise< + MutationTypes, + "regenerateSSOKey" >; diff --git a/src/core/client/admin/mutations/RejectCommentMutation.ts b/src/core/client/admin/mutations/RejectCommentMutation.ts index b7d3f6a8a..8dc0a3f31 100644 --- a/src/core/client/admin/mutations/RejectCommentMutation.ts +++ b/src/core/client/admin/mutations/RejectCommentMutation.ts @@ -4,16 +4,14 @@ import { ConnectionHandler, Environment } from "relay-runtime"; import { commitMutationPromiseNormalized, createMutationContainer, + MutationInput, + MutationResponsePromise, } from "talk-framework/lib/relay"; -import { Omit } from "talk-framework/types"; import { RejectCommentMutation as MutationTypes } from "talk-admin/__generated__/RejectCommentMutation.graphql"; import { getQueueConnection } from "talk-admin/helpers"; -export type RejectCommentInput = Omit< - MutationTypes["variables"]["input"], - "clientMutationId" ->; +export type RejectCommentInput = MutationInput; const mutation = graphql` mutation RejectCommentMutation($input: RejectCommentInput!) { @@ -78,4 +76,4 @@ export const withRejectCommentMutation = createMutationContainer( export type RejectCommentMutation = ( input: RejectCommentInput -) => Promise; +) => MutationResponsePromise; diff --git a/src/core/client/admin/mutations/SetEmailMutation.ts b/src/core/client/admin/mutations/SetEmailMutation.ts index 7479fcf4a..804540283 100644 --- a/src/core/client/admin/mutations/SetEmailMutation.ts +++ b/src/core/client/admin/mutations/SetEmailMutation.ts @@ -4,15 +4,13 @@ import { Environment } from "relay-runtime"; import { commitMutationPromiseNormalized, createMutationContainer, + MutationInput, + MutationResponsePromise, } from "talk-framework/lib/relay"; -import { Omit } from "talk-framework/types"; import { SetEmailMutation as MutationTypes } from "talk-admin/__generated__/SetEmailMutation.graphql"; -export type SetEmailInput = Omit< - MutationTypes["variables"]["input"], - "clientMutationId" ->; +export type SetEmailInput = MutationInput; const mutation = graphql` mutation SetEmailMutation($input: SetEmailInput!) { @@ -43,4 +41,4 @@ export const withSetEmailMutation = createMutationContainer("setEmail", commit); export type SetEmailMutation = ( input: SetEmailInput -) => Promise; +) => MutationResponsePromise; diff --git a/src/core/client/admin/mutations/SetPasswordMutation.ts b/src/core/client/admin/mutations/SetPasswordMutation.ts index e9cb7bd38..b99d2970c 100644 --- a/src/core/client/admin/mutations/SetPasswordMutation.ts +++ b/src/core/client/admin/mutations/SetPasswordMutation.ts @@ -4,15 +4,13 @@ import { Environment } from "relay-runtime"; import { commitMutationPromiseNormalized, createMutationContainer, + MutationInput, + MutationResponsePromise, } from "talk-framework/lib/relay"; -import { Omit } from "talk-framework/types"; import { SetPasswordMutation as MutationTypes } from "talk-admin/__generated__/SetPasswordMutation.graphql"; -export type SetPasswordInput = Omit< - MutationTypes["variables"]["input"], - "clientMutationId" ->; +export type SetPasswordInput = MutationInput; const mutation = graphql` mutation SetPasswordMutation($input: SetPasswordInput!) { @@ -48,4 +46,4 @@ export const withSetPasswordMutation = createMutationContainer( export type SetPasswordMutation = ( input: SetPasswordInput -) => Promise; +) => MutationResponsePromise; diff --git a/src/core/client/admin/mutations/SetUsernameMutation.ts b/src/core/client/admin/mutations/SetUsernameMutation.ts index 6f3a5b6d4..5e8518519 100644 --- a/src/core/client/admin/mutations/SetUsernameMutation.ts +++ b/src/core/client/admin/mutations/SetUsernameMutation.ts @@ -4,15 +4,13 @@ import { Environment } from "relay-runtime"; import { commitMutationPromiseNormalized, createMutationContainer, + MutationInput, + MutationResponsePromise, } from "talk-framework/lib/relay"; -import { Omit } from "talk-framework/types"; import { SetUsernameMutation as MutationTypes } from "talk-admin/__generated__/SetUsernameMutation.graphql"; -export type SetUsernameInput = Omit< - MutationTypes["variables"]["input"], - "clientMutationId" ->; +export type SetUsernameInput = MutationInput; const mutation = graphql` mutation SetUsernameMutation($input: SetUsernameInput!) { @@ -46,4 +44,4 @@ export const withSetUsernameMutation = createMutationContainer( export type SetUsernameMutation = ( input: SetUsernameInput -) => Promise; +) => MutationResponsePromise; diff --git a/src/core/client/admin/mutations/UpdateSettingsMutation.ts b/src/core/client/admin/mutations/UpdateSettingsMutation.ts index 50185fc6f..3d4a28833 100644 --- a/src/core/client/admin/mutations/UpdateSettingsMutation.ts +++ b/src/core/client/admin/mutations/UpdateSettingsMutation.ts @@ -4,15 +4,13 @@ import { Environment } from "relay-runtime"; import { commitMutationPromiseNormalized, createMutationContainer, + MutationInput, + MutationResponsePromise, } from "talk-framework/lib/relay"; -import { Omit } from "talk-framework/types"; import { UpdateSettingsMutation as MutationTypes } from "talk-admin/__generated__/UpdateSettingsMutation.graphql"; -export type UpdateSettingsInput = Omit< - MutationTypes["variables"]["input"], - "clientMutationId" ->; +export type UpdateSettingsInput = MutationInput; const mutation = graphql` mutation UpdateSettingsMutation($input: UpdateSettingsInput!) { @@ -53,4 +51,4 @@ export const withUpdateSettingsMutation = createMutationContainer( export type UpdateSettingsMutation = ( input: UpdateSettingsInput -) => Promise; +) => MutationResponsePromise; diff --git a/src/core/client/framework/lib/relay/createMutationContainer.tsx b/src/core/client/framework/lib/relay/createMutationContainer.tsx index a07c04981..1c91bc641 100644 --- a/src/core/client/framework/lib/relay/createMutationContainer.tsx +++ b/src/core/client/framework/lib/relay/createMutationContainer.tsx @@ -6,9 +6,24 @@ import { wrapDisplayName, } from "recompose"; import { Environment } from "relay-runtime"; +import { Omit } from "talk-framework/types"; import { TalkContext, withContext } from "../bootstrap"; +export type MutationInput< + T extends { variables: { input: { clientMutationId: string } } } +> = Omit; + +export type MutationResponse< + T extends { response: { [P in U]: any } }, + U extends string +> = Exclude; + +export type MutationResponsePromise< + T extends { response: { [P in U]: any } }, + U extends string +> = Promise>; + /** * createMutationContainer creates a HOC that * injects a property with the name specified in `propName` diff --git a/src/core/client/framework/lib/relay/index.ts b/src/core/client/framework/lib/relay/index.ts index d82563737..6da94c702 100644 --- a/src/core/client/framework/lib/relay/index.ts +++ b/src/core/client/framework/lib/relay/index.ts @@ -5,7 +5,12 @@ export { default as withLocalStateContainer } from "./withLocalStateContainer"; export * from "./withLocalStateContainer"; export { default as QueryRenderer } from "./QueryRenderer"; export * from "./QueryRenderer"; -export { default as createMutationContainer } from "./createMutationContainer"; +export { + default as createMutationContainer, + MutationInput, + MutationResponse, + MutationResponsePromise, +} from "./createMutationContainer"; export { default as createFetchContainer } from "./createFetchContainer"; export { default as createAndRetain } from "./createAndRetain"; export { default as wrapFetchWithLogger } from "./wrapFetchWithLogger"; diff --git a/src/core/client/stream/helpers/index.ts b/src/core/client/stream/helpers/index.ts index 32b82ca36..4341c989b 100644 --- a/src/core/client/stream/helpers/index.ts +++ b/src/core/client/stream/helpers/index.ts @@ -4,3 +4,7 @@ export { export { default as prependCommentEdgeToProfile, } from "./prependCommentEdgeToProfile"; +export { default as isVisible } from "./isVisible"; +export { default as isInReview } from "./isInReview"; +export { default as isRejected } from "./isRejected"; +export { default as isRolePriviledged } from "./isRolePriviledged"; diff --git a/src/core/client/stream/helpers/isInReview.ts b/src/core/client/stream/helpers/isInReview.ts new file mode 100644 index 000000000..c093f6749 --- /dev/null +++ b/src/core/client/stream/helpers/isInReview.ts @@ -0,0 +1,5 @@ +// TODO: use generated schema types. +const inReviewStatuses = ["PREMOD", "SYSTEM_WITHHELD"]; +export default function isInReview(status: any) { + return inReviewStatuses.includes(status); +} diff --git a/src/core/client/stream/helpers/isRejected.ts b/src/core/client/stream/helpers/isRejected.ts new file mode 100644 index 000000000..c81ad26f7 --- /dev/null +++ b/src/core/client/stream/helpers/isRejected.ts @@ -0,0 +1,4 @@ +// TODO: use generated schema types. +export default function isRejected(status: any) { + return status === "REJECTED"; +} diff --git a/src/core/client/stream/helpers/isRolePriviledged.ts b/src/core/client/stream/helpers/isRolePriviledged.ts new file mode 100644 index 000000000..033294953 --- /dev/null +++ b/src/core/client/stream/helpers/isRolePriviledged.ts @@ -0,0 +1,5 @@ +// TODO: use generated schema types. +const priviledgedRoles = ["ADMIN", "MODERATOR"]; +export default function isRolePriviledged(role: any) { + return priviledgedRoles.includes(role); +} diff --git a/src/core/client/stream/helpers/isVisible.ts b/src/core/client/stream/helpers/isVisible.ts new file mode 100644 index 000000000..0ca890184 --- /dev/null +++ b/src/core/client/stream/helpers/isVisible.ts @@ -0,0 +1,6 @@ +// TODO: use generated schema types. +const visibleStatuses = ["NONE", "ACCEPTED"]; + +export default function isVisible(status: any) { + return visibleStatuses.includes(status); +} diff --git a/src/core/client/stream/mutations/CreateCommentDontAgreeMutation.ts b/src/core/client/stream/mutations/CreateCommentDontAgreeMutation.ts index cb1af6d57..ab6485375 100644 --- a/src/core/client/stream/mutations/CreateCommentDontAgreeMutation.ts +++ b/src/core/client/stream/mutations/CreateCommentDontAgreeMutation.ts @@ -5,15 +5,13 @@ import { Environment } from "relay-runtime"; import { commitMutationPromiseNormalized, createMutationContainer, + MutationInput, + MutationResponsePromise, } from "talk-framework/lib/relay"; -import { Omit } from "talk-framework/types"; import { CreateCommentDontAgreeMutation as MutationTypes } from "talk-stream/__generated__/CreateCommentDontAgreeMutation.graphql"; -export type CreateCommentDontAgreeInput = Omit< - MutationTypes["variables"]["input"], - "clientMutationId" ->; +export type CreateCommentDontAgreeInput = MutationInput; const mutation = graphql` mutation CreateCommentDontAgreeMutation( @@ -52,4 +50,4 @@ export const withCreateCommentDontAgreeMutation = createMutationContainer( export type CreateCommentDontAgreeMutation = ( input: CreateCommentDontAgreeInput -) => Promise; +) => MutationResponsePromise; diff --git a/src/core/client/stream/mutations/CreateCommentFlagMutation.ts b/src/core/client/stream/mutations/CreateCommentFlagMutation.ts index e0e9b88cf..f06d137dd 100644 --- a/src/core/client/stream/mutations/CreateCommentFlagMutation.ts +++ b/src/core/client/stream/mutations/CreateCommentFlagMutation.ts @@ -5,15 +5,13 @@ import { Environment } from "relay-runtime"; import { commitMutationPromiseNormalized, createMutationContainer, + MutationInput, + MutationResponsePromise, } from "talk-framework/lib/relay"; -import { Omit } from "talk-framework/types"; import { CreateCommentFlagMutation as MutationTypes } from "talk-stream/__generated__/CreateCommentFlagMutation.graphql"; -export type CreateCommentFlagInput = Omit< - MutationTypes["variables"]["input"], - "clientMutationId" ->; +export type CreateCommentFlagInput = MutationInput; const mutation = graphql` mutation CreateCommentFlagMutation($input: CreateCommentFlagInput!) { @@ -55,4 +53,4 @@ export const withCreateCommentFlagMutation = createMutationContainer( export type CreateCommentFlagMutation = ( input: CreateCommentFlagInput -) => Promise; +) => MutationResponsePromise; diff --git a/src/core/client/stream/mutations/CreateCommentMutation.ts b/src/core/client/stream/mutations/CreateCommentMutation.ts index c1e71c936..1af1b1a1b 100644 --- a/src/core/client/stream/mutations/CreateCommentMutation.ts +++ b/src/core/client/stream/mutations/CreateCommentMutation.ts @@ -2,6 +2,7 @@ import { graphql } from "react-relay"; import { ConnectionHandler, Environment, + RecordProxy, RecordSourceSelectorProxy, } from "relay-runtime"; @@ -10,32 +11,38 @@ import { TalkContext } from "talk-framework/lib/bootstrap"; import { commitMutationPromiseNormalized, createMutationContainer, + MutationInput, + MutationResponsePromise, } from "talk-framework/lib/relay"; -import { Omit } from "talk-framework/types"; import { CreateCommentMutation as MutationTypes } from "talk-stream/__generated__/CreateCommentMutation.graphql"; import { incrementStoryCommentCounts, + isRolePriviledged, + isVisible, prependCommentEdgeToProfile, } from "../helpers"; -export type CreateCommentInput = Omit< - MutationTypes["variables"]["input"], - "clientMutationId" ->; +export type CreateCommentInput = MutationInput; function sharedUpdater( environment: Environment, store: RecordSourceSelectorProxy, input: CreateCommentInput ) { + const commentEdge = store + .getRootField("createComment")! + .getLinkedRecord("edge")!; + const status = commentEdge.getLinkedRecord("node")!.getValue("status"); + + // If comment is not visible, we don't need to add it. + if (!isVisible(status)) { + return; + } + incrementStoryCommentCounts(store, input.storyID); - prependCommentEdgeToProfile( - environment, - store, - store.getRootField("createComment")!.getLinkedRecord("edge")! - ); - addCommentToStory(store, input); + prependCommentEdgeToProfile(environment, store, commentEdge); + addCommentToStory(store, input, commentEdge); } /** @@ -43,14 +50,9 @@ function sharedUpdater( */ function addCommentToStory( store: RecordSourceSelectorProxy, - input: CreateCommentInput + input: CreateCommentInput, + commentEdge: RecordProxy ) { - // Get the payload returned from the server. - const payload = store.getRootField("createComment")!; - - // Get the edge of the newly created comment. - const newEdge = payload.getLinkedRecord("edge")!; - // Get stream proxy. const streamProxy = store.get(input.storyID); const connectionKey = "Stream_comments"; @@ -63,11 +65,23 @@ function addCommentToStory( filters ); if (con) { - ConnectionHandler.insertEdgeBefore(con, newEdge); + ConnectionHandler.insertEdgeBefore(con, commentEdge); } } } +/** These are needed to be included when querying for the stream. */ +// tslint:disable-next-line:no-unused-expression +graphql` + fragment CreateCommentMutation_story on Story { + moderation + } + fragment CreateCommentMutation_me on User { + role + } +`; +/** end */ + const mutation = graphql` mutation CreateCommentMutation($input: CreateCommentInput!) { createComment(input: $input) { @@ -75,6 +89,7 @@ const mutation = graphql` cursor node { ...StreamContainer_comment @relay(mask: false) + status } } clientMutationId @@ -87,11 +102,24 @@ let clientMutationId = 0; function commit( environment: Environment, input: CreateCommentInput, - { uuidGenerator }: TalkContext + { uuidGenerator, relayEnvironment }: TalkContext ) { const me = getMe(environment)!; const currentDate = new Date().toISOString(); const id = uuidGenerator(); + + const story = relayEnvironment + .getStore() + .getSource() + .get(input.storyID); + if (!story || !story.moderation) { + throw new Error("Moderation mode of the story was not included"); + } + + // TODO: Generate and use schema types. + const expectPremoderation = + !isRolePriviledged(me.role) && story.moderation === "PRE"; + return commitMutationPromiseNormalized(environment, { mutation, variables: { @@ -108,6 +136,7 @@ function commit( node: { id, createdAt: currentDate, + status: "NONE", author: { id: me.id, username: me.username, @@ -127,6 +156,10 @@ function commit( }, } as any, // TODO: (cvle) generated types should contain one for the optimistic response. optimisticUpdater: store => { + // Skip optimistic update if comment is probably premoderated. + if (expectPremoderation) { + return; + } sharedUpdater(environment, store, input); store.get(id)!.setValue(true, "pending"); }, @@ -143,4 +176,4 @@ export const withCreateCommentMutation = createMutationContainer( export type CreateCommentMutation = ( input: CreateCommentInput -) => Promise; +) => MutationResponsePromise; diff --git a/src/core/client/stream/mutations/CreateCommentReactionMutation.ts b/src/core/client/stream/mutations/CreateCommentReactionMutation.ts index 8290c4fec..371230a1d 100644 --- a/src/core/client/stream/mutations/CreateCommentReactionMutation.ts +++ b/src/core/client/stream/mutations/CreateCommentReactionMutation.ts @@ -5,15 +5,13 @@ import { Environment } from "relay-runtime"; import { commitMutationPromiseNormalized, createMutationContainer, + MutationInput, + MutationResponsePromise, } from "talk-framework/lib/relay"; -import { Omit } from "talk-framework/types"; import { CreateCommentReactionMutation as MutationTypes } from "talk-stream/__generated__/CreateCommentReactionMutation.graphql"; -export type CreateCommentReactionInput = Omit< - MutationTypes["variables"]["input"], - "clientMutationId" ->; +export type CreateCommentReactionInput = MutationInput; const mutation = graphql` mutation CreateCommentReactionMutation($input: CreateCommentReactionInput!) { @@ -68,4 +66,4 @@ export const withCreateCommentReactionMutation = createMutationContainer( export type CreateCommentReactionMutation = ( input: CreateCommentReactionInput -) => Promise; +) => MutationResponsePromise; diff --git a/src/core/client/stream/mutations/CreateCommentReplyMutation.ts b/src/core/client/stream/mutations/CreateCommentReplyMutation.ts index 8d2fecaa4..52af45b74 100644 --- a/src/core/client/stream/mutations/CreateCommentReplyMutation.ts +++ b/src/core/client/stream/mutations/CreateCommentReplyMutation.ts @@ -2,6 +2,7 @@ import { graphql } from "react-relay"; import { ConnectionHandler, Environment, + RecordProxy, RecordSourceSelectorProxy, } from "relay-runtime"; @@ -10,35 +11,43 @@ import { TalkContext } from "talk-framework/lib/bootstrap"; import { commitMutationPromiseNormalized, createMutationContainer, + MutationInput, + MutationResponsePromise, } from "talk-framework/lib/relay"; -import { Omit } from "talk-framework/types"; import { CreateCommentReplyMutation as MutationTypes } from "talk-stream/__generated__/CreateCommentReplyMutation.graphql"; import { incrementStoryCommentCounts, + isRolePriviledged, + isVisible, prependCommentEdgeToProfile, } from "../helpers"; -export type CreateCommentReplyInput = Omit< - MutationTypes["variables"]["input"], - "clientMutationId" -> & { local?: boolean }; +export type CreateCommentReplyInput = MutationInput & { + local?: boolean; +}; function sharedUpdater( environment: Environment, store: RecordSourceSelectorProxy, input: CreateCommentReplyInput ) { + const commentEdge = store + .getRootField("createCommentReply")! + .getLinkedRecord("edge")!; + const status = commentEdge.getLinkedRecord("node")!.getValue("status"); + + // If comment is not visible, we don't need to add it. + if (!isVisible(status)) { + return; + } + incrementStoryCommentCounts(store, input.storyID); - prependCommentEdgeToProfile( - environment, - store, - store.getRootField("createCommentReply")!.getLinkedRecord("edge")! - ); + prependCommentEdgeToProfile(environment, store, commentEdge); if (input.local) { - addLocalCommentReplyToStory(store, input); + addLocalCommentReplyToStory(store, input, commentEdge); } else { - addCommentReplyToStory(store, input); + addCommentReplyToStory(store, input, commentEdge); } } @@ -47,14 +56,9 @@ function sharedUpdater( */ function addCommentReplyToStory( store: RecordSourceSelectorProxy, - input: CreateCommentReplyInput + input: CreateCommentReplyInput, + commentEdge: RecordProxy ) { - // Get the payload returned from the server. - const payload = store.getRootField("createCommentReply")!; - - // Get the edge of the newly created comment. - const newEdge = payload.getLinkedRecord("edge")!; - // Get parent proxy. const parentProxy = store.get(input.parentID); const connectionKey = "ReplyList_replies"; @@ -67,7 +71,7 @@ function addCommentReplyToStory( filters ); if (con) { - ConnectionHandler.insertEdgeAfter(con, newEdge); + ConnectionHandler.insertEdgeAfter(con, commentEdge); } } } @@ -77,14 +81,10 @@ function addCommentReplyToStory( */ function addLocalCommentReplyToStory( store: RecordSourceSelectorProxy, - input: CreateCommentReplyInput + input: CreateCommentReplyInput, + commentEdge: RecordProxy ) { - // Get the payload returned from the server. - const payload = store.getRootField("createCommentReply")!; - - // Get the edge of the newly created comment. - const newEdge = payload.getLinkedRecord("edge")!; - const newComment = newEdge.getLinkedRecord("node"); + const newComment = commentEdge.getLinkedRecord("node"); // Get parent proxy. const parentProxy = store.get(input.parentID!); @@ -98,6 +98,17 @@ function addLocalCommentReplyToStory( } } +/** These are needed to be included when querying for the stream. */ +// tslint:disable-next-line:no-unused-expression +graphql` + fragment CreateCommentReplyMutation_story on Story { + moderation + } + fragment CreateCommentReplyMutation_me on User { + role + } +`; +/** end */ const mutation = graphql` mutation CreateCommentReplyMutation($input: CreateCommentReplyInput!) { createCommentReply(input: $input) { @@ -105,6 +116,7 @@ const mutation = graphql` cursor node { ...StreamContainer_comment @relay(mask: false) + status } } clientMutationId @@ -117,11 +129,24 @@ let clientMutationId = 0; function commit( environment: Environment, input: CreateCommentReplyInput, - { uuidGenerator }: TalkContext + { uuidGenerator, relayEnvironment }: TalkContext ) { const me = getMe(environment)!; const currentDate = new Date().toISOString(); const id = uuidGenerator(); + + const story = relayEnvironment + .getStore() + .getSource() + .get(input.storyID); + if (!story || !story.moderation) { + throw new Error("Moderation mode of the story was not included"); + } + + // TODO: Generate and use schema types. + const expectPremoderation = + !isRolePriviledged(me.role) && story.moderation === "PRE"; + return commitMutationPromiseNormalized(environment, { mutation, variables: { @@ -140,6 +165,7 @@ function commit( node: { id, createdAt: currentDate, + status: "NONE", author: { id: me.id, username: me.username, @@ -159,6 +185,10 @@ function commit( }, } as any, // TODO: (cvle) generated types should contain one for the optimistic response. optimisticUpdater: store => { + // Skip optimistic update if comment is probably premoderated. + if (expectPremoderation) { + return; + } sharedUpdater(environment, store, input); store.get(id)!.setValue(true, "pending"); }, @@ -175,4 +205,4 @@ export const withCreateCommentReplyMutation = createMutationContainer( export type CreateCommentReplyMutation = ( input: CreateCommentReplyInput -) => Promise; +) => MutationResponsePromise; diff --git a/src/core/client/stream/mutations/EditCommentMutation.ts b/src/core/client/stream/mutations/EditCommentMutation.ts index cd59f464c..83cca5715 100644 --- a/src/core/client/stream/mutations/EditCommentMutation.ts +++ b/src/core/client/stream/mutations/EditCommentMutation.ts @@ -5,21 +5,20 @@ import { Environment } from "relay-runtime"; import { commitMutationPromiseNormalized, createMutationContainer, + MutationInput, + MutationResponsePromise, } 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" ->; +export type EditCommentInput = MutationInput; const mutation = graphql` mutation EditCommentMutation($input: EditCommentInput!) { editComment(input: $input) { comment { body + status revision { id } @@ -63,4 +62,4 @@ export const withEditCommentMutation = createMutationContainer( export type EditCommentMutation = ( input: EditCommentInput -) => Promise; +) => MutationResponsePromise; diff --git a/src/core/client/stream/mutations/RemoveCommentReactionMutation.ts b/src/core/client/stream/mutations/RemoveCommentReactionMutation.ts index 1abc3444f..74d7dfd6c 100644 --- a/src/core/client/stream/mutations/RemoveCommentReactionMutation.ts +++ b/src/core/client/stream/mutations/RemoveCommentReactionMutation.ts @@ -5,15 +5,13 @@ import { Environment } from "relay-runtime"; import { commitMutationPromiseNormalized, createMutationContainer, + MutationInput, + MutationResponsePromise, } from "talk-framework/lib/relay"; -import { Omit } from "talk-framework/types"; import { RemoveCommentReactionMutation as MutationTypes } from "talk-stream/__generated__/RemoveCommentReactionMutation.graphql"; -export type RemoveCommentReactionInput = Omit< - MutationTypes["variables"]["input"], - "clientMutationId" ->; +export type RemoveCommentReactionInput = MutationInput; const mutation = graphql` mutation RemoveCommentReactionMutation($input: RemoveCommentReactionInput!) { @@ -67,4 +65,4 @@ export const withRemoveCommentReactionMutation = createMutationContainer( export type RemoveCommentReactionMutation = ( input: RemoveCommentReactionInput -) => Promise; +) => MutationResponsePromise; diff --git a/src/core/client/stream/tabs/comments/components/PostCommentForm.tsx b/src/core/client/stream/tabs/comments/components/PostCommentForm.tsx index 01c673a29..3644be6d0 100644 --- a/src/core/client/stream/tabs/comments/components/PostCommentForm.tsx +++ b/src/core/client/stream/tabs/comments/components/PostCommentForm.tsx @@ -2,6 +2,7 @@ import { FormApi, FormState } from "final-form"; import { Localized } from "fluent-react/compat"; import React, { StatelessComponent } from "react"; import { Field, Form, FormSpy } from "react-final-form"; +import { PropTypesOf } from "talk-framework/types"; import ValidationMessage from "talk-admin/routes/configure/components/ValidationMessage"; import { OnSubmit } from "talk-framework/lib/form"; @@ -11,6 +12,7 @@ import RemainingCharactersContainer from "../containers/RemainingCharactersConta import { cleanupRTEEmptyHTML, getCommentBodyValidators } from "../helpers"; import RTE from "./RTE"; +import PostCommentSubmitStatusContainer from "../containers/PostCommentSubmitStatusContainer"; import styles from "./PostCommentForm.css"; interface FormProps { @@ -25,6 +27,7 @@ export interface PostCommentFormProps { max: number | null; disabled?: boolean; disabledMessage?: React.ReactNode; + submitStatus: PropTypesOf["status"]; } const PostCommentForm: StatelessComponent = props => ( @@ -91,6 +94,9 @@ const PostCommentForm: StatelessComponent = props => ( {submitError} )} + {props.max && ( void; +} + +const PostCommentInReview: StatelessComponent< + PostCommentInReviewProps +> = props => { + return ( + + + +
+ Your comment has been submitted and will be reviewed by a moderator +
+
+ + + +
+
+ ); +}; + +export default PostCommentInReview; diff --git a/src/core/client/stream/tabs/comments/components/ReplyEditSubmitStatus.css b/src/core/client/stream/tabs/comments/components/ReplyEditSubmitStatus.css new file mode 100644 index 000000000..c81599100 --- /dev/null +++ b/src/core/client/stream/tabs/comments/components/ReplyEditSubmitStatus.css @@ -0,0 +1,3 @@ +.callout { + min-height: calc(10 * var(--spacing-unit)); +} diff --git a/src/core/client/stream/tabs/comments/components/ReplyEditSubmitStatus.tsx b/src/core/client/stream/tabs/comments/components/ReplyEditSubmitStatus.tsx new file mode 100644 index 000000000..3ebe5c312 --- /dev/null +++ b/src/core/client/stream/tabs/comments/components/ReplyEditSubmitStatus.tsx @@ -0,0 +1,49 @@ +import React from "react"; + +import { Button, CallOut, Flex, HorizontalGutter } from "talk-ui/components"; +import { SubmitStatus } from "../helpers/getSubmitStatus"; + +import { Localized } from "fluent-react/compat"; +import styles from "./ReplyEditSubmitStatus.css"; + +interface Props { + status: SubmitStatus; + onDismiss: () => void; +} + +function getMessage(status: SubmitStatus) { + switch (status) { + case "RETRY": + throw new Error(`Invalid status ${status}`); + case "REJECTED": + // TODO: Show a different message when rejected? + case "IN_REVIEW": + return ( + + + Your comment has been submitted and will be reviewed by a moderator + + + ); + case "APPROVED": + case null: + return null; + default: + throw new Error(`Unknown status ${status}`); + } +} + +export default function ReplyEditSubmitStatus(props: Props) { + return ( + + {getMessage(props.status)} + + + + + + + ); +} diff --git a/src/core/client/stream/tabs/comments/containers/CommentContainer.spec.tsx b/src/core/client/stream/tabs/comments/containers/CommentContainer.spec.tsx index 5dbaf3d13..b90c37cf8 100644 --- a/src/core/client/stream/tabs/comments/containers/CommentContainer.spec.tsx +++ b/src/core/client/stream/tabs/comments/containers/CommentContainer.spec.tsx @@ -23,6 +23,7 @@ function createDefaultProps(add: DeepPartial = {}): Props { }, comment: { id: "comment-id", + status: "NONE", author: { id: "author-id", username: "Marvin", diff --git a/src/core/client/stream/tabs/comments/containers/CommentContainer.tsx b/src/core/client/stream/tabs/comments/containers/CommentContainer.tsx index bb32a2f81..2ae25cdaa 100644 --- a/src/core/client/stream/tabs/comments/containers/CommentContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/CommentContainer.tsx @@ -25,6 +25,7 @@ import Comment, { ShowConversationLink, } from "../components/Comment"; import ReplyButton from "../components/Comment/ReplyButton"; +import { isCommentVisible } from "../helpers"; import EditCommentFormContainer from "./EditCommentFormContainer"; import PermalinkButtonContainer from "./PermalinkButtonContainer"; import ReplyCommentFormContainer from "./ReplyCommentFormContainer"; @@ -159,6 +160,12 @@ export class CommentContainer extends Component { ); } + // Comment is not visible, so don't render it. + // This is the case when the comment was just edited and + // the comment status has changed. + if (!isCommentVisible(comment)) { + return null; + } return (
@@ -282,6 +289,7 @@ const enhanced = withSetCommentIDMutation( } body createdAt + status editing { edited editableUntil diff --git a/src/core/client/stream/tabs/comments/containers/EditCommentFormContainer.tsx b/src/core/client/stream/tabs/comments/containers/EditCommentFormContainer.tsx index ea9b940d2..61178bf36 100644 --- a/src/core/client/stream/tabs/comments/containers/EditCommentFormContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/EditCommentFormContainer.tsx @@ -23,7 +23,12 @@ import { import EditCommentForm, { EditCommentFormProps, } from "../components/EditCommentForm"; -import { shouldTriggerSettingsRefresh } from "../helpers"; +import ReplyEditSubmitStatus from "../components/ReplyEditSubmitStatus"; +import { + getSubmitStatus, + shouldTriggerSettingsRefresh, + SubmitStatus, +} from "../helpers"; interface Props { editComment: EditCommentMutation; @@ -39,6 +44,7 @@ interface State { initialValues?: EditCommentFormProps["initialValues"]; initialized: boolean; expired: boolean; + submitStatus: SubmitStatus | null; } export class EditCommentFormContainer extends Component { @@ -47,6 +53,7 @@ export class EditCommentFormContainer extends Component { public state: State = { initialized: false, expired: !isBeforeDate(this.props.comment.editing.editableUntil), + submitStatus: null, }; constructor(props: Props) { @@ -84,13 +91,19 @@ export class EditCommentFormContainer extends Component { form ) => { try { - await this.props.editComment({ - commentID: this.props.comment.id, - body: input.body, - }); - if (this.props.onClose) { - this.props.onClose(); + const submitStatus = getSubmitStatus( + await this.props.editComment({ + commentID: this.props.comment.id, + body: input.body, + }) + ); + if (submitStatus !== "RETRY") { + if (submitStatus === "APPROVED" && this.props.onClose) { + this.props.onClose(); + return; + } } + this.setState({ submitStatus }); } catch (error) { if (error instanceof InvalidRequestError) { if (shouldTriggerSettingsRefresh(error.code)) { @@ -101,10 +114,18 @@ export class EditCommentFormContainer extends Component { // tslint:disable-next-line:no-console console.error(error); } - return undefined; + return; }; public render() { + if (this.state.submitStatus && this.state.submitStatus !== "RETRY") { + return ( + + ); + } return ( { it("creates a comment", async () => { const storyID = "story-id"; const input = { body: "Hello World!" }; - const createCommentStub = sinon.stub(); + const createCommentStub = sinon.stub().returns({ edge: { node: {} } }); const form = { reset: noop }; const formMock = sinon.mock(form); formMock diff --git a/src/core/client/stream/tabs/comments/containers/PostCommentFormContainer.tsx b/src/core/client/stream/tabs/comments/containers/PostCommentFormContainer.tsx index a9091ba08..31a4d01fe 100644 --- a/src/core/client/stream/tabs/comments/containers/PostCommentFormContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/PostCommentFormContainer.tsx @@ -26,7 +26,11 @@ import PostCommentForm, { } from "../components/PostCommentForm"; import PostCommentFormCollapsed from "../components/PostCommentFormCollapsed"; import PostCommentFormFake from "../components/PostCommentFormFake"; -import { shouldTriggerSettingsRefresh } from "../helpers"; +import { + getSubmitStatus, + shouldTriggerSettingsRefresh, + SubmitStatus, +} from "../helpers"; interface Props { createComment: CreateCommentMutation; @@ -41,6 +45,7 @@ interface State { initialValues?: PostCommentFormProps["initialValues"]; initialized: boolean; keepFormWhenClosed: boolean; + submitStatus: SubmitStatus | null; } const contextKey = "postCommentFormBody"; @@ -52,6 +57,7 @@ export class PostCommentFormContainer extends Component { this.props.local.loggedIn && !this.props.story.isClosed && !this.props.settings.disableCommenting.enabled, + submitStatus: null, }; constructor(props: Props) { @@ -78,11 +84,16 @@ export class PostCommentFormContainer extends Component { form ) => { try { - await this.props.createComment({ - storyID: this.props.story.id, - ...input, - }); - form.reset({}); + const submitStatus = getSubmitStatus( + await this.props.createComment({ + storyID: this.props.story.id, + ...input, + }) + ); + if (submitStatus !== "RETRY") { + form.reset({}); + } + this.setState({ submitStatus }); } catch (error) { if (error instanceof InvalidRequestError) { if (shouldTriggerSettingsRefresh(error.code)) { @@ -93,10 +104,13 @@ export class PostCommentFormContainer extends Component { // tslint:disable-next-line:no-console console.error(error); } - return undefined; + return; }; private handleOnChange: PostCommentFormProps["onChange"] = (state, form) => { + if (this.state.submitStatus && state.dirty) { + this.setState({ submitStatus: null }); + } if (state.values.body) { this.props.sessionStorage.setItem(contextKey, state.values.body); } else { @@ -155,6 +169,7 @@ export class PostCommentFormContainer extends Component { this.props.settings.disableCommenting.message) || this.props.settings.closedMessage } + submitStatus={this.state.submitStatus} /> ); } diff --git a/src/core/client/stream/tabs/comments/containers/PostCommentSubmitStatusContainer.tsx b/src/core/client/stream/tabs/comments/containers/PostCommentSubmitStatusContainer.tsx new file mode 100644 index 000000000..69dd13f73 --- /dev/null +++ b/src/core/client/stream/tabs/comments/containers/PostCommentSubmitStatusContainer.tsx @@ -0,0 +1,48 @@ +import React from "react"; + +import PostCommentInReviewMessage from "../components/PostCommentInReviewMessage"; +import { SubmitStatus } from "../helpers/getSubmitStatus"; + +interface Props { + status: SubmitStatus | null; +} + +interface State { + dismissed: boolean; +} + +export default class PostCommentSubmitStatusContainer extends React.Component< + Props +> { + public state: State = { + dismissed: false, + }; + + public componentWillReceiveProps(nextProps: Props) { + if (this.props.status !== nextProps.status) { + this.setState({ dismissed: false }); + } + } + + private handleOnDismiss = () => { + this.setState({ dismissed: true }); + }; + + public render() { + switch (this.props.status) { + case "RETRY": + throw new Error("Not implemented"); + case "REJECTED": + // TODO: Show a different message when rejected? + case "IN_REVIEW": + return this.state.dismissed ? null : ( + + ); + case "APPROVED": + case null: + return null; + default: + throw new Error(`Unknown status ${this.props.status}`); + } + } +} diff --git a/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.spec.tsx b/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.spec.tsx index 38e89d882..933f058d1 100644 --- a/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.spec.tsx +++ b/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.spec.tsx @@ -104,7 +104,7 @@ it("save values", async () => { it("creates a comment", async () => { const storyID = "story-id"; const input = { body: "Hello World!", local: false }; - const createCommentStub = sinon.stub(); + const createCommentStub = sinon.stub().returns({ edge: { node: {} } }); const form = { reset: noop }; const onCloseStub = sinon.stub(); diff --git a/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.tsx b/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.tsx index 520f80b1a..9fc7a6c31 100644 --- a/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/ReplyCommentFormContainer.tsx @@ -22,7 +22,12 @@ import { import ReplyCommentForm, { ReplyCommentFormProps, } from "../components/ReplyCommentForm"; -import { shouldTriggerSettingsRefresh } from "../helpers"; +import ReplyEditSubmitStatus from "../components/ReplyEditSubmitStatus"; +import { + getSubmitStatus, + shouldTriggerSettingsRefresh, + SubmitStatus, +} from "../helpers"; interface Props { createCommentReply: CreateCommentReplyMutation; @@ -39,10 +44,14 @@ interface Props { interface State { initialValues?: ReplyCommentFormProps["initialValues"]; initialized: boolean; + submitStatus: SubmitStatus | null; } export class ReplyCommentFormContainer extends Component { - public state: State = { initialized: false }; + public state: State = { + initialized: false, + submitStatus: null, + }; private contextKey = `replyCommentFormBody-${this.props.comment.id}`; constructor(props: Props) { @@ -70,30 +79,32 @@ export class ReplyCommentFormContainer extends Component { }); } - private handleOnCancel = () => { + private handleOnCancelOrDismiss = () => { this.props.sessionStorage.removeItem(this.contextKey); if (this.props.onClose) { this.props.onClose(); } }; - private handleOnSubmit: ReplyCommentFormProps["onSubmit"] = async ( - input, - form - ) => { + private handleOnSubmit: ReplyCommentFormProps["onSubmit"] = async input => { try { - await this.props.createCommentReply({ - storyID: this.props.story.id, - parentID: this.props.comment.id, - parentRevisionID: this.props.comment.revision.id, - local: this.props.localReply, - ...input, - }); - - this.props.sessionStorage.removeItem(this.contextKey); - if (this.props.onClose) { - this.props.onClose(); + const submitStatus = getSubmitStatus( + await this.props.createCommentReply({ + storyID: this.props.story.id, + parentID: this.props.comment.id, + parentRevisionID: this.props.comment.revision.id, + local: this.props.localReply, + ...input, + }) + ); + if (submitStatus !== "RETRY") { + this.props.sessionStorage.removeItem(this.contextKey); + if (submitStatus === "APPROVED" && this.props.onClose) { + this.props.onClose(); + return; + } } + this.setState({ submitStatus }); } catch (error) { if (error instanceof InvalidRequestError) { if (shouldTriggerSettingsRefresh(error.code)) { @@ -104,7 +115,7 @@ export class ReplyCommentFormContainer extends Component { // tslint:disable-next-line:no-console console.error(error); } - return undefined; + return; }; private handleOnChange: ReplyCommentFormProps["onChange"] = (state, form) => { @@ -123,13 +134,21 @@ export class ReplyCommentFormContainer extends Component { if (!this.state.initialized) { return null; } + if (this.state.submitStatus && this.state.submitStatus !== "RETRY") { + return ( + + ); + } return ( { }, comment: { id: "comment-id", + status: "NONE", replies: { edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }], }, @@ -48,6 +49,7 @@ it("renders correctly when replies are empty", () => { }, comment: { id: "comment-id", + status: "NONE", replies: { edges: [] }, }, relay: { @@ -77,6 +79,7 @@ describe("when has more replies", () => { }, comment: { id: "comment-id", + status: "NONE", replies: { edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }], }, diff --git a/src/core/client/stream/tabs/comments/containers/ReplyListContainer.tsx b/src/core/client/stream/tabs/comments/containers/ReplyListContainer.tsx index 6268f24ce..ecfa729b2 100644 --- a/src/core/client/stream/tabs/comments/containers/ReplyListContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/ReplyListContainer.tsx @@ -17,6 +17,7 @@ import { ReplyListContainer5_comment as Comment5Data } from "talk-stream/__gener import { StatelessComponent } from "enzyme"; import { FragmentKeys } from "talk-framework/lib/relay/types"; import ReplyList from "../components/ReplyList"; +import { isCommentVisible } from "../helpers"; import LocalReplyListContainer from "./LocalReplyListContainer"; type UnpackArray = T extends ReadonlyArray ? U : any; @@ -56,19 +57,27 @@ export class ReplyListContainer extends React.Component { ) { return null; } - const comments = this.props.comment.replies.edges.map(edge => ({ - ...edge.node, - replyListElement: this.props.ReplyListComponent && ( - - ), - // ReplyListContainer5 contains replyCount. - showConversationLink: ((edge.node as any) as ReplyNode5).replyCount > 0, - })); + // Don't render replies if comment is not visible. + // We have this comment here because it's + // status just changed due to an edit. + // We keep it around for the CommentContainer to show + // some information to the user. + const comments = !isCommentVisible(this.props.comment) + ? [] + : this.props.comment.replies.edges.map(edge => ({ + ...edge.node, + replyListElement: this.props.ReplyListComponent && ( + + ), + // ReplyListContainer5 contains replyCount. + showConversationLink: + ((edge.node as any) as ReplyNode5).replyCount > 0, + })); return ( `; @@ -26,6 +27,7 @@ exports[`renders when commenting has been disabled (non-collapsing) 1`] = ` min={3} onChange={[Function]} onSubmit={[Function]} + submitStatus={null} /> `; @@ -44,6 +46,7 @@ exports[`renders when story has been closed (non-collapsing) 1`] = ` min={3} onChange={[Function]} onSubmit={[Function]} + submitStatus={null} /> `; @@ -60,5 +63,6 @@ exports[`renders with initialValues 1`] = ` min={3} onChange={[Function]} onSubmit={[Function]} + submitStatus={null} /> `; diff --git a/src/core/client/stream/tabs/comments/containers/__snapshots__/ReplyListContainer.spec.tsx.snap b/src/core/client/stream/tabs/comments/containers/__snapshots__/ReplyListContainer.spec.tsx.snap index 409d0a4f3..446d4106e 100644 --- a/src/core/client/stream/tabs/comments/containers/__snapshots__/ReplyListContainer.spec.tsx.snap +++ b/src/core/client/stream/tabs/comments/containers/__snapshots__/ReplyListContainer.spec.tsx.snap @@ -19,6 +19,7 @@ exports[`renders correctly 1`] = ` }, ], }, + "status": "NONE", } } comments={ @@ -117,6 +118,7 @@ exports[`when has more replies renders hasMore 1`] = ` }, ], }, + "status": "NONE", } } comments={ @@ -174,6 +176,7 @@ exports[`when has more replies when showing all disables show all button 1`] = ` }, ], }, + "status": "NONE", } } comments={ @@ -231,6 +234,7 @@ exports[`when has more replies when showing all enable show all button after loa }, ], }, + "status": "NONE", } } comments={ diff --git a/src/core/client/stream/tabs/comments/helpers/getSubmitStatus.ts b/src/core/client/stream/tabs/comments/helpers/getSubmitStatus.ts new file mode 100644 index 000000000..ce2b98672 --- /dev/null +++ b/src/core/client/stream/tabs/comments/helpers/getSubmitStatus.ts @@ -0,0 +1,23 @@ +import { MutationResponse } from "talk-framework/lib/relay"; +import { CreateCommentMutation } from "talk-stream/__generated__/CreateCommentMutation.graphql"; +import { CreateCommentReplyMutation } from "talk-stream/__generated__/CreateCommentReplyMutation.graphql"; +import { EditCommentMutation } from "talk-stream/__generated__/EditCommentMutation.graphql"; +import { isInReview, isRejected } from "talk-stream/helpers"; + +export type SubmitStatus = "APPROVED" | "RETRY" | "IN_REVIEW" | "REJECTED"; + +export default function getSubmitStatus( + response: + | MutationResponse + | MutationResponse + | MutationResponse +): SubmitStatus { + const node = "edge" in response ? response.edge.node : response.comment; + if (isInReview(node.status)) { + return "IN_REVIEW"; + } + if (isRejected(node.status)) { + return "REJECTED"; + } + return "APPROVED"; +} diff --git a/src/core/client/stream/tabs/comments/helpers/index.ts b/src/core/client/stream/tabs/comments/helpers/index.ts index 4d6c97756..61e183328 100644 --- a/src/core/client/stream/tabs/comments/helpers/index.ts +++ b/src/core/client/stream/tabs/comments/helpers/index.ts @@ -7,3 +7,5 @@ export { default as shouldTriggerSettingsRefresh, } from "./shouldTriggerSettingsRefresh"; export { default as getHTMLText } from "./getHTMLText"; +export { default as getSubmitStatus, SubmitStatus } from "./getSubmitStatus"; +export { default as isCommentVisible } from "./isCommentVisible"; diff --git a/src/core/client/stream/tabs/comments/helpers/isCommentVisible.ts b/src/core/client/stream/tabs/comments/helpers/isCommentVisible.ts new file mode 100644 index 000000000..8715cb5ed --- /dev/null +++ b/src/core/client/stream/tabs/comments/helpers/isCommentVisible.ts @@ -0,0 +1,9 @@ +import { COMMENT_STATUS } from "talk-stream/__generated__/CreateCommentMutation.graphql"; + +const VisibleStatus = ["ACCEPTED", "NONE"]; + +export default function isCommentVisible(comment: { + status: COMMENT_STATUS; +}): boolean { + return VisibleStatus.includes(comment.status); +} diff --git a/src/core/client/stream/test/comments/__snapshots__/editComment.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/editComment.spec.tsx.snap index 53ca82644..98ee01990 100644 --- a/src/core/client/stream/test/comments/__snapshots__/editComment.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/editComment.spec.tsx.snap @@ -2,7 +2,7 @@ exports[`cancel edit 1`] = `
@@ -75,7 +75,7 @@ exports[`cancel edit 1`] = `