Skip to main content

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

ComponentDescriptionPurpose
DetectionIdentify @ symbols and potential mentionsTrigger autocomplete
SearchFind users matching typed textProvide selection options
MetadataStore mention position and user infoEnable custom rendering
RenderingDisplay mentions with custom stylingVisual differentiation
NotificationsAlert mentioned usersDrive engagement

Create Comments with Mentions

Integrate user mentions into comment creation with proper metadata for notifications and rendering.
  • iOS
  • Android
  • TypeScript
  • Flutter
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

Build sophisticated autocomplete with fuzzy search, recent users, and custom ranking.
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;
        });
    }
}
Create beautiful mention displays with avatars, hover cards, and custom styling.
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
    }
}
Implement comprehensive notification system for mentioned users.
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

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 TypeDescriptionRecommended Action
USER_NOT_FOUNDMentioned user doesn’t existRemove mention or show placeholder
PERMISSION_DENIEDCannot mention userShow permission error message
MENTION_LIMIT_EXCEEDEDToo many mentions in commentEnforce mention limits
INVALID_MENTION_FORMATMalformed mention dataValidate and sanitize mentions
NETWORK_ERRORFailed to search usersImplement retry with offline cache

Use Cases

Enable team members to mention each other for focused discussions.
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'
        });
    }
}
Build engaging social features with celebrity mentions and verified accounts.
class SocialMentionManager {
    func createInfluencerComment(
        text: String,
        mentions: [MentionData],
        influencerBoost: Bool = false
    ) {
        // Enhanced mention handling for influencers
        let enhancedMentions = mentions.map { mention -> [String: Any] in
            var mentionDict = [
                "userId": mention.userId,
                "displayName": mention.displayName,
                "index": mention.index,
                "length": mention.length
            ]
            
            // Add verification status
            if let user = getUserInfo(mention.userId) {
                mentionDict["verified"] = user.isVerified
                mentionDict["followerCount"] = user.followerCount
                mentionDict["influencerTier"] = user.influencerTier
            }
            
            return mentionDict
        }
        
        let metadata: [String: Any] = [
            "mentions": enhancedMentions,
            "originalText": text,
            "influencerBoost": influencerBoost,
            "engagementFeatures": [
                "priorityNotifications": true,
                "richPreview": true,
                "crossPlatformSync": true
            ]
        ]
        
        commentRepository.createComment(
            referenceId: postId,
            referenceType: .post,
            text: text,
            mentionUsers: mentions.map { $0.userId },
            metadata: metadata
        ) { result in
            // Handle result and trigger influencer-specific features
            self.handleInfluencerMentionResult(result, mentions: mentions)
        }
    }
    
    private func handleInfluencerMentionResult(
        _ result: Result<AmityComment, Error>,
        mentions: [MentionData]
    ) {
        switch result {
        case .success(let comment):
            // Trigger special notifications for verified users
            mentions.forEach { mention in
                if isVerifiedUser(mention.userId) {
                    sendPriorityNotification(to: mention.userId, comment: comment)
                }
            }
            
            // Analytics for influencer engagement
            trackInfluencerMention(comment: comment, mentions: mentions)
            
        case .failure(let error):
            handleMentionError(error)
        }
    }
}
Implement mention system for customer support with automatic routing.
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.