From d37c7450a02b498e21898ab3832bc1f62bc6cc43 Mon Sep 17 00:00:00 2001 From: Ruby Date: Fri, 19 Sep 2025 01:11:06 +0200 Subject: [PATCH 1/4] Add randomization option for post scheduling in ManageModal and update CreatePostDto --- .../components/new-launch/manage.modal.tsx | 30 +++++++++++++++++ .../database/prisma/posts/posts.service.ts | 33 ++++++++++++++++--- .../src/dtos/posts/create.post.dto.ts | 4 +++ 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/apps/frontend/src/components/new-launch/manage.modal.tsx b/apps/frontend/src/components/new-launch/manage.modal.tsx index 168ff4038..16c5a541d 100644 --- a/apps/frontend/src/components/new-launch/manage.modal.tsx +++ b/apps/frontend/src/components/new-launch/manage.modal.tsx @@ -27,6 +27,7 @@ import { SelectCustomer } from '@gitroom/frontend/components/launches/select.cus import { CopilotPopup } from '@copilotkit/react-ui'; import { DummyCodeComponent } from '@gitroom/frontend/components/new-launch/dummy.code.component'; import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation'; +import { Checkbox } from '@gitroom/react/form/checkbox'; function countCharacters(text: string, type: string): number { if (type !== 'x') { @@ -41,6 +42,7 @@ export const ManageModal: FC = (props) => { const ref = useRef(null); const existingData = useExistingData(); const [loading, setLoading] = useState(false); + const [randomizeMinute, setRandomizeMinute] = useState(true); const toaster = useToaster(); const modal = useModals(); @@ -233,6 +235,7 @@ export const ManageModal: FC = (props) => { ...(repeater ? { inter: repeater } : {}), tags, shortLink, + randomizeMinute, date: date.utc().format('YYYY-MM-DDTHH:mm:ss'), posts: checkAllValid.map((post: any) => ({ integration: { @@ -332,6 +335,33 @@ export const ManageModal: FC = (props) => { )} + {!dummy && ( +
+ { + const next = e?.target ? !!e.target.value : !!e; + setRandomizeMinute(next); + const base = date.clone().second(0).millisecond(0); + if (next) { + const currentMinute = base.minute(); + let minute = Math.floor(Math.random() * 60); + let tries = 0; + while (minute === currentMinute && tries < 5) { + minute = Math.floor(Math.random() * 60); + tries++; + } + setDate(base.minute(minute)); + } else { + setDate(base.minute(0)); + } + }} + /> +
{t('randomize_minute', 'Randomize minute')}
+
+ )} diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index fea332712..c5e4bfa2e 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -663,6 +663,33 @@ export class PostsService { async createPost(orgId: string, body: CreatePostDto): Promise { const postList = []; + + const targetDateForAll = + body.type === 'now' + ? dayjs().format('YYYY-MM-DDTHH:mm:00') + : body.date; + + const shouldRandomize = body.type === 'schedule' && (body.randomizeMinute ?? true); + + const scheduleDateToUse = (() => { + const input = dayjs.utc(targetDateForAll).second(0).millisecond(0); + if (shouldRandomize) { + // Only randomize if the incoming minute is exactly 0; otherwise respect chosen minute + if (input.minute() === 0) { + let minute = Math.floor(Math.random() * 60); + const now = dayjs.utc(); + if (input.isSame(now, 'hour')) { + const minNowPlus1 = now.minute() + 1; + if (minute < minNowPlus1) minute = Math.min(minNowPlus1, 59); + } + return input.minute(minute).format('YYYY-MM-DDTHH:mm:00'); + } + return input.format('YYYY-MM-DDTHH:mm:00'); + } + // Not randomizing: clamp to exact hour (minute:00) + return input.minute(0).format('YYYY-MM-DDTHH:mm:00'); + })(); + for (const post of body.posts) { const messages = (post.value || []).map((p) => p.content); const updateContent = !body.shortLink @@ -678,9 +705,7 @@ export class PostsService { await this._postRepository.createOrUpdatePost( body.type, orgId, - body.type === 'now' - ? dayjs().format('YYYY-MM-DDTHH:mm:00') - : body.date, + scheduleDateToUse, post, body.tags, body.inter @@ -697,7 +722,7 @@ export class PostsService { if ( body.type === 'now' || - (body.type === 'schedule' && dayjs(body.date).isAfter(dayjs())) + (body.type === 'schedule' && dayjs(posts[0].publishDate).isAfter(dayjs())) ) { this._workerServiceProducer.emit('post', { id: posts[0].id, diff --git a/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts index dd9a8471b..f95d4f9e0 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts @@ -83,6 +83,10 @@ export class CreatePostDto { @IsNumber() inter?: number; + @IsOptional() + @IsBoolean() + randomizeMinute?: boolean; + @IsDefined() @IsDateString() date: string; From 9124840f377a1f18b45d04b0ed523ff486795782 Mon Sep 17 00:00:00 2001 From: Ruby Date: Fri, 19 Sep 2025 01:29:03 +0200 Subject: [PATCH 2/4] Clamp to seconds instead of minutes of randomization is off --- .../src/database/prisma/posts/posts.service.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index c5e4bfa2e..9538cdc57 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -674,7 +674,6 @@ export class PostsService { const scheduleDateToUse = (() => { const input = dayjs.utc(targetDateForAll).second(0).millisecond(0); if (shouldRandomize) { - // Only randomize if the incoming minute is exactly 0; otherwise respect chosen minute if (input.minute() === 0) { let minute = Math.floor(Math.random() * 60); const now = dayjs.utc(); @@ -686,8 +685,7 @@ export class PostsService { } return input.format('YYYY-MM-DDTHH:mm:00'); } - // Not randomizing: clamp to exact hour (minute:00) - return input.minute(0).format('YYYY-MM-DDTHH:mm:00'); + return input.format('YYYY-MM-DDTHH:mm:00'); })(); for (const post of body.posts) { @@ -913,14 +911,14 @@ export class PostsService { ...toPost.list.map((l) => ({ id: '', content: l.post, - image: [], + image: [] as any[], })), { id: '', content: `Check out the full story here:\n${ body.postId || body.url }`, - image: [], + image: [] as any[], }, ], }, From 16f2597624b209e64d45ffe9bf7ff5e06b8071f5 Mon Sep 17 00:00:00 2001 From: Ruby Date: Fri, 19 Sep 2025 12:14:24 +0200 Subject: [PATCH 3/4] Implement initialization for minutes randomization in ManageModal, hide when editing and cancelling changes in backend --- .../components/new-launch/manage.modal.tsx | 25 +++++++++++-- .../database/prisma/posts/posts.service.ts | 35 ++++--------------- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/apps/frontend/src/components/new-launch/manage.modal.tsx b/apps/frontend/src/components/new-launch/manage.modal.tsx index 16c5a541d..72aa0db10 100644 --- a/apps/frontend/src/components/new-launch/manage.modal.tsx +++ b/apps/frontend/src/components/new-launch/manage.modal.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { FC, useCallback, useRef, useState } from 'react'; +import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import { AddEditModalProps } from '@gitroom/frontend/components/new-launch/add.edit.modal'; import clsx from 'clsx'; import { useT } from '@gitroom/react/translation/get.transation.service.client'; @@ -28,6 +28,7 @@ import { CopilotPopup } from '@copilotkit/react-ui'; import { DummyCodeComponent } from '@gitroom/frontend/components/new-launch/dummy.code.component'; import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation'; import { Checkbox } from '@gitroom/react/form/checkbox'; +import dayjs from 'dayjs'; function countCharacters(text: string, type: string): number { if (type !== 'x') { @@ -134,6 +135,26 @@ export const ManageModal: FC = (props) => { [integrations] ); + const didRandomizeInitially = useRef(false); + useEffect(() => { + if (dummy) return; + if (!randomizeMinute) return; + if (existingData.integration) return; + if (!date) return; + if (didRandomizeInitially.current) return; + if (date.minute() !== 0) return; + + const base = date.clone().second(0).millisecond(0); + let minute = Math.floor(Math.random() * 60); + const now = dayjs(); + if (base.isSame(now, 'hour')) { + const minNowPlus1 = now.minute() + 1; + if (minute < minNowPlus1) minute = Math.min(minNowPlus1, 59); + } + setDate(base.minute(minute)); + didRandomizeInitially.current = true; + }, [randomizeMinute, date, dummy, existingData, setDate]); + const schedule = useCallback( (type: 'draft' | 'now' | 'schedule') => async () => { setLoading(true); @@ -335,7 +356,7 @@ export const ManageModal: FC = (props) => { )} - {!dummy && ( + {!dummy && !existingData.integration && (
{ const postList = []; - - const targetDateForAll = - body.type === 'now' - ? dayjs().format('YYYY-MM-DDTHH:mm:00') - : body.date; - - const shouldRandomize = body.type === 'schedule' && (body.randomizeMinute ?? true); - - const scheduleDateToUse = (() => { - const input = dayjs.utc(targetDateForAll).second(0).millisecond(0); - if (shouldRandomize) { - if (input.minute() === 0) { - let minute = Math.floor(Math.random() * 60); - const now = dayjs.utc(); - if (input.isSame(now, 'hour')) { - const minNowPlus1 = now.minute() + 1; - if (minute < minNowPlus1) minute = Math.min(minNowPlus1, 59); - } - return input.minute(minute).format('YYYY-MM-DDTHH:mm:00'); - } - return input.format('YYYY-MM-DDTHH:mm:00'); - } - return input.format('YYYY-MM-DDTHH:mm:00'); - })(); - for (const post of body.posts) { const messages = (post.value || []).map((p) => p.content); const updateContent = !body.shortLink @@ -703,7 +678,9 @@ export class PostsService { await this._postRepository.createOrUpdatePost( body.type, orgId, - scheduleDateToUse, + body.type === 'now' + ? dayjs().format('YYYY-MM-DDTHH:mm:00') + : body.date, post, body.tags, body.inter @@ -720,7 +697,7 @@ export class PostsService { if ( body.type === 'now' || - (body.type === 'schedule' && dayjs(posts[0].publishDate).isAfter(dayjs())) + (body.type === 'schedule' && dayjs(body.date).isAfter(dayjs())) ) { this._workerServiceProducer.emit('post', { id: posts[0].id, @@ -918,7 +895,7 @@ export class PostsService { content: `Check out the full story here:\n${ body.postId || body.url }`, - image: [] as any[], + image: [], }, ], }, @@ -1029,4 +1006,4 @@ export class PostsService { message ); } -} +} \ No newline at end of file From ed69317d83076609cf2fb48cb9a5431511c200c7 Mon Sep 17 00:00:00 2001 From: Ruby Date: Fri, 19 Sep 2025 12:30:08 +0200 Subject: [PATCH 4/4] Cancel changes in backend completely for minute randomization --- .../nestjs-libraries/src/database/prisma/posts/posts.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index b158e2486..9722c54ca 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -888,7 +888,7 @@ export class PostsService { ...toPost.list.map((l) => ({ id: '', content: l.post, - image: [] as any[], + image: [], })), { id: '',