diff --git a/ceres/src/application/api_service/mono/cl/lifecycle.rs b/ceres/src/application/api_service/mono/cl/lifecycle.rs index 6f9e49a72..3e557aa92 100644 --- a/ceres/src/application/api_service/mono/cl/lifecycle.rs +++ b/ceres/src/application/api_service/mono/cl/lifecycle.rs @@ -9,6 +9,7 @@ use jupiter::model::cl_dto::CLDetails; use crate::{ application::{ api_service::mono::ClApplicationService, + member_identity::display_label_for_actor, webhook::{WebhookEvent, dispatch_cl_webhook}, }, model::change_list::{CLDetailRes, Condition, MergeBoxRes, UpdateClStatusPayload}, @@ -66,7 +67,7 @@ impl ClApplicationService { Ok(detail) } - pub async fn reopen_cl(&self, link: &str, username: &str) -> Result<(), MegaError> { + pub async fn reopen_cl(&self, link: &str, actor: &str) -> Result<(), MegaError> { let cl_storage = self.storage().cl_service.cl_store(); let model = cl_storage .get_cl(link) @@ -79,13 +80,14 @@ impl ClApplicationService { let link = model.link.clone(); cl_storage.reopen_cl(model.clone()).await?; + let display = display_label_for_actor(self.storage(), actor).await; self.storage() .cl_service .conversation_store() .add_conversation( &link, - username, - Some(format!("{username} reopen this")), + actor, + Some(format!("{display} reopen this")), ConvTypeEnum::Reopen, ) .await?; @@ -96,7 +98,7 @@ impl ClApplicationService { Ok(()) } - pub async fn close_cl(&self, link: &str, username: &str) -> Result<(), MegaError> { + pub async fn close_cl(&self, link: &str, actor: &str) -> Result<(), MegaError> { let cl_storage = self.storage().cl_service.cl_store(); let model = cl_storage .get_cl(link) @@ -109,13 +111,14 @@ impl ClApplicationService { let link = model.link.clone(); cl_storage.close_cl(model.clone()).await?; + let display = display_label_for_actor(self.storage(), actor).await; self.storage() .cl_service .conversation_store() .add_conversation( &link, - username, - Some(format!("{username} closed this")), + actor, + Some(format!("{display} closed this")), ConvTypeEnum::Closed, ) .await?; @@ -126,7 +129,7 @@ impl ClApplicationService { Ok(()) } - pub async fn merge_open_cl(&self, username: &str, link: &str) -> Result<(), MegaError> { + pub async fn merge_open_cl(&self, actor: &str, link: &str) -> Result<(), MegaError> { let cl_storage = self.storage().cl_service.cl_store(); let model = cl_storage .get_cl(link) @@ -138,7 +141,7 @@ impl ClApplicationService { } if model.status == MergeStatusEnum::Open { - self.merge_cl(username, model.clone()).await?; + self.merge_cl(actor, model.clone()).await?; if let Some(updated_model) = cl_storage.get_cl(link).await? { dispatch_cl_webhook(self.storage(), WebhookEvent::ClMerged, &updated_model); } @@ -199,13 +202,13 @@ impl ClApplicationService { pub async fn save_cl_comment( &self, link: &str, - username: &str, + actor: &str, content: &str, ) -> Result<(), MegaError> { let conv_type = if self .storage() .reviewer_storage() - .is_reviewer(link, username) + .is_reviewer(link, actor) .await? { ConvTypeEnum::Review @@ -216,10 +219,10 @@ impl ClApplicationService { self.storage() .cl_service .conversation_store() - .add_conversation(link, username, Some(content.to_string()), conv_type) + .add_conversation(link, actor, Some(content.to_string()), conv_type) .await?; - if let Err(e) = enqueue_cl_comment_notifications(self, username, link, content).await { + if let Err(e) = enqueue_cl_comment_notifications(self, actor, link, content).await { tracing::warn!("failed to enqueue cl comment notifications: {e}"); } @@ -244,7 +247,7 @@ impl ClApplicationService { pub async fn update_cl_status( &self, link: &str, - username: &str, + actor: &str, payload: &UpdateClStatusPayload, ) -> Result<(), MegaError> { let cl_storage = self.storage().cl_service.cl_store(); @@ -268,13 +271,14 @@ impl ClApplicationService { cl_storage .update_cl_status(model.clone(), new_status.clone()) .await?; + let display = display_label_for_actor(self.storage(), actor).await; self.storage() .cl_service .conversation_store() .add_conversation( link, - username, - Some(format!("{username} marked this as ready for review")), + actor, + Some(format!("{display} marked this as ready for review")), ConvTypeEnum::Review, ) .await?; @@ -286,13 +290,14 @@ impl ClApplicationService { cl_storage .update_cl_status(model.clone(), new_status.clone()) .await?; + let display = display_label_for_actor(self.storage(), actor).await; self.storage() .cl_service .conversation_store() .add_conversation( link, - username, - Some(format!("{username} marked this as draft")), + actor, + Some(format!("{display} marked this as draft")), ConvTypeEnum::Draft, ) .await?; @@ -311,10 +316,10 @@ impl ClApplicationService { pub async fn update_branch_with_webhook( &self, - username: &str, + actor: &str, link: &str, ) -> Result { - let new_head = self.update_branch(username, link).await?; + let new_head = self.update_branch(actor, link).await?; if let Some(cl_model) = self.storage().cl_service.cl_store().get_cl(link).await? { dispatch_cl_webhook(self.storage(), WebhookEvent::ClUpdated, &cl_model); } @@ -326,7 +331,7 @@ const EVENT_CL_COMMENT_CREATED: &str = "cl.comment.created"; async fn enqueue_cl_comment_notifications( service: &ClApplicationService, - actor_username: &str, + actor: &str, cl_link: &str, comment_text: &str, ) -> Result<(), MegaError> { @@ -350,7 +355,7 @@ async fn enqueue_cl_comment_notifications( for r in reviewers { recipients.insert(r.campsite_user_id); } - recipients.remove(actor_username); + recipients.remove(actor); for username in recipients { if !notif_stg @@ -366,9 +371,9 @@ async fn enqueue_cl_comment_notifications( }; let subject = format!("New comment on CL {cl_link}"); - let body_text = format!("{actor_username} commented on {cl_link}: {comment_text}"); + let body_text = format!("{actor} commented on {cl_link}: {comment_text}"); let body_html = format!( - "

{actor_username} commented on {cl_link}:

{}

", + "

{actor} commented on {cl_link}:

{}

", escape_html(comment_text) ); diff --git a/ceres/src/application/api_service/mono/conversation.rs b/ceres/src/application/api_service/mono/conversation.rs index 7949853f4..35ed250a2 100644 --- a/ceres/src/application/api_service/mono/conversation.rs +++ b/ceres/src/application/api_service/mono/conversation.rs @@ -1,9 +1,12 @@ use common::errors::MegaError; use super::context::ConversationApplicationService; -use crate::model::{ - change_list::MergeStatus, - conversation::{ConvType, ReferenceType}, +use crate::{ + application::member_identity::display_label_for_actor, + model::{ + change_list::MergeStatus, + conversation::{ConvType, ReferenceType}, + }, }; impl ConversationApplicationService { @@ -22,6 +25,19 @@ impl ConversationApplicationService { .await } + /// System timeline event: persist actor as campsite public id, comment uses display label. + pub async fn add_system_event( + &self, + link: &str, + actor: &str, + phrase: &str, + conv_type: ConvType, + ) -> Result { + let display = display_label_for_actor(self.ctx.storage(), actor).await; + self.add_conversation(link, actor, Some(format!("{display} {phrase}")), conv_type) + .await + } + pub async fn add_issue_mention_reference( &self, source_link: &str, @@ -34,13 +50,8 @@ impl ConversationApplicationService { .issue_store() .add_reference(source_link, ref_link, ReferenceType::Mention.into()) .await?; - self.add_conversation( - ref_link, - username, - Some(format!("{username} mentioned this on")), - ConvType::Mention, - ) - .await?; + self.add_system_event(ref_link, username, "mentioned this on", ConvType::Mention) + .await?; Ok(()) } diff --git a/ceres/src/application/api_service/mono/label_assignee.rs b/ceres/src/application/api_service/mono/label_assignee.rs index c9d62fc11..ceb158f7a 100644 --- a/ceres/src/application/api_service/mono/label_assignee.rs +++ b/ceres/src/application/api_service/mono/label_assignee.rs @@ -5,6 +5,7 @@ use common::errors::MegaError; use jupiter::model::common::LabelAssigneeParams; use super::context::IssueApplicationService; +use crate::application::member_identity::display_label_for_actor; impl IssueApplicationService { pub async fn update_item_labels( @@ -33,6 +34,8 @@ impl IssueApplicationService { .modify_labels(to_add.clone(), to_remove.clone(), params) .await?; + let display = display_label_for_actor(self.ctx.storage(), username).await; + if !to_remove.is_empty() { self.ctx .storage() @@ -41,7 +44,7 @@ impl IssueApplicationService { .add_conversation( link, username, - Some(format!("{username} removed {to_remove:?}")), + Some(format!("{display} removed {to_remove:?}")), ConvTypeEnum::Label, ) .await?; @@ -55,7 +58,7 @@ impl IssueApplicationService { .add_conversation( link, username, - Some(format!("{username} added {to_add:?}")), + Some(format!("{display} added {to_add:?}")), ConvTypeEnum::Label, ) .await?; @@ -93,6 +96,8 @@ impl IssueApplicationService { .modify_assignees(to_add.clone(), to_remove.clone(), params) .await?; + let display = display_label_for_actor(self.ctx.storage(), username).await; + if !to_remove.is_empty() { self.ctx .storage() @@ -101,7 +106,7 @@ impl IssueApplicationService { .add_conversation( link, username, - Some(format!("{username} unassigned {to_remove:?}")), + Some(format!("{display} unassigned {to_remove:?}")), ConvTypeEnum::Assignee, ) .await?; @@ -115,7 +120,7 @@ impl IssueApplicationService { .add_conversation( link, username, - Some(format!("{username} assigned {to_add:?}")), + Some(format!("{display} assigned {to_add:?}")), ConvTypeEnum::Assignee, ) .await?; diff --git a/mono/src/api/router/issue_router.rs b/mono/src/api/router/issue_router.rs index 874a68b8b..492d1adaa 100644 --- a/mono/src/api/router/issue_router.rs +++ b/mono/src/api/router/issue_router.rs @@ -135,12 +135,7 @@ async fn close_issue( state .services() .conversation() - .add_conversation( - &link, - actor, - Some(format!("{} closed this", actor)), - ConvType::Closed, - ) + .add_system_event(&link, actor, "closed this", ConvType::Closed) .await?; Ok(Json(CommonResult::success(None))) } @@ -167,12 +162,7 @@ async fn reopen_issue( state .services() .conversation() - .add_conversation( - &link, - actor, - Some(format!("{} reopen this", actor)), - ConvType::Closed, - ) + .add_system_event(&link, actor, "reopen this", ConvType::Reopen) .await?; Ok(Json(CommonResult::success(None))) } diff --git a/moon/apps/web/components/ClView/CloseItem.tsx b/moon/apps/web/components/ClView/CloseItem.tsx index 75df4d16f..8f13255e6 100644 --- a/moon/apps/web/components/ClView/CloseItem.tsx +++ b/moon/apps/web/components/ClView/CloseItem.tsx @@ -4,7 +4,7 @@ import { ConditionalWrap } from '@gitmono/ui' import { ActorAvatar } from '@/components/ActorAvatar' import { BotBadge } from '@/components/BotBadge' import { useMemberByActor } from '@/hooks/useMemberByActor' -import { megaUserHandle } from '@/utils/megaUser' +import { megaUserHandle, systemEventPhrase } from '@/utils/megaUser' import { MemberHovercard } from '../InlinePost/MemberHovercard' import HandleTime from './components/HandleTime' @@ -18,6 +18,7 @@ const CloseItem = ({ conv }: CloseItemProps) => { const { data: member } = useMemberByActor(conv.username, !isBot) const profileUsername = member?.user.username || conv.username const displayName = megaUserHandle(member?.user, conv.username) || conv.username + const phrase = systemEventPhrase(conv.comment, conv.username, 'closed this') return ( <> @@ -37,13 +38,9 @@ const CloseItem = ({ conv }: CloseItemProps) => {
- {!conv.comment && ( - <> - {displayName} - {isBot && } - - )} - {conv.comment} + {displayName} + {isBot && } + {phrase}
diff --git a/moon/apps/web/components/ClView/EditItem.tsx b/moon/apps/web/components/ClView/EditItem.tsx index 3aec02569..02b43f548 100644 --- a/moon/apps/web/components/ClView/EditItem.tsx +++ b/moon/apps/web/components/ClView/EditItem.tsx @@ -4,7 +4,7 @@ import { ConditionalWrap } from '@gitmono/ui' import { ActorAvatar } from '@/components/ActorAvatar' import { BotBadge } from '@/components/BotBadge' import { useMemberByActor } from '@/hooks/useMemberByActor' -import { megaUserHandle } from '@/utils/megaUser' +import { megaUserHandle, systemEventPhrase } from '@/utils/megaUser' import { MemberHovercard } from '../InlinePost/MemberHovercard' import HandleTime from './components/HandleTime' @@ -19,6 +19,7 @@ const EditItem = ({ conv }: EditItemProps) => { const { data: member } = useMemberByActor(conv.username, !isBot) const profileUsername = member?.user.username || conv.username const displayName = megaUserHandle(member?.user, conv.username) || conv.username + const phrase = systemEventPhrase(conv.comment, conv.username, 'updated this') return ( <> @@ -38,14 +39,9 @@ const EditItem = ({ conv }: EditItemProps) => {
- {!conv.comment && ( - <> - {displayName} - {isBot && } - - )} - {conv.comment} - {!!conv.comment && isBot && } + {displayName} + {isBot && } + {phrase}
diff --git a/moon/apps/web/components/ClView/ReopenItem.tsx b/moon/apps/web/components/ClView/ReopenItem.tsx index 3a96c076b..5bc8d4453 100644 --- a/moon/apps/web/components/ClView/ReopenItem.tsx +++ b/moon/apps/web/components/ClView/ReopenItem.tsx @@ -4,7 +4,7 @@ import { ConditionalWrap } from '@gitmono/ui' import { ActorAvatar } from '@/components/ActorAvatar' import { BotBadge } from '@/components/BotBadge' import { useMemberByActor } from '@/hooks/useMemberByActor' -import { megaUserHandle } from '@/utils/megaUser' +import { megaUserHandle, systemEventPhrase } from '@/utils/megaUser' import { MemberHovercard } from '../InlinePost/MemberHovercard' import HandleTime from './components/HandleTime' @@ -18,6 +18,7 @@ const ReopenItem = ({ conv }: ReopenItemProps) => { const { data: member } = useMemberByActor(conv.username, !isBot) const profileUsername = member?.user.username || conv.username const displayName = megaUserHandle(member?.user, conv.username) || conv.username + const phrase = systemEventPhrase(conv.comment, conv.username, 'reopen this') return ( <> @@ -37,14 +38,9 @@ const ReopenItem = ({ conv }: ReopenItemProps) => {
- {!conv.comment && ( - <> - {displayName} - {isBot && } - - )} - {conv.comment} - {!!conv.comment && isBot && } + {displayName} + {isBot && } + {phrase}
diff --git a/moon/apps/web/utils/megaUser.ts b/moon/apps/web/utils/megaUser.ts index c69ebb61a..2f45f7aa5 100644 --- a/moon/apps/web/utils/megaUser.ts +++ b/moon/apps/web/utils/megaUser.ts @@ -22,3 +22,43 @@ export function megaUserHandlesMatch( // Transitional: pre-backfill rows may still hold github_login / username strings. return stored === megaUserHandle(user) || (!!user.username && stored === user.username) } + +/** + * System timeline comments historically prefix the campsite public id, and + * newer writes may prefix a display label. Strip that leading actor token so + * the UI can render displayName + phrase separately. + */ +export function systemEventPhrase(comment: string | null | undefined, actor: string, fallback: string): string { + const text = comment?.trim() + + if (!text) return fallback + + let rest = text + const actorPrefix = `${actor} ` + + if (rest.startsWith(actorPrefix)) { + rest = rest.slice(actorPrefix.length) + } + + if (rest === fallback) return fallback + if (rest.endsWith(` ${fallback}`)) return fallback + + // "{displayLabel} marked this as draft|ready for review|..." + const space = rest.indexOf(' ') + + if (space > 0) { + const after = rest.slice(space + 1) + + if ( + after === fallback || + after.startsWith('marked this as ') || + after === 'closed this' || + after === 'reopen this' || + after === 'mentioned this on' + ) { + return after + } + } + + return rest +}