From 9ea080024bb112459dd4fb9bb2cf33c00ad7e06e Mon Sep 17 00:00:00 2001 From: Ben Cooke Date: Wed, 19 Nov 2025 14:00:02 -0500 Subject: [PATCH] Ai-generated post fixes (#34490) --- .../components/post/post_component.test.tsx | 104 ++++++++++++++++++ .../src/components/post/post_component.tsx | 9 +- .../src/components/post/user_profile.tsx | 19 +++- .../post_message_preview.tsx | 10 ++ .../channels/src/sass/components/_post.scss | 6 +- webapp/channels/src/utils/post_utils.ts | 23 +++- 6 files changed, 159 insertions(+), 12 deletions(-) diff --git a/webapp/channels/src/components/post/post_component.test.tsx b/webapp/channels/src/components/post/post_component.test.tsx index 548fce1e1e4..c63f581cd04 100644 --- a/webapp/channels/src/components/post/post_component.test.tsx +++ b/webapp/channels/src/components/post/post_component.test.tsx @@ -550,4 +550,108 @@ describe('PostComponent', () => { expect(screen.queryByTestId('post-priority-label')).not.toBeInTheDocument(); }); }); + + describe('AI-generated indicator', () => { + const aiGeneratedPost = TestHelper.getPostMock({ + channel_id: channel.id, + props: { + ai_generated_by: 'ai_user_id', + ai_generated_by_username: 'aibot', + }, + }); + + test('should show AI-generated indicator for AI posts in non-compact mode', () => { + const props = { + ...baseProps, + post: aiGeneratedPost, + compactDisplay: false, + }; + renderWithContext(); + + expect(screen.getByLabelText('Message posted by @aibot')).toBeInTheDocument(); + }); + + test('should not show AI-generated indicator for regular posts', () => { + const regularPost = TestHelper.getPostMock({ + channel_id: channel.id, + }); + const props = { + ...baseProps, + post: regularPost, + compactDisplay: false, + }; + renderWithContext(); + + expect(screen.queryByLabelText(/AI-generated|Message posted by/)).not.toBeInTheDocument(); + }); + + test('should not show AI-generated indicator for consecutive posts', () => { + const props = { + ...baseProps, + post: aiGeneratedPost, + compactDisplay: false, + isConsecutivePost: true, + }; + renderWithContext(); + + expect(screen.queryByLabelText(/AI-generated|Message posted by/)).not.toBeInTheDocument(); + }); + + test('should show AI-generated indicator in PostUserProfile for compact mode in CENTER', () => { + const props = { + ...baseProps, + post: aiGeneratedPost, + compactDisplay: true, + location: Locations.CENTER, + }; + renderWithContext(); + + // In compact CENTER mode, indicator is rendered by PostUserProfile (after username) + // Verify it appears exactly once + const indicators = screen.queryAllByLabelText(/AI-generated|Message posted by/); + expect(indicators.length).toBe(1); + }); + + test('should hide AI-generated indicator for consecutive posts in threads', () => { + const threadPost = TestHelper.getPostMock({ + channel_id: channel.id, + root_id: 'root_post_id', + props: { + ai_generated_by: 'ai_user_id', + ai_generated_by_username: 'aibot', + }, + }); + const props = { + ...baseProps, + post: threadPost, + compactDisplay: false, + isConsecutivePost: true, + location: Locations.RHS_COMMENT, + }; + renderWithContext(); + + expect(screen.queryByLabelText(/AI-generated|Message posted by/)).not.toBeInTheDocument(); + }); + + test('should show AI-generated indicator for non-consecutive posts in threads', () => { + const threadPost = TestHelper.getPostMock({ + channel_id: channel.id, + root_id: 'root_post_id', + props: { + ai_generated_by: 'ai_user_id', + ai_generated_by_username: 'aibot', + }, + }); + const props = { + ...baseProps, + post: threadPost, + compactDisplay: false, + isConsecutivePost: false, + location: Locations.RHS_COMMENT, + }; + renderWithContext(); + + expect(screen.getByLabelText('Message posted by @aibot')).toBeInTheDocument(); + }); + }); }); diff --git a/webapp/channels/src/components/post/post_component.tsx b/webapp/channels/src/components/post/post_component.tsx index 81bdc0a9ee6..8fb86c9f865 100644 --- a/webapp/channels/src/components/post/post_component.tsx +++ b/webapp/channels/src/components/post/post_component.tsx @@ -589,12 +589,11 @@ function PostComponent(props: Props) { /> } {priority} - {Boolean(post.props && post.props.ai_generated_by && post.props.ai_generated_by_username) && - typeof post.props.ai_generated_by === 'string' && - typeof post.props.ai_generated_by_username === 'string' && ( + {((!props.compactDisplay && !(hasSameRoot(props) && props.isConsecutivePost)) || (props.compactDisplay && isRHS)) && + PostUtils.hasAiGeneratedMetadata(post) && ( )} diff --git a/webapp/channels/src/components/post/user_profile.tsx b/webapp/channels/src/components/post/user_profile.tsx index 7db3b7a6c80..27355b3f56b 100644 --- a/webapp/channels/src/components/post/user_profile.tsx +++ b/webapp/channels/src/components/post/user_profile.tsx @@ -9,12 +9,13 @@ import type {Post} from '@mattermost/types/posts'; import {ensureString} from 'mattermost-redux/utils/post_utils'; +import AiGeneratedIndicator from 'components/post_view/ai_generated_indicator/ai_generated_indicator'; import PostHeaderCustomStatus from 'components/post_view/post_header_custom_status/post_header_custom_status'; import UserProfile from 'components/user_profile'; import BotTag from 'components/widgets/tag/bot_tag'; import Tag from 'components/widgets/tag/tag'; -import {fromAutoResponder, isFromWebhook} from 'utils/post_utils'; +import {fromAutoResponder, hasAiGeneratedMetadata, isFromWebhook} from 'utils/post_utils'; type Props = { post: Post; @@ -25,20 +26,33 @@ type Props = { isBot: boolean; isSystemMessage: boolean; isMobileView: boolean; + location: string; }; const PostUserProfile = (props: Props): JSX.Element | null => { const intl = useIntl(); - const {post, compactDisplay, isMobileView, isConsecutivePost, enablePostUsernameOverride, isBot, isSystemMessage, colorizeUsernames} = props; + const {post, compactDisplay, isMobileView, isConsecutivePost, enablePostUsernameOverride, isBot, isSystemMessage, colorizeUsernames, location} = props; const isFromAutoResponder = fromAutoResponder(post); const colorize = compactDisplay && colorizeUsernames; let userProfile: ReactNode = null; let botIndicator = null; let colon = null; + let aiIndicator = null; if (props.compactDisplay) { colon = {':'}; + + // Add AI indicator in compact mode after username, but not in RHS thread view (it goes after timestamp there) + if (hasAiGeneratedMetadata(post) && !(location === 'RHS_ROOT' || location === 'RHS_COMMENT')) { + aiIndicator = ( + + ); + } } const customStatus = ( @@ -148,6 +162,7 @@ const PostUserProfile = (props: Props): JSX.Element | null => { {colon} {botIndicator} {customStatus} + {aiIndicator} ); }; diff --git a/webapp/channels/src/components/post_view/post_message_preview/post_message_preview.tsx b/webapp/channels/src/components/post_view/post_message_preview/post_message_preview.tsx index d33b45c0309..8730766171e 100644 --- a/webapp/channels/src/components/post_view/post_message_preview/post_message_preview.tsx +++ b/webapp/channels/src/components/post_view/post_message_preview/post_message_preview.tsx @@ -13,11 +13,14 @@ import {ensureString} from 'mattermost-redux/utils/post_utils'; import FileAttachmentListContainer from 'components/file_attachment_list'; import PriorityLabel from 'components/post_priority/post_priority_label'; +import AiGeneratedIndicator from 'components/post_view/ai_generated_indicator/ai_generated_indicator'; import PostAttachmentOpenGraph from 'components/post_view/post_attachment_opengraph'; import PostMessageView from 'components/post_view/post_message_view'; import Timestamp from 'components/timestamp'; import UserProfileComponent from 'components/user_profile'; +import * as PostUtils from 'utils/post_utils'; + import PreviewPostAvatar from './avatar/avatar'; import PostAttachmentContainer from '../post_attachment_container/post_attachment_container'; @@ -161,6 +164,13 @@ const PostMessagePreview = (props: Props) => { )} + {PostUtils.hasAiGeneratedMetadata(previewPost) && ( + + )}