add @mentions — parsed server-side, rendered as highlighted spans
- Backend: parse_mentions() extracts @username from content - Message API response includes mentions: string[] field - Frontend: renderContent() highlights @mentions in hot orange - Works with alphanumeric + hyphens + underscores Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,7 @@ pub struct Message {
|
|||||||
pub content: String,
|
pub content: String,
|
||||||
#[ts(optional)]
|
#[ts(optional)]
|
||||||
pub metadata: Option<serde_json::Value>,
|
pub metadata: Option<serde_json::Value>,
|
||||||
|
pub mentions: Vec<String>,
|
||||||
#[ts(optional)]
|
#[ts(optional)]
|
||||||
pub reply_to: Option<Uuid>,
|
pub reply_to: Option<Uuid>,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
|
|||||||
@@ -2,6 +2,16 @@ use chrono::{DateTime, Utc};
|
|||||||
use sqlx::FromRow;
|
use sqlx::FromRow;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// Extract @mentions from message content
|
||||||
|
fn parse_mentions(content: &str) -> Vec<String> {
|
||||||
|
content
|
||||||
|
.split_whitespace()
|
||||||
|
.filter(|w| w.starts_with('@') && w.len() > 1)
|
||||||
|
.map(|w| w.trim_start_matches('@').trim_end_matches(|c: char| !c.is_alphanumeric() && c != '_' && c != '-').to_string())
|
||||||
|
.filter(|m| !m.is_empty())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// DB row types — these map directly to SQL tables.
|
/// DB row types — these map directly to SQL tables.
|
||||||
/// Separate from API types in colony-types.
|
/// Separate from API types in colony-types.
|
||||||
|
|
||||||
@@ -87,6 +97,7 @@ impl MessageRow {
|
|||||||
} else {
|
} else {
|
||||||
self.content.clone()
|
self.content.clone()
|
||||||
},
|
},
|
||||||
|
mentions: parse_mentions(&self.content),
|
||||||
metadata: self
|
metadata: self
|
||||||
.metadata
|
.metadata
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|||||||
@@ -26,6 +26,21 @@ function timeAgo(dateStr: string): string {
|
|||||||
return `${Math.floor(hrs / 24)}d`;
|
return `${Math.floor(hrs / 24)}d`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderContent(text: string) {
|
||||||
|
// Split on @mentions and render them as highlighted spans
|
||||||
|
const parts = text.split(/(@[\w-]+)/g);
|
||||||
|
return parts.map((part, i) => {
|
||||||
|
if (part.startsWith("@")) {
|
||||||
|
return (
|
||||||
|
<span key={i} className="text-primary font-bold cursor-default">
|
||||||
|
{part}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return part;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function MessageItem({ message, replyTarget, onReply }: Props) {
|
export function MessageItem({ message, replyTarget, onReply }: Props) {
|
||||||
const [metaOpen, setMetaOpen] = useState(false);
|
const [metaOpen, setMetaOpen] = useState(false);
|
||||||
const isAgent = message.user.role === "agent";
|
const isAgent = message.user.role === "agent";
|
||||||
@@ -110,7 +125,7 @@ export function MessageItem({ message, replyTarget, onReply }: Props) {
|
|||||||
{isDeleted ? (
|
{isDeleted ? (
|
||||||
<span className="italic text-muted-foreground/40">[deleted]</span>
|
<span className="italic text-muted-foreground/40">[deleted]</span>
|
||||||
) : (
|
) : (
|
||||||
message.content
|
renderContent(message.content)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user