diff --git a/src/core/client/stream/tabs/Comments/Comment/ReplyCommentForm/CreateCommentReplyMutation.ts b/src/core/client/stream/tabs/Comments/Comment/ReplyCommentForm/CreateCommentReplyMutation.ts index 656eeec64..5202d3932 100644 --- a/src/core/client/stream/tabs/Comments/Comment/ReplyCommentForm/CreateCommentReplyMutation.ts +++ b/src/core/client/stream/tabs/Comments/Comment/ReplyCommentForm/CreateCommentReplyMutation.ts @@ -136,6 +136,15 @@ const mutation = graphql` ...AllCommentsTabContainer_comment @relay(mask: false) id status + story { + settings { + # Load the story live settings so new comments can verify if live + # updates are still enabled (and enable then if they are). + live { + enabled + } + } + } parent { id tags { @@ -234,6 +243,14 @@ async function commit( dontAgree: false, flag: false, }, + story: { + id: input.storyID, + settings: { + live: { + enabled: storySettings.live.enabled, + }, + }, + }, replies: { edges: [], pageInfo: { endCursor: null, hasNextPage: false }, diff --git a/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/CreateCommentMutation.ts b/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/CreateCommentMutation.ts index 331003ca6..b434433eb 100644 --- a/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/CreateCommentMutation.ts +++ b/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/CreateCommentMutation.ts @@ -131,6 +131,15 @@ const mutation = graphql` node { ...AllCommentsTabContainer_comment @relay(mask: false) status + story { + settings { + # Load the story live settings so new comments can verify if live + # updates are still enabled (and enable then if they are). + live { + enabled + } + } + } } } clientMutationId @@ -219,6 +228,14 @@ async function commit( edges: [], pageInfo: { endCursor: null, hasNextPage: false }, }, + story: { + id: input.storyID, + settings: { + live: { + enabled: storySettings.live.enabled, + }, + }, + }, deleted: false, }, }, diff --git a/src/core/client/stream/test/fixtures.ts b/src/core/client/stream/test/fixtures.ts index 065875dcd..64b872b41 100644 --- a/src/core/client/stream/test/fixtures.ts +++ b/src/core/client/stream/test/fixtures.ts @@ -260,6 +260,41 @@ export const commenters = createFixtures( baseUser ); +export const baseStory = createFixture({ + id: "story-0", + metadata: { + title: "title", + }, + isClosed: false, + comments: { + edges: [], + pageInfo: { + hasNextPage: false, + }, + }, + commentCounts: { + totalPublished: 0, + tags: { + FEATURED: 0, + UNANSWERED: 0, + }, + }, + settings: { + moderation: GQLMODERATION_MODE.POST, + premodLinksEnable: false, + messageBox: { + enabled: false, + }, + live: { + enabled: true, + configurable: true, + }, + mode: GQLSTORY_MODE.COMMENTS, + experts: [], + }, + site, +}); + export const baseComment = createFixture({ author: commenters[0], body: "Comment Body", @@ -279,6 +314,7 @@ export const baseComment = createFixture({ total: 0, }, }, + story: baseStory, parent: undefined, viewerActionPresence: { reaction: false, dontAgree: false, flag: false }, tags: [], @@ -514,40 +550,6 @@ export const commentWithDeepestReplies = denormalizeComment( }) ); -export const baseStory = createFixture({ - metadata: { - title: "title", - }, - isClosed: false, - comments: { - edges: [], - pageInfo: { - hasNextPage: false, - }, - }, - commentCounts: { - totalPublished: 0, - tags: { - FEATURED: 0, - UNANSWERED: 0, - }, - }, - settings: { - moderation: GQLMODERATION_MODE.POST, - premodLinksEnable: false, - messageBox: { - enabled: false, - }, - live: { - enabled: true, - configurable: true, - }, - mode: GQLSTORY_MODE.COMMENTS, - experts: [], - }, - site, -}); - export const moderators = createFixtures( [ { diff --git a/src/core/server/config.ts b/src/core/server/config.ts index c1b421e1c..a0703bdef 100644 --- a/src/core/server/config.ts +++ b/src/core/server/config.ts @@ -217,6 +217,13 @@ const config = convict({ default: false, env: "DISABLE_LIVE_UPDATES", }, + disable_live_updates_timeout: { + doc: + "Disables subscriptions for the comment stream for all stories across all tenants where a comment has not been left within the timeout", + format: "ms", + default: "2 weeks", + env: "DISABLE_LIVE_UPDATES_TIMEOUT", + }, disable_client_routes: { doc: "Disables mounting of client routes for developing with Webpack Dev Server", diff --git a/src/core/server/graph/resolvers/LiveConfiguration.ts b/src/core/server/graph/resolvers/LiveConfiguration.ts index 667e173fc..23e3ea355 100644 --- a/src/core/server/graph/resolvers/LiveConfiguration.ts +++ b/src/core/server/graph/resolvers/LiveConfiguration.ts @@ -1,9 +1,14 @@ import { isUndefined } from "lodash"; +import { DateTime } from "luxon"; -import { GQLLiveConfigurationTypeResolver } from "coral-server/graph/schema/__generated__/types"; import * as settings from "coral-server/models/settings"; -export type LiveConfigurationInput = settings.LiveConfiguration; +import { GQLLiveConfigurationTypeResolver } from "coral-server/graph/schema/__generated__/types"; + +export interface LiveConfigurationInput extends settings.LiveConfiguration { + lastCommentedAt?: Date; + createdAt?: Date; +} export const LiveConfiguration: GQLLiveConfigurationTypeResolver = { configurable: (source, args, ctx) => @@ -13,6 +18,32 @@ export const LiveConfiguration: GQLLiveConfigurationTypeResolver 0) { + // If one of these is available, use it to determine the time since the + // last comment. + const lastCommentedAt = source.lastCommentedAt || source.createdAt; + if ( + // If a date is found... + lastCommentedAt && + // And the date (when we add the timeout duration) is before the current + // date... + DateTime.fromJSDate(lastCommentedAt) + .plus({ + milliseconds: disableLiveUpdatesTimeout, + }) + .toJSDate() <= ctx.now + ) { + // Then we know that the last comment (or lack there of) was left more + // than the timeout specified in configuration. + return false; + } + } + if (isUndefined(source.enabled)) { return ctx.tenant.live.enabled; } diff --git a/src/core/server/graph/resolvers/Story.ts b/src/core/server/graph/resolvers/Story.ts index 4e4c028d0..8e8309194 100644 --- a/src/core/server/graph/resolvers/Story.ts +++ b/src/core/server/graph/resolvers/Story.ts @@ -11,6 +11,7 @@ import { import { CommentCountsInput } from "./CommentCounts"; import { storyModerationInputResolver } from "./ModerationQueues"; +import { StorySettingsInput } from "./StorySettings"; export const Story: GQLStoryTypeResolver = { comments: (s, input, ctx) => ctx.loaders.Comments.forStory(s.id, input), @@ -26,7 +27,16 @@ export const Story: GQLStoryTypeResolver = { commentCounts: (s): CommentCountsInput => s, // Merge tenant settings into the story settings so we can easily inherit the // options if they exist. - settings: (s, input, ctx) => defaultsDeep({}, s.settings, ctx.tenant), + settings: (s, input, ctx): StorySettingsInput => + defaultsDeep( + { + // Pass these options as required by StorySettingsInput. + lastCommentedAt: s.lastCommentedAt, + createdAt: s.createdAt, + }, + s.settings, + ctx.tenant + ), moderationQueues: storyModerationInputResolver, site: (s, input, ctx) => ctx.loaders.Sites.site.load(s.siteID), }; diff --git a/src/core/server/graph/resolvers/StorySettings.ts b/src/core/server/graph/resolvers/StorySettings.ts index 024711504..d0100efb4 100644 --- a/src/core/server/graph/resolvers/StorySettings.ts +++ b/src/core/server/graph/resolvers/StorySettings.ts @@ -7,8 +7,21 @@ import { GQLStorySettingsTypeResolver, } from "../schema/__generated__/types"; -export const StorySettings: GQLStorySettingsTypeResolver = { - live: (s) => s.live || {}, +import { LiveConfigurationInput } from "./LiveConfiguration"; + +export interface StorySettingsInput extends story.StorySettings { + lastCommentedAt?: Date; + createdAt?: Date; +} + +export const StorySettings: GQLStorySettingsTypeResolver = { + live: (s): LiveConfigurationInput => ({ + // Live may not be available sometimes, fix it here with the inline ||. + ...(s.live || { enabled: false }), + // Pass these options as required by LiveConfigurationInput. + lastCommentedAt: s.lastCommentedAt, + createdAt: s.createdAt, + }), moderation: (s, input, ctx) => s.moderation || ctx.tenant.moderation, premodLinksEnable: (s, input, ctx) => s.premodLinksEnable || ctx.tenant.premodLinksEnable, diff --git a/src/core/server/models/story/index.ts b/src/core/server/models/story/index.ts index 817c7d738..24779814d 100644 --- a/src/core/server/models/story/index.ts +++ b/src/core/server/models/story/index.ts @@ -48,11 +48,9 @@ export interface StreamModeSettings { expertIDs?: string[]; } -export type StorySettings = DeepPartial< - StreamModeSettings & - GlobalModerationSettings & - Pick ->; +export type StorySettings = StreamModeSettings & + GlobalModerationSettings & + Pick; export type StoryMetadata = GQLStoryMetadata; @@ -83,7 +81,7 @@ export interface Story extends TenantResource { * settings provides a point where the settings can be overridden for a * specific Story. */ - settings: StorySettings; + settings: DeepPartial; /** * closedAt is the date that the Story was forced closed at, or false to @@ -378,7 +376,7 @@ export async function updateStory( throw err; } } -export type UpdateStorySettingsInput = StorySettings; +export type UpdateStorySettingsInput = DeepPartial; export async function updateStorySettings( mongo: Db,