mirror of
https://github.com/wassname/talk.git
synced 2026-07-31 12:50:48 +08:00
[next] Comment Moderation Actions (#2068)
* fix: renamed snake case to camel case * fix: changed case for mutators * fix: renamed all snake case to camel case for db * feat: added support for comment revisions + split comment actions * fix: updated tests * feat: implemented CommentModerationAction * fix: fixed case issues * feat: enabled WeakMap for wordList processsing * chore: npm audit
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import TenantContext from "talk-server/graph/tenant/context";
|
||||
import {
|
||||
CommentModerationActionFilter,
|
||||
retrieveCommentModerationActionConnection,
|
||||
retrieveCommentModerationActions,
|
||||
} from "talk-server/models/action/moderation/comment";
|
||||
import { UserToCommentModerationActionHistoryArgs } from "../schema/__generated__/types";
|
||||
|
||||
export default (ctx: TenantContext) => ({
|
||||
commentModerationActions: (filter: CommentModerationActionFilter) =>
|
||||
retrieveCommentModerationActions(ctx.mongo, ctx.tenant.id, filter),
|
||||
commentModerationActionsConnection: (
|
||||
{ first = 10, after }: UserToCommentModerationActionHistoryArgs,
|
||||
moderatorID: string
|
||||
) =>
|
||||
retrieveCommentModerationActionConnection(ctx.mongo, ctx.tenant.id, {
|
||||
first,
|
||||
after,
|
||||
filter: {
|
||||
moderatorID,
|
||||
},
|
||||
}),
|
||||
});
|
||||
+3
-7
@@ -8,10 +8,7 @@ import {
|
||||
GQLCOMMENT_SORT,
|
||||
StoryToCommentsArgs,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import {
|
||||
ACTION_ITEM_TYPE,
|
||||
retrieveManyUserActionPresence,
|
||||
} from "talk-server/models/action";
|
||||
import { retrieveManyUserActionPresence } from "talk-server/models/action/comment";
|
||||
import {
|
||||
Comment,
|
||||
retrieveCommentParentsConnection,
|
||||
@@ -44,14 +41,13 @@ export default (ctx: Context) => ({
|
||||
retrieveManyComments(ctx.mongo, ctx.tenant.id, ids)
|
||||
),
|
||||
retrieveMyActionPresence: new DataLoader<string, GQLActionPresence>(
|
||||
(itemIDs: string[]) =>
|
||||
(commentIDs: string[]) =>
|
||||
retrieveManyUserActionPresence(
|
||||
ctx.mongo,
|
||||
ctx.tenant.id,
|
||||
// This should only ever be accessed when a user is logged in.
|
||||
ctx.user!.id,
|
||||
ACTION_ITEM_TYPE.COMMENTS,
|
||||
itemIDs
|
||||
commentIDs
|
||||
)
|
||||
),
|
||||
forUser: (
|
||||
@@ -1,12 +1,14 @@
|
||||
import Context from "talk-server/graph/tenant/context";
|
||||
|
||||
import Auth from "./auth";
|
||||
import Comments from "./comments";
|
||||
import Stories from "./stories";
|
||||
import Users from "./users";
|
||||
import Actions from "./Actions";
|
||||
import Auth from "./Auth";
|
||||
import Comments from "./Comments";
|
||||
import Stories from "./Stories";
|
||||
import Users from "./Users";
|
||||
|
||||
export default (ctx: Context) => ({
|
||||
Auth: Auth(ctx),
|
||||
Actions: Actions(ctx),
|
||||
Stories: Stories(ctx),
|
||||
Comments: Comments(ctx),
|
||||
Users: Users(ctx),
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import TenantContext from "talk-server/graph/tenant/context";
|
||||
import { accept, reject } from "talk-server/services/moderation";
|
||||
import {
|
||||
GQLAcceptCommentInput,
|
||||
GQLRejectCommentInput,
|
||||
} from "../schema/__generated__/types";
|
||||
|
||||
export const Actions = (ctx: TenantContext) => ({
|
||||
acceptComment: (input: GQLAcceptCommentInput) =>
|
||||
accept(ctx.mongo, ctx.tenant, {
|
||||
commentID: input.commentID,
|
||||
commentRevisionID: input.commentRevisionID,
|
||||
moderatorID: ctx.user!.id,
|
||||
}),
|
||||
rejectComment: (input: GQLRejectCommentInput) =>
|
||||
reject(ctx.mongo, ctx.tenant, {
|
||||
commentID: input.commentID,
|
||||
commentRevisionID: input.commentRevisionID,
|
||||
moderatorID: ctx.user!.id,
|
||||
}),
|
||||
});
|
||||
+32
-24
@@ -19,54 +19,62 @@ import {
|
||||
removeReaction,
|
||||
} from "talk-server/services/comments/actions";
|
||||
|
||||
export default (ctx: TenantContext) => ({
|
||||
create: (input: GQLCreateCommentInput) =>
|
||||
export const Comment = (ctx: TenantContext) => ({
|
||||
create: ({ storyID, body, parentID }: GQLCreateCommentInput) =>
|
||||
create(
|
||||
ctx.mongo,
|
||||
ctx.tenant,
|
||||
ctx.user!,
|
||||
{
|
||||
author_id: ctx.user!.id,
|
||||
story_id: input.storyID,
|
||||
body: input.body,
|
||||
parent_id: input.parentID,
|
||||
},
|
||||
{ authorID: ctx.user!.id, storyID, body, parentID },
|
||||
ctx.req
|
||||
),
|
||||
edit: (input: GQLEditCommentInput) =>
|
||||
edit: ({ commentID, body }: GQLEditCommentInput) =>
|
||||
edit(
|
||||
ctx.mongo,
|
||||
ctx.tenant,
|
||||
ctx.user!,
|
||||
{
|
||||
id: input.commentID,
|
||||
body: input.body,
|
||||
id: commentID,
|
||||
body,
|
||||
},
|
||||
ctx.req
|
||||
),
|
||||
createReaction: (input: GQLCreateCommentReactionInput) =>
|
||||
createReaction: ({
|
||||
commentID,
|
||||
commentRevisionID,
|
||||
}: GQLCreateCommentReactionInput) =>
|
||||
createReaction(ctx.mongo, ctx.tenant, ctx.user!, {
|
||||
item_id: input.commentID,
|
||||
commentID,
|
||||
commentRevisionID,
|
||||
}),
|
||||
removeReaction: (input: GQLRemoveCommentReactionInput) =>
|
||||
removeReaction: ({ commentID }: GQLRemoveCommentReactionInput) =>
|
||||
removeReaction(ctx.mongo, ctx.tenant, ctx.user!, {
|
||||
item_id: input.commentID,
|
||||
commentID,
|
||||
}),
|
||||
createDontAgree: (input: GQLCreateCommentDontAgreeInput) =>
|
||||
createDontAgree: ({
|
||||
commentID,
|
||||
commentRevisionID,
|
||||
}: GQLCreateCommentDontAgreeInput) =>
|
||||
createDontAgree(ctx.mongo, ctx.tenant, ctx.user!, {
|
||||
item_id: input.commentID,
|
||||
commentID,
|
||||
commentRevisionID,
|
||||
}),
|
||||
removeDontAgree: (input: GQLRemoveCommentDontAgreeInput) =>
|
||||
removeDontAgree: ({ commentID }: GQLRemoveCommentDontAgreeInput) =>
|
||||
removeDontAgree(ctx.mongo, ctx.tenant, ctx.user!, {
|
||||
item_id: input.commentID,
|
||||
commentID,
|
||||
}),
|
||||
createFlag: (input: GQLCreateCommentFlagInput) =>
|
||||
createFlag: ({
|
||||
commentID,
|
||||
commentRevisionID,
|
||||
reason,
|
||||
}: GQLCreateCommentFlagInput) =>
|
||||
createFlag(ctx.mongo, ctx.tenant, ctx.user!, {
|
||||
item_id: input.commentID,
|
||||
reason: input.reason,
|
||||
commentID,
|
||||
commentRevisionID,
|
||||
reason,
|
||||
}),
|
||||
removeFlag: (input: GQLRemoveCommentFlagInput) =>
|
||||
removeFlag: ({ commentID }: GQLRemoveCommentFlagInput) =>
|
||||
removeFlag(ctx.mongo, ctx.tenant, ctx.user!, {
|
||||
item_id: input.commentID,
|
||||
commentID,
|
||||
}),
|
||||
});
|
||||
+6
-1
@@ -16,7 +16,12 @@ import {
|
||||
updateOIDCAuthIntegration,
|
||||
} from "talk-server/services/tenant";
|
||||
|
||||
export default ({ mongo, redis, tenantCache, tenant }: TenantContext) => ({
|
||||
export const Settings = ({
|
||||
mongo,
|
||||
redis,
|
||||
tenantCache,
|
||||
tenant,
|
||||
}: TenantContext) => ({
|
||||
update: (input: GQLSettingsInput): Promise<Tenant | null> =>
|
||||
update(mongo, redis, tenantCache, tenant, omitBy(input, isNull)),
|
||||
regenerateSSOKey: (): Promise<Tenant | null> =>
|
||||
+17
-7
@@ -8,12 +8,14 @@ import {
|
||||
GQLScrapeStoryInput,
|
||||
GQLUpdateStoryInput,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { Story } from "talk-server/models/story";
|
||||
import * as story from "talk-server/models/story";
|
||||
import { create, merge, remove, update } from "talk-server/services/stories";
|
||||
import { scrape } from "talk-server/services/stories/scraper";
|
||||
|
||||
export default (ctx: TenantContext) => ({
|
||||
create: async (input: GQLCreateStoryInput): Promise<Readonly<Story> | null> =>
|
||||
export const Story = (ctx: TenantContext) => ({
|
||||
create: async (
|
||||
input: GQLCreateStoryInput
|
||||
): Promise<Readonly<story.Story> | null> =>
|
||||
create(
|
||||
ctx.mongo,
|
||||
ctx.tenant,
|
||||
@@ -21,12 +23,20 @@ export default (ctx: TenantContext) => ({
|
||||
input.story.url,
|
||||
omitBy(input.story, isNull)
|
||||
),
|
||||
update: async (input: GQLUpdateStoryInput): Promise<Readonly<Story> | null> =>
|
||||
update: async (
|
||||
input: GQLUpdateStoryInput
|
||||
): Promise<Readonly<story.Story> | null> =>
|
||||
update(ctx.mongo, ctx.tenant, input.id, omitBy(input.story, isNull)),
|
||||
merge: async (input: GQLMergeStoriesInput): Promise<Readonly<Story> | null> =>
|
||||
merge: async (
|
||||
input: GQLMergeStoriesInput
|
||||
): Promise<Readonly<story.Story> | null> =>
|
||||
merge(ctx.mongo, ctx.tenant, input.destinationID, input.sourceIDs),
|
||||
remove: async (input: GQLRemoveStoryInput): Promise<Readonly<Story> | null> =>
|
||||
remove: async (
|
||||
input: GQLRemoveStoryInput
|
||||
): Promise<Readonly<story.Story> | null> =>
|
||||
remove(ctx.mongo, ctx.tenant, input.id, input.includeComments),
|
||||
scrape: async (input: GQLScrapeStoryInput): Promise<Readonly<Story> | null> =>
|
||||
scrape: async (
|
||||
input: GQLScrapeStoryInput
|
||||
): Promise<Readonly<story.Story> | null> =>
|
||||
scrape(ctx.mongo, ctx.tenant.id, input.id),
|
||||
});
|
||||
@@ -1,10 +1,12 @@
|
||||
import TenantContext from "talk-server/graph/tenant/context";
|
||||
|
||||
import Comment from "./comment";
|
||||
import Settings from "./settings";
|
||||
import Story from "./story";
|
||||
import { Actions } from "./Actions";
|
||||
import { Comment } from "./Comment";
|
||||
import { Settings } from "./Settings";
|
||||
import { Story } from "./Story";
|
||||
|
||||
export default (ctx: TenantContext) => ({
|
||||
Actions: Actions(ctx),
|
||||
Comment: Comment(ctx),
|
||||
Settings: Settings(ctx),
|
||||
Story: Story(ctx),
|
||||
|
||||
+3
-3
@@ -5,12 +5,12 @@ import {
|
||||
|
||||
const disabled = { enabled: false };
|
||||
|
||||
const AuthIntegrations: GQLAuthIntegrationsTypeResolver<GQLAuthIntegrations> = {
|
||||
export const AuthIntegrations: GQLAuthIntegrationsTypeResolver<
|
||||
GQLAuthIntegrations
|
||||
> = {
|
||||
local: auth => auth.local || disabled,
|
||||
sso: auth => auth.sso || disabled,
|
||||
oidc: auth => auth.oidc || disabled,
|
||||
google: auth => auth.google || disabled,
|
||||
facebook: auth => auth.facebook || disabled,
|
||||
};
|
||||
|
||||
export default AuthIntegrations;
|
||||
@@ -0,0 +1,80 @@
|
||||
import { GraphQLResolveInfo } from "graphql";
|
||||
import { getRequestedFields } from "talk-server/graph/tenant/resolvers/util";
|
||||
import {
|
||||
GQLComment,
|
||||
GQLCommentTypeResolver,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { decodeActionCounts } from "talk-server/models/action/comment";
|
||||
import * as comment from "talk-server/models/comment";
|
||||
import { getLatestRevision } from "talk-server/models/comment";
|
||||
import { createConnection } from "talk-server/models/connection";
|
||||
|
||||
import TenantContext from "../context";
|
||||
|
||||
const maybeLoadOnlyID = (
|
||||
ctx: TenantContext,
|
||||
info: GraphQLResolveInfo,
|
||||
id?: string
|
||||
) => {
|
||||
// If there isn't an id, then return nothing!
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the field names of the fields being requested, if it's only the ID,
|
||||
// we have that, so no need to make a database request.
|
||||
const fields = getRequestedFields<GQLComment>(info);
|
||||
if (fields.length === 1 && fields[0] === "id") {
|
||||
return {
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
// We want more than the ID! Get the comment!
|
||||
// TODO: (wyattjoh) if the parent and the parents (containing the parent) are requested, the parent comment is retrieved from the database twice. Investigate ways of reducing i/o.
|
||||
return ctx.loaders.Comments.comment.load(id);
|
||||
};
|
||||
|
||||
export const Comment: GQLCommentTypeResolver<comment.Comment> = {
|
||||
body: c => getLatestRevision(c).body,
|
||||
// Send the whole comment back when you request revisions. This way, we get to
|
||||
// know the comment ID. The field mapping is handled by the CommentRevision
|
||||
// resolver.
|
||||
revision: c => ({ revision: getLatestRevision(c), comment: c }),
|
||||
revisionHistory: c => c.revisions.map(revision => ({ revision, comment: c })),
|
||||
editing: ({ revisions, createdAt }, input, ctx) => ({
|
||||
// When there is more than one body history, then the comment has been
|
||||
// edited.
|
||||
edited: revisions.length > 1,
|
||||
// The date that the comment is editable until is the tenant's edit window
|
||||
// length added to the comment created date.
|
||||
editableUntil: new Date(
|
||||
createdAt.valueOf() + ctx.tenant.editCommentWindowLength
|
||||
),
|
||||
}),
|
||||
author: (c, input, ctx) => ctx.loaders.Users.user.load(c.authorID),
|
||||
statusHistory: ({ id }, input, ctx) =>
|
||||
ctx.loaders.Actions.commentModerationActions({
|
||||
commentID: id,
|
||||
}),
|
||||
replies: (c, input, ctx) =>
|
||||
c.replyCount > 0
|
||||
? ctx.loaders.Comments.forParent(c.storyID, c.id, input)
|
||||
: createConnection(),
|
||||
actionCounts: c => decodeActionCounts(c.actionCounts),
|
||||
myActionPresence: (c, input, ctx) =>
|
||||
ctx.user ? ctx.loaders.Comments.retrieveMyActionPresence.load(c.id) : null,
|
||||
parentCount: c => (c.parentID ? c.grandparentIDs.length + 1 : 0),
|
||||
depth: c => (c.parentID ? c.grandparentIDs.length + 1 : 0),
|
||||
rootParent: (c, input, ctx, info) =>
|
||||
maybeLoadOnlyID(
|
||||
ctx,
|
||||
info,
|
||||
c.grandparentIDs.length > 0 ? c.grandparentIDs[0] : c.parentID
|
||||
),
|
||||
parent: (c, input, ctx, info) => maybeLoadOnlyID(ctx, info, c.parentID),
|
||||
parents: (c, input, ctx) =>
|
||||
// Some resolver optimization.
|
||||
c.parentID ? ctx.loaders.Comments.parents(c, input) : createConnection(),
|
||||
story: (c, input, ctx) => ctx.loaders.Stories.story.load(c.storyID),
|
||||
};
|
||||
+3
-3
@@ -4,11 +4,11 @@ import {
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { CommentStatusCounts } from "talk-server/models/story";
|
||||
|
||||
const CommentCounts: GQLCommentCountsTypeResolver<CommentStatusCounts> = {
|
||||
export const CommentCounts: GQLCommentCountsTypeResolver<
|
||||
CommentStatusCounts
|
||||
> = {
|
||||
totalVisible: commentCounts =>
|
||||
commentCounts[GQLCOMMENT_STATUS.ACCEPTED] +
|
||||
commentCounts[GQLCOMMENT_STATUS.NONE],
|
||||
statuses: commentCounts => commentCounts,
|
||||
};
|
||||
|
||||
export default CommentCounts;
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as actions from "talk-server/models/action/moderation/comment";
|
||||
import { GQLCommentModerationActionTypeResolver } from "../schema/__generated__/types";
|
||||
|
||||
export const CommentModerationAction: GQLCommentModerationActionTypeResolver<
|
||||
actions.CommentModerationAction
|
||||
> = {
|
||||
revision: async (action, input, ctx) => {
|
||||
const comment = await ctx.loaders.Comments.comment.load(action.commentID);
|
||||
if (!comment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const revision = comment.revisions.find(
|
||||
({ id }) => id === action.commentRevisionID
|
||||
);
|
||||
if (!revision) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { comment, revision };
|
||||
},
|
||||
moderator: (action, input, ctx) =>
|
||||
action.moderatorID ? ctx.loaders.Users.user.load(action.moderatorID) : null,
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { GQLCommentRevisionTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { decodeActionCounts } from "talk-server/models/action/comment";
|
||||
import * as comment from "talk-server/models/comment";
|
||||
|
||||
export interface WrappedCommentRevision {
|
||||
revision: comment.Revision;
|
||||
comment: comment.Comment;
|
||||
}
|
||||
|
||||
export const CommentRevision: Required<
|
||||
GQLCommentRevisionTypeResolver<WrappedCommentRevision>
|
||||
> = {
|
||||
id: w => w.revision.id,
|
||||
comment: w => w.comment,
|
||||
actionCounts: w => decodeActionCounts(w.revision.actionCounts),
|
||||
body: w => w.revision.body,
|
||||
createdAt: w => w.revision.createdAt,
|
||||
};
|
||||
+1
-3
@@ -4,7 +4,7 @@ import {
|
||||
GQLFacebookAuthIntegrationTypeResolver,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
const FacebookAuthIntegration: GQLFacebookAuthIntegrationTypeResolver<
|
||||
export const FacebookAuthIntegration: GQLFacebookAuthIntegrationTypeResolver<
|
||||
GQLFacebookAuthIntegration
|
||||
> = {
|
||||
callbackURL: (integration, args, ctx) => {
|
||||
@@ -22,5 +22,3 @@ const FacebookAuthIntegration: GQLFacebookAuthIntegrationTypeResolver<
|
||||
return constructTenantURL(ctx.config, ctx.tenant, path);
|
||||
},
|
||||
};
|
||||
|
||||
export default FacebookAuthIntegration;
|
||||
+1
-3
@@ -4,7 +4,7 @@ import {
|
||||
GQLGoogleAuthIntegrationTypeResolver,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
const GoogleAuthIntegration: GQLGoogleAuthIntegrationTypeResolver<
|
||||
export const GoogleAuthIntegration: GQLGoogleAuthIntegrationTypeResolver<
|
||||
GQLGoogleAuthIntegration
|
||||
> = {
|
||||
callbackURL: (integration, args, ctx) => {
|
||||
@@ -22,5 +22,3 @@ const GoogleAuthIntegration: GQLGoogleAuthIntegrationTypeResolver<
|
||||
return constructTenantURL(ctx.config, ctx.tenant, path);
|
||||
},
|
||||
};
|
||||
|
||||
export default GoogleAuthIntegration;
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
import { GQLMutationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
const Mutation: GQLMutationTypeResolver<void> = {
|
||||
export const Mutation: GQLMutationTypeResolver<void> = {
|
||||
editComment: async (source, { input }, ctx) => ({
|
||||
comment: await ctx.mutators.Comment.edit(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
@@ -107,6 +107,12 @@ const Mutation: GQLMutationTypeResolver<void> = {
|
||||
story: await ctx.mutators.Story.scrape(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
acceptComment: async (source, { input }, ctx) => ({
|
||||
comment: await ctx.mutators.Actions.acceptComment(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
rejectComment: async (source, { input }, ctx) => ({
|
||||
comment: await ctx.mutators.Actions.rejectComment(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
};
|
||||
|
||||
export default Mutation;
|
||||
+1
-3
@@ -4,7 +4,7 @@ import {
|
||||
GQLOIDCAuthIntegrationTypeResolver,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
const OIDCAuthIntegration: GQLOIDCAuthIntegrationTypeResolver<
|
||||
export const OIDCAuthIntegration: GQLOIDCAuthIntegrationTypeResolver<
|
||||
GQLOIDCAuthIntegration
|
||||
> = {
|
||||
callbackURL: (integration, args, ctx) => {
|
||||
@@ -22,5 +22,3 @@ const OIDCAuthIntegration: GQLOIDCAuthIntegrationTypeResolver<
|
||||
return constructTenantURL(ctx.config, ctx.tenant, path);
|
||||
},
|
||||
};
|
||||
|
||||
export default OIDCAuthIntegration;
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
import { GQLProfileTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
import { Profile } from "talk-server/models/user";
|
||||
import * as user from "talk-server/models/user";
|
||||
|
||||
const resolveType: GQLProfileTypeResolver<Profile> = profile => {
|
||||
const resolveType: GQLProfileTypeResolver<user.Profile> = profile => {
|
||||
switch (profile.type) {
|
||||
case "local":
|
||||
return "LocalProfile";
|
||||
@@ -16,6 +16,6 @@ const resolveType: GQLProfileTypeResolver<Profile> = profile => {
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
export const Profile = {
|
||||
__resolveType: resolveType,
|
||||
};
|
||||
+1
-3
@@ -1,6 +1,6 @@
|
||||
import { GQLQueryTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
const Query: GQLQueryTypeResolver<void> = {
|
||||
export const Query: GQLQueryTypeResolver<void> = {
|
||||
story: (source, args, ctx) => ctx.loaders.Stories.findOrCreate(args),
|
||||
comment: (source, { id }, ctx) =>
|
||||
id ? ctx.loaders.Comments.comment.load(id) : null,
|
||||
@@ -11,5 +11,3 @@ const Query: GQLQueryTypeResolver<void> = {
|
||||
debugScrapeStoryMetadata: (source, { url }, ctx) =>
|
||||
ctx.loaders.Stories.debugScrapeMetadata.load(url),
|
||||
};
|
||||
|
||||
export default Query;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import { GQLStoryTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { decodeActionCounts } from "talk-server/models/action/comment";
|
||||
import * as story from "talk-server/models/story";
|
||||
|
||||
export const Story: GQLStoryTypeResolver<story.Story> = {
|
||||
comments: (s, input, ctx) => ctx.loaders.Comments.forStory(s.id, input),
|
||||
isClosed: () => false,
|
||||
closedAt: (s, input, ctx) => {
|
||||
if (s.closedAt) {
|
||||
return s.closedAt;
|
||||
}
|
||||
|
||||
if (ctx.tenant.autoCloseStream && ctx.tenant.closedTimeout) {
|
||||
return DateTime.fromJSDate(s.createdAt)
|
||||
.plus(ctx.tenant.closedTimeout)
|
||||
.toJSDate();
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
commentActionCounts: s => decodeActionCounts(s.commentActionCounts),
|
||||
commentCounts: s => s.commentCounts,
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { GQLUserTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import * as user from "talk-server/models/user";
|
||||
|
||||
export const User: GQLUserTypeResolver<user.User> = {
|
||||
comments: ({ id }, input, ctx) => ctx.loaders.Comments.forUser(id, input),
|
||||
commentModerationActionHistory: ({ id }, input, ctx) =>
|
||||
ctx.loaders.Actions.commentModerationActionsConnection(input, id),
|
||||
};
|
||||
@@ -1,91 +0,0 @@
|
||||
import { getRequestedFields } from "talk-server/graph/tenant/resolvers/util";
|
||||
import {
|
||||
GQLComment,
|
||||
GQLCommentTypeResolver,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { decodeActionCounts } from "talk-server/models/action";
|
||||
import { Comment } from "talk-server/models/comment";
|
||||
import { createConnection } from "talk-server/models/connection";
|
||||
|
||||
const Comment: GQLCommentTypeResolver<Comment> = {
|
||||
editing: (comment, input, ctx) => ({
|
||||
// When there is more than one body history, then the comment has been
|
||||
// edited.
|
||||
edited: comment.body_history.length > 1,
|
||||
// The date that the comment is editable until is the tenant's edit window
|
||||
// length added to the comment created date.
|
||||
editableUntil: new Date(
|
||||
comment.created_at.valueOf() + ctx.tenant.editCommentWindowLength
|
||||
),
|
||||
}),
|
||||
createdAt: comment => comment.created_at,
|
||||
author: (comment, input, ctx) =>
|
||||
ctx.loaders.Users.user.load(comment.author_id),
|
||||
replies: (comment, input, ctx) =>
|
||||
comment.reply_count > 0
|
||||
? ctx.loaders.Comments.forParent(comment.story_id, comment.id, input)
|
||||
: createConnection(),
|
||||
actionCounts: comment => decodeActionCounts(comment.action_counts),
|
||||
myActionPresence: (comment, input, ctx) =>
|
||||
ctx.user
|
||||
? ctx.loaders.Comments.retrieveMyActionPresence.load(comment.id)
|
||||
: null,
|
||||
parentCount: comment =>
|
||||
comment.parent_id ? comment.grandparent_ids.length + 1 : 0,
|
||||
depth: comment =>
|
||||
comment.parent_id ? comment.grandparent_ids.length + 1 : 0,
|
||||
replyCount: comment => comment.reply_count,
|
||||
rootParent: (comment, input, ctx, info) => {
|
||||
// If there isn't a parent, then return nothing!
|
||||
if (!comment.parent_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// rootParentID is the root parent id for a given comment.
|
||||
const rootParentID =
|
||||
comment.grandparent_ids.length > 0
|
||||
? comment.grandparent_ids[0]
|
||||
: comment.parent_id;
|
||||
|
||||
// Get the field names of the fields being requested, if it's only the ID,
|
||||
// we have that, so no need to make a database request.
|
||||
const fields = getRequestedFields<GQLComment>(info);
|
||||
if (fields.length === 1 && fields[0] === "id") {
|
||||
return {
|
||||
id: rootParentID,
|
||||
};
|
||||
}
|
||||
|
||||
// We want more than the ID! Get the comment!
|
||||
// TODO: (wyattjoh) if the parent and the parents (containing the parent) are requested, the parent comment is retrieved from the database twice. Investigate ways of reducing i/o.
|
||||
return ctx.loaders.Comments.comment.load(rootParentID);
|
||||
},
|
||||
parent: (comment, input, ctx, info) => {
|
||||
// If there isn't a parent, then return nothing!
|
||||
if (!comment.parent_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the field names of the fields being requested, if it's only the ID,
|
||||
// we have that, so no need to make a database request.
|
||||
const fields = getRequestedFields<GQLComment>(info);
|
||||
if (fields.length === 1 && fields[0] === "id") {
|
||||
return {
|
||||
id: comment.parent_id,
|
||||
};
|
||||
}
|
||||
|
||||
// We want more than the ID! Get the comment!
|
||||
// TODO: (wyattjoh) if the parent and the parents (containing the parent) are requested, the parent comment is retrieved from the database twice. Investigate ways of reducing i/o.
|
||||
return ctx.loaders.Comments.comment.load(comment.parent_id);
|
||||
},
|
||||
parents: (comment, input, ctx) =>
|
||||
// Some resolver optimization.
|
||||
comment.parent_id
|
||||
? ctx.loaders.Comments.parents(comment, input)
|
||||
: createConnection(),
|
||||
story: (comment, input, ctx) =>
|
||||
ctx.loaders.Stories.story.load(comment.story_id),
|
||||
};
|
||||
|
||||
export default Comment;
|
||||
@@ -3,22 +3,26 @@ import Time from "talk-server/graph/common/scalars/time";
|
||||
|
||||
import { GQLResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
import AuthIntegrations from "./auth_integrations";
|
||||
import Comment from "./comment";
|
||||
import CommentCounts from "./comment_counts";
|
||||
import FacebookAuthIntegration from "./facebook_auth_integration";
|
||||
import GoogleAuthIntegration from "./google_auth_integration";
|
||||
import Mutation from "./mutation";
|
||||
import OIDCAuthIntegration from "./oidc_auth_integration";
|
||||
import Profile from "./profile";
|
||||
import Query from "./query";
|
||||
import Story from "./story";
|
||||
import User from "./user";
|
||||
import { AuthIntegrations } from "./AuthIntegrations";
|
||||
import { Comment } from "./Comment";
|
||||
import { CommentCounts } from "./CommentCounts";
|
||||
import { CommentModerationAction } from "./CommentModerationAction";
|
||||
import { CommentRevision } from "./CommentRevision";
|
||||
import { FacebookAuthIntegration } from "./FacebookAuthIntegration";
|
||||
import { GoogleAuthIntegration } from "./GoogleAuthIntegration";
|
||||
import { Mutation } from "./Mutation";
|
||||
import { OIDCAuthIntegration } from "./OIDCAuthIntegration";
|
||||
import { Profile } from "./Profile";
|
||||
import { Query } from "./Query";
|
||||
import { Story } from "./Story";
|
||||
import { User } from "./User";
|
||||
|
||||
const Resolvers: GQLResolver = {
|
||||
AuthIntegrations,
|
||||
Comment,
|
||||
CommentCounts,
|
||||
CommentModerationAction,
|
||||
CommentRevision,
|
||||
Cursor,
|
||||
Mutation,
|
||||
OIDCAuthIntegration,
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import { GQLStoryTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { decodeActionCounts } from "talk-server/models/action";
|
||||
import { Story } from "talk-server/models/story";
|
||||
|
||||
const Story: GQLStoryTypeResolver<Story> = {
|
||||
comments: (story, input, ctx) =>
|
||||
ctx.loaders.Comments.forStory(story.id, input),
|
||||
isClosed: () => false,
|
||||
closedAt: (story, input, ctx) => {
|
||||
if (story.closedAt) {
|
||||
return story.closedAt;
|
||||
}
|
||||
|
||||
if (ctx.tenant.autoCloseStream && ctx.tenant.closedTimeout) {
|
||||
return DateTime.fromJSDate(story.created_at)
|
||||
.plus(ctx.tenant.closedTimeout)
|
||||
.toJSDate();
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
actionCounts: story => decodeActionCounts(story.action_counts),
|
||||
commentCounts: story => story.comment_counts,
|
||||
};
|
||||
|
||||
export default Story;
|
||||
@@ -1,8 +0,0 @@
|
||||
import { GQLUserTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { User } from "talk-server/models/user";
|
||||
|
||||
const User: GQLUserTypeResolver<User> = {
|
||||
comments: (user, input, ctx) => ctx.loaders.Comments.forUser(user.id, input),
|
||||
};
|
||||
|
||||
export default User;
|
||||
@@ -975,6 +975,15 @@ type User {
|
||||
orderBy: COMMENT_SORT = CREATED_AT_DESC
|
||||
after: Cursor
|
||||
): CommentsConnection! @auth(roles: [ADMIN, MODERATOR], userIDField: "id")
|
||||
|
||||
"""
|
||||
commentModerationActionHistory returns a CommentModerationActionConnection that this User has
|
||||
created.
|
||||
"""
|
||||
commentModerationActionHistory(
|
||||
first: Int = 10
|
||||
after: Cursor
|
||||
): CommentModerationActionConnection! @auth(role: [MODERATOR, ADMIN])
|
||||
}
|
||||
|
||||
################################################################################
|
||||
@@ -1024,6 +1033,85 @@ enum COMMENT_STATUS {
|
||||
SYSTEM_WITHHELD
|
||||
}
|
||||
|
||||
type CommentModerationAction {
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
revision is the moderated CommentRevision.
|
||||
"""
|
||||
revision: CommentRevision!
|
||||
|
||||
"""
|
||||
status represents the status that was assigned by the moderator.
|
||||
"""
|
||||
status: COMMENT_STATUS!
|
||||
|
||||
"""
|
||||
moderator is the User that performed the Moderator action. If null, this means
|
||||
that the system has assigned the moderation status.
|
||||
"""
|
||||
moderator: User
|
||||
|
||||
"""
|
||||
createdAt is the time that the CommentModerationAction was created.
|
||||
"""
|
||||
createdAt: Time!
|
||||
}
|
||||
|
||||
type CommentModerationActionEdge {
|
||||
"""
|
||||
node is the CommentModerationAction for this edge.
|
||||
"""
|
||||
node: CommentModerationAction!
|
||||
|
||||
"""
|
||||
|
||||
"""
|
||||
cursor: Cursor
|
||||
}
|
||||
|
||||
type CommentModerationActionConnection {
|
||||
"""
|
||||
edges are a subset of CommentModerationActionEdge's.
|
||||
"""
|
||||
edges: [CommentModerationActionEdge!]!
|
||||
|
||||
"""
|
||||
pageInfo is
|
||||
"""
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type CommentRevision {
|
||||
"""
|
||||
id is the identifier of the CommentRevision.
|
||||
"""
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
comment is the reference to the original Comment associated with the current
|
||||
Comment.
|
||||
"""
|
||||
comment: Comment!
|
||||
|
||||
"""
|
||||
actionCounts stores the counts of all the actions for the CommentRevision
|
||||
specifically.
|
||||
"""
|
||||
actionCounts: ActionCounts! @auth(roles: [MODERATOR, ADMIN])
|
||||
|
||||
"""
|
||||
body is the content of the CommentRevision. If null, it indicates that the
|
||||
body text was deleted.
|
||||
"""
|
||||
body: String
|
||||
|
||||
"""
|
||||
createdAt is the time that the CommentRevision was created.
|
||||
"""
|
||||
createdAt: Time!
|
||||
}
|
||||
|
||||
"""
|
||||
Comment is a comment left by a User on an Story or another Comment as a reply.
|
||||
"""
|
||||
@@ -1034,10 +1122,23 @@ type Comment {
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
body is the content of the Comment.
|
||||
body is the content of the Comment, and is an alias to the body of the
|
||||
`currentRevision.body`.
|
||||
"""
|
||||
body: String
|
||||
|
||||
"""
|
||||
revision is the current revision of the Comment's body.
|
||||
"""
|
||||
revision: CommentRevision!
|
||||
|
||||
"""
|
||||
revisionHistory stores the previous CommentRevision's, with the most recent
|
||||
edit last.
|
||||
"""
|
||||
revisionHistory: [CommentRevision!]!
|
||||
@auth(roles: [MODERATOR, ADMIN], userIDField: "author_id")
|
||||
|
||||
"""
|
||||
createdAt is the date in which the Comment was created.
|
||||
"""
|
||||
@@ -1053,6 +1154,13 @@ type Comment {
|
||||
"""
|
||||
status: COMMENT_STATUS!
|
||||
|
||||
"""
|
||||
statusHistory returns a CommentModerationActionConnection that will list
|
||||
the history of moderator actions performed on the Comment, with the most
|
||||
recent last.
|
||||
"""
|
||||
statusHistory: [CommentModerationAction!]! @auth(role: [MODERATOR, ADMIN])
|
||||
|
||||
"""
|
||||
parentCount is the number of direct parents for this Comment. Currently this
|
||||
value is the same as depth.
|
||||
@@ -1107,7 +1215,7 @@ type Comment {
|
||||
actionCounts: ActionCounts!
|
||||
|
||||
"""
|
||||
myActionPresence stores the presense information for all the actions
|
||||
myActionPresence stores the presence information for all the actions
|
||||
left by the current User on this Comment.
|
||||
"""
|
||||
myActionPresence: ActionPresence
|
||||
@@ -1302,10 +1410,10 @@ type Story {
|
||||
): CommentsConnection!
|
||||
|
||||
"""
|
||||
actionCounts stores the counts of all the actions against this Story and it's
|
||||
commentActionCounts stores the counts of all the actions against this Story and it's
|
||||
Comments.
|
||||
"""
|
||||
actionCounts: ActionCounts! @auth(roles: [ADMIN, MODERATOR])
|
||||
commentActionCounts: ActionCounts! @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
closedAt is the Time that the Story is closed for commenting.
|
||||
@@ -1927,6 +2035,12 @@ input CreateCommentReactionInput {
|
||||
"""
|
||||
commentID: ID!
|
||||
|
||||
"""
|
||||
commentRevisionID is the revision ID of the Comment that we're creating the
|
||||
Reaction on.
|
||||
"""
|
||||
commentRevisionID: ID!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
@@ -1983,6 +2097,12 @@ input CreateCommentDontAgreeInput {
|
||||
"""
|
||||
commentID: ID!
|
||||
|
||||
"""
|
||||
commentRevisionID is the revision ID of the Comment that we're creating the
|
||||
DontAgree on.
|
||||
"""
|
||||
commentRevisionID: ID!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
@@ -2039,6 +2159,12 @@ input CreateCommentFlagInput {
|
||||
"""
|
||||
commentID: ID!
|
||||
|
||||
"""
|
||||
commentRevisionID is the revision ID of the Comment that we're creating the
|
||||
Flag on.
|
||||
"""
|
||||
commentRevisionID: ID!
|
||||
|
||||
"""
|
||||
reason is the selected reason why the Flag is being created.
|
||||
"""
|
||||
@@ -2603,6 +2729,72 @@ type ScrapeStoryPayload {
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
# acceptComment
|
||||
##################
|
||||
|
||||
input AcceptCommentInput {
|
||||
"""
|
||||
commentID is the ID of the Comment that was accepted.
|
||||
"""
|
||||
commentID: ID!
|
||||
|
||||
"""
|
||||
commentRevisionID is the ID of the CommentRevision that is being accepted.
|
||||
"""
|
||||
commentRevisionID: ID!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
type AcceptCommentPayload {
|
||||
"""
|
||||
comment is the Comment that was accepted.
|
||||
"""
|
||||
comment: Comment
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
# rejectComment
|
||||
##################
|
||||
|
||||
input RejectCommentInput {
|
||||
"""
|
||||
commentID is the ID of the Comment that was rejected.
|
||||
"""
|
||||
commentID: ID!
|
||||
|
||||
"""
|
||||
commentRevisionID is the ID of the CommentRevision that is being rejected.
|
||||
"""
|
||||
commentRevisionID: ID!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
type RejectCommentPayload {
|
||||
"""
|
||||
comment is the Comment that was rejected.
|
||||
"""
|
||||
comment: Comment
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
## Mutation
|
||||
##################
|
||||
@@ -2730,6 +2922,18 @@ type Mutation {
|
||||
"""
|
||||
scrapeStory(input: ScrapeStoryInput!): ScrapeStoryPayload
|
||||
@auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
acceptComment will mark the Comment as ACCEPTED.
|
||||
"""
|
||||
acceptComment(input: AcceptCommentInput!): AcceptCommentPayload
|
||||
@auth(roles: [MODERATOR, ADMIN])
|
||||
|
||||
"""
|
||||
rejectComment will mark the Comment as REJECTED.
|
||||
"""
|
||||
rejectComment(input: RejectCommentInput!): RejectCommentPayload
|
||||
@auth(roles: [MODERATOR, ADMIN])
|
||||
}
|
||||
|
||||
################################################################################
|
||||
|
||||
Reference in New Issue
Block a user