mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
Ai-generated post fixes (#34490)
This commit is contained in:
parent
188b57fbcb
commit
9ea080024b
6 changed files with 159 additions and 12 deletions
|
|
@ -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(<PostComponent {...props}/>);
|
||||
|
||||
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(<PostComponent {...props}/>);
|
||||
|
||||
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(<PostComponent {...props}/>);
|
||||
|
||||
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(<PostComponent {...props}/>);
|
||||
|
||||
// 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(<PostComponent {...props}/>);
|
||||
|
||||
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(<PostComponent {...props}/>);
|
||||
|
||||
expect(screen.getByLabelText('Message posted by @aibot')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) && (
|
||||
<AiGeneratedIndicator
|
||||
userId={post.props.ai_generated_by}
|
||||
username={post.props.ai_generated_by_username}
|
||||
userId={post.props.ai_generated_by as string}
|
||||
username={post.props.ai_generated_by_username as string}
|
||||
postAuthorId={post.user_id}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 = <strong className='colon'>{':'}</strong>;
|
||||
|
||||
// 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 = (
|
||||
<AiGeneratedIndicator
|
||||
userId={post.props.ai_generated_by as string}
|
||||
username={post.props.ai_generated_by_username as string}
|
||||
postAuthorId={post.user_id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const customStatus = (
|
||||
|
|
@ -148,6 +162,7 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
|
|||
{colon}
|
||||
{botIndicator}
|
||||
{customStatus}
|
||||
{aiIndicator}
|
||||
</div>);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
|||
<PriorityLabel priority={previewPost.metadata.priority.priority}/>
|
||||
</span>
|
||||
)}
|
||||
{PostUtils.hasAiGeneratedMetadata(previewPost) && (
|
||||
<AiGeneratedIndicator
|
||||
userId={previewPost.props.ai_generated_by as string}
|
||||
username={previewPost.props.ai_generated_by_username as string}
|
||||
postAuthorId={previewPost.user_id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<PostMessageView
|
||||
|
|
|
|||
|
|
@ -1613,7 +1613,7 @@
|
|||
.ai-generated-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
margin-left: 4px;
|
||||
color: inherit;
|
||||
opacity: 0.73;
|
||||
|
||||
|
|
@ -1623,6 +1623,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.post--compact .col__name .ai-generated-indicator {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.post__permalink {
|
||||
display: inline-block;
|
||||
color: inherit;
|
||||
|
|
|
|||
|
|
@ -635,10 +635,25 @@ export function areConsecutivePostsBySameUser(post: Post, previousPost: Post): b
|
|||
if (!(post && previousPost)) {
|
||||
return false;
|
||||
}
|
||||
return post.user_id === previousPost.user_id && // The post is by the same user
|
||||
post.create_at - previousPost.create_at <= Posts.POST_COLLAPSE_TIMEOUT && // And was within a short time period
|
||||
!(post.props && post.props.from_webhook) && !(previousPost.props && previousPost.props.from_webhook) && // And neither is from a webhook
|
||||
!isSystemMessage(post) && !isSystemMessage(previousPost); // And neither is a system message
|
||||
|
||||
const sameUser = post.user_id === previousPost.user_id;
|
||||
const withinTimeWindow = post.create_at - previousPost.create_at <= Posts.POST_COLLAPSE_TIMEOUT;
|
||||
const notFromWebhook = !(post.props && post.props.from_webhook) && !(previousPost.props && previousPost.props.from_webhook);
|
||||
const notSystemMessage = !isSystemMessage(post) && !isSystemMessage(previousPost);
|
||||
const sameAiGeneratedStatus = post.props?.ai_generated_by === previousPost.props?.ai_generated_by;
|
||||
|
||||
return sameUser &&
|
||||
withinTimeWindow &&
|
||||
notFromWebhook &&
|
||||
notSystemMessage &&
|
||||
sameAiGeneratedStatus;
|
||||
}
|
||||
|
||||
// Checks if a post has valid AI-generated metadata
|
||||
export function hasAiGeneratedMetadata(post: Post): boolean {
|
||||
return 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';
|
||||
}
|
||||
|
||||
// Constructs the URL of a post.
|
||||
|
|
|
|||
Loading…
Reference in a new issue