mirror of
https://github.com/wassname/talk.git
synced 2026-08-13 12:40:11 +08:00
[CORL-421, CORL-415] Live Comments on Stream (#2379)
* feat: support comment replies * feat: comment created * feat: live update top level comments * feat: live updates on the stream embed * fix: tests * chore: refactor FadeInTransition * fix: add missing translation and a live update bug * fix: graqphql * feat: improve loading experiene :-) * fix: live comment bugs * chore: adapt translation * feat: stop live updates when story is closed or commenting is disabled * test: add tests for stream live updates * fix: remove forgotten piece of code * fix: tests
This commit is contained in:
@@ -12,6 +12,7 @@ import NotAvailable from "coral-admin/components/NotAvailable";
|
||||
import { getModerationLink } from "coral-admin/helpers";
|
||||
import { ApproveCommentMutation } from "coral-admin/mutations";
|
||||
import { RejectCommentMutation } from "coral-admin/mutations";
|
||||
import FadeInTransition from "coral-framework/components/FadeInTransition";
|
||||
import {
|
||||
MutationProp,
|
||||
withFragmentContainer,
|
||||
@@ -19,7 +20,6 @@ import {
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLTAG } from "coral-framework/schema";
|
||||
|
||||
import FadeInTransition from "./FadeInTransition";
|
||||
import FeatureCommentMutation from "./FeatureCommentMutation";
|
||||
import ModerateCard from "./ModerateCard";
|
||||
import ModeratedByContainer from "./ModeratedByContainer";
|
||||
|
||||
@@ -4,4 +4,5 @@ export { default as FacebookButton } from "./FacebookButton";
|
||||
export { default as GoogleButton } from "./GoogleButton";
|
||||
export { default as OIDCButton } from "./OIDCButton";
|
||||
export { default as Markdown } from "./Markdown";
|
||||
export { default as FadeInTransition } from "./FadeInTransition";
|
||||
export { default as DurationField, DURATION_UNIT } from "./DurationField";
|
||||
|
||||
@@ -32,8 +32,10 @@ export function denormalizeComment(
|
||||
return createFixture<GQLComment>({
|
||||
...comment,
|
||||
replies: { edges: replyEdges, pageInfo: repliesPageInfo },
|
||||
replyCount: replyEdges.length,
|
||||
replyCount:
|
||||
comment.replyCount !== undefined ? comment.replyCount : replyEdges.length,
|
||||
parentCount: parents.length,
|
||||
parent: parents.length > 0 ? parents[parents.length - 1].node : undefined,
|
||||
parents: {
|
||||
edges: parents,
|
||||
pageInfo: { startCursor: null, hasPreviousPage: false },
|
||||
@@ -55,9 +57,15 @@ export function denormalizeStory(story: Fixture<GQLStory>) {
|
||||
}))) ||
|
||||
[];
|
||||
const commentsPageInfo = (story.comments && story.comments.pageInfo) || {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
};
|
||||
if (commentsPageInfo.endCursor === undefined) {
|
||||
commentsPageInfo.endCursor =
|
||||
commentNodes.length > 0
|
||||
? commentNodes[commentNodes.length - 1].node.createdAt
|
||||
: null;
|
||||
}
|
||||
|
||||
const featuredCommentsCount = commentNodes.filter(
|
||||
n => n.tags && n.tags.some((t: GQLTag) => t.code === GQLTAG.FEATURED)
|
||||
).length;
|
||||
|
||||
@@ -40,6 +40,14 @@ extend type Comment {
|
||||
|
||||
# Remember last viewer action that could have caused a status change.
|
||||
lastViewerAction: COMMENT_VIEWER_ACTION
|
||||
|
||||
# If true then Comment came in live.
|
||||
enteredLive: Boolean
|
||||
}
|
||||
|
||||
extend type CommentsConnection {
|
||||
# Contains comment that came in live and is still behind the `View New` button.
|
||||
viewNewEdges: [CommentEdge!]
|
||||
}
|
||||
|
||||
type Local {
|
||||
|
||||
+1
@@ -114,6 +114,7 @@ graphql`
|
||||
graphql`
|
||||
fragment CreateCommentReplyMutation_viewer on User {
|
||||
role
|
||||
createdAt
|
||||
}
|
||||
`;
|
||||
/** end */
|
||||
|
||||
@@ -92,6 +92,7 @@ const PermalinkView: FunctionComponent<PermalinkViewProps> = ({
|
||||
comment={comment}
|
||||
story={story}
|
||||
settings={settings}
|
||||
liveDirectRepliesInsertion
|
||||
/>
|
||||
</div>
|
||||
</HorizontalGutter>
|
||||
|
||||
+1
@@ -120,6 +120,7 @@ exports[`renders correctly 1`] = `
|
||||
>
|
||||
<withProps(Relay(ReplyListContainer))
|
||||
comment={Object {}}
|
||||
liveDirectRepliesInsertion={true}
|
||||
settings={Object {}}
|
||||
story={Object {}}
|
||||
viewer={Object {}}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { graphql, requestSubscription } from "react-relay";
|
||||
import { ConnectionHandler, Environment, RecordProxy } from "relay-runtime";
|
||||
|
||||
import {
|
||||
createSubscription,
|
||||
SubscriptionVariables,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { CommentReplyCreatedSubscription } from "coral-stream/__generated__/CommentReplyCreatedSubscription.graphql";
|
||||
|
||||
/**
|
||||
* Returns depth until ancestor.
|
||||
*/
|
||||
function determineDepthTillAncestor(comment: RecordProxy, ancestorID: string) {
|
||||
let depth = 0;
|
||||
let cur: RecordProxy | null = comment;
|
||||
while (cur) {
|
||||
cur = cur.getLinkedRecord("parent");
|
||||
if (cur) {
|
||||
depth++;
|
||||
// Stop when reaching base ancestor.
|
||||
if (cur!.getValue("id") === ancestorID) {
|
||||
return depth;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const CommentReplyCreatedSubscription = createSubscription(
|
||||
"subscribeToCommentCreated",
|
||||
(
|
||||
environment: Environment,
|
||||
variables: SubscriptionVariables<CommentReplyCreatedSubscription> & {
|
||||
liveDirectRepliesInsertion?: boolean;
|
||||
}
|
||||
) =>
|
||||
requestSubscription(environment, {
|
||||
subscription: graphql`
|
||||
subscription CommentReplyCreatedSubscription($ancestorID: ID!) {
|
||||
commentReplyCreated(ancestorID: $ancestorID) {
|
||||
comment {
|
||||
id
|
||||
createdAt
|
||||
parent {
|
||||
id
|
||||
}
|
||||
...AllCommentsTabContainer_comment
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables,
|
||||
updater: store => {
|
||||
const rootField = store.getRootField("commentReplyCreated");
|
||||
if (!rootField) {
|
||||
return;
|
||||
}
|
||||
const comment = rootField.getLinkedRecord("comment")!;
|
||||
comment.setValue(true, "enteredLive");
|
||||
|
||||
const parentProxy = store.get(
|
||||
comment.getLinkedRecord("parent")!.getValue("id")!
|
||||
)!;
|
||||
|
||||
const depth = determineDepthTillAncestor(comment, variables.ancestorID);
|
||||
if (depth === null) {
|
||||
// could not trace back to ancestor, discard.
|
||||
return;
|
||||
}
|
||||
// Comment is just outside our visible depth.
|
||||
if (depth === 6) {
|
||||
// Inform last comment in visible tree about the available replies.
|
||||
// This will trigger to show the `Read More of this Conversation` link.
|
||||
const replyCount = parentProxy.getValue("replyCount") || 0;
|
||||
parentProxy.setValue(replyCount + 1, "replyCount");
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionKey = "ReplyList_replies";
|
||||
const filters = { orderBy: "CREATED_AT_ASC" };
|
||||
const connection = ConnectionHandler.getConnection(
|
||||
parentProxy,
|
||||
connectionKey,
|
||||
filters
|
||||
);
|
||||
if (!connection) {
|
||||
// If it has no connection, it could not have been
|
||||
// in our visible tree.
|
||||
return;
|
||||
}
|
||||
if (connection.getLinkedRecord("pageInfo").getValue("hasNextPage")) {
|
||||
// It hasn't loaded all comments yet, ignore this one.
|
||||
return;
|
||||
}
|
||||
const commentsEdge = store.create(
|
||||
`edge-${comment.getValue("id")!}`,
|
||||
"CommentsEdge"
|
||||
);
|
||||
commentsEdge.setValue(comment.getValue("createdAt"), "cursor");
|
||||
commentsEdge.setLinkedRecord(comment, "node");
|
||||
if (
|
||||
parentProxy.getValue("id") === variables.ancestorID &&
|
||||
variables.liveDirectRepliesInsertion
|
||||
) {
|
||||
ConnectionHandler.insertEdgeAfter(connection, commentsEdge);
|
||||
} else {
|
||||
const linked = connection.getLinkedRecords("viewNewEdges") || [];
|
||||
connection.setLinkedRecords(
|
||||
linked.concat(commentsEdge),
|
||||
"viewNewEdges"
|
||||
);
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default CommentReplyCreatedSubscription;
|
||||
@@ -2,6 +2,7 @@ import { Localized } from "fluent-react/compat";
|
||||
import * as React from "react";
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
import FadeInTransition from "coral-framework/components/FadeInTransition";
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
import { Button, HorizontalGutter } from "coral-ui/components";
|
||||
|
||||
@@ -21,6 +22,7 @@ export interface ReplyListProps {
|
||||
id: string;
|
||||
replyListElement?: React.ReactElement<any>;
|
||||
showConversationLink?: boolean;
|
||||
enteredLive?: boolean | null;
|
||||
} & PropTypesOf<typeof CommentContainer>["comment"] &
|
||||
PropTypesOf<typeof IgnoredTombstoneOrHideContainer>["comment"]
|
||||
>;
|
||||
@@ -31,6 +33,8 @@ export interface ReplyListProps {
|
||||
indentLevel?: number;
|
||||
localReply?: boolean;
|
||||
disableReplies?: boolean;
|
||||
viewNewCount?: number;
|
||||
onViewNew?: () => void;
|
||||
}
|
||||
|
||||
const ReplyList: FunctionComponent<ReplyListProps> = props => {
|
||||
@@ -41,26 +45,30 @@ const ReplyList: FunctionComponent<ReplyListProps> = props => {
|
||||
role="log"
|
||||
>
|
||||
{props.comments.map(comment => (
|
||||
<IgnoredTombstoneOrHideContainer
|
||||
<FadeInTransition
|
||||
key={comment.id}
|
||||
viewer={props.viewer}
|
||||
comment={comment}
|
||||
active={Boolean(comment.enteredLive)}
|
||||
>
|
||||
<HorizontalGutter key={comment.id}>
|
||||
<CommentContainer
|
||||
key={comment.id}
|
||||
viewer={props.viewer}
|
||||
comment={comment}
|
||||
story={props.story}
|
||||
settings={props.settings}
|
||||
indentLevel={props.indentLevel}
|
||||
localReply={props.localReply}
|
||||
disableReplies={props.disableReplies}
|
||||
showConversationLink={!!comment.showConversationLink}
|
||||
/>
|
||||
{comment.replyListElement}
|
||||
</HorizontalGutter>
|
||||
</IgnoredTombstoneOrHideContainer>
|
||||
<IgnoredTombstoneOrHideContainer
|
||||
viewer={props.viewer}
|
||||
comment={comment}
|
||||
>
|
||||
<HorizontalGutter key={comment.id}>
|
||||
<CommentContainer
|
||||
key={comment.id}
|
||||
viewer={props.viewer}
|
||||
comment={comment}
|
||||
story={props.story}
|
||||
settings={props.settings}
|
||||
indentLevel={props.indentLevel}
|
||||
localReply={props.localReply}
|
||||
disableReplies={props.disableReplies}
|
||||
showConversationLink={!!comment.showConversationLink}
|
||||
/>
|
||||
{comment.replyListElement}
|
||||
</HorizontalGutter>
|
||||
</IgnoredTombstoneOrHideContainer>
|
||||
</FadeInTransition>
|
||||
))}
|
||||
{props.hasMore && (
|
||||
<Indent level={props.indentLevel} noBorder>
|
||||
@@ -80,6 +88,22 @@ const ReplyList: FunctionComponent<ReplyListProps> = props => {
|
||||
</Localized>
|
||||
</Indent>
|
||||
)}
|
||||
{Boolean(props.viewNewCount && props.viewNewCount > 0) && (
|
||||
<Indent level={props.indentLevel} noBorder>
|
||||
<Localized id="comments-replyList-showMoreReplies">
|
||||
<Button
|
||||
aria-controls={`coral-comments-replyList-log--${
|
||||
props.comment.id
|
||||
}`}
|
||||
onClick={props.onViewNew}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
>
|
||||
Show More Replies
|
||||
</Button>
|
||||
</Localized>
|
||||
</Indent>
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,20 +14,23 @@ const ReplyListContainerN = removeFragmentRefs(ReplyListContainer);
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof ReplyListContainerN> = {
|
||||
story: {
|
||||
id: "story-id",
|
||||
isClosed: false,
|
||||
},
|
||||
comment: {
|
||||
id: "comment-id",
|
||||
status: "NONE",
|
||||
replies: {
|
||||
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
|
||||
edges: [
|
||||
{ node: { id: "comment-1", enteredLive: false } },
|
||||
{ node: { id: "comment-2", enteredLive: false } },
|
||||
],
|
||||
viewNewEdges: [],
|
||||
},
|
||||
lastViewerAction: null,
|
||||
},
|
||||
settings: {
|
||||
reaction: {
|
||||
icon: "thumb_up_alt",
|
||||
label: "Respect",
|
||||
disableCommenting: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
relay: {
|
||||
@@ -46,12 +49,12 @@ it("renders correctly", () => {
|
||||
it("renders correctly when replies are empty", () => {
|
||||
const props: PropTypesOf<typeof ReplyListContainerN> = {
|
||||
story: {
|
||||
id: "story-id",
|
||||
isClosed: false,
|
||||
},
|
||||
comment: {
|
||||
id: "comment-id",
|
||||
status: "NONE",
|
||||
replies: { edges: [] },
|
||||
replies: { edges: [], viewNewEdges: [] },
|
||||
lastViewerAction: null,
|
||||
},
|
||||
relay: {
|
||||
@@ -60,9 +63,8 @@ it("renders correctly when replies are empty", () => {
|
||||
} as any,
|
||||
viewer: null,
|
||||
settings: {
|
||||
reaction: {
|
||||
icon: "thumb_up_alt",
|
||||
label: "Respect",
|
||||
disableCommenting: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
indentLevel: 1,
|
||||
@@ -77,20 +79,23 @@ describe("when has more replies", () => {
|
||||
let finishLoading: ((error?: Error) => void) | null = null;
|
||||
const props: PropTypesOf<typeof ReplyListContainerN> = {
|
||||
story: {
|
||||
id: "story-id",
|
||||
isClosed: false,
|
||||
},
|
||||
comment: {
|
||||
id: "comment-id",
|
||||
status: "NONE",
|
||||
replies: {
|
||||
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
|
||||
edges: [
|
||||
{ node: { id: "comment-1", enteredLive: false } },
|
||||
{ node: { id: "comment-2", enteredLive: false } },
|
||||
],
|
||||
viewNewEdges: [],
|
||||
},
|
||||
lastViewerAction: null,
|
||||
},
|
||||
settings: {
|
||||
reaction: {
|
||||
icon: "thumb_up_alt",
|
||||
label: "Respect",
|
||||
disableCommenting: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
relay: {
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React, { FunctionComponent, useCallback, useEffect } from "react";
|
||||
import { graphql, GraphQLTaggedNode, RelayPaginationProp } from "react-relay";
|
||||
import { withProps } from "recompose";
|
||||
|
||||
import { withPaginationContainer } from "coral-framework/lib/relay";
|
||||
import {
|
||||
useLoadMore,
|
||||
useMutation,
|
||||
useSubscription,
|
||||
withPaginationContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { FragmentKeys } from "coral-framework/lib/relay/types";
|
||||
import { Omit, PropTypesOf } from "coral-framework/types";
|
||||
import { ReplyListContainer1_comment as CommentData } from "coral-stream/__generated__/ReplyListContainer1_comment.graphql";
|
||||
@@ -13,8 +18,10 @@ import { ReplyListContainer1PaginationQueryVariables } from "coral-stream/__gene
|
||||
import { ReplyListContainer5_comment as Comment5Data } from "coral-stream/__generated__/ReplyListContainer5_comment.graphql";
|
||||
|
||||
import { isCommentVisible } from "../helpers";
|
||||
import CommentReplyCreatedSubscription from "./CommentReplyCreatedSubscription";
|
||||
import LocalReplyListContainer from "./LocalReplyListContainer";
|
||||
import ReplyList from "./ReplyList";
|
||||
import ReplyListViewNewMutation from "./ReplyListViewNewMutation";
|
||||
|
||||
type UnpackArray<T> = T extends ReadonlyArray<infer U> ? U : any;
|
||||
type ReplyNode5 = UnpackArray<Comment5Data["replies"]["edges"]>["node"];
|
||||
@@ -33,6 +40,13 @@ type Props = BaseProps & {
|
||||
ReplyListComponent:
|
||||
| React.ComponentType<{ [P in FragmentKeys<BaseProps>]: any }>
|
||||
| undefined;
|
||||
/**
|
||||
* liveDirectRepliesInsertion if set to true,
|
||||
* live replies to the first level of comments
|
||||
* will be inserted directly into the comment stream
|
||||
* instead of hiding behind a button.
|
||||
*/
|
||||
liveDirectRepliesInsertion?: boolean;
|
||||
};
|
||||
|
||||
// TODO: (cvle) If this could be autogenerated.
|
||||
@@ -41,71 +55,87 @@ type FragmentVariables = Omit<
|
||||
"commentID"
|
||||
>;
|
||||
|
||||
export class ReplyListContainer extends React.Component<Props> {
|
||||
public state = {
|
||||
disableShowAll: false,
|
||||
};
|
||||
|
||||
public render() {
|
||||
if (
|
||||
this.props.comment.replies == null ||
|
||||
this.props.comment.replies.edges.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const comments =
|
||||
// Comment is not visible after a viewer action, so don't render it anymore.
|
||||
this.props.comment.lastViewerAction &&
|
||||
!isCommentVisible(this.props.comment)
|
||||
? []
|
||||
: this.props.comment.replies.edges.map(edge => ({
|
||||
...edge.node,
|
||||
replyListElement: this.props.ReplyListComponent && (
|
||||
<this.props.ReplyListComponent
|
||||
viewer={this.props.viewer}
|
||||
comment={edge.node}
|
||||
story={this.props.story}
|
||||
settings={this.props.settings}
|
||||
/>
|
||||
),
|
||||
// ReplyListContainer5 contains replyCount.
|
||||
showConversationLink:
|
||||
((edge.node as any) as ReplyNode5).replyCount > 0,
|
||||
}));
|
||||
return (
|
||||
<ReplyList
|
||||
viewer={this.props.viewer}
|
||||
comment={this.props.comment}
|
||||
comments={comments}
|
||||
story={this.props.story}
|
||||
settings={this.props.settings}
|
||||
onShowAll={this.showAll}
|
||||
hasMore={this.props.relay.hasMore()}
|
||||
disableShowAll={this.state.disableShowAll}
|
||||
indentLevel={this.props.indentLevel}
|
||||
localReply={this.props.localReply}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private showAll = () => {
|
||||
if (!this.props.relay.hasMore() || this.props.relay.isLoading()) {
|
||||
export const ReplyListContainer: React.FunctionComponent<Props> = props => {
|
||||
const [showAll, isLoadingShowAll] = useLoadMore(props.relay, 999999999);
|
||||
const subcribeToCommentReplyCreated = useSubscription(
|
||||
CommentReplyCreatedSubscription
|
||||
);
|
||||
useEffect(() => {
|
||||
// TODO: (cvle) check for story or settings state
|
||||
// for whether or not we should turn on subscriptions:
|
||||
// e.g. `if (!props.story.settings.live) { return; }`
|
||||
if (props.story.isClosed || props.settings.disableCommenting.enabled) {
|
||||
return;
|
||||
}
|
||||
if (props.indentLevel !== 1) {
|
||||
return;
|
||||
}
|
||||
const disposable = subcribeToCommentReplyCreated({
|
||||
ancestorID: props.comment.id,
|
||||
liveDirectRepliesInsertion: props.liveDirectRepliesInsertion,
|
||||
});
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, [
|
||||
subcribeToCommentReplyCreated,
|
||||
props.comment.id,
|
||||
props.indentLevel,
|
||||
props.relay.hasMore(),
|
||||
props.liveDirectRepliesInsertion,
|
||||
]);
|
||||
|
||||
this.setState({ disableShowAll: true });
|
||||
this.props.relay.loadMore(
|
||||
999999999, // Fetch All Replies
|
||||
error => {
|
||||
this.setState({ disableShowAll: false });
|
||||
if (error) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
const viewNew = useMutation(ReplyListViewNewMutation);
|
||||
const onViewNew = useCallback(() => {
|
||||
viewNew({ commentID: props.comment.id });
|
||||
}, [props.comment.id, viewNew]);
|
||||
|
||||
const viewNewCount =
|
||||
(props.comment.replies.viewNewEdges &&
|
||||
props.comment.replies.viewNewEdges.length) ||
|
||||
0;
|
||||
|
||||
if (
|
||||
props.comment.replies == null ||
|
||||
(props.comment.replies.edges.length === 0 && viewNewCount === 0)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const comments =
|
||||
// Comment is not visible after a viewer action, so don't render it anymore.
|
||||
props.comment.lastViewerAction && !isCommentVisible(props.comment)
|
||||
? []
|
||||
: props.comment.replies.edges.map(edge => ({
|
||||
...edge.node,
|
||||
replyListElement: props.ReplyListComponent && (
|
||||
<props.ReplyListComponent
|
||||
viewer={props.viewer}
|
||||
comment={edge.node}
|
||||
story={props.story}
|
||||
settings={props.settings}
|
||||
/>
|
||||
),
|
||||
// ReplyListContainer5 contains replyCount.
|
||||
showConversationLink:
|
||||
((edge.node as any) as ReplyNode5).replyCount > 0,
|
||||
}));
|
||||
return (
|
||||
<ReplyList
|
||||
viewer={props.viewer}
|
||||
comment={props.comment}
|
||||
comments={comments}
|
||||
story={props.story}
|
||||
settings={props.settings}
|
||||
onShowAll={showAll}
|
||||
hasMore={props.relay.hasMore()}
|
||||
disableShowAll={isLoadingShowAll}
|
||||
indentLevel={props.indentLevel}
|
||||
localReply={props.localReply}
|
||||
viewNewCount={viewNewCount}
|
||||
onViewNew={onViewNew}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function createReplyListContainer(
|
||||
indentLevel: number,
|
||||
@@ -168,12 +198,16 @@ const ReplyListContainer5 = createReplyListContainer(
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment ReplyListContainer5_settings on Settings {
|
||||
disableCommenting {
|
||||
enabled
|
||||
}
|
||||
...LocalReplyListContainer_settings
|
||||
...CommentContainer_settings
|
||||
}
|
||||
`,
|
||||
story: graphql`
|
||||
fragment ReplyListContainer5_story on Story {
|
||||
isClosed
|
||||
...CommentContainer_story
|
||||
...LocalReplyListContainer_story
|
||||
}
|
||||
@@ -190,10 +224,14 @@ const ReplyListContainer5 = createReplyListContainer(
|
||||
lastViewerAction
|
||||
replies(first: $count, after: $cursor, orderBy: $orderBy)
|
||||
@connection(key: "ReplyList_replies") {
|
||||
viewNewEdges {
|
||||
cursor
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
replyCount
|
||||
enteredLive
|
||||
...CommentContainer_comment
|
||||
...IgnoredTombstoneOrHideContainer_comment
|
||||
...LocalReplyListContainer_comment
|
||||
@@ -234,12 +272,16 @@ const ReplyListContainer4 = createReplyListContainer(
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment ReplyListContainer4_settings on Settings {
|
||||
disableCommenting {
|
||||
enabled
|
||||
}
|
||||
...ReplyListContainer5_settings
|
||||
...CommentContainer_settings
|
||||
}
|
||||
`,
|
||||
story: graphql`
|
||||
fragment ReplyListContainer4_story on Story {
|
||||
isClosed
|
||||
...ReplyListContainer5_story
|
||||
...CommentContainer_story
|
||||
}
|
||||
@@ -256,9 +298,13 @@ const ReplyListContainer4 = createReplyListContainer(
|
||||
lastViewerAction
|
||||
replies(first: $count, after: $cursor, orderBy: $orderBy)
|
||||
@connection(key: "ReplyList_replies") {
|
||||
viewNewEdges {
|
||||
cursor
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
enteredLive
|
||||
...CommentContainer_comment
|
||||
...IgnoredTombstoneOrHideContainer_comment
|
||||
...ReplyListContainer5_comment
|
||||
@@ -298,12 +344,16 @@ const ReplyListContainer3 = createReplyListContainer(
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment ReplyListContainer3_settings on Settings {
|
||||
disableCommenting {
|
||||
enabled
|
||||
}
|
||||
...ReplyListContainer4_settings
|
||||
...CommentContainer_settings
|
||||
}
|
||||
`,
|
||||
story: graphql`
|
||||
fragment ReplyListContainer3_story on Story {
|
||||
isClosed
|
||||
...ReplyListContainer4_story
|
||||
...CommentContainer_story
|
||||
}
|
||||
@@ -320,9 +370,13 @@ const ReplyListContainer3 = createReplyListContainer(
|
||||
lastViewerAction
|
||||
replies(first: $count, after: $cursor, orderBy: $orderBy)
|
||||
@connection(key: "ReplyList_replies") {
|
||||
viewNewEdges {
|
||||
cursor
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
enteredLive
|
||||
...CommentContainer_comment
|
||||
...IgnoredTombstoneOrHideContainer_comment
|
||||
...ReplyListContainer4_comment
|
||||
@@ -362,12 +416,16 @@ const ReplyListContainer2 = createReplyListContainer(
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment ReplyListContainer2_settings on Settings {
|
||||
disableCommenting {
|
||||
enabled
|
||||
}
|
||||
...ReplyListContainer3_settings
|
||||
...CommentContainer_settings
|
||||
}
|
||||
`,
|
||||
story: graphql`
|
||||
fragment ReplyListContainer2_story on Story {
|
||||
isClosed
|
||||
...ReplyListContainer3_story
|
||||
...CommentContainer_story
|
||||
}
|
||||
@@ -384,9 +442,13 @@ const ReplyListContainer2 = createReplyListContainer(
|
||||
lastViewerAction
|
||||
replies(first: $count, after: $cursor, orderBy: $orderBy)
|
||||
@connection(key: "ReplyList_replies") {
|
||||
viewNewEdges {
|
||||
cursor
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
enteredLive
|
||||
...CommentContainer_comment
|
||||
...IgnoredTombstoneOrHideContainer_comment
|
||||
...ReplyListContainer3_comment
|
||||
@@ -426,12 +488,16 @@ const ReplyListContainer1 = createReplyListContainer(
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment ReplyListContainer1_settings on Settings {
|
||||
disableCommenting {
|
||||
enabled
|
||||
}
|
||||
...ReplyListContainer2_settings
|
||||
...CommentContainer_settings
|
||||
}
|
||||
`,
|
||||
story: graphql`
|
||||
fragment ReplyListContainer1_story on Story {
|
||||
isClosed
|
||||
...ReplyListContainer2_story
|
||||
...CommentContainer_story
|
||||
}
|
||||
@@ -448,9 +514,13 @@ const ReplyListContainer1 = createReplyListContainer(
|
||||
lastViewerAction
|
||||
replies(first: $count, after: $cursor, orderBy: $orderBy)
|
||||
@connection(key: "ReplyList_replies") {
|
||||
viewNewEdges {
|
||||
cursor
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
enteredLive
|
||||
...CommentContainer_comment
|
||||
...IgnoredTombstoneOrHideContainer_comment
|
||||
...ReplyListContainer2_comment
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ConnectionHandler, Environment, RecordProxy } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitLocalUpdatePromisified,
|
||||
createMutation,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
interface ReplyListViewNewInput {
|
||||
commentID: string;
|
||||
}
|
||||
|
||||
const QueueViewNewMutation = createMutation(
|
||||
"viewNew",
|
||||
async (environment: Environment, input: ReplyListViewNewInput) => {
|
||||
await commitLocalUpdatePromisified(environment, async store => {
|
||||
const parentProxy = store.get(input.commentID);
|
||||
if (!parentProxy) {
|
||||
return;
|
||||
}
|
||||
const connectionKey = "ReplyList_replies";
|
||||
const filters = { orderBy: "CREATED_AT_ASC" };
|
||||
const connection = ConnectionHandler.getConnection(
|
||||
parentProxy,
|
||||
connectionKey,
|
||||
filters
|
||||
);
|
||||
if (!connection) {
|
||||
return;
|
||||
}
|
||||
const viewNewEdges = connection.getLinkedRecords(
|
||||
"viewNewEdges"
|
||||
) as RecordProxy[];
|
||||
if (!viewNewEdges || viewNewEdges.length === 0) {
|
||||
return;
|
||||
}
|
||||
viewNewEdges.forEach(edge => {
|
||||
ConnectionHandler.insertEdgeAfter(connection, edge);
|
||||
});
|
||||
connection.setLinkedRecords([], "viewNewEdges");
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default QueueViewNewMutation;
|
||||
+228
-204
@@ -6,88 +6,96 @@ exports[`renders correctly 1`] = `
|
||||
id="coral-comments-replyList-log--comment-id"
|
||||
role="log"
|
||||
>
|
||||
<Relay(IgnoredTombstoneOrHideContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
<FadeInTransition
|
||||
active={false}
|
||||
key="comment-1"
|
||||
viewer={null}
|
||||
>
|
||||
<ForwardRef(forwardRef)
|
||||
key="comment-1"
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer)))))
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
<Relay(IgnoredTombstoneOrHideContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
disableReplies={false}
|
||||
indentLevel={1}
|
||||
key="comment-1"
|
||||
localReply={false}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
showConversationLink={false}
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
}
|
||||
}
|
||||
viewer={null}
|
||||
/>
|
||||
</ForwardRef(forwardRef)>
|
||||
</Relay(IgnoredTombstoneOrHideContainer)>
|
||||
<Relay(IgnoredTombstoneOrHideContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
"showConversationLink": true,
|
||||
}
|
||||
}
|
||||
key="comment-2"
|
||||
viewer={null}
|
||||
>
|
||||
<ForwardRef(forwardRef)
|
||||
key="comment-2"
|
||||
viewer={null}
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer)))))
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
"showConversationLink": true,
|
||||
<ForwardRef(forwardRef)
|
||||
key="comment-1"
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer)))))
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
disableReplies={false}
|
||||
indentLevel={1}
|
||||
key="comment-1"
|
||||
localReply={false}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
showConversationLink={false}
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
}
|
||||
}
|
||||
viewer={null}
|
||||
/>
|
||||
</ForwardRef(forwardRef)>
|
||||
</Relay(IgnoredTombstoneOrHideContainer)>
|
||||
</FadeInTransition>
|
||||
<FadeInTransition
|
||||
active={false}
|
||||
key="comment-2"
|
||||
>
|
||||
<Relay(IgnoredTombstoneOrHideContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
"showConversationLink": true,
|
||||
}
|
||||
disableReplies={false}
|
||||
indentLevel={1}
|
||||
}
|
||||
viewer={null}
|
||||
>
|
||||
<ForwardRef(forwardRef)
|
||||
key="comment-2"
|
||||
localReply={false}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer)))))
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
"showConversationLink": true,
|
||||
}
|
||||
}
|
||||
}
|
||||
showConversationLink={true}
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
disableReplies={false}
|
||||
indentLevel={1}
|
||||
key="comment-2"
|
||||
localReply={false}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
viewer={null}
|
||||
/>
|
||||
</ForwardRef(forwardRef)>
|
||||
</Relay(IgnoredTombstoneOrHideContainer)>
|
||||
showConversationLink={true}
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
}
|
||||
}
|
||||
viewer={null}
|
||||
/>
|
||||
</ForwardRef(forwardRef)>
|
||||
</Relay(IgnoredTombstoneOrHideContainer)>
|
||||
</FadeInTransition>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
@@ -97,82 +105,90 @@ exports[`when there is more disables load more button 1`] = `
|
||||
id="coral-comments-replyList-log--comment-id"
|
||||
role="log"
|
||||
>
|
||||
<Relay(IgnoredTombstoneOrHideContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
<FadeInTransition
|
||||
active={false}
|
||||
key="comment-1"
|
||||
viewer={null}
|
||||
>
|
||||
<ForwardRef(forwardRef)
|
||||
key="comment-1"
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer)))))
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
<Relay(IgnoredTombstoneOrHideContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
indentLevel={1}
|
||||
key="comment-1"
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
showConversationLink={false}
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
}
|
||||
}
|
||||
viewer={null}
|
||||
/>
|
||||
</ForwardRef(forwardRef)>
|
||||
</Relay(IgnoredTombstoneOrHideContainer)>
|
||||
<Relay(IgnoredTombstoneOrHideContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
key="comment-2"
|
||||
viewer={null}
|
||||
>
|
||||
<ForwardRef(forwardRef)
|
||||
key="comment-2"
|
||||
viewer={null}
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer)))))
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
<ForwardRef(forwardRef)
|
||||
key="comment-1"
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer)))))
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
indentLevel={1}
|
||||
key="comment-1"
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
showConversationLink={false}
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
}
|
||||
}
|
||||
viewer={null}
|
||||
/>
|
||||
</ForwardRef(forwardRef)>
|
||||
</Relay(IgnoredTombstoneOrHideContainer)>
|
||||
</FadeInTransition>
|
||||
<FadeInTransition
|
||||
active={false}
|
||||
key="comment-2"
|
||||
>
|
||||
<Relay(IgnoredTombstoneOrHideContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
indentLevel={1}
|
||||
}
|
||||
viewer={null}
|
||||
>
|
||||
<ForwardRef(forwardRef)
|
||||
key="comment-2"
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer)))))
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
}
|
||||
showConversationLink={false}
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
indentLevel={1}
|
||||
key="comment-2"
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
viewer={null}
|
||||
/>
|
||||
</ForwardRef(forwardRef)>
|
||||
</Relay(IgnoredTombstoneOrHideContainer)>
|
||||
showConversationLink={false}
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
}
|
||||
}
|
||||
viewer={null}
|
||||
/>
|
||||
</ForwardRef(forwardRef)>
|
||||
</Relay(IgnoredTombstoneOrHideContainer)>
|
||||
</FadeInTransition>
|
||||
<Indent
|
||||
level={1}
|
||||
noBorder={true}
|
||||
@@ -201,82 +217,90 @@ exports[`when there is more renders a load more button 1`] = `
|
||||
id="coral-comments-replyList-log--comment-id"
|
||||
role="log"
|
||||
>
|
||||
<Relay(IgnoredTombstoneOrHideContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
<FadeInTransition
|
||||
active={false}
|
||||
key="comment-1"
|
||||
viewer={null}
|
||||
>
|
||||
<ForwardRef(forwardRef)
|
||||
key="comment-1"
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer)))))
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
<Relay(IgnoredTombstoneOrHideContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
indentLevel={1}
|
||||
key="comment-1"
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
showConversationLink={false}
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
}
|
||||
}
|
||||
viewer={null}
|
||||
/>
|
||||
</ForwardRef(forwardRef)>
|
||||
</Relay(IgnoredTombstoneOrHideContainer)>
|
||||
<Relay(IgnoredTombstoneOrHideContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
key="comment-2"
|
||||
viewer={null}
|
||||
>
|
||||
<ForwardRef(forwardRef)
|
||||
key="comment-2"
|
||||
viewer={null}
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer)))))
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
<ForwardRef(forwardRef)
|
||||
key="comment-1"
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer)))))
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
indentLevel={1}
|
||||
key="comment-1"
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
showConversationLink={false}
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
}
|
||||
}
|
||||
viewer={null}
|
||||
/>
|
||||
</ForwardRef(forwardRef)>
|
||||
</Relay(IgnoredTombstoneOrHideContainer)>
|
||||
</FadeInTransition>
|
||||
<FadeInTransition
|
||||
active={false}
|
||||
key="comment-2"
|
||||
>
|
||||
<Relay(IgnoredTombstoneOrHideContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
indentLevel={1}
|
||||
}
|
||||
viewer={null}
|
||||
>
|
||||
<ForwardRef(forwardRef)
|
||||
key="comment-2"
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer)))))
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
}
|
||||
showConversationLink={false}
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
indentLevel={1}
|
||||
key="comment-2"
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
viewer={null}
|
||||
/>
|
||||
</ForwardRef(forwardRef)>
|
||||
</Relay(IgnoredTombstoneOrHideContainer)>
|
||||
showConversationLink={false}
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
}
|
||||
}
|
||||
viewer={null}
|
||||
/>
|
||||
</ForwardRef(forwardRef)>
|
||||
</Relay(IgnoredTombstoneOrHideContainer)>
|
||||
</FadeInTransition>
|
||||
<Indent
|
||||
level={1}
|
||||
noBorder={true}
|
||||
|
||||
+48
-24
@@ -10,15 +10,18 @@ exports[`renders correctly 1`] = `
|
||||
"edges": Array [
|
||||
Object {
|
||||
"node": Object {
|
||||
"enteredLive": false,
|
||||
"id": "comment-1",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"node": Object {
|
||||
"enteredLive": false,
|
||||
"id": "comment-2",
|
||||
},
|
||||
},
|
||||
],
|
||||
"viewNewEdges": Array [],
|
||||
},
|
||||
"status": "NONE",
|
||||
}
|
||||
@@ -26,24 +29,25 @@ exports[`renders correctly 1`] = `
|
||||
comments={
|
||||
Array [
|
||||
Object {
|
||||
"enteredLive": false,
|
||||
"id": "comment-1",
|
||||
"replyListElement": <ReplyListComponent
|
||||
comment={
|
||||
Object {
|
||||
"enteredLive": false,
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
"disableCommenting": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
viewer={null}
|
||||
@@ -51,24 +55,25 @@ exports[`renders correctly 1`] = `
|
||||
"showConversationLink": false,
|
||||
},
|
||||
Object {
|
||||
"enteredLive": false,
|
||||
"id": "comment-2",
|
||||
"replyListElement": <ReplyListComponent
|
||||
comment={
|
||||
Object {
|
||||
"enteredLive": false,
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
"disableCommenting": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
viewer={null}
|
||||
@@ -81,19 +86,20 @@ exports[`renders correctly 1`] = `
|
||||
indentLevel={1}
|
||||
localReply={false}
|
||||
onShowAll={[Function]}
|
||||
onViewNew={[Function]}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
"disableCommenting": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
viewNewCount={0}
|
||||
viewer={null}
|
||||
/>
|
||||
`;
|
||||
@@ -110,15 +116,18 @@ exports[`when has more replies renders hasMore 1`] = `
|
||||
"edges": Array [
|
||||
Object {
|
||||
"node": Object {
|
||||
"enteredLive": false,
|
||||
"id": "comment-1",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"node": Object {
|
||||
"enteredLive": false,
|
||||
"id": "comment-2",
|
||||
},
|
||||
},
|
||||
],
|
||||
"viewNewEdges": Array [],
|
||||
},
|
||||
"status": "NONE",
|
||||
}
|
||||
@@ -126,11 +135,13 @@ exports[`when has more replies renders hasMore 1`] = `
|
||||
comments={
|
||||
Array [
|
||||
Object {
|
||||
"enteredLive": false,
|
||||
"id": "comment-1",
|
||||
"replyListElement": undefined,
|
||||
"showConversationLink": false,
|
||||
},
|
||||
Object {
|
||||
"enteredLive": false,
|
||||
"id": "comment-2",
|
||||
"replyListElement": undefined,
|
||||
"showConversationLink": false,
|
||||
@@ -142,19 +153,20 @@ exports[`when has more replies renders hasMore 1`] = `
|
||||
indentLevel={1}
|
||||
localReply={false}
|
||||
onShowAll={[Function]}
|
||||
onViewNew={[Function]}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
"disableCommenting": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
viewNewCount={0}
|
||||
viewer={null}
|
||||
/>
|
||||
`;
|
||||
@@ -169,15 +181,18 @@ exports[`when has more replies when showing all disables show all button 1`] = `
|
||||
"edges": Array [
|
||||
Object {
|
||||
"node": Object {
|
||||
"enteredLive": false,
|
||||
"id": "comment-1",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"node": Object {
|
||||
"enteredLive": false,
|
||||
"id": "comment-2",
|
||||
},
|
||||
},
|
||||
],
|
||||
"viewNewEdges": Array [],
|
||||
},
|
||||
"status": "NONE",
|
||||
}
|
||||
@@ -185,11 +200,13 @@ exports[`when has more replies when showing all disables show all button 1`] = `
|
||||
comments={
|
||||
Array [
|
||||
Object {
|
||||
"enteredLive": false,
|
||||
"id": "comment-1",
|
||||
"replyListElement": undefined,
|
||||
"showConversationLink": false,
|
||||
},
|
||||
Object {
|
||||
"enteredLive": false,
|
||||
"id": "comment-2",
|
||||
"replyListElement": undefined,
|
||||
"showConversationLink": false,
|
||||
@@ -201,19 +218,20 @@ exports[`when has more replies when showing all disables show all button 1`] = `
|
||||
indentLevel={1}
|
||||
localReply={false}
|
||||
onShowAll={[Function]}
|
||||
onViewNew={[Function]}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
"disableCommenting": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
viewNewCount={0}
|
||||
viewer={null}
|
||||
/>
|
||||
`;
|
||||
@@ -228,15 +246,18 @@ exports[`when has more replies when showing all enable show all button after loa
|
||||
"edges": Array [
|
||||
Object {
|
||||
"node": Object {
|
||||
"enteredLive": false,
|
||||
"id": "comment-1",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"node": Object {
|
||||
"enteredLive": false,
|
||||
"id": "comment-2",
|
||||
},
|
||||
},
|
||||
],
|
||||
"viewNewEdges": Array [],
|
||||
},
|
||||
"status": "NONE",
|
||||
}
|
||||
@@ -244,11 +265,13 @@ exports[`when has more replies when showing all enable show all button after loa
|
||||
comments={
|
||||
Array [
|
||||
Object {
|
||||
"enteredLive": false,
|
||||
"id": "comment-1",
|
||||
"replyListElement": undefined,
|
||||
"showConversationLink": false,
|
||||
},
|
||||
Object {
|
||||
"enteredLive": false,
|
||||
"id": "comment-2",
|
||||
"replyListElement": undefined,
|
||||
"showConversationLink": false,
|
||||
@@ -260,19 +283,20 @@ exports[`when has more replies when showing all enable show all button after loa
|
||||
indentLevel={1}
|
||||
localReply={false}
|
||||
onShowAll={[Function]}
|
||||
onViewNew={[Function]}
|
||||
settings={
|
||||
Object {
|
||||
"reaction": Object {
|
||||
"icon": "thumb_up_alt",
|
||||
"label": "Respect",
|
||||
"disableCommenting": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
story={
|
||||
Object {
|
||||
"id": "story-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
viewNewCount={0}
|
||||
viewer={null}
|
||||
/>
|
||||
`;
|
||||
|
||||
+113
-28
@@ -1,25 +1,34 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React, { FunctionComponent, useCallback, useEffect } from "react";
|
||||
import { graphql, RelayPaginationProp } from "react-relay";
|
||||
|
||||
import FadeInTransition from "coral-framework/components/FadeInTransition";
|
||||
import {
|
||||
useLoadMore,
|
||||
useLocal,
|
||||
useMutation,
|
||||
useSubscription,
|
||||
withPaginationContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLCOMMENT_SORT } from "coral-framework/schema";
|
||||
import { Omit, PropTypesOf } from "coral-framework/types";
|
||||
import { AllCommentsTabContainer_settings as SettingsData } from "coral-stream/__generated__/AllCommentsTabContainer_settings.graphql";
|
||||
import { AllCommentsTabContainer_story as StoryData } from "coral-stream/__generated__/AllCommentsTabContainer_story.graphql";
|
||||
import { AllCommentsTabContainer_viewer as ViewerData } from "coral-stream/__generated__/AllCommentsTabContainer_viewer.graphql";
|
||||
import { AllCommentsTabContainer_settings } from "coral-stream/__generated__/AllCommentsTabContainer_settings.graphql";
|
||||
import { AllCommentsTabContainer_story } from "coral-stream/__generated__/AllCommentsTabContainer_story.graphql";
|
||||
import { AllCommentsTabContainer_viewer } from "coral-stream/__generated__/AllCommentsTabContainer_viewer.graphql";
|
||||
import { AllCommentsTabContainerLocal } from "coral-stream/__generated__/AllCommentsTabContainerLocal.graphql";
|
||||
import { AllCommentsTabContainerPaginationQueryVariables } from "coral-stream/__generated__/AllCommentsTabContainerPaginationQuery.graphql";
|
||||
import { Button, HorizontalGutter } from "coral-ui/components";
|
||||
import { Box, Button, HorizontalGutter } from "coral-ui/components";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
|
||||
import { CommentContainer } from "../../Comment";
|
||||
import IgnoredTombstoneOrHideContainer from "../../IgnoredTombstoneOrHideContainer";
|
||||
import { ReplyListContainer } from "../../ReplyList";
|
||||
import AllCommentsTabViewNewMutation from "./AllCommentsTabViewNewMutation";
|
||||
import CommentCreatedSubscription from "./CommentCreatedSubscription";
|
||||
|
||||
interface Props {
|
||||
story: StoryData;
|
||||
settings: SettingsData;
|
||||
viewer: ViewerData | null;
|
||||
story: AllCommentsTabContainer_story;
|
||||
settings: AllCommentsTabContainer_settings;
|
||||
viewer: AllCommentsTabContainer_viewer | null;
|
||||
relay: RelayPaginationProp;
|
||||
}
|
||||
|
||||
@@ -29,17 +38,85 @@ graphql`
|
||||
id
|
||||
...CommentContainer_comment
|
||||
...ReplyListContainer1_comment
|
||||
...IgnoredTombstoneOrHideContainer_comment
|
||||
}
|
||||
`;
|
||||
|
||||
export const AllCommentsTabContainer: FunctionComponent<Props> = props => {
|
||||
const [{ commentsOrderBy }] = useLocal<AllCommentsTabContainerLocal>(
|
||||
graphql`
|
||||
fragment AllCommentsTabContainerLocal on Local {
|
||||
commentsOrderBy
|
||||
}
|
||||
`
|
||||
);
|
||||
const subscribeToCommentCreated = useSubscription(CommentCreatedSubscription);
|
||||
useEffect(() => {
|
||||
// TODO: (cvle) check for story or settings state
|
||||
// for whether or not we should turn on subscriptions:
|
||||
// e.g. `if (!props.story.settings.live) { return; }`
|
||||
if (props.story.isClosed || props.settings.disableCommenting.enabled) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
commentsOrderBy === GQLCOMMENT_SORT.CREATED_AT_ASC &&
|
||||
props.relay.hasMore()
|
||||
) {
|
||||
// If sort by oldest we only need to know if there is more to load.
|
||||
return;
|
||||
}
|
||||
if (
|
||||
![
|
||||
GQLCOMMENT_SORT.CREATED_AT_ASC,
|
||||
GQLCOMMENT_SORT.CREATED_AT_DESC,
|
||||
].includes(commentsOrderBy as GQLCOMMENT_SORT)
|
||||
) {
|
||||
// Only chronological sort supports top level live updates of incoming comments.
|
||||
return;
|
||||
}
|
||||
const disposable = subscribeToCommentCreated({
|
||||
storyID: props.story.id,
|
||||
orderBy: commentsOrderBy,
|
||||
});
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, [
|
||||
commentsOrderBy,
|
||||
subscribeToCommentCreated,
|
||||
props.story.id,
|
||||
props.relay.hasMore(),
|
||||
]);
|
||||
const [loadMore, isLoadingMore] = useLoadMore(props.relay, 10);
|
||||
const viewMore = useMutation(AllCommentsTabViewNewMutation);
|
||||
const onViewMore = useCallback(() => viewMore({ storyID: props.story.id }), [
|
||||
props.story.id,
|
||||
viewMore,
|
||||
]);
|
||||
const comments = props.story.comments.edges.map(edge => edge.node);
|
||||
const viewNewCount =
|
||||
(props.story.comments.viewNewEdges &&
|
||||
props.story.comments.viewNewEdges.length) ||
|
||||
0;
|
||||
return (
|
||||
<>
|
||||
{Boolean(viewNewCount && viewNewCount > 0) && (
|
||||
<Box mb={4} clone>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={onViewMore}
|
||||
fullWidth
|
||||
>
|
||||
<Localized id="comments-viewNew" $count={viewNewCount}>
|
||||
<span>View {viewNewCount} New Comments</span>
|
||||
</Localized>
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<HorizontalGutter
|
||||
id="coral-comments-stream-log"
|
||||
data-testid="comments-stream-log"
|
||||
id="comments-allComments-log"
|
||||
data-testid="comments-allComments-log"
|
||||
role="log"
|
||||
aria-live="polite"
|
||||
>
|
||||
@@ -49,31 +126,32 @@ export const AllCommentsTabContainer: FunctionComponent<Props> = props => {
|
||||
viewer={props.viewer}
|
||||
comment={comment}
|
||||
>
|
||||
<HorizontalGutter>
|
||||
<CommentContainer
|
||||
viewer={props.viewer}
|
||||
settings={props.settings}
|
||||
comment={comment}
|
||||
story={props.story}
|
||||
/>
|
||||
<ReplyListContainer
|
||||
settings={props.settings}
|
||||
viewer={props.viewer}
|
||||
comment={comment}
|
||||
story={props.story}
|
||||
/>
|
||||
</HorizontalGutter>
|
||||
<FadeInTransition active={Boolean(comment.enteredLive)}>
|
||||
<HorizontalGutter>
|
||||
<CommentContainer
|
||||
viewer={props.viewer}
|
||||
settings={props.settings}
|
||||
comment={comment}
|
||||
story={props.story}
|
||||
/>
|
||||
<ReplyListContainer
|
||||
settings={props.settings}
|
||||
viewer={props.viewer}
|
||||
comment={comment}
|
||||
story={props.story}
|
||||
/>
|
||||
</HorizontalGutter>
|
||||
</FadeInTransition>
|
||||
</IgnoredTombstoneOrHideContainer>
|
||||
))}
|
||||
{props.relay.hasMore() && (
|
||||
<Localized id="comments-stream-loadMore">
|
||||
<Localized id="comments-loadMore">
|
||||
<Button
|
||||
id={"coral-comments-stream-loadMore"}
|
||||
onClick={loadMore}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
disabled={isLoadingMore}
|
||||
aria-controls="coral-comments-stream-log"
|
||||
aria-controls="comments-allComments-log"
|
||||
>
|
||||
Load More
|
||||
</Button>
|
||||
@@ -104,12 +182,16 @@ const enhanced = withPaginationContainer<
|
||||
orderBy: { type: "COMMENT_SORT!", defaultValue: CREATED_AT_DESC }
|
||||
) {
|
||||
id
|
||||
isClosed
|
||||
comments(first: $count, after: $cursor, orderBy: $orderBy)
|
||||
@connection(key: "Stream_comments") {
|
||||
viewNewEdges {
|
||||
cursor
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
enteredLive
|
||||
...AllCommentsTabContainer_comment @relay(mask: false)
|
||||
...IgnoredTombstoneOrHideContainer_comment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,6 +219,9 @@ const enhanced = withPaginationContainer<
|
||||
reaction {
|
||||
sortLabel
|
||||
}
|
||||
disableCommenting {
|
||||
enabled
|
||||
}
|
||||
...ReplyListContainer1_settings
|
||||
...CommentContainer_settings
|
||||
}
|
||||
|
||||
+12
-19
@@ -5,11 +5,12 @@ import {
|
||||
} from "coral-framework/lib/relay";
|
||||
import { AllCommentsTabQuery as QueryTypes } from "coral-stream/__generated__/AllCommentsTabQuery.graphql";
|
||||
import { AllCommentsTabQueryLocal as Local } from "coral-stream/__generated__/AllCommentsTabQueryLocal.graphql";
|
||||
import { Delay, Flex, Spinner } from "coral-ui/components";
|
||||
import { Flex, Spinner } from "coral-ui/components";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { ReadyState } from "react-relay";
|
||||
|
||||
import AllCommentsTabContainer from "./AllCommentsTabContainer";
|
||||
import SpinnerWhileRendering from "./SpinnerWhileRendering";
|
||||
|
||||
interface Props {
|
||||
local: Local;
|
||||
@@ -20,29 +21,21 @@ export const render = (data: ReadyState<QueryTypes["response"]>) => {
|
||||
if (data.error) {
|
||||
return <div>{data.error.message}</div>;
|
||||
}
|
||||
if (!data.props) {
|
||||
return (
|
||||
<Flex justifyContent="center">
|
||||
<Spinner />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
if (data.props) {
|
||||
return (
|
||||
<AllCommentsTabContainer
|
||||
settings={data.props.settings}
|
||||
viewer={data.props.viewer}
|
||||
story={data.props.story!}
|
||||
/>
|
||||
<SpinnerWhileRendering>
|
||||
<AllCommentsTabContainer
|
||||
settings={data.props.settings}
|
||||
viewer={data.props.viewer}
|
||||
story={data.props.story!}
|
||||
/>
|
||||
</SpinnerWhileRendering>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Delay>
|
||||
<Flex justifyContent="center">
|
||||
<Spinner />
|
||||
</Flex>
|
||||
</Delay>
|
||||
<Flex justifyContent="center">
|
||||
<Spinner />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { ConnectionHandler, Environment, RecordProxy } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitLocalUpdatePromisified,
|
||||
createMutation,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLCOMMENT_SORT } from "coral-framework/schema";
|
||||
|
||||
interface Input {
|
||||
storyID: string;
|
||||
}
|
||||
|
||||
const AllCommentsTabViewNewMutation = createMutation(
|
||||
"viewNew",
|
||||
async (environment: Environment, input: Input) => {
|
||||
await commitLocalUpdatePromisified(environment, async store => {
|
||||
const story = store.get(input.storyID)!;
|
||||
const connection = ConnectionHandler.getConnection(
|
||||
story,
|
||||
"Stream_comments",
|
||||
{
|
||||
orderBy: GQLCOMMENT_SORT.CREATED_AT_DESC,
|
||||
}
|
||||
)! as RecordProxy;
|
||||
const viewNewEdges = connection.getLinkedRecords("viewNewEdges");
|
||||
if (!viewNewEdges || viewNewEdges.length === 0) {
|
||||
return;
|
||||
}
|
||||
viewNewEdges.forEach(edge => {
|
||||
ConnectionHandler.insertEdgeBefore(connection, edge);
|
||||
});
|
||||
connection.setLinkedRecords([], "viewNewEdges");
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default AllCommentsTabViewNewMutation;
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import { graphql, requestSubscription } from "react-relay";
|
||||
import {
|
||||
ConnectionHandler,
|
||||
Environment,
|
||||
RecordProxy,
|
||||
RecordSourceSelectorProxy,
|
||||
} from "relay-runtime";
|
||||
|
||||
import {
|
||||
createSubscription,
|
||||
SubscriptionVariables,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLCOMMENT_SORT, GQLCOMMENT_SORT_RL } from "coral-framework/schema";
|
||||
import { CommentCreatedSubscription } from "coral-stream/__generated__/CommentCreatedSubscription.graphql";
|
||||
|
||||
function updateForNewestFirst(
|
||||
store: RecordSourceSelectorProxy,
|
||||
storyID: string
|
||||
) {
|
||||
const rootField = store.getRootField("commentCreated");
|
||||
if (!rootField) {
|
||||
return;
|
||||
}
|
||||
const comment = rootField.getLinkedRecord("comment")!;
|
||||
comment.setValue(true, "enteredLive");
|
||||
const commentsEdge = store.create(
|
||||
`edge-${comment.getValue("id")!}`,
|
||||
"CommentsEdge"
|
||||
);
|
||||
commentsEdge.setValue(comment.getValue("createdAt"), "cursor");
|
||||
commentsEdge.setLinkedRecord(comment, "node");
|
||||
const story = store.get(storyID)!;
|
||||
const connection = ConnectionHandler.getConnection(story, "Stream_comments", {
|
||||
orderBy: GQLCOMMENT_SORT.CREATED_AT_DESC,
|
||||
})!;
|
||||
const linked = connection.getLinkedRecords("viewNewEdges") || [];
|
||||
connection.setLinkedRecords(linked.concat(commentsEdge), "viewNewEdges");
|
||||
}
|
||||
|
||||
function updateForOldestFirst(
|
||||
store: RecordSourceSelectorProxy,
|
||||
storyID: string
|
||||
) {
|
||||
const story = store.get(storyID)!;
|
||||
const connection = ConnectionHandler.getConnection(story, "Stream_comments", {
|
||||
orderBy: GQLCOMMENT_SORT.CREATED_AT_ASC,
|
||||
})!;
|
||||
const pageInfo = connection.getLinkedRecord("pageInfo") as RecordProxy;
|
||||
pageInfo.setValue(true, "hasNextPage");
|
||||
}
|
||||
|
||||
const CommentCreatedSubscription = createSubscription(
|
||||
"subscribeToCommentCreated",
|
||||
(
|
||||
environment: Environment,
|
||||
variables: SubscriptionVariables<CommentCreatedSubscription> & {
|
||||
orderBy: GQLCOMMENT_SORT_RL;
|
||||
}
|
||||
) =>
|
||||
requestSubscription(environment, {
|
||||
subscription: graphql`
|
||||
subscription CommentCreatedSubscription($storyID: ID!) {
|
||||
commentCreated(storyID: $storyID) {
|
||||
comment {
|
||||
id
|
||||
createdAt
|
||||
...AllCommentsTabContainer_comment
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables,
|
||||
updater: store => {
|
||||
if (variables.orderBy === GQLCOMMENT_SORT.CREATED_AT_DESC) {
|
||||
updateForNewestFirst(store, variables.storyID);
|
||||
return;
|
||||
}
|
||||
if (variables.orderBy === GQLCOMMENT_SORT.CREATED_AT_ASC) {
|
||||
updateForOldestFirst(store, variables.storyID);
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
`Unsupport new top level comment live updates for sort ${
|
||||
variables.orderBy
|
||||
}`
|
||||
);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default CommentCreatedSubscription;
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Flex, Spinner } from "coral-ui/components";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function callWhenReallyIdle(callback: () => void) {
|
||||
let handle: any = null;
|
||||
const rIC = (cb: () => void) => {
|
||||
if ((window as any).requestIdleCallback) {
|
||||
handle = (window as any).requestIdleCallback(cb, { timeout: 300 });
|
||||
} else {
|
||||
handle = setTimeout(cb, 0);
|
||||
}
|
||||
};
|
||||
|
||||
// Call `requestIdleCallback` multiple times to ensure
|
||||
// that the browser is really idelling.
|
||||
const times = 5;
|
||||
let chained = callback;
|
||||
for (let i = 0; i <= times; i++) {
|
||||
const cur = chained;
|
||||
chained = () => rIC(cur);
|
||||
}
|
||||
chained();
|
||||
|
||||
return () => {
|
||||
if ((window as any).requestIdleCallback) {
|
||||
(window as any).cancelIdleCallback(handle);
|
||||
} else {
|
||||
clearTimeout(handle);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Show spinner, wait for browser to idle and start rendering.
|
||||
*/
|
||||
const SpinnerWhileRendering: FunctionComponent<Props> = props => {
|
||||
// In our tests, we don't actually "render", so just skip this.
|
||||
if (process.env.NODE_ENV === "test") {
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
const [hidden, setHidden] = useState(true);
|
||||
useEffect(() => {
|
||||
// Ensure window has bee
|
||||
return callWhenReallyIdle(() => setHidden(false));
|
||||
}, [setHidden]);
|
||||
return (
|
||||
<>
|
||||
{hidden && (
|
||||
<Flex justifyContent="center">
|
||||
<Spinner />
|
||||
</Flex>
|
||||
)}
|
||||
{!hidden && props.children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpinnerWhileRendering;
|
||||
+4
-5
@@ -29,8 +29,8 @@ export const FeaturedCommentsContainer: FunctionComponent<Props> = props => {
|
||||
return (
|
||||
<>
|
||||
<HorizontalGutter
|
||||
id="coral-comments-stream-log"
|
||||
data-testid="comments-stream-log"
|
||||
id="comments-featuredComments-log"
|
||||
data-testid="comments-featuredComments-log"
|
||||
role="log"
|
||||
aria-live="polite"
|
||||
spacing={3}
|
||||
@@ -50,14 +50,13 @@ export const FeaturedCommentsContainer: FunctionComponent<Props> = props => {
|
||||
</IgnoredTombstoneOrHideContainer>
|
||||
))}
|
||||
{props.relay.hasMore() && (
|
||||
<Localized id="comments-stream-loadMore">
|
||||
<Localized id="comments-loadMore">
|
||||
<Button
|
||||
id={"coral-comments-stream-loadMore"}
|
||||
onClick={loadMore}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
disabled={isLoadingMore}
|
||||
aria-controls="coral-comments-stream-log"
|
||||
aria-controls="comments-featuredComments-log"
|
||||
>
|
||||
Load More
|
||||
</Button>
|
||||
|
||||
@@ -77,6 +77,7 @@ function addCommentToStory(
|
||||
graphql`
|
||||
fragment CreateCommentMutation_viewer on User {
|
||||
role
|
||||
createdAt
|
||||
}
|
||||
`;
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
|
||||
@@ -89,7 +89,7 @@ export const StreamContainer: FunctionComponent<Props> = props => {
|
||||
/>
|
||||
)}
|
||||
{banned && <BannedInfo />}
|
||||
<HorizontalGutter spacing={5} className={styles.tabBarContainer}>
|
||||
<HorizontalGutter spacing={4} className={styles.tabBarContainer}>
|
||||
<SortMenu
|
||||
className={styles.sortMenu}
|
||||
orderBy={local.commentsOrderBy}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
QueryRenderer,
|
||||
withLocalStateContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { COMMENTS_TAB } from "coral-stream/__generated__/StreamContainerLocal.graphql";
|
||||
import { StreamQuery as QueryTypes } from "coral-stream/__generated__/StreamQuery.graphql";
|
||||
import { StreamQueryLocal as Local } from "coral-stream/__generated__/StreamQueryLocal.graphql";
|
||||
import { Delay, Flex, Spinner } from "coral-ui/components";
|
||||
@@ -17,7 +18,10 @@ interface Props {
|
||||
local: Local;
|
||||
}
|
||||
|
||||
export const render = (data: ReadyState<QueryTypes["response"]>) => {
|
||||
export const render = (
|
||||
data: ReadyState<QueryTypes["response"]>,
|
||||
commentsTab: COMMENTS_TAB
|
||||
) => {
|
||||
if (data.error) {
|
||||
return <div>{data.error.message}</div>;
|
||||
}
|
||||
@@ -40,11 +44,19 @@ export const render = (data: ReadyState<QueryTypes["response"]>) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Delay>
|
||||
<Flex justifyContent="center">
|
||||
<Spinner />
|
||||
</Flex>
|
||||
</Delay>
|
||||
<>
|
||||
{// TODO: (cvle) For some reason this way of preloading
|
||||
// causes weird errors in the
|
||||
// tests. Needs further investigation.
|
||||
process.env.NODE_ENV !== "test" && commentsTab === "ALL_COMMENTS" && (
|
||||
<AllCommentsTabQuery preload />
|
||||
)}
|
||||
<Delay>
|
||||
<Flex justifyContent="center">
|
||||
<Spinner />
|
||||
</Flex>
|
||||
</Delay>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -73,23 +85,7 @@ const StreamQuery: FunctionComponent<Props> = props => {
|
||||
storyURL,
|
||||
}}
|
||||
render={data => {
|
||||
if (
|
||||
// TODO: (cvle) For some reason this way of preloading
|
||||
// causes weird errors in the
|
||||
// tests. Needs further investigation.
|
||||
process.env.NODE_ENV !== "test" &&
|
||||
!data.props &&
|
||||
!data.error
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
{commentsTab === "ALL_COMMENTS" && (
|
||||
<AllCommentsTabQuery preload />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return render(data);
|
||||
return render(data, commentsTab);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
+3
-3
@@ -173,7 +173,7 @@ exports[`renders comment stream 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root StreamContainer-tabBarContainer HorizontalGutter-spacing-5"
|
||||
className="Box-root HorizontalGutter-root StreamContainer-tabBarContainer HorizontalGutter-spacing-4"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root StreamContainer-sortMenu Flex-flex Flex-itemGutter Flex-justifyFlexEnd Flex-alignCenter gutter"
|
||||
@@ -278,8 +278,8 @@ exports[`renders comment stream 1`] = `
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-3"
|
||||
data-testid="comments-stream-log"
|
||||
id="coral-comments-stream-log"
|
||||
data-testid="comments-featuredComments-log"
|
||||
id="comments-featuredComments-log"
|
||||
role="log"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -70,7 +70,7 @@ async function createTestRenderer(
|
||||
it("loads more", async () => {
|
||||
const { testRenderer } = await createTestRenderer();
|
||||
const streamLog = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-stream-log")
|
||||
within(testRenderer.root).getByTestID("comments-featuredComments-log")
|
||||
);
|
||||
|
||||
// Get amount of comments before.
|
||||
|
||||
@@ -50,7 +50,7 @@ async function createTestRenderer(
|
||||
it("renders comment stream", async () => {
|
||||
const { testRenderer } = await createTestRenderer();
|
||||
await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-stream-log")
|
||||
within(testRenderer.root).getByTestID("comments-featuredComments-log")
|
||||
);
|
||||
expect(within(testRenderer.root).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
+54
@@ -786,6 +786,33 @@ exports[`renders permalink view 1`] = `
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Comment-subBar"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm InReplyTo-icon"
|
||||
>
|
||||
reply
|
||||
</span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<span
|
||||
className="Box-root Typography-root Typography-timestamp Typography-colorTextPrimary InReplyTo-inReplyTo"
|
||||
>
|
||||
In reply to
|
||||
<span
|
||||
className="Box-root Typography-root Typography-heading5 Typography-colorTextPrimary InReplyTo-username"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-1"
|
||||
>
|
||||
@@ -975,6 +1002,33 @@ exports[`renders permalink view 1`] = `
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Comment-subBar"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm InReplyTo-icon"
|
||||
>
|
||||
reply
|
||||
</span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<span
|
||||
className="Box-root Typography-root Typography-timestamp Typography-colorTextPrimary InReplyTo-inReplyTo"
|
||||
>
|
||||
In reply to
|
||||
<span
|
||||
className="Box-root Typography-root Typography-heading5 Typography-colorTextPrimary InReplyTo-username"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-1"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { pureMerge } from "coral-common/utils";
|
||||
import {
|
||||
GQLResolver,
|
||||
SubscriptionToCommentReplyCreatedResolver,
|
||||
} from "coral-framework/schema";
|
||||
import {
|
||||
createResolversStub,
|
||||
CreateTestRendererParams,
|
||||
waitForElement,
|
||||
within,
|
||||
} from "coral-framework/testHelpers";
|
||||
|
||||
import { comments, settings, stories } from "../../fixtures";
|
||||
import create from "./create";
|
||||
|
||||
const story = stories[0];
|
||||
const rootComment = comments[0];
|
||||
|
||||
async function createTestRenderer(
|
||||
params: CreateTestRendererParams<GQLResolver> = {}
|
||||
) {
|
||||
const { testRenderer, context, subscriptionHandler } = create({
|
||||
...params,
|
||||
resolvers: pureMerge(
|
||||
createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
settings: () => settings,
|
||||
story: () => story,
|
||||
comment: () => comments[0],
|
||||
},
|
||||
}),
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(story.id, "storyID");
|
||||
localRecord.setValue(rootComment.id, "commentID");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
testRenderer,
|
||||
context,
|
||||
subscriptionHandler,
|
||||
};
|
||||
}
|
||||
|
||||
it("direct replies to the permalink comment should immediately appear", async () => {
|
||||
const liveComment = comments[3];
|
||||
const { testRenderer, subscriptionHandler } = await createTestRenderer();
|
||||
const container = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("current-tab-pane")
|
||||
);
|
||||
expect(subscriptionHandler.has("commentReplyCreated")).toBe(true);
|
||||
|
||||
expect(() =>
|
||||
within(container).getByTestID(`comment-${liveComment.id}`)
|
||||
).toThrow();
|
||||
|
||||
subscriptionHandler.dispatch<SubscriptionToCommentReplyCreatedResolver>(
|
||||
"commentReplyCreated",
|
||||
variables => {
|
||||
if (variables.ancestorID !== rootComment.id) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
comment: pureMerge<typeof liveComment>(liveComment, {
|
||||
parent: rootComment,
|
||||
}),
|
||||
};
|
||||
}
|
||||
);
|
||||
// Comment should immediately appear.
|
||||
within(container).getByTestID(`comment-${liveComment.id}`);
|
||||
});
|
||||
@@ -102,7 +102,7 @@ it("show all comments", async () => {
|
||||
.getByText("View Full Discussion")
|
||||
.props.onClick(mockEvent);
|
||||
await waitForElement(() =>
|
||||
within(tabPane).getByTestID("comments-stream-log")
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ it("show all comments", async () => {
|
||||
.props.onClick(mockEvent);
|
||||
|
||||
await waitForElement(() =>
|
||||
within(tabPane).getByTestID("comments-stream-log")
|
||||
within(tabPane).getByTestID("comments-allComments-log")
|
||||
);
|
||||
});
|
||||
mockEvent.preventDefault.verify();
|
||||
|
||||
@@ -4,8 +4,8 @@ exports[`renders comment stream with load more button 1`] = `
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
data-testid="comments-stream-log"
|
||||
id="coral-comments-stream-log"
|
||||
data-testid="comments-allComments-log"
|
||||
id="comments-allComments-log"
|
||||
role="log"
|
||||
>
|
||||
<div
|
||||
@@ -387,10 +387,9 @@ exports[`renders comment stream with load more button 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
aria-controls="coral-comments-stream-log"
|
||||
aria-controls="comments-allComments-log"
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantOutlined Button-fullWidth"
|
||||
disabled={false}
|
||||
id="coral-comments-stream-loadMore"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
|
||||
+164
-2
@@ -79,6 +79,33 @@ exports[`post a reply: open reply form 1`] = `
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Comment-subBar"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm InReplyTo-icon"
|
||||
>
|
||||
reply
|
||||
</span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<span
|
||||
className="Box-root Typography-root Typography-timestamp Typography-colorTextPrimary InReplyTo-inReplyTo"
|
||||
>
|
||||
In reply to
|
||||
<span
|
||||
className="Box-root Typography-root Typography-heading5 Typography-colorTextPrimary InReplyTo-username"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-1"
|
||||
>
|
||||
@@ -622,8 +649,8 @@ exports[`renders comment stream 1`] = `
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
data-testid="comments-stream-log"
|
||||
id="coral-comments-stream-log"
|
||||
data-testid="comments-allComments-log"
|
||||
id="comments-allComments-log"
|
||||
role="log"
|
||||
>
|
||||
<div
|
||||
@@ -919,6 +946,33 @@ exports[`renders comment stream 1`] = `
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Comment-subBar"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm InReplyTo-icon"
|
||||
>
|
||||
reply
|
||||
</span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<span
|
||||
className="Box-root Typography-root Typography-timestamp Typography-colorTextPrimary InReplyTo-inReplyTo"
|
||||
>
|
||||
In reply to
|
||||
<span
|
||||
className="Box-root Typography-root Typography-heading5 Typography-colorTextPrimary InReplyTo-username"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-1"
|
||||
>
|
||||
@@ -1131,6 +1185,33 @@ exports[`renders comment stream 1`] = `
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Comment-subBar"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm InReplyTo-icon"
|
||||
>
|
||||
reply
|
||||
</span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<span
|
||||
className="Box-root Typography-root Typography-timestamp Typography-colorTextPrimary InReplyTo-inReplyTo"
|
||||
>
|
||||
In reply to
|
||||
<span
|
||||
className="Box-root Typography-root Typography-heading5 Typography-colorTextPrimary InReplyTo-username"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-1"
|
||||
>
|
||||
@@ -1343,6 +1424,33 @@ exports[`renders comment stream 1`] = `
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Comment-subBar"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm InReplyTo-icon"
|
||||
>
|
||||
reply
|
||||
</span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<span
|
||||
className="Box-root Typography-root Typography-timestamp Typography-colorTextPrimary InReplyTo-inReplyTo"
|
||||
>
|
||||
In reply to
|
||||
<span
|
||||
className="Box-root Typography-root Typography-heading5 Typography-colorTextPrimary InReplyTo-username"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-1"
|
||||
>
|
||||
@@ -1555,6 +1663,33 @@ exports[`renders comment stream 1`] = `
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Comment-subBar"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm InReplyTo-icon"
|
||||
>
|
||||
reply
|
||||
</span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<span
|
||||
className="Box-root Typography-root Typography-timestamp Typography-colorTextPrimary InReplyTo-inReplyTo"
|
||||
>
|
||||
In reply to
|
||||
<span
|
||||
className="Box-root Typography-root Typography-heading5 Typography-colorTextPrimary InReplyTo-username"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-1"
|
||||
>
|
||||
@@ -1767,6 +1902,33 @@ exports[`renders comment stream 1`] = `
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Comment-subBar"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm InReplyTo-icon"
|
||||
>
|
||||
reply
|
||||
</span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<span
|
||||
className="Box-root Typography-root Typography-timestamp Typography-colorTextPrimary InReplyTo-inReplyTo"
|
||||
>
|
||||
In reply to
|
||||
<span
|
||||
className="Box-root Typography-root Typography-heading5 Typography-colorTextPrimary InReplyTo-username"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-1"
|
||||
>
|
||||
|
||||
+3
-3
@@ -157,7 +157,7 @@ exports[`renders comment stream with community guidelines 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root StreamContainer-tabBarContainer HorizontalGutter-spacing-5"
|
||||
className="Box-root HorizontalGutter-root StreamContainer-tabBarContainer HorizontalGutter-spacing-4"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root StreamContainer-sortMenu Flex-flex Flex-itemGutter Flex-justifyFlexEnd Flex-alignCenter gutter"
|
||||
@@ -262,8 +262,8 @@ exports[`renders comment stream with community guidelines 1`] = `
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
data-testid="comments-stream-log"
|
||||
id="coral-comments-stream-log"
|
||||
data-testid="comments-allComments-log"
|
||||
id="comments-allComments-log"
|
||||
role="log"
|
||||
>
|
||||
<div
|
||||
|
||||
+12
-12
@@ -78,7 +78,7 @@ exports[`renders message box when commenting disabled 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root StreamContainer-tabBarContainer HorizontalGutter-spacing-5"
|
||||
className="Box-root HorizontalGutter-root StreamContainer-tabBarContainer HorizontalGutter-spacing-4"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root StreamContainer-sortMenu Flex-flex Flex-itemGutter Flex-justifyFlexEnd Flex-alignCenter gutter"
|
||||
@@ -183,8 +183,8 @@ exports[`renders message box when commenting disabled 1`] = `
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
data-testid="comments-stream-log"
|
||||
id="coral-comments-stream-log"
|
||||
data-testid="comments-allComments-log"
|
||||
id="comments-allComments-log"
|
||||
role="log"
|
||||
/>
|
||||
</section>
|
||||
@@ -382,7 +382,7 @@ exports[`renders message box when logged in 1`] = `
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root StreamContainer-tabBarContainer HorizontalGutter-spacing-5"
|
||||
className="Box-root HorizontalGutter-root StreamContainer-tabBarContainer HorizontalGutter-spacing-4"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root StreamContainer-sortMenu Flex-flex Flex-itemGutter Flex-justifyFlexEnd Flex-alignCenter gutter"
|
||||
@@ -487,8 +487,8 @@ exports[`renders message box when logged in 1`] = `
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
data-testid="comments-stream-log"
|
||||
id="coral-comments-stream-log"
|
||||
data-testid="comments-allComments-log"
|
||||
id="comments-allComments-log"
|
||||
role="log"
|
||||
/>
|
||||
</section>
|
||||
@@ -658,7 +658,7 @@ exports[`renders message box when not logged in 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root StreamContainer-tabBarContainer HorizontalGutter-spacing-5"
|
||||
className="Box-root HorizontalGutter-root StreamContainer-tabBarContainer HorizontalGutter-spacing-4"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root StreamContainer-sortMenu Flex-flex Flex-itemGutter Flex-justifyFlexEnd Flex-alignCenter gutter"
|
||||
@@ -763,8 +763,8 @@ exports[`renders message box when not logged in 1`] = `
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
data-testid="comments-stream-log"
|
||||
id="coral-comments-stream-log"
|
||||
data-testid="comments-allComments-log"
|
||||
id="comments-allComments-log"
|
||||
role="log"
|
||||
/>
|
||||
</section>
|
||||
@@ -843,7 +843,7 @@ exports[`renders message box when story isClosed 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root StreamContainer-tabBarContainer HorizontalGutter-spacing-5"
|
||||
className="Box-root HorizontalGutter-root StreamContainer-tabBarContainer HorizontalGutter-spacing-4"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root StreamContainer-sortMenu Flex-flex Flex-itemGutter Flex-justifyFlexEnd Flex-alignCenter gutter"
|
||||
@@ -948,8 +948,8 @@ exports[`renders message box when story isClosed 1`] = `
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
data-testid="comments-stream-log"
|
||||
id="coral-comments-stream-log"
|
||||
data-testid="comments-allComments-log"
|
||||
id="comments-allComments-log"
|
||||
role="log"
|
||||
/>
|
||||
</section>
|
||||
|
||||
+108
@@ -88,6 +88,33 @@ exports[`renders reply list 1`] = `
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Comment-subBar"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm InReplyTo-icon"
|
||||
>
|
||||
reply
|
||||
</span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<span
|
||||
className="Box-root Typography-root Typography-timestamp Typography-colorTextPrimary InReplyTo-inReplyTo"
|
||||
>
|
||||
In reply to
|
||||
<span
|
||||
className="Box-root Typography-root Typography-heading5 Typography-colorTextPrimary InReplyTo-username"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-1"
|
||||
>
|
||||
@@ -282,6 +309,33 @@ exports[`renders reply list 1`] = `
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Comment-subBar"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm InReplyTo-icon"
|
||||
>
|
||||
reply
|
||||
</span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<span
|
||||
className="Box-root Typography-root Typography-timestamp Typography-colorTextPrimary InReplyTo-inReplyTo"
|
||||
>
|
||||
In reply to
|
||||
<span
|
||||
className="Box-root Typography-root Typography-heading5 Typography-colorTextPrimary InReplyTo-username"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-1"
|
||||
>
|
||||
@@ -471,6 +525,33 @@ exports[`renders reply list 1`] = `
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Comment-subBar"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm InReplyTo-icon"
|
||||
>
|
||||
reply
|
||||
</span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<span
|
||||
className="Box-root Typography-root Typography-timestamp Typography-colorTextPrimary InReplyTo-inReplyTo"
|
||||
>
|
||||
In reply to
|
||||
<span
|
||||
className="Box-root Typography-root Typography-heading5 Typography-colorTextPrimary InReplyTo-username"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-1"
|
||||
>
|
||||
@@ -662,6 +743,33 @@ exports[`renders reply list 1`] = `
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Comment-subBar"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm InReplyTo-icon"
|
||||
>
|
||||
reply
|
||||
</span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<span
|
||||
className="Box-root Typography-root Typography-timestamp Typography-colorTextPrimary InReplyTo-inReplyTo"
|
||||
>
|
||||
In reply to
|
||||
<span
|
||||
className="Box-root Typography-root Typography-heading5 Typography-colorTextPrimary InReplyTo-username"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-1"
|
||||
>
|
||||
|
||||
+3
-3
@@ -173,7 +173,7 @@ exports[`renders comment stream 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root StreamContainer-tabBarContainer HorizontalGutter-spacing-5"
|
||||
className="Box-root HorizontalGutter-root StreamContainer-tabBarContainer HorizontalGutter-spacing-4"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root StreamContainer-sortMenu Flex-flex Flex-itemGutter Flex-justifyFlexEnd Flex-alignCenter gutter"
|
||||
@@ -278,8 +278,8 @@ exports[`renders comment stream 1`] = `
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
data-testid="comments-stream-log"
|
||||
id="coral-comments-stream-log"
|
||||
data-testid="comments-allComments-log"
|
||||
id="comments-allComments-log"
|
||||
role="log"
|
||||
>
|
||||
<div
|
||||
|
||||
+27
@@ -79,6 +79,33 @@ exports[`renders deepest comment with link 1`] = `
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Comment-subBar"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm InReplyTo-icon"
|
||||
>
|
||||
reply
|
||||
</span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<span
|
||||
className="Box-root Typography-root Typography-timestamp Typography-colorTextPrimary InReplyTo-inReplyTo"
|
||||
>
|
||||
In reply to
|
||||
<span
|
||||
className="Box-root Typography-root Typography-heading5 Typography-colorTextPrimary InReplyTo-username"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-1"
|
||||
>
|
||||
|
||||
@@ -4,8 +4,8 @@ exports[`renders app with comment stream 1`] = `
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
data-testid="comments-stream-log"
|
||||
id="coral-comments-stream-log"
|
||||
data-testid="comments-allComments-log"
|
||||
id="comments-allComments-log"
|
||||
role="log"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -80,7 +80,7 @@ it("disables comment stream", async () => {
|
||||
timekeeper.freeze(firstComment.createdAt);
|
||||
const { testRenderer, tabPane } = await createTestRenderer();
|
||||
await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-stream-log")
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
within(tabPane).getAllByText("Your account has been banned", {
|
||||
exact: false,
|
||||
|
||||
@@ -90,7 +90,7 @@ it("auto close comment stream when story closed at has been reached", async () =
|
||||
).toBeNull();
|
||||
|
||||
await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-stream-log")
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
|
||||
jest.advanceTimersByTime(closeIn);
|
||||
|
||||
@@ -121,7 +121,7 @@ it("render stream with ignored user", async () => {
|
||||
stories[0]
|
||||
);
|
||||
await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-stream-log")
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
expect(
|
||||
within(tabPane).queryByTestID(`comment-${firstComment.id}`)
|
||||
@@ -143,7 +143,7 @@ it("render stream with only staff comments, ignore user button should not be pre
|
||||
storyWithOnlyStaffComments
|
||||
);
|
||||
await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-stream-log")
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
const moderator = moderators[0];
|
||||
const username = within(tabPane).getByText(moderator!.username!, {
|
||||
@@ -176,7 +176,7 @@ it("render stream with regular comments, ignore user button should be present",
|
||||
stories[0]
|
||||
);
|
||||
await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-stream-log")
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
const commenter = commenters[0];
|
||||
const username = within(tabPane).getByText(commenter!.username!, {
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
import { pureMerge } from "coral-common/utils";
|
||||
import {
|
||||
GQLComment,
|
||||
GQLResolver,
|
||||
GQLStory,
|
||||
SubscriptionToCommentReplyCreatedResolver,
|
||||
} from "coral-framework/schema";
|
||||
import {
|
||||
createFixture,
|
||||
createResolversStub,
|
||||
CreateTestRendererParams,
|
||||
denormalizeComment,
|
||||
denormalizeStory,
|
||||
waitForElement,
|
||||
within,
|
||||
} from "coral-framework/testHelpers";
|
||||
|
||||
import { baseComment, baseStory, comments, settings } from "../../fixtures";
|
||||
import create from "./create";
|
||||
|
||||
const commentData = comments[0];
|
||||
|
||||
const rootComment = denormalizeComment(
|
||||
createFixture<GQLComment>({
|
||||
...baseComment,
|
||||
id: "my-comment",
|
||||
body: "body 0",
|
||||
replyCount: 1,
|
||||
replies: {
|
||||
...baseComment.replies,
|
||||
edges: [
|
||||
{
|
||||
cursor: baseComment.createdAt,
|
||||
node: {
|
||||
...baseComment,
|
||||
id: "my-comment-1",
|
||||
body: "body 1",
|
||||
replyCount: 1,
|
||||
replies: {
|
||||
...baseComment.replies,
|
||||
edges: [
|
||||
{
|
||||
cursor: baseComment.createdAt,
|
||||
node: {
|
||||
...baseComment,
|
||||
id: "my-comment-2",
|
||||
body: "body 2",
|
||||
replyCount: 1,
|
||||
replies: {
|
||||
...baseComment.replies,
|
||||
edges: [
|
||||
{
|
||||
cursor: baseComment.createdAt,
|
||||
node: {
|
||||
...baseComment,
|
||||
id: "my-comment-3",
|
||||
body: "body 3",
|
||||
replyCount: 1,
|
||||
replies: {
|
||||
...baseComment.replies,
|
||||
edges: [
|
||||
{
|
||||
cursor: baseComment.createdAt,
|
||||
node: {
|
||||
...baseComment,
|
||||
id: "my-comment-4",
|
||||
body: "body 4",
|
||||
replyCount: 1,
|
||||
replies: {
|
||||
...baseComment.replies,
|
||||
edges: [
|
||||
{
|
||||
cursor: baseComment.createdAt,
|
||||
node: {
|
||||
...baseComment,
|
||||
id: "my-comment-5",
|
||||
body: "body 5",
|
||||
replyCount: 0,
|
||||
replies: {
|
||||
...baseComment.replies,
|
||||
edges: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const story = denormalizeStory(
|
||||
createFixture<GQLStory>(
|
||||
{
|
||||
id: "story-with-deep-replies",
|
||||
url: "http://localhost/stories/story-with-replies",
|
||||
comments: {
|
||||
edges: [
|
||||
{
|
||||
node: rootComment,
|
||||
cursor: rootComment.createdAt,
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
baseStory
|
||||
)
|
||||
);
|
||||
|
||||
async function createTestRenderer(
|
||||
params: CreateTestRendererParams<GQLResolver> = {}
|
||||
) {
|
||||
const { testRenderer, context, subscriptionHandler } = create({
|
||||
...params,
|
||||
resolvers: pureMerge(
|
||||
createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
settings: () => settings,
|
||||
story: () => story,
|
||||
},
|
||||
}),
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(story.id, "storyID");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
testRenderer,
|
||||
context,
|
||||
subscriptionHandler,
|
||||
};
|
||||
}
|
||||
|
||||
it("should show more replies", async () => {
|
||||
const { testRenderer, subscriptionHandler } = await createTestRenderer();
|
||||
const container = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
expect(subscriptionHandler.has("commentReplyCreated")).toBe(true);
|
||||
expect(() =>
|
||||
within(container).getByTestID(`comment-${commentData.id}`)
|
||||
).toThrow();
|
||||
|
||||
subscriptionHandler.dispatch<SubscriptionToCommentReplyCreatedResolver>(
|
||||
"commentReplyCreated",
|
||||
variables => {
|
||||
if (variables.ancestorID !== rootComment.id) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
comment: pureMerge<typeof commentData>(commentData, {
|
||||
parent: { ...baseComment, id: "my-comment" },
|
||||
}),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const showMoreButton = await waitForElement(() =>
|
||||
within(testRenderer.root).getByText("Show More Replies", {
|
||||
exact: false,
|
||||
selector: "button",
|
||||
})
|
||||
);
|
||||
showMoreButton.props.onClick();
|
||||
within(container).getByTestID(`comment-${commentData.id}`);
|
||||
});
|
||||
|
||||
it("should show Read More of this Conversation", async () => {
|
||||
const { testRenderer, subscriptionHandler } = await createTestRenderer();
|
||||
const container = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
expect(subscriptionHandler.has("commentReplyCreated")).toBe(true);
|
||||
expect(() =>
|
||||
within(testRenderer.root).getByText("Read More of this Conversation", {
|
||||
exact: false,
|
||||
selector: "a",
|
||||
})
|
||||
).toThrow();
|
||||
|
||||
subscriptionHandler.dispatch<SubscriptionToCommentReplyCreatedResolver>(
|
||||
"commentReplyCreated",
|
||||
variables => {
|
||||
if (variables.ancestorID !== rootComment.id) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
comment: pureMerge<typeof commentData>(commentData, {
|
||||
parent: { ...baseComment, id: "my-comment-5" },
|
||||
}),
|
||||
};
|
||||
}
|
||||
);
|
||||
within(container).getByText("Read More of this Conversation", {
|
||||
exact: false,
|
||||
selector: "a",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not subscribe when story is closed", async () => {
|
||||
const { testRenderer, subscriptionHandler } = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
story: () => pureMerge<typeof story>(story, { isClosed: true }),
|
||||
},
|
||||
}),
|
||||
});
|
||||
await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
expect(subscriptionHandler.has("commentReplyCreated")).toBe(false);
|
||||
});
|
||||
|
||||
it("should not subscribe when commenting is disabled", async () => {
|
||||
const { testRenderer, subscriptionHandler } = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
settings: () =>
|
||||
pureMerge<typeof settings>(settings, {
|
||||
disableCommenting: {
|
||||
enabled: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
});
|
||||
await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
expect(subscriptionHandler.has("commentReplyCreated")).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
import { pureMerge } from "coral-common/utils";
|
||||
import {
|
||||
GQLResolver,
|
||||
SubscriptionToCommentCreatedResolver,
|
||||
} from "coral-framework/schema";
|
||||
import {
|
||||
act,
|
||||
createResolversStub,
|
||||
CreateTestRendererParams,
|
||||
waitForElement,
|
||||
within,
|
||||
} from "coral-framework/testHelpers";
|
||||
|
||||
import { comments, settings, stories } from "../../fixtures";
|
||||
import create from "./create";
|
||||
|
||||
const story = stories[0];
|
||||
|
||||
async function createTestRenderer(
|
||||
params: CreateTestRendererParams<GQLResolver> = {}
|
||||
) {
|
||||
const { testRenderer, context, subscriptionHandler } = create({
|
||||
...params,
|
||||
resolvers: pureMerge(
|
||||
createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
settings: () => settings,
|
||||
story: () => story,
|
||||
},
|
||||
}),
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(story.id, "storyID");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
testRenderer,
|
||||
context,
|
||||
subscriptionHandler,
|
||||
};
|
||||
}
|
||||
it("should view more when ordering by newest", async () => {
|
||||
const { testRenderer, subscriptionHandler } = await createTestRenderer();
|
||||
const container = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
const commentData = comments[5];
|
||||
|
||||
expect(subscriptionHandler.has("commentCreated")).toBe(true);
|
||||
expect(() =>
|
||||
within(container).getByTestID(`comment-${commentData.id}`)
|
||||
).toThrow();
|
||||
|
||||
subscriptionHandler.dispatch<SubscriptionToCommentCreatedResolver>(
|
||||
"commentCreated",
|
||||
variables => {
|
||||
if (variables.storyID !== story.id) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
comment: commentData,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const viewMoreButton = await waitForElement(() =>
|
||||
within(testRenderer.root).getByText("View 1 New Comment", {
|
||||
exact: false,
|
||||
selector: "button",
|
||||
})
|
||||
);
|
||||
viewMoreButton.props.onClick();
|
||||
within(container).getByTestID(`comment-${commentData.id}`);
|
||||
});
|
||||
|
||||
it("should load more when ordering by oldest", async () => {
|
||||
const { testRenderer, subscriptionHandler } = await createTestRenderer({
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue("CREATED_AT_ASC", "commentsOrderBy");
|
||||
},
|
||||
});
|
||||
await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
const commentData = comments[5];
|
||||
|
||||
expect(subscriptionHandler.has("commentCreated")).toBe(true);
|
||||
expect(() =>
|
||||
within(testRenderer.root).getByText("Load More", {
|
||||
exact: false,
|
||||
selector: "button",
|
||||
})
|
||||
).toThrow();
|
||||
|
||||
await act(async () => {
|
||||
subscriptionHandler.dispatch<SubscriptionToCommentCreatedResolver>(
|
||||
"commentCreated",
|
||||
variables => {
|
||||
if (variables.storyID !== story.id) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
comment: commentData,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
await waitForElement(() =>
|
||||
within(testRenderer.root).getByText("Load More", {
|
||||
exact: false,
|
||||
selector: "button",
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not subscribe when story is closed", async () => {
|
||||
const { testRenderer, subscriptionHandler } = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
story: () => pureMerge<typeof story>(story, { isClosed: true }),
|
||||
},
|
||||
}),
|
||||
});
|
||||
await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
expect(subscriptionHandler.has("commentCreated")).toBe(false);
|
||||
});
|
||||
|
||||
it("should not subscribe when commenting is disabled", async () => {
|
||||
const { testRenderer, subscriptionHandler } = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
settings: () =>
|
||||
pureMerge<typeof settings>(settings, {
|
||||
disableCommenting: {
|
||||
enabled: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
});
|
||||
await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
expect(subscriptionHandler.has("commentCreated")).toBe(false);
|
||||
});
|
||||
@@ -97,14 +97,14 @@ beforeEach(() => {
|
||||
|
||||
it("renders comment stream with load more button", async () => {
|
||||
const streamLog = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-stream-log")
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
expect(within(streamLog).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("loads more comments", async () => {
|
||||
const streamLog = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-stream-log")
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
|
||||
// Get amount of comments before.
|
||||
|
||||
@@ -66,7 +66,7 @@ beforeEach(() => {
|
||||
|
||||
it("renders comment stream", async () => {
|
||||
const streamLog = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-stream-log")
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
// Wait for loading.
|
||||
expect(within(streamLog).toJSON()).toMatchSnapshot();
|
||||
@@ -74,7 +74,7 @@ it("renders comment stream", async () => {
|
||||
|
||||
it("post a reply", async () => {
|
||||
const streamLog = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-stream-log")
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
|
||||
const deepestReply = within(streamLog).getByTestID(
|
||||
|
||||
@@ -36,7 +36,7 @@ async function createTestRenderer(
|
||||
});
|
||||
|
||||
await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-stream-log")
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
|
||||
const tabPane = await waitForElement(() =>
|
||||
|
||||
@@ -42,7 +42,7 @@ async function createTestRenderer(
|
||||
it("renders comment stream", async () => {
|
||||
const { testRenderer } = await createTestRenderer();
|
||||
await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-stream-log")
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
expect(within(testRenderer.root).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ReactTestRenderer } from "react-test-renderer";
|
||||
import sinon from "sinon";
|
||||
|
||||
import {
|
||||
act,
|
||||
createSinonStub,
|
||||
wait,
|
||||
waitForElement,
|
||||
@@ -113,13 +114,15 @@ it("show all replies", async () => {
|
||||
const commentsBefore = within(commentReplyList).getAllByTestID(/^comment-/)
|
||||
.length;
|
||||
|
||||
within(commentReplyList)
|
||||
.getByText("Show All")
|
||||
.props.onClick();
|
||||
// Wait for loading.
|
||||
await wait(() =>
|
||||
expect(within(commentReplyList).queryByText("Show All")).toBeNull()
|
||||
);
|
||||
await act(async () => {
|
||||
within(commentReplyList)
|
||||
.getByText("Show All")
|
||||
.props.onClick();
|
||||
// Wait for loading.
|
||||
await wait(() =>
|
||||
expect(within(commentReplyList).queryByText("Show All")).toBeNull()
|
||||
);
|
||||
});
|
||||
|
||||
expect(within(commentReplyList).getAllByTestID(/^comment-/).length).toBe(
|
||||
commentsBefore + 1
|
||||
|
||||
@@ -45,7 +45,7 @@ beforeEach(() => {
|
||||
|
||||
it("renders deepest comment with link", async () => {
|
||||
const streamLog = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-stream-log")
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
const deepestReply = within(streamLog).getByTestID(
|
||||
"comment-comment-with-deepest-replies-5"
|
||||
@@ -58,7 +58,7 @@ it("shows conversation", async () => {
|
||||
preventDefault: sinon.mock().once(),
|
||||
};
|
||||
const streamLog = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-stream-log")
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
await act(async () => {
|
||||
within(streamLog)
|
||||
|
||||
@@ -67,7 +67,7 @@ it("renders app with comment stream", async () => {
|
||||
});
|
||||
|
||||
let streamLog = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-stream-log")
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
const selectField = within(testRenderer.root).getByLabelText("Sort By");
|
||||
const oldestOption = within(selectField).getByText("Oldest");
|
||||
@@ -79,7 +79,7 @@ it("renders app with comment stream", async () => {
|
||||
});
|
||||
|
||||
streamLog = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("comments-stream-log")
|
||||
within(testRenderer.root).getByTestID("comments-allComments-log")
|
||||
);
|
||||
});
|
||||
expect(within(streamLog).toJSON()).toMatchSnapshot();
|
||||
|
||||
@@ -155,6 +155,8 @@ export const baseComment = createFixture<GQLComment>({
|
||||
total: 0,
|
||||
},
|
||||
},
|
||||
parent: undefined,
|
||||
viewerActionPresence: { reaction: false, dontAgree: false, flag: false },
|
||||
tags: [],
|
||||
});
|
||||
|
||||
@@ -405,9 +407,6 @@ export const stories = denormalizeStories(
|
||||
{ node: comments[0], cursor: comments[0].createdAt },
|
||||
{ node: comments[1], cursor: comments[1].createdAt },
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -418,9 +417,6 @@ export const stories = denormalizeStories(
|
||||
{ node: comments[2], cursor: comments[2].createdAt },
|
||||
{ node: comments[3], cursor: comments[3].createdAt },
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -434,9 +430,6 @@ export const stories = denormalizeStories(
|
||||
cursor: commentsFromStaff[0].createdAt,
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -19,7 +19,7 @@ import { hasAncestors } from "coral-server/models/comment/helpers";
|
||||
import TenantContext from "../context";
|
||||
import { getURLWithCommentID } from "./util";
|
||||
|
||||
const maybeLoadOnlyID = (
|
||||
export const maybeLoadOnlyID = (
|
||||
ctx: TenantContext,
|
||||
info: GraphQLResolveInfo,
|
||||
id: string
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { GQLCommentCreatedPayloadTypeResolver } from "coral-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
import { maybeLoadOnlyID } from "./Comment";
|
||||
import { CommentCreatedInput } from "./Subscription/commentCreated";
|
||||
|
||||
export const CommentCreatedPayload: GQLCommentCreatedPayloadTypeResolver<
|
||||
CommentCreatedInput
|
||||
> = {
|
||||
comment: ({ commentID }, args, ctx, info) =>
|
||||
maybeLoadOnlyID(ctx, info, commentID),
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { GQLCommentEnteredModerationQueuePayloadTypeResolver } from "coral-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
import { maybeLoadOnlyID } from "./Comment";
|
||||
import { CommentEnteredModerationQueueInput } from "./Subscription/commentEnteredModerationQueue";
|
||||
|
||||
export const CommentEnteredModerationQueuePayload: GQLCommentEnteredModerationQueuePayloadTypeResolver<
|
||||
CommentEnteredModerationQueueInput
|
||||
> = {
|
||||
comment: ({ commentID }, args, ctx, info) =>
|
||||
maybeLoadOnlyID(ctx, info, commentID),
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { GQLCommentLeftModerationQueuePayloadTypeResolver } from "coral-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
import { maybeLoadOnlyID } from "./Comment";
|
||||
import { CommentLeftModerationQueueInput } from "./Subscription/commentLeftModerationQueue";
|
||||
|
||||
export const CommentLeftModerationQueuePayload: GQLCommentLeftModerationQueuePayloadTypeResolver<
|
||||
CommentLeftModerationQueueInput
|
||||
> = {
|
||||
comment: ({ commentID }, args, ctx, info) =>
|
||||
maybeLoadOnlyID(ctx, info, commentID),
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { GQLCommentReplyCreatedPayloadTypeResolver } from "coral-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
import { maybeLoadOnlyID } from "./Comment";
|
||||
import { CommentReplyCreatedInput } from "./Subscription/commentReplyCreated";
|
||||
|
||||
export const CommentReplyCreatedPayload: GQLCommentReplyCreatedPayloadTypeResolver<
|
||||
CommentReplyCreatedInput
|
||||
> = {
|
||||
comment: ({ commentID }, args, ctx, info) =>
|
||||
maybeLoadOnlyID(ctx, info, commentID),
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { GQLCommentStatusUpdatedPayloadTypeResolver } from "coral-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
import { maybeLoadOnlyID } from "./Comment";
|
||||
import { CommentStatusUpdatedInput } from "./Subscription/commentStatusUpdated";
|
||||
|
||||
export const CommentStatusUpdatedPayload: GQLCommentStatusUpdatedPayloadTypeResolver<
|
||||
CommentStatusUpdatedInput
|
||||
> = {
|
||||
moderator: ({ moderatorID }, args, ctx) =>
|
||||
moderatorID ? ctx.loaders.Users.user.load(moderatorID) : null,
|
||||
comment: ({ commentID }, args, ctx, info) =>
|
||||
maybeLoadOnlyID(ctx, info, commentID),
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { SubscriptionToCommentCreatedResolver } from "coral-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
import { createIterator } from "./helpers";
|
||||
import { SUBSCRIPTION_CHANNELS, SubscriptionPayload } from "./types";
|
||||
|
||||
export interface CommentCreatedInput extends SubscriptionPayload {
|
||||
storyID: string;
|
||||
commentID: string;
|
||||
}
|
||||
|
||||
export const commentCreated: SubscriptionToCommentCreatedResolver<
|
||||
CommentCreatedInput
|
||||
> = createIterator(SUBSCRIPTION_CHANNELS.COMMENT_CREATED, {
|
||||
filter: (source, { storyID }) => {
|
||||
if (source.storyID !== storyID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
@@ -31,8 +31,4 @@ export const commentEnteredModerationQueue: SubscriptionToCommentEnteredModerati
|
||||
|
||||
return true;
|
||||
},
|
||||
resolve: ({ queue, commentID }, args, ctx) => ({
|
||||
queue: () => queue,
|
||||
comment: () => ctx.loaders.Comments.comment.load(commentID),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -30,8 +30,4 @@ export const commentLeftModerationQueue: SubscriptionToCommentLeftModerationQueu
|
||||
|
||||
return true;
|
||||
},
|
||||
resolve: ({ queue, commentID }, args, ctx) => ({
|
||||
queue: () => queue,
|
||||
comment: () => ctx.loaders.Comments.comment.load(commentID),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { SubscriptionToCommentReplyCreatedResolver } from "coral-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
import { createIterator } from "./helpers";
|
||||
import { SUBSCRIPTION_CHANNELS, SubscriptionPayload } from "./types";
|
||||
|
||||
export interface CommentReplyCreatedInput extends SubscriptionPayload {
|
||||
ancestorIDs: string[];
|
||||
commentID: string;
|
||||
}
|
||||
|
||||
export const commentReplyCreated: SubscriptionToCommentReplyCreatedResolver<
|
||||
CommentReplyCreatedInput
|
||||
> = createIterator(SUBSCRIPTION_CHANNELS.COMMENT_REPLY_CREATED, {
|
||||
filter: (source, { ancestorID }) => {
|
||||
if (!source.ancestorIDs.includes(ancestorID)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
@@ -25,16 +25,4 @@ export const commentStatusUpdated: SubscriptionToCommentStatusUpdatedResolver<
|
||||
|
||||
return true;
|
||||
},
|
||||
resolve: ({ newStatus, oldStatus, moderatorID, commentID }, args, ctx) => ({
|
||||
newStatus: () => newStatus,
|
||||
oldStatus: () => oldStatus,
|
||||
moderator: () => {
|
||||
if (moderatorID) {
|
||||
return ctx.loaders.Users.user.load(moderatorID);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
comment: () => ctx.loaders.Comments.comment.load(commentID),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ type Resolver<TParent, TArgs, TResult> = (
|
||||
|
||||
interface SubscriptionResolver<TParent, TArgs, TResult> {
|
||||
subscribe: Resolver<TParent, TArgs, AsyncIterator<TResult>>;
|
||||
resolve?: Resolver<TParent, TArgs, TResult>;
|
||||
resolve: Resolver<TParent, TArgs, TParent>;
|
||||
}
|
||||
|
||||
export function createTenantAsyncIterator<TParent, TArgs, TResult>(
|
||||
@@ -87,7 +87,6 @@ export function createFilterFn<TParent, TArgs>(
|
||||
|
||||
export interface CreateIteratorInput<TParent, TArgs, TResult> {
|
||||
filter?: FilterFn<TParent, TArgs, TenantContext>;
|
||||
resolve?: Resolver<TParent, TArgs, TResult>;
|
||||
}
|
||||
|
||||
export function createIterator<
|
||||
@@ -96,13 +95,13 @@ export function createIterator<
|
||||
TResult
|
||||
>(
|
||||
channel: SUBSCRIPTION_CHANNELS,
|
||||
{ filter, resolve }: CreateIteratorInput<TParent, TArgs, TResult> = {}
|
||||
{ filter }: CreateIteratorInput<TParent, TArgs, TResult> = {}
|
||||
): SubscriptionResolver<TParent, TArgs, TResult> {
|
||||
return {
|
||||
subscribe: withFilter(
|
||||
createTenantAsyncIterator(channel),
|
||||
createFilterFn(filter)
|
||||
),
|
||||
resolve,
|
||||
resolve: payload => payload,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { GQLSubscriptionTypeResolver } from "coral-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
import { commentCreated } from "./commentCreated";
|
||||
import { commentEnteredModerationQueue } from "./commentEnteredModerationQueue";
|
||||
import { commentLeftModerationQueue } from "./commentLeftModerationQueue";
|
||||
import { commentReplyCreated } from "./commentReplyCreated";
|
||||
import { commentStatusUpdated } from "./commentStatusUpdated";
|
||||
|
||||
export const Subscription: GQLSubscriptionTypeResolver = {
|
||||
commentCreated,
|
||||
commentEnteredModerationQueue,
|
||||
commentLeftModerationQueue,
|
||||
commentReplyCreated,
|
||||
commentStatusUpdated,
|
||||
};
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { CommentCreatedInput } from "./commentCreated";
|
||||
import { CommentEnteredModerationQueueInput } from "./commentEnteredModerationQueue";
|
||||
import { CommentLeftModerationQueueInput } from "./commentLeftModerationQueue";
|
||||
import { CommentReplyCreatedInput } from "./commentReplyCreated";
|
||||
import { CommentStatusUpdatedInput } from "./commentStatusUpdated";
|
||||
|
||||
export enum SUBSCRIPTION_CHANNELS {
|
||||
COMMENT_ENTERED_MODERATION_QUEUE = "COMMENT_ENTERED_MODERATION_QUEUE",
|
||||
COMMENT_LEFT_MODERATION_QUEUE = "COMMENT_LEFT_MODERATION_QUEUE",
|
||||
COMMENT_STATUS_UPDATED = "COMMENT_STATUS_UPDATED",
|
||||
COMMENT_REPLY_CREATED = "COMMENT_REPLY_CREATED",
|
||||
COMMENT_CREATED = "COMMENT_CREATED",
|
||||
}
|
||||
|
||||
export interface SubscriptionPayload {
|
||||
@@ -32,4 +36,12 @@ export type SUBSCRIPTION_INPUT =
|
||||
| SubscriptionType<
|
||||
SUBSCRIPTION_CHANNELS.COMMENT_STATUS_UPDATED,
|
||||
CommentStatusUpdatedInput
|
||||
>
|
||||
| SubscriptionType<
|
||||
SUBSCRIPTION_CHANNELS.COMMENT_REPLY_CREATED,
|
||||
CommentReplyCreatedInput
|
||||
>
|
||||
| SubscriptionType<
|
||||
SUBSCRIPTION_CHANNELS.COMMENT_CREATED,
|
||||
CommentCreatedInput
|
||||
>;
|
||||
|
||||
@@ -9,8 +9,13 @@ import { BanStatusHistory } from "./BanStatusHistory";
|
||||
import { CloseCommenting } from "./CloseCommenting";
|
||||
import { Comment } from "./Comment";
|
||||
import { CommentCounts } from "./CommentCounts";
|
||||
import { CommentCreatedPayload } from "./CommentCreatedPayload";
|
||||
import { CommentEnteredModerationQueuePayload } from "./CommentEnteredModerationQueuePayload";
|
||||
import { CommentLeftModerationQueuePayload } from "./CommentLeftModerationQueuePayload";
|
||||
import { CommentModerationAction } from "./CommentModerationAction";
|
||||
import { CommentReplyCreatedPayload } from "./CommentReplyCreatedPayload";
|
||||
import { CommentRevision } from "./CommentRevision";
|
||||
import { CommentStatusUpdatedPayload } from "./CommentStatusUpdatedPayload";
|
||||
import { DisableCommenting } from "./DisableCommenting";
|
||||
import { FacebookAuthIntegration } from "./FacebookAuthIntegration";
|
||||
import { FeatureCommentPayload } from "./FeatureCommentPayload";
|
||||
@@ -41,8 +46,13 @@ const Resolvers: GQLResolver = {
|
||||
CloseCommenting,
|
||||
Comment,
|
||||
CommentCounts,
|
||||
CommentCreatedPayload,
|
||||
CommentEnteredModerationQueuePayload,
|
||||
CommentLeftModerationQueuePayload,
|
||||
CommentModerationAction,
|
||||
CommentReplyCreatedPayload,
|
||||
CommentRevision,
|
||||
CommentStatusUpdatedPayload,
|
||||
Cursor,
|
||||
DisableCommenting,
|
||||
FacebookAuthIntegration,
|
||||
|
||||
@@ -4633,6 +4633,28 @@ type CommentLeftModerationQueuePayload {
|
||||
comment: Comment!
|
||||
}
|
||||
|
||||
"""
|
||||
CommentCreatedPayload is returned when a new top level Comment is created on a
|
||||
Story.
|
||||
"""
|
||||
type CommentCreatedPayload {
|
||||
"""
|
||||
comment is the new top level Comment that was created on the Story.
|
||||
"""
|
||||
comment: Comment!
|
||||
}
|
||||
|
||||
"""
|
||||
CommentReplyCreatedPayload is returned when a Comment is created as a reply to
|
||||
another Comment where the selected ancestor Comment is in the ancestor chain.
|
||||
"""
|
||||
type CommentReplyCreatedPayload {
|
||||
"""
|
||||
comment is the new reply Comment that was created.
|
||||
"""
|
||||
comment: Comment!
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
"""
|
||||
commentEnteredModerationQueue returns when a Comment enters a ModerationQueue.
|
||||
@@ -4658,4 +4680,16 @@ type Subscription {
|
||||
"""
|
||||
commentStatusUpdated(id: ID): CommentStatusUpdatedPayload!
|
||||
@auth(roles: [MODERATOR, ADMIN])
|
||||
|
||||
"""
|
||||
commentCreated returns when a Comment is created on the top level of a Story
|
||||
that is visible.
|
||||
"""
|
||||
commentCreated(storyID: ID!): CommentCreatedPayload!
|
||||
|
||||
"""
|
||||
commentReplyCreated returns when a Comment is posted in the ancestor chain of
|
||||
comments.
|
||||
"""
|
||||
commentReplyCreated(ancestorID: ID!): CommentReplyCreatedPayload!
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ import {
|
||||
import { Tenant } from "coral-server/models/tenant";
|
||||
import { User } from "coral-server/models/user";
|
||||
import {
|
||||
publishCommentCreated,
|
||||
publishCommentReplyCreated,
|
||||
publishCommentStatusChanges,
|
||||
publishModerationQueueChanges,
|
||||
} from "coral-server/services/events";
|
||||
@@ -214,9 +216,19 @@ export async function create(
|
||||
}
|
||||
const moderationQueue = calculateCounts(comment);
|
||||
|
||||
// Publish changes to the queue.
|
||||
// Publish changes.
|
||||
publishModerationQueueChanges(publish, moderationQueue, comment);
|
||||
|
||||
// If this is a reply, publish it.
|
||||
if (input.parentID) {
|
||||
publishCommentReplyCreated(publish, comment);
|
||||
}
|
||||
|
||||
// If this comment is visible (and not a reply), publish it.
|
||||
if (!input.parentID && hasVisibleStatus(comment)) {
|
||||
publishCommentCreated(publish, comment);
|
||||
}
|
||||
|
||||
// Compile the changes we want to apply to the story counts.
|
||||
const storyCounts: Required<Omit<StoryCounts, "action">> = {
|
||||
// This is a new comment, so we need to increment for this status.
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
GQLMODERATION_QUEUE,
|
||||
} from "coral-server/graph/tenant/schema/__generated__/types";
|
||||
import { Publisher } from "coral-server/graph/tenant/subscriptions/publisher";
|
||||
import { Comment } from "coral-server/models/comment";
|
||||
import { Comment, hasVisibleStatus } from "coral-server/models/comment";
|
||||
import { CommentModerationQueueCounts } from "coral-server/models/story/counts";
|
||||
|
||||
export function publishCommentStatusChanges(
|
||||
@@ -27,6 +27,36 @@ export function publishCommentStatusChanges(
|
||||
}
|
||||
}
|
||||
|
||||
export function publishCommentReplyCreated(
|
||||
publish: Publisher,
|
||||
comment: Pick<Comment, "id" | "status" | "ancestorIDs">
|
||||
) {
|
||||
if (comment.ancestorIDs.length > 0 && hasVisibleStatus(comment)) {
|
||||
publish({
|
||||
channel: SUBSCRIPTION_CHANNELS.COMMENT_REPLY_CREATED,
|
||||
payload: {
|
||||
ancestorIDs: comment.ancestorIDs,
|
||||
commentID: comment.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function publishCommentCreated(
|
||||
publish: Publisher,
|
||||
comment: Pick<Comment, "id" | "storyID" | "parentID" | "status">
|
||||
) {
|
||||
if (!comment.parentID && hasVisibleStatus(comment)) {
|
||||
publish({
|
||||
channel: SUBSCRIPTION_CHANNELS.COMMENT_CREATED,
|
||||
payload: {
|
||||
commentID: comment.id,
|
||||
storyID: comment.storyID,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function publishModerationQueueChanges(
|
||||
publish: Publisher,
|
||||
moderationQueue: Pick<CommentModerationQueueCounts, "queues">,
|
||||
|
||||
@@ -30,8 +30,16 @@ comments-featuredCommentTooltip-toggleButton =
|
||||
comments-streamQuery-storyNotFound = Story not found
|
||||
|
||||
comments-postCommentForm-submit = Submit
|
||||
comments-stream-loadMore = Load More
|
||||
comments-replyList-showAll = Show All
|
||||
comments-replyList-showMoreReplies = Show More Replies
|
||||
|
||||
|
||||
comments-viewNew =
|
||||
{ $count ->
|
||||
[1] View {$count} New Comment
|
||||
*[other] View {$count} New Comments
|
||||
}
|
||||
comments-loadMore = Load More
|
||||
|
||||
comments-permalinkPopover =
|
||||
.description = A dialog showing a permalink to the comment
|
||||
|
||||
Reference in New Issue
Block a user