Recipes by Category

App Distribution (2) Bundle logic, interface and services for distribution. App Logic (37) The Apex programming language, workflow and formulas for logic. Collaboration (6) The Salesforce Chatter collaboration platform. Database (29) Data persistence, reporting and analytics. Integration (33) Web Service APIs and toolkits for integration. Security (9) Platform, application and data security. Tools (4) Force.com tooling User Interface (36) Visualforce MVC and metadata-drive user interfaces. Web Sites (12) Public web sites and apps with optional user registration and login.
Beta Feedback
Cookbook Home » Chatter in Apex - Displaying a Feed

Chatter in Apex - Displaying a Feed

Post by Jody Bleyle  (2012-05-09)

Status: Unverified
Level: intermediate

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.
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();
        }
    }
}

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.

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;
    }
}

Create a Visualforce Page that Displays 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>

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.

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);
    }
}

Share

Recipe Activity - Please Log in to write a comment

 
The code samples were working for us in our sandbox prior to the summer13 release as we were on the chatter pilot program.
Then, about a week or two before the prod release, it ceased to work.

Deleting the following, and replacing with new instances did not fix the problem:

FeedFormatter
CommentInfo
FeedItemInfo
FeedBodyParser
FeedItemPoster

Our Sandbox API has been v28 the entire time.

When testing, the above I get the following error:


Class.FeedBodyParser.buildSegments: line 57, column 1
Class.FeedBodyParser.convertToFeedItemInput: line 98, column 1

Any ideas on this, or where I can look to pinpoint the issue?
It looks like a native issue to me.

by Pilot Testre  (2013-06-19)

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


by Vijay Singh  (2012-07-10)

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


by logan henriquez ♣  (2012-06-08)

In which release is this planned as pilot feature. How do i enable this in current spring 12 developer box.

voted as verified by Jatin Jain  (2012-06-07)

Hi Logan,
I want to enable this feature in my developer account. What will be the process for the same.
Thanks,
Naveen

by Naveen Gabrani  (2012-06-06)

by logan henriquez ♣  (2012-05-09)

X

Vote to Verify a Recipe

Verifying a recipe is a way to give feedback to others and broaden your own understanding of the capabilities on Force.com. When you verify a recipe, please make sure the code runs, and the functionality solves the articulated problem as expected.

Please make sure:
  • All the necessary pieces are mentioned
  • You have tested the recipe in practice
  • Have sent any suggestions for improvements to the author

Please Log in to verify a recipe

You have voted to verify this recipe.