diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index b3a923438..72326c030 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -3234,9 +3234,11 @@ class CommentViewSet( permission_classes = [permissions.CommentPermission] pagination_class = Pagination serializer_class = serializers.CommentSerializer - queryset = models.Comment.objects.select_related("user").prefetch_related( - "reactions__users" - ).all() + queryset = ( + models.Comment.objects.select_related("user") + .prefetch_related("reactions__users") + .all() + ) def get_queryset(self): """Override to filter on related resource.""" diff --git a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx index 2cda2fa51..58e4d8c7e 100644 --- a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx +++ b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx @@ -60,6 +60,7 @@ export interface ConfigResponse { POSTHOG_HOST?: PostHogConf['host']; RELEASE_VERSION: string; SENTRY_DSN?: string; + REACTIONS_MAX_PER_COMMENT: number; TRASHBIN_CUTOFF_DAYS?: number; theme_customization?: ThemeCustomization; } diff --git a/src/frontend/apps/impress/src/features/docs/doc-comments/api/DocsThreadStoreAuth.tsx b/src/frontend/apps/impress/src/features/docs/doc-comments/api/DocsThreadStoreAuth.tsx index 1d5f8eca9..1c8c17d80 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-comments/api/DocsThreadStoreAuth.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-comments/api/DocsThreadStoreAuth.tsx @@ -6,6 +6,7 @@ export class DocsThreadStoreAuth extends ThreadStoreAuth { constructor( private readonly userId: string, public canSee: boolean, + private readonly maxReactions: number, ) { super(); } @@ -67,13 +68,27 @@ export class DocsThreadStoreAuth extends ThreadStoreAuth { } if (!emoji) { - return true; + return comment.reactions.length < this.maxReactions; } - return !comment.reactions.some( + const hasReactedWithEmoji = comment.reactions.some( (reaction) => reaction.emoji === emoji && reaction.userIds.includes(this.userId), ); + + if (hasReactedWithEmoji) { + return false; + } + + const reactionExists = comment.reactions.some( + (reaction) => reaction.emoji === emoji, + ); + + if (reactionExists) { + return true; + } + + return comment.reactions.length < this.maxReactions; } canDeleteReaction(comment: ClientCommentData, emoji?: string): boolean { diff --git a/src/frontend/apps/impress/src/features/docs/doc-comments/hooks/useComments.ts b/src/frontend/apps/impress/src/features/docs/doc-comments/hooks/useComments.ts index ff2a4ac9e..1a6502241 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-comments/hooks/useComments.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-comments/hooks/useComments.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { useConfig } from '@/core'; import { useCunninghamTheme } from '@/cunningham'; import { User, avatarUrlFromName } from '@/features/auth'; import { Doc, useProviderStore } from '@/features/docs/doc-management'; @@ -18,6 +19,7 @@ export function useComments( const { t } = useTranslation(); const { themeTokens } = useCunninghamTheme(); const { setThreadStore } = useThreadStore(); + const { data: config } = useConfig(); const threadStore = useMemo(() => { return new DocsThreadStore( @@ -26,6 +28,7 @@ export function useComments( new DocsThreadStoreAuth( encodeURIComponent(user?.full_name || ''), canComment, + config?.REACTIONS_MAX_PER_COMMENT ?? 0, ), provider?.document, ); @@ -35,6 +38,7 @@ export function useComments( provider?.awareness, provider?.document, user?.full_name, + config?.REACTIONS_MAX_PER_COMMENT, ]); useEffect(() => {