Overview
The Connect in Apex pilot provides Chatter REST API functionality inside Apex. Use Connect in Apex to build user interfaces that include Chatter resources such as feeds, groups, users, followers, likes, and comments. (Files, recommendations, topics, and private messages are not included in the pilot, but are expected at a later date.) The primary benefit of Connect in Apex is that developers building Chatter integrations and custom chatter UI on Force.com no longer need to perform an Apex callout to the HTTP API to use the Chatter REST API. The data is available as a method on the new ConnectAPI namespace inside apex. Connect in Apex provides a simple programming model and more complete data for building Chatter UI, including getting and posting @mentions and likes.
To enroll in the Connect in Apex pilot, contact Gregg Johnson (gregg.johnson@salesforce.com).
View Helper Classes
To massage the output to display exactly the way you want it to in VisualForce, create view helper classes.
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 += '<span style=\"color:blue\">' + mention.user.name + '</span>';
} else if (seg instanceof ConnectApi.HashtagSegment) {
ConnectApi.HashtagSegment hashtag = (ConnectApi.HashtagSegment)seg;
formattedText += '<span style=\"color:red\">#' + hashtag.tag + '</span>';
} else if (seg instanceof ConnectApi.LinkSegment) {
ConnectApi.LinkSegment link = (ConnectApi.LinkSegment)seg;
formattedText += '<a href=\"' + link.url + '\">' + link.url + '</a>';
} else {
// Default.
formattedText += seg.text;
}
}
return formattedText;
}
}
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; }
}
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.
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<CommentInfo>();
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<CommentInfo> comments { get; private set; }
global Integer commentCount {
get {
return feedItem.comments.comments.size();
}
}
}
Getting 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 you are part of the Communities pilot you can choose the target community by passing in a non-null value for the first parameter.
The DemoController uses the FeedItemInfo view helper class we created earlier to format the Chatter data.
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<FeedItemInfo> getNewsFeedForDisplay() {
ConnectApi.FeedItemPage feed = getNewsFeed();
List<FeedItemInfo> result = new List<FeedItemInfo>();
for (ConnectApi.FeedItem item : feed.items) {
result.add(new FeedItemInfo(item));
}
return result;
}
}
VisualForce for Displaying a Feed
Here's some example VisualForce markup that uses the DemoController to display feed items.
<apex:page controller="DemoController" id="democontroller" sidebar="false" showHeader="false" standardStylesheets="false" >
<div id="feed-display-div">
<apex:repeat value="{!newsFeedForDisplay}" var="feedItemInfo">
<div class="row">
<div>
<div style="display:inline-block;vertical-align:top;">
<apex:image style="margin:4px" width="25" url="{!feedItemInfo.feedItem.photoUrl}"/>
</div>
<div style="display:inline-block;vertical-align:top;width:350px">
{!feedItemInfo.userName}<br/>
<apex:outputText value="{!feedItemInfo.formattedText}" escape="false"/>
<apex:outputPanel layout="block" rendered="{!IF(feedItemInfo.linkUrl == null, false, true)}" >
<a href="{!feedItemInfo.linkUrl}">{!feedItemInfo.linkTitle}</a>
</apex:outputPanel>
</div>
<apex:outputPanel layout="block" rendered="{!IF(feedItemInfo.contentDownloadUrl != null && feedItemInfo.imageUrl != null && feedItemInfo.hasImagePreview, true, false)}" >
<apex:image style="margin:4px" width="90" url="{!feedItemInfo.imageUrl}"/>
<a href="{!feedItemInfo.contentDownLoadUrl}">{!feedItemInfo.contentTitle }</a>
</apex:outputPanel>
</div>
<apex:outputPanel rendered="{!IF(feedItemInfo.commentCount > 0, true, false)}">
<div style="width:85%;padding:4px;font-size:0.95em;position:relative;left:3em;" >
<apex:repeat value="{!feedItemInfo.comments}" var="commentInfo">
<div style="margin:4px;padding:4px;width:100%;">
<div style="display:inline-block;vertical-align:top;">
<apex:image style="margin:4px" width="25" url="{!commentInfo.comment.user.photo.smallPhotoUrl}"/>
</div>
<div style="display:inline-block;vertical-align:top;width:250px">
{!commentInfo.userName}<br/>
<apex:outputText value="{!commentInfo.formattedText}" escape="false"/>
</div>
<apex:outputPanel rendered="{!IF(commentInfo.imageUrl == null, false, true)}" >
<div style="display:inline-block;vertical-align:top;float:right;position:relative;right:1em" >
<apex:image style="margin:4px" width="100" url="{!commentInfo.imageUrl}"/>
</div>
<div style="clear: both;"/>
</apex:outputPanel>
</div>
</apex:repeat>
</div>
</apex:outputPanel>
</div>
</apex:repeat>
</div>
</apex:page>
Posting 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.
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<Mention> extractAllMentions(String post) {
Pattern pat = Pattern.compile(MENTIONPATTERN);
Matcher matcher = pat.matcher(post);
List<Mention> mentions = new List<Mention>();
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<ConnectApi.MessageSegmentInput> segments) {
List<Mention> 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;
}
}
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.
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);
}
}
Recipe Activity - Please Log in to write a comment
Hi I am unable to trace the sequence of create class module .
Once I am trying to save the DemoController then its ask for "Compile Error: Invalid class: feeditempage" .
I did not fine the class feeditempage anywhere ? And in the same way when I tried to create the another class "connectTools" first then it ask for MessageSegmentInputList & FeedItemInput like these class , where all these classes are exist .
Please help me out
Thanks
Vijay
Hi Naveen
Which company are you with? Your Salesforce Account executive or Sales Engineer can nominate your company to join the pilot if you're with a salesforce subscriber - we require a legal pilot agreement in most cases. If you are an ISV or independent developer, I can activate your developer organization if you tell me a little more about your use case and give me the organization id. I don't get notified when this stream is updated, so best if you email me: my email is my first initial appended to my last name @salesforce.com
Thanks
Logan
In which release is this planned as pilot feature. How do i enable this in current spring 12 developer box.
Hi Logan,
I want to enable this feature in my developer account. What will be the process for the same.
Thanks,
Naveen