Mentions in Comments
Enable powerful user tagging in comments with mentions that automatically notify users, support custom rendering, and provide rich autocomplete experiences. Mentions enhance engagement by connecting users and facilitating targeted conversations.Mentions automatically trigger push notifications to tagged users and support custom metadata for rich mention rendering with avatars, names, and custom styling.
Architecture Overview
Mention Components
| Component | Description | Purpose |
|---|---|---|
| Detection | Identify @ symbols and potential mentions | Trigger autocomplete |
| Search | Find users matching typed text | Provide selection options |
| Metadata | Store mention position and user info | Enable custom rendering |
| Rendering | Display mentions with custom styling | Visual differentiation |
| Notifications | Alert mentioned users | Drive engagement |
Create Comments with Mentions
Integrate user mentions into comment creation with proper metadata for notifications and rendering.- iOS
- Android
- TypeScript
- Flutter
Copy
Ask AI
import AmitySDK
struct MentionData {
let userId: String
let displayName: String
let index: Int
let length: Int
}
class CommentMentionManager {
private let commentRepository: AmityCommentRepository
private let userRepository: AmityUserRepository
init(client: AmityClient) {
self.commentRepository = AmityCommentRepository(client: client)
self.userRepository = AmityUserRepository(client: client)
}
// Create comment with mentions
func createCommentWithMentions(
referenceId: String,
referenceType: AmityCommentReferenceType,
text: String,
mentions: [MentionData],
parentId: String? = nil,
completion: @escaping (Result<AmityComment, Error>) -> Void
) {
// Extract user IDs from mentions
let mentionUserIds = mentions.map { $0.userId }
// Create mention metadata for proper rendering
let mentionMetadata = createMentionMetadata(from: mentions, in: text)
let builder = AmityCommentCreationDataBuilder()
builder.setText(text)
builder.setMentionUsers(mentionUserIds)
builder.setMetadata(mentionMetadata)
builder.setParentId(parentId)
commentRepository.createComment(
for: referenceId,
referenceType: referenceType,
with: builder.build()
).observeOnce { result in
completion(result)
}
}
// Create mention metadata for custom rendering
private func createMentionMetadata(from mentions: [MentionData], in text: String) -> [String: Any] {
let mentionArray = mentions.map { mention in
return [
"userId": mention.userId,
"displayName": mention.displayName,
"type": "user",
"index": mention.index,
"length": mention.length
]
}
return [
"mentions": mentionArray,
"originalText": text
]
}
// Search users for autocomplete
func searchUsers(
query: String,
completion: @escaping (Result<[AmityUser], Error>) -> Void
) {
let userQuery = AmityUserQuery.Builder()
.setDisplayName(query)
.setLimit(10)
.build()
userRepository.getUsers(with: userQuery).observeOnce { result in
switch result {
case .success(let userCollection):
let users = Array(userCollection.allObjects())
completion(.success(users))
case .failure(let error):
completion(.failure(error))
}
}
}
// Update comment with mentions
func updateCommentWithMentions(
commentId: String,
newText: String,
mentions: [MentionData],
completion: @escaping (Result<AmityComment, Error>) -> Void
) {
let mentionUserIds = mentions.map { $0.userId }
let mentionMetadata = createMentionMetadata(from: mentions, in: newText)
let editor = AmityCommentEditor(client: commentRepository.client)
editor.edit(commentId: commentId)
.setText(newText)
.setMentionUsers(mentionUserIds)
.setMetadata(mentionMetadata)
.update { result in
completion(result)
}
}
// Remove mentions from comment
func removeMentionsFromComment(
commentId: String,
newText: String,
completion: @escaping (Result<AmityComment, Error>) -> Void
) {
let editor = AmityCommentEditor(client: commentRepository.client)
editor.edit(commentId: commentId)
.setText(newText)
.setMentionUsers([]) // Empty array removes all mentions
.setMetadata([:]) // Empty metadata
.update { result in
completion(result)
}
}
}
// Custom text view with mention support
class MentionTextView: UITextView {
var onMentionTriggered: ((String) -> Void)?
var onUserSelected: ((AmityUser) -> Void)?
private var currentMentionRange: NSRange?
private var mentionData: [MentionData] = []
override func awakeFromNib() {
super.awakeFromNib()
delegate = self
}
// Parse text for mentions and apply styling
func applyMentionStyling() {
let attributedText = NSMutableAttributedString(string: text)
// Reset styling
attributedText.addAttribute(.foregroundColor, value: UIColor.label, range: NSRange(location: 0, length: text.count))
// Apply mention styling
for mention in mentionData {
let range = NSRange(location: mention.index, length: mention.length)
attributedText.addAttributes([
.foregroundColor: UIColor.systemBlue,
.font: UIFont.boldSystemFont(ofSize: font?.pointSize ?? 16)
], range: range)
}
self.attributedText = attributedText
}
// Insert mention at current position
func insertMention(_ user: AmityUser) {
guard let mentionRange = currentMentionRange else { return }
let mentionText = "@\(user.displayName ?? user.userId)"
let newText = (text as NSString).replacingCharacters(in: mentionRange, with: mentionText)
// Update mention data
let mention = MentionData(
userId: user.userId,
displayName: user.displayName ?? user.userId,
index: mentionRange.location,
length: mentionText.count
)
mentionData.append(mention)
text = newText
applyMentionStyling()
currentMentionRange = nil
}
// Get current mentions for comment creation
func getCurrentMentions() -> [MentionData] {
return mentionData
}
}
extension MentionTextView: UITextViewDelegate {
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
// Detect @ symbol for mention triggering
if text == "@" {
currentMentionRange = NSRange(location: range.location, length: 1)
onMentionTriggered?("")
return true
}
// Handle mention typing
if let mentionRange = currentMentionRange, text != " " && text != "\n" {
let currentQuery = (textView.text as NSString).substring(with: NSRange(
location: mentionRange.location + 1,
length: range.location - mentionRange.location - 1
)) + text
onMentionTriggered?(currentQuery)
}
return true
}
}
// Usage in view controller
class CommentComposeViewController: UIViewController {
@IBOutlet weak var mentionTextView: MentionTextView!
@IBOutlet weak var userSuggestionsTableView: UITableView!
private let mentionManager: CommentMentionManager
private var suggestedUsers: [AmityUser] = []
private let postId: String
init(postId: String, client: AmityClient) {
self.postId = postId
self.mentionManager = CommentMentionManager(client: client)
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
setupMentionHandling()
}
private func setupMentionHandling() {
mentionTextView.onMentionTriggered = { [weak self] query in
self?.searchUsersForMention(query: query)
}
mentionTextView.onUserSelected = { [weak self] user in
self?.mentionTextView.insertMention(user)
self?.hideSuggestions()
}
}
private func searchUsersForMention(query: String) {
guard !query.isEmpty else {
hideSuggestions()
return
}
mentionManager.searchUsers(query: query) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let users):
self?.showUserSuggestions(users)
case .failure(let error):
print("Failed to search users: \(error)")
self?.hideSuggestions()
}
}
}
}
private func showUserSuggestions(_ users: [AmityUser]) {
suggestedUsers = users
userSuggestionsTableView.reloadData()
userSuggestionsTableView.isHidden = false
}
private func hideSuggestions() {
userSuggestionsTableView.isHidden = true
suggestedUsers.removeAll()
}
@IBAction func postComment() {
let text = mentionTextView.text ?? ""
let mentions = mentionTextView.getCurrentMentions()
mentionManager.createCommentWithMentions(
referenceId: postId,
referenceType: .post,
text: text,
mentions: mentions
) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success:
self?.navigationController?.popViewController(animated: true)
case .failure(let error):
self?.showError("Failed to post comment: \(error.localizedDescription)")
}
}
}
}
}
Advanced Mention Features
Custom Mention Autocomplete
Custom Mention Autocomplete
Build sophisticated autocomplete with fuzzy search, recent users, and custom ranking.
Copy
Ask AI
interface MentionAutocomplete {
searchUsers(query: string, options?: SearchOptions): Promise<User[]>;
getRankedSuggestions(query: string, context: MentionContext): Promise<User[]>;
cacheRecentMentions(userId: string): void;
}
class AdvancedMentionAutocomplete implements MentionAutocomplete {
private recentMentions = new Map<string, Date>();
private userCache = new Map<string, User>();
async searchUsers(query: string, options: SearchOptions = {}): Promise<User[]> {
const {
limit = 10,
includeRecent = true,
fuzzySearch = true,
contextUsers = []
} = options;
// Search with multiple strategies
const results = await Promise.all([
this.exactMatch(query),
fuzzySearch ? this.fuzzyMatch(query) : Promise.resolve([]),
includeRecent ? this.getRecentMentions() : Promise.resolve([]),
this.searchFromContext(query, contextUsers)
]);
// Merge and rank results
const allUsers = this.mergeAndDeduplicateUsers(results.flat());
return this.rankUsers(allUsers, query).slice(0, limit);
}
private async exactMatch(query: string): Promise<User[]> {
return SocialPlus.searchUsers({
displayName: query,
exactMatch: true
});
}
private async fuzzyMatch(query: string): Promise<User[]> {
const users = await SocialPlus.searchUsers({
displayName: query,
fuzzyMatch: true
});
// Apply fuzzy search scoring
return users.map(user => ({
...user,
score: this.calculateFuzzyScore(query, user.displayName || user.userId)
})).filter(user => user.score > 0.3);
}
private rankUsers(users: User[], query: string): User[] {
return users.sort((a, b) => {
// Prioritize recent mentions
const aRecent = this.recentMentions.has(a.userId) ? 1000 : 0;
const bRecent = this.recentMentions.has(b.userId) ? 1000 : 0;
// Calculate relevance score
const aScore = this.calculateRelevanceScore(query, a) + aRecent;
const bScore = this.calculateRelevanceScore(query, b) + bRecent;
return bScore - aScore;
});
}
}
Rich Mention Rendering
Rich Mention Rendering
Create beautiful mention displays with avatars, hover cards, and custom styling.
Copy
Ask AI
class RichMentionRenderer {
func renderMentionInAttributedString(
_ mention: MentionData,
in attributedString: NSMutableAttributedString,
with context: RenderingContext
) {
let range = NSRange(location: mention.index, length: mention.length)
// Create custom mention attachment
let mentionAttachment = MentionTextAttachment()
mentionAttachment.user = mention.user
mentionAttachment.bounds = CGRect(x: 0, y: -2, width: 20, height: 16)
// Style the mention text
attributedString.addAttributes([
.foregroundColor: UIColor.systemBlue,
.backgroundColor: UIColor.systemBlue.withAlphaComponent(0.1),
.font: UIFont.boldSystemFont(ofSize: context.fontSize),
.link: URL(string: "mention://\(mention.userId)")!,
.attachment: mentionAttachment
], range: range)
// Add custom interaction handling
attributedString.addAttribute(
.init("mentionData"),
value: mention,
range: range
)
}
func createHoverCard(for user: AmityUser) -> UIView {
let card = UIView()
card.backgroundColor = .systemBackground
card.layer.cornerRadius = 8
card.layer.shadowOpacity = 0.1
card.layer.shadowRadius = 4
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 12
stackView.alignment = .center
// Avatar
let avatarView = UIImageView()
avatarView.widthAnchor.constraint(equalToConstant: 40).isActive = true
avatarView.heightAnchor.constraint(equalToConstant: 40).isActive = true
avatarView.layer.cornerRadius = 20
avatarView.clipsToBounds = true
if let avatarUrl = user.avatarUrl {
avatarView.loadImage(from: avatarUrl)
} else {
avatarView.backgroundColor = .systemGray4
avatarView.contentMode = .center
}
// User info
let infoStack = UIStackView()
infoStack.axis = .vertical
infoStack.spacing = 2
let nameLabel = UILabel()
nameLabel.text = user.displayName
nameLabel.font = .boldSystemFont(ofSize: 16)
let usernameLabel = UILabel()
usernameLabel.text = "@\(user.userId)"
usernameLabel.font = .systemFont(ofSize: 14)
usernameLabel.textColor = .secondaryLabel
infoStack.addArrangedSubview(nameLabel)
infoStack.addArrangedSubview(usernameLabel)
stackView.addArrangedSubview(avatarView)
stackView.addArrangedSubview(infoStack)
card.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: card.topAnchor, constant: 12),
stackView.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
stackView.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
stackView.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -12)
])
return card
}
}
Mention Notifications
Mention Notifications
Implement comprehensive notification system for mentioned users.
Copy
Ask AI
class MentionNotificationManager(private val context: Context) {
fun sendMentionNotifications(
comment: AmityComment,
mentionedUsers: List<String>
) {
mentionedUsers.forEach { userId ->
sendNotificationToUser(userId, comment)
}
}
private fun sendNotificationToUser(userId: String, comment: AmityComment) {
// Create rich notification
val notification = NotificationCompat.Builder(context, MENTION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_mention)
.setContentTitle("You were mentioned")
.setContentText("${comment.creator?.displayName} mentioned you in a comment")
.setStyle(createBigTextStyle(comment))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setColor(ContextCompat.getColor(context, R.color.mention_color))
.addAction(createReplyAction(comment))
.addAction(createViewAction(comment))
.build()
val notificationId = generateNotificationId(userId, comment.commentId)
NotificationManagerCompat.from(context).notify(notificationId, notification)
// Track mention analytics
trackMentionNotification(userId, comment)
}
private fun createBigTextStyle(comment: AmityComment): NotificationCompat.Style {
val text = when (comment.dataType) {
AmityDataType.TEXT -> (comment.data as? AmityCommentTextData)?.text
AmityDataType.IMAGE -> "📷 ${(comment.data as? AmityCommentImageData)?.caption ?: "Image comment"}"
else -> "Comment"
}
return NotificationCompat.BigTextStyle()
.bigText("${comment.creator?.displayName}: $text")
.setSummaryText("Tap to reply or view")
}
private fun createReplyAction(comment: AmityComment): NotificationCompat.Action {
val replyIntent = createReplyIntent(comment)
val replyPendingIntent = PendingIntent.getActivity(
context,
0,
replyIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
return NotificationCompat.Action.Builder(
R.drawable.ic_reply,
"Reply",
replyPendingIntent
).build()
}
private fun trackMentionNotification(userId: String, comment: AmityComment) {
AnalyticsManager.track("mention_notification_sent", mapOf(
"mentioned_user_id" to userId,
"comment_id" to comment.commentId,
"post_id" to comment.referenceId,
"mention_count" to comment.mentionUsers?.size,
"comment_type" to comment.dataType.name
))
}
}
Best Practices
Performance Optimization
- Cache user search results to reduce API calls
- Implement debounced search for autocomplete
- Use virtual scrolling for large user lists
- Optimize mention detection algorithms
User Experience
- Provide visual feedback during mention typing
- Show user avatars in autocomplete suggestions
- Implement keyboard navigation for suggestions
- Support both @ and # mention triggers
Accessibility
- Provide proper screen reader support for mentions
- Implement keyboard shortcuts for mention actions
- Use semantic HTML for web implementations
- Support high contrast mode for mention styling
Data Management
- Validate mention data before sending
- Handle deleted/deactivated user mentions gracefully
- Implement proper mention metadata structure
- Cache frequently mentioned users
Mention Data Structure
Copy
Ask AI
interface Mention {
userId: string;
displayName: string;
type: 'user' | 'channel' | 'custom';
index: number; // Position in text
length: number; // Length of mention text
metadata?: {
avatar?: string;
verified?: boolean;
customData?: Record<string, any>;
};
}
interface MentionMetadata {
mentions: Mention[];
originalText: string;
renderingHints?: {
showAvatars: boolean;
customStyling: Record<string, any>;
};
}
interface Comment {
commentId: string;
text: string;
mentionUsers: string[]; // Array of mentioned user IDs
metadata: MentionMetadata; // Rich mention data
// ...other comment properties
}
Error Handling
| Error Type | Description | Recommended Action |
|---|---|---|
| USER_NOT_FOUND | Mentioned user doesn’t exist | Remove mention or show placeholder |
| PERMISSION_DENIED | Cannot mention user | Show permission error message |
| MENTION_LIMIT_EXCEEDED | Too many mentions in comment | Enforce mention limits |
| INVALID_MENTION_FORMAT | Malformed mention data | Validate and sanitize mentions |
| NETWORK_ERROR | Failed to search users | Implement retry with offline cache |
Use Cases
Team Collaboration Comments
Team Collaboration Comments
Enable team members to mention each other for focused discussions.
Copy
Ask AI
class TeamMentionSystem {
private teamMembers: User[] = [];
async initializeTeamContext(teamId: string): Promise<void> {
this.teamMembers = await SocialPlus.getTeamMembers(teamId);
}
async createTeamComment(
content: string,
mentions: Mention[],
priority: 'low' | 'medium' | 'high' = 'medium'
): Promise<Comment> {
// Validate mentions are team members
const validMentions = mentions.filter(mention =>
this.teamMembers.some(member => member.userId === mention.userId)
);
// Add priority metadata
const metadata = {
mentions: validMentions,
originalText: content,
priority,
teamId: this.teamId,
notificationSettings: {
urgentNotification: priority === 'high',
emailDigest: priority !== 'low'
}
};
return SocialPlus.createComment({
text: content,
mentionUsers: validMentions.map(m => m.userId),
metadata,
referenceType: 'team_discussion'
});
}
}
Social Media Engagement
Social Media Engagement
Build engaging social features with celebrity mentions and verified accounts.
Copy
Ask AI
Customer Support Mentions
Customer Support Mentions
Implement mention system for customer support with automatic routing.
Copy
Ask AI
class SupportMentionSystem {
private val supportAgents = mutableMapOf<String, SupportAgent>()
private val departmentRouting = mapOf(
"billing" to listOf("agent_billing_1", "agent_billing_2"),
"technical" to listOf("agent_tech_1", "agent_tech_2"),
"general" to listOf("agent_general_1", "agent_general_2")
)
fun createSupportComment(
ticketId: String,
text: String,
mentions: List<MentionData>,
department: String? = null,
priority: SupportPriority = SupportPriority.NORMAL
) {
// Auto-route mentions based on department
val routedMentions = if (mentions.isEmpty() && department != null) {
getAvailableAgents(department).take(1).map { agent ->
MentionData(
userId = agent.userId,
displayName = agent.displayName,
index = text.length,
length = 0
)
}
} else {
mentions
}
val enhancedText = if (routedMentions.isNotEmpty() && mentions.isEmpty()) {
"$text ${routedMentions.joinToString(" ") { "@${it.displayName}" }}"
} else {
text
}
val metadata = mapOf(
"mentions" to routedMentions.map { mention ->
mapOf(
"userId" to mention.userId,
"displayName" to mention.displayName,
"type" to "support_agent",
"department" to (supportAgents[mention.userId]?.department ?: "general"),
"autoRouted" to (mentions.isEmpty())
)
},
"supportMetadata" to mapOf(
"ticketId" to ticketId,
"priority" to priority.name,
"department" to department,
"escalationLevel" to if (priority == SupportPriority.URGENT) 1 else 0
)
)
commentRepository.createComment(
referenceId = ticketId,
referenceType = AmityCommentReferenceType.CUSTOM,
text = enhancedText,
mentionUsers = routedMentions.map { it.userId },
metadata = metadata
) { result ->
handleSupportCommentResult(result, routedMentions, priority)
}
}
private fun getAvailableAgents(department: String): List<SupportAgent> {
return departmentRouting[department]
?.mapNotNull { supportAgents[it] }
?.filter { it.isOnline && it.availableCapacity > 0 }
?.sortedBy { it.currentWorkload }
?: emptyList()
}
}
Mention Rendering
For comprehensive mention rendering support including custom styling, hover cards, and interactive elements, refer to our Mentions Guide documentation which covers:- Custom Mention Objects: Rich mention data structures
- Rendering Patterns: Platform-specific rendering implementations
- Interactive Features: Click handlers, hover cards, and user profiles
- Styling Guidelines: Design system integration and theming
- Accessibility Support: Screen reader compatibility and keyboard navigation
Mentions automatically trigger push notifications to tagged users. Ensure your app has proper notification permissions and handles notification preferences per user settings.
Related Documentation
- Mentions Guide - Complete mention system guide
- Create Comment - Basic comment creation
- Push Notifications - Notification setup and handling
- User Management - User search and profile features
- Real-time Events - Live mention updates