Chatter postings with @Mentions are a good way to raise user engagement in a Salesforce implementation, and a nice feature to have in a custom visualforce page. The introduction of the ConnectAPI makes it easier than ever before to tap into the powerful potential of Chatter from a custom application. But going fully custom means we have to give up some things, like the handy Chatter feed Visualforce component that allows for auto-completion of @mentions, among other useful features.

Getting possible matches for @mentions is the easy part. The ConnectApi provides handy methods and classes to get you just the information you need to feed into plugins like the incredibly awesome ment.io plugin for Angular.js, or to your own custom front end solution. Such solutions should allow you as a developer to autocomplete user or chatter group names and add them to the textarea in the form of @[“User Name”]. This occurs when the user starts typing the first letters of the first or last name of a chatter user or on the first letters of any single word in a Chatter group name. This effectively mimics native functionality for autocompleting @mentions in the Chatter Feed component. Even if you don’t use auto-completion, this solution will let you allow users to type in their mentions in the form of @[“User Name”], where the “User Name” part exactly matches a user or group name. This can be handy for integration with many different input sources that follow the convention.

public static ConnectApi.MentionCompletionPage getMentionCompletionPage( String communityId, String queryString, string contextId){
    return ConnectApi.Mentions.getMentionCompletions(communityId,queryString,contextId);
}

The harder part turns out to be parsing a message that might contain multiple mentions to users or groups. Here is the sample code direct from the ConnectAPI documentation, which handles a fairly naive use case.

String communityId = null;
ConnectApi.FeedType feedType = ConnectApi.FeedType.UserProfile;
ConnectApi.FeedItemInput input = new ConnectApi.FeedItemInput();
ConnectApi.MessageBodyInput messageInput = new ConnectApi.MessageBodyInput();
ConnectApi.TextSegmentInput textSegment;
ConnectApi.MentionSegmentInput mentionSegment = new ConnectApi.MentionSegmentInput();
messageInput.messageSegments = new List<ConnectApi.MessageSegmentInput>();
textSegment = new ConnectApi.TextSegmentInput();
textSegment.text = 'Hey there ';
messageInput.messageSegments.add(textSegment);
mentionSegment.id = '005D0000001LLO1';
messageInput.messageSegments.add(mentionSegment);
textSegment = new ConnectApi.TextSegmentInput();
textSegment.text = '. How are you?';
messageInput.messageSegments.add(textSegment);
input.body = messageInput;
ConnectApi.FeedItem feedItemRep = ConnectApi.ChatterFeeds.postFeedItem(communityId, feedType, 'me', input, null);

Although this is technically a solution for posting an @mention via apex, it’s not very useful, is it? This shows how to sandwich an @mention between two strings, and nothing else. But what if you need to post a more complex message with multiple group and user @mentions? You’ll need to do the parsing and id lookups yourself, which means diving into the world of regular expressions and string manipulation.

What we really need is a method that takes a string which may or may not have @mentions from a user or group anywhere in the message. The following is one possible solution:

public Static ConnectApi.FeedItemInput parseChatterPostWithMentions( String msg){
    ConnectApi.FeedItemInput feedItemInput = new ConnectApi.FeedItemInput();     
    
    ConnectApi.MessageBodyInput messageBodyInput = new ConnectApi.MessageBodyInput();
    
    messageBodyInput.messageSegments = new List<ConnectApi.MessageSegmentInput>();
    //parse @[User Name]
    //use pattern matching group ()  so we can easily extract name later*
    Pattern p = Pattern.compile('\\@\\[([a-z|A-Z|0-9].*?)\\]'); 
    //use regex to split all all non-mentions into segments
    String [] textSegments = p.split(msg,-1);
    
    //create a pattern matcher object
    Matcher pm = p.matcher( msg );
    //all @[mentions] in order (we will need this to build the segments later**)
    List<String> mentionedNames = new List<String>{};   
    Map<String,ConnectApi.MentionSegmentInput> nameToInputMap = new Map<String,ConnectApi.MentionSegmentInput>{};
    //iterate through each @mention and build a map of unique @mentions 
    if( pm.find() ){
        do {
            //now we can extract the pattern matching group defined earlier*
            String mentionName = pm.group(1);
            //use lowercase to ensure case neutrality later***
            String lcName = mentionName.toLowerCase();
            if(!nameToInputMap.containsKey(lcName)){
                nameToInputMap.put(lcName, new ConnectApi.MentionSegmentInput());               
            }
            mentionedNames.add(lcName);
        } while(pm.find());
    }
    ConnectApi.MentionSegmentInput[] mentions = new ConnectApi.MentionSegmentInput[]{};
    //check the @[chatter names] against user Names
    for(User u: [SELECT Id, Name FROM User WHERE name IN: mentionedNames]){
        ConnectApi.MentionSegmentInput mentionSegmentInput = new ConnectApi.MentionSegmentInput();
        //making sure to fetch using lowercased name***
        nameToInputMap.get(u.Name.toLowerCase()).id = u.id;
        mentionSegmentInput.id = u.id;   
    }
    //check the @[chatter names] against Chatter group Names (aka CollaborationGroup)
    for(CollaborationGroup cg: [SELECT Id, Name FROM CollaborationGroup WHERE name IN: mentionedNames]){
        ConnectApi.MentionSegmentInput mentionSegmentInput = new ConnectApi.MentionSegmentInput();
        //making sure to fetch using lowercased name***
        nameToInputMap.get(cg.Name.toLowerCase()).id = cg.id;
        mentionSegmentInput.id = cg.id;  
    }
    //shuffle the mentions back in with the text segments
    for(Integer i=0;i<textSegments.size();i++){
        ConnectApi.TextSegmentInput textSegmentInput = new ConnectApi.TextSegmentInput();
        textSegmentInput.text = textSegments[i];
        messageBodyInput.messageSegments.add(textSegmentInput);
        //due to splitting, there should always be one less mention than text segments
        if(mentionedNames.size() > i){
            // now "sandwich" the @mentions into the text Segments in their original order
            messageBodyInput.messageSegments.add(nameToInputMap.get(mentionedNames[i]));
        }
    }
    feedItemInput.body = messageBodyInput;
    feedItemInput.feedElementType = ConnectApi.FeedElementType.FeedItem;
    
    return feedItemInput;
}

The above code leverages a single regular expression to split the message into pieces, and then uses the same regex to iterate over each mention and construct ConnectApi.mentionSegmentInput objects and then join all the text segments and mentions in their original order and returns a new ConnectApi.FeedItemInput object, ready to post to a chatter feed. Use this technique to incorporate dynamic @ mentions in your next custom Visualforce page and add a new level of Chatter integration to your next app.