Chatter in Apex - Displaying a Feed

Source code:

Overview

Chatter in Apex is a collection of Apex classes in the ConnectApi namespace.

Use Chatter in Apex to build custom experiences in Salesforce that include Chatter resources such as feeds, groups, users, followers, likes, and comments. Chatter in Apex provides a simple programming model and data that's structured to build Chatter UI, including getting and posting @mentions and likes.

Chatter in Apex provides Chatter REST API functionality in Apex. Build Chatter integrations and custom Chatter UI on Force.com without making HTTP callouts to the Chatter REST API. The Chatter REST API resource actions are exposed as static methods on Apex classes in the ConnectApi namespace. These methods use other ConnectApi classes to input and return information.

Create View Helper Classes

To massage the output to display exactly the way you want it to in Visualforce, create view helper classes. [code apex] global class FeedFormatter { global static String formatBodyText(ConnectApi.FeedBody body) { String formattedText = ''; for (ConnectApi.MessageSegment seg : body.messageSegments) { if (seg instanceof ConnectApi.MentionSegment) { ConnectApi.MentionSegment mention = (ConnectApi.MentionSegment)seg; formattedText += '' + mention.user.name + ''; } else if (seg instanceof ConnectApi.HashtagSegment) { ConnectApi.HashtagSegment hashtag = (ConnectApi.HashtagSegment)seg; formattedText += '#' + hashtag.tag + ''; } else if (seg instanceof ConnectApi.LinkSegment) { ConnectApi.LinkSegment link = (ConnectApi.LinkSegment)seg; formattedText += '' + link.url + ''; } else { // Default. formattedText += seg.text; } } return formattedText; } } [/code] [code apex] global class CommentInfo { global CommentInfo(ConnectApi.Comment inComment) { comment = inComment; userName = inComment.user.name; imageUrl = ''; if (inComment.attachment != null && inComment.attachment instanceof ConnectApi.ContentAttachment) { ConnectApi.ContentAttachment content = (ConnectApi.ContentAttachment)inComment.attachment; imageUrl = content.renditionUrl; } formattedText = FeedFormatter.formatBodyText(inComment.body); } global ConnectApi.Comment comment { get; private set; } global String userName { get; private set; } global String imageUrl { get; private set; } global String formattedText { get; private set; } } [/code] The FeedItemInfo class uses the FeedFormatter and CommentInfo classes and assembles the Apex output to display @mentions, hashtags, embedded links and other rich text in HTML. [code apex] global class FeedItemInfo { global FeedItemInfo(ConnectApi.FeedItem inFeedItem) { feedItem = inFeedItem; userName = ''; if (inFeedItem.actor != null && inFeedItem.actor instanceof ConnectApi.UserSummary) { userName = ((ConnectApi.UserSummary)inFeedItem.actor).name; } imageUrl = ''; if (inFeedItem.attachment != null) { if (inFeedItem.attachment instanceof ConnectApi.ContentAttachment) { ConnectApi.ContentAttachment content = (ConnectApi.ContentAttachment)inFeedItem.attachment; imageUrl = content.renditionUrl; contentDescription = content.description; contentTitle = content.title; hasImagePreview = content.hasImagePreview; contentId = content.versionId; // contentDownloadUrl = content.downloadUrl; // not cookie enabled so unusable contentDownloadUrl = '/sfc/servlet.shepherd/version/download/' + content.versionId + '?asPdf=false&operationContext=CHATTER'; } else if (inFeedItem.attachment instanceof ConnectApi.LinkAttachment) { ConnectApi.LinkAttachment link = (ConnectApi.LinkAttachment)inFeedItem.attachment; linkUrl = link.url; linkTitle = link.title; } } formattedText= FeedFormatter.formatBodyText(inFeedItem.body); comments = new List(); for (ConnectApi.Comment comment : inFeedItem.comments.comments) { comments.add(new CommentInfo(comment)); } } //-------- properties ----------// global ConnectApi.FeedItem feedItem { get; private set; } global String userName { get; private set; } global String imageUrl { get; private set; } global String linkUrl { get; private set; } global String linkTitle { get; private set; } global String contentDescription { get; private set; } global String contentDownloadUrl { get; private set; } global String contentTitle { get; private set; } global Boolean hasImagePreview { get; private set; } global String formattedText { get; private set; } global String contentId { get; private set; } global List comments { get; private set; } global Integer commentCount { get { return feedItem.comments.comments.size(); } } } [/code]

Create a Controller that Gets a Feed

Getting a feed is simple. Use the getFeedItemsFromFeed static method to access pages of different feeds. Specify the feed type in the second parameter. The method also takes additional parameters such as a page number, page size, and sort order.

If your organization uses Salesforce Communities, you can pass a Community ID as the first parameter. If not, or to target the internal community, pass null.

The DemoController uses the FeedItemInfo view helper class we created earlier to format the Chatter data.

[code apex] global class DemoController { // get first page of news feed global ConnectApi.FeedItemPage getNewsFeed() { return ConnectApi.ChatterFeeds.getFeedItemsFromFeed(null, ConnectApi.FeedType.News, 'me'); } // build list of wrapped feed items for display in VisualForce global List getNewsFeedForDisplay() { ConnectApi.FeedItemPage feed = getNewsFeed(); List result = new List(); for (ConnectApi.FeedItem item : feed.items) { result.add(new FeedItemInfo(item)); } return result; } } [/code]

Create a Visualforce Page that Displays a Feed

Here's some example Visualforce markup that uses the DemoController to display feed items. [code apex]
{!commentInfo.userName}
[/code]

Post to a Feed

To post a feed item, build an instance of ConnectApi.FeedItemInput and set its body to an instance of MessageBodyInput. MessageBodyInput contains the data necessary to post @mentions and other rich text.

This code parses out @-mentions from user-provided text and converts the text into a ConnectApi.FeedItemInput that's ready to post.

[code apex] public class FeedBodyParser { // use to extract the user id as a group // Put in double backslashes into regex because Apex doesn't recognize that its a regex // actual regex is @\[[^\]]{4,}\]\(contact:(\w{15,18})\) public static final String MENTIONGROUPPATTERN = '@\\[[^\\]]{4,}\\]\\(user:(\\w{15,18})\\)'; // use to extract the entire mention format public static final String MENTIONPATTERN = '@\\[[^\\]]{4,}\\]\\(user:\\w{15,18}\\)'; public class postTooLargeException extends Exception {} // used to store mentions extracted from the post public class Mention { public Integer groupStart { get; set; } public Integer groupEnd { get; set; } public String text { get; set; } } // parse post text into a list of Mentions public static List extractAllMentions(String post) { Pattern pat = Pattern.compile(MENTIONPATTERN); Matcher matcher = pat.matcher(post); List mentions = new List(); while(matcher.find()) { Mention mention = new Mention(); mention.text = matcher.group(); // text of matched mention mention.groupStart = matcher.start(); // position in string where mention starts mention.groupEnd = matcher.end() ; // position after the last char of the mention mentions.add(mention); } return mentions; } // parse the post body, placing @mentions and text into the passed in // segments list. Works with both feed items and comments - just pass in the // segments that are assigned to the appropriate object. public static void buildSegments(String postText, List segments) { List mentions = extractAllMentions(postText); if (mentions.size() > 0) { System.debug('mention[0]' + mentions[0]); // the cursor is the char position inside post Integer cursor = 0; for (Mention mention: mentions) { // cursor is maintained at the beginning of a mention or text segment by moving it one char past the current // mention at every iteration of this loop. if (mention.groupStart > cursor) { // there is text between where the cursor is and the start of this mention so store the text first. // we know there is text (and we're not at end of string) because there's a mention with a start point to the right // of the cursor. ConnectApi.TextSegmentInput textSegment = new ConnectApi.TextSegmentInput(); textSegment.text = postText.subString(cursor, mention.groupStart); segments.add(textSegment); } // next, store the mention ConnectApi.MentionSegmentInput mentionSegment = new ConnectApi.MentionSegmentInput(); String temptxt = parseOneMention(mention.text); mentionSegment.id = parseOneMention(mention.text); segments.add(mentionSegment); cursor = mention.groupEnd; // move cursor 1 char past where this mention ended } // After the last mention, there may be a text segment if (cursor < postText.length()) { ConnectApi.TextSegmentInput textSegment = new ConnectApi.TextSegmentInput(); textSegment.text = postText.subString(cursor, postText.length()); segments.add(textSegment); } } else { // no mentions in the post, just store the whole post as a text segment. ConnectApi.TextSegmentInput textSegment = new ConnectApi.TextSegmentInput(); textSegment.text = postText; segments.add(textSegment); } } // parse the user id out of a string that has one mention in it and return it. public static String parseOneMention(String mentionStr) { Pattern pat = Pattern.compile(MENTIONGROUPPATTERN); Matcher matcher = pat.matcher(mentionStr); matcher.find(); return matcher.group(1); } // returns a corresponding FeedItemInput which can be used to post a new feed item public static ConnectApi.FeedItemInput convertToFeedItemInput(String postText) { // failsafe - postText size should be controlled by browser. if (postText.length() > 2000) { throw new postTooLargeException('post too large'); } ConnectApi.FeedItemInput feedItemInput = new ConnectApi.FeedItemInput(); feedItemInput.body = new ConnectApi.MessageBodyInput(); buildSegments(postText, feedItemInput.body.messageSegments); return feedItemInput; } } [/code] Use the following code with a Visualforce page that makes use of jquery-mentions formatting for the text that gets passed in as feedItemText for each of the methods. [code apex] global class FeedItemPoster { global static ConnectApi.FeedItem postTextFeedItem(String feedItemText) { ConnectApi.FeedItemInput feedItemInput = FeedBodyParser.convertToFeedItemInput(feedItemText); return ConnectApi.ChatterFeeds.postFeedItem(null, ConnectApi.FeedType.News, 'me', feedItemInput, null); } global static ConnectApi.FeedItem postFileFeedItem(String feedItemText, Blob fileBlob, String title, String description, String filename) { ConnectApi.FeedItemInput feedItemInput = FeedBodyParser.convertToFeedItemInput(feedItemText); // file attachment ConnectApi.NewFileAttachmentInput fileIn = new ConnectApi.NewFileAttachmentInput(); fileIn.title = title; // user-given "name" is set using the title fileIn.description = description; feedItemInput.attachment = fileIn; ConnectApi.BinaryInput feedBinary = new ConnectApi.BinaryInput(fileBlob, null, filename); return ConnectApi.ChatterFeeds.postFeedItem(null, ConnectApi.FeedType.News, 'me', feedItemInput, feedBinary); } global ConnectApi.FeedItem postLinkFeedItem(String feedItemText, String linkName, String linkUrl) { ConnectApi.FeedItemInput feedItemInput = FeedBodyParser.convertToFeedItemInput(feedItemText); ConnectApi.LinkAttachmentInput linkIn = new ConnectApi.LinkAttachmentInput(); linkIn.urlName = linkName; linkIn.url = linkUrl; feedItemInput.attachment = linkIn; return ConnectApi.ChatterFeeds.postFeedItem(null, ConnectApi.FeedType.News, 'me', feedItemInput, null); } } [/code]