Chatter Image Gallery using ConnectApi

The ConnectApi namespace (also called Chatter in Apex) provides classes for accessing the same data available in Chatter REST API through Apex. You can use Chatter in Apex to create custom Chatter experiences in Salesforce. In this article, we will be reviewing the first step in creating a Chatter Image Gallery inside of Salesforce. We will create a custom tab for a Visualforce page backed by an Apex Controller utilizing the ConnectApi’s expansive method list. This article will assume you have a working knowledge of Apex and Visualforce, but I will delve into the ConnectApi calls more in depth.

Goals

The goals of this iteration of the Chatter Image Gallery are:

  • Loop over the first page of the logged in user’s feed and grab all images
  • Display images in a simple gallery

Over the course of the next few weeks, the Chatter Image Gallery will continue to expand adding more features with each iteration.

Apex Controller

Let’s jump right into the Apex we will need to use. Below you will find the code for both the Controller and the Test Class.

public class ChatterGalleryPageController{
    public transient List<ConnectApi.ContentAttachment> images;
    
	public ChatterGalleryPageController(){
        instantiateImages();
        ConnectApi.FeedItemPage feedPage = getFirstFeedItemPageOfInternalPosts();
        generateImages(feedPage);
    }
    
    private void instantiateImages(){
        images = new List<ConnectApi.ContentAttachment>();
    }
    
    private ConnectApi.FeedItemPage getFirstFeedItemPageOfInternalPosts(){
        return ConnectApi.ChatterFeeds.getFeedItemsFromFeed('internal', ConnectApi.FeedType.Files, UserInfo.getUserId());
    }
    
    private void generateImages(ConnectApi.FeedItemPage feedPage){
        for(ConnectApi.FeedItem item:feedPage.items){
            if(doesFeedItemContainContentAttachment(item)){
                ifAttachmentIsImageAddToList((ConnectApi.ContentAttachment)item.attachment);
            }
        }
    }
    
    private Boolean doesFeedItemContainContentAttachment(ConnectApi.FeedItem item){
        return item.attachment != null && item.attachment instanceOf ConnectApi.ContentAttachment;
    }
    
    private void ifAttachmentIsImageAddToList(ConnectApi.ContentAttachment image){
        if(image.mimeType.contains('image')){
            images.add(image);
        }
    }
    
    public List<ConnectApi.ContentAttachment> getImages(){
        return images;
    }
}

@isTest
public class ChatterPageControllerTest{
	static testMethod void testEmptyFeed(){
        instantiateMockFeedItems(false);
        
        Test.startTest();
        ChatterGalleryPageController controller = new ChatterGalleryPageController();
        Test.stopTest();
        
        System.assertEquals(0, controller.getImages().size());
    }
    
    static testMethod void testFeedWithItems(){
        instantiateMockFeedItems(true);
        
        Test.startTest();
        ChatterGalleryPageController controller = new ChatterGalleryPageController();
        Test.stopTest();
        
        System.assertEquals(3, controller.getImages().size());
    }
    
    private static void instantiateMockFeedItems(Boolean hasFeedItems){
        ConnectApi.FeedItemPage mockFeedItemPage = generateMockFeedItemPage(hasFeedItems);
        ConnectApi.ChatterFeeds.setTestGetFeedItemsFromFeed('internal', ConnectApi.FeedType.Files, UserInfo.getUserId(), mockFeedItemPage);
    }
    
    private static ConnectApi.FeedItemPage generateMockFeedItemPage(Boolean addFeedItems){
        ConnectApi.FeedItemPage mockFeedItemPage = new ConnectApi.FeedItemPage();
        mockFeedItemPage.items = new List<ConnectApi.FeedItem>();
        if(addFeedItems){
            mockFeedItemPage.items = generateMockFeedItems();
        }
        
        return mockFeedItemPage;
    }
    
    private static List<ConnectApi.FeedItem> generateMockFeedItems(){
        List<ConnectApi.FeedItem> mockItems = new List<ConnectApi.FeedItem>();
        mockItems.add(generateMockFeedItem(true, true));
        mockItems.add(generateMockFeedItem(true, false));
        mockItems.add(generateMockFeedItem(false, false));
        mockItems.add(generateMockFeedItem(true, true));
        mockItems.add(generateMockFeedItem(true, true));
        return mockItems;
    }
    
    private static ConnectApi.FeedItem generateMockFeedItem(Boolean hasAttachment, Boolean isImageType){
        ConnectApi.FeedItem mockFeedItem = new ConnectApi.FeedItem();
        if(hasAttachment){
            mockFeedItem.attachment = generateMockContentAttachment(isImageType);
        }
        
        return mockFeedItem;
    }
    
    private static ConnectApi.ContentAttachment generateMockContentAttachment(Boolean isImageType){
        ConnectApi.ContentAttachment mockContentAttachment = new ConnectApi.ContentAttachment();
        if(isImageType){
        	mockContentAttachment.mimeType = 'image';   
        }else{
            mockContentAttachment.mimeType = 'pdf';
        }
        
        return mockContentAttachment;
    }
}

The main purpose of this code block is to generate a List of ConnectApi.ContentAttachment (line 2 – accessed via getImages (lines 36-38)).

Note

An important thing to call out is that the images variable is transient. To keep the view state as small as possible, we can mark images transient because we will never be posting those values back to the server.

Before we dive into detail around the ConnectApi.ContentAttachment class, let’s talk about how we even get down to that level. Immediately after we instantiate the images variable (line 5 – done by calling the instantiateImages() method (lines 10-12)), we need to get all the items in the feed. If you typically go to the Chatter tab, you only get a single page of posts at a time. You have to hit the load more posts by scrolling down and clicking link. The ConnectApi works in a similar fashion.

In order to get the feed of the current user, we need to call the ConnectApi.ChatterFeeds.getFeedItemsFromFeed(String communityId, ConnectApi.FeedType feedType, String subjectId) method (line 15). There are actually several different versions of this method that are overridden with different parameters. For this version, the communityId will allow you to specify a specific Salesforce Community or 'internal' which indicates the internal organization. The feedType indicates the type of feed you are looking to return. In this case, we want ConnectApi.FeedType.Files. Notice that ConnectApi.FeedType is an enum. It provides several values to only look at specific feeds such as groups, records, and specific people. ConnectApi.FeedType.Files contains all feed items that contain files posted by people or groups that the logged-in user follows. The final parameter is the subjectId which can be the Id of a group, person, or record. This Id provides context around what items to show in the feed. The final piece to this is that the method returns a class of type ConnectApi.FeedItem​Page. This class provides a few different methods, but the most important is items which we will use to iterate over the list of available feed items. So, to recap, line 15 returns a ConnectApi.FeedItemPage object that contains a list of feed items. These feed items have been filtered to only return internal Chatter posts that contain file attachments for the currently logged in user.

Now that we have a filtered list of feed items, we need to iterate over that list and only display their attachments. At this point, we don’t really care about the actual post, only the attached image. As I mentioned above, we need to get our feed items from the items method of the ConnectApi.FeedItemPage. The items method returns a List of ConnectApi.FeedItem. ConnectApi.FeedItem represents a single Chatter post. We are concerned with the attachment on ConnectApi.FeedItem which will return an instance of ConnectApi.​FeedItem​Attachment if an attachment exists or null if there is none. The interesting thing about the ConnectApi.​FeedItem​Attachment class is that it is abstract. You will need to cast it to the correct type before doing anything with it. In this particular case, we believe our feed items will have attachments of type ConnectApi.ContentAttachment. The ConnectApi.ContentAttachment represents any attachment that is a piece of content such as an image, PDF, Excel document, etc. All of this logic is performed in the generateImages(ConnectApi.FeedItemPage feedPage) method (lines 18-24). We iterate over the List of ConnectApi.FeedItem (line 19) then perform our null check and ensure we have a ConnectApi.​FeedItem​Attachment of type ConnectApi.ContentAttachment using the instanceof keyword (doesFeedItemContainContentAttachment(ConnectApi.FeedItem item) method – lines 26-28).

The final thing we need to do is filter out the list to only images. There are several different ways we could do this, but I chose to just check the MIME type. ConnectApi.ContentAttachment has mimeType on it which will return the MIME type of the attachment as a String. This allows us to use the contains method on the String and just check against 'image' which should be present in any image. This logic can be found in the ifAttachmentIsImageAddToList(ConnectApi.ContentAttachment image) method (lines 30-34). It is important to note that as part of the generateImages method, we cast the item.attachment to a ConnectApi.ContentAttachment when passing it into the ifAttachmentIsImageAddToList method (line 21).

With all of this logic running as part of the constructor (lines 4-8), we will have images ready to go for the Visualforce page as soon as the page loads.

Test Class

As I was writing the controller, I was also writing a test class. Now, this doesn’t necessarily follow what I would consider the best way to write a unit test in Apex since I didn’t create a Test utility class, but for the sake of simplicity I just kept everything in a single class for now. Let’s look at the code.

@isTest
public class ChatterPageControllerTest{
	static testMethod void testEmptyFeed(){
        instantiateMockFeedItems(false);
        
        Test.startTest();
        ChatterGalleryPageController controller = new ChatterGalleryPageController();
        Test.stopTest();
        
        System.assertEquals(0, controller.getImages().size());
    }
    
    static testMethod void testFeedWithItems(){
        instantiateMockFeedItems(true);
        
        Test.startTest();
        ChatterGalleryPageController controller = new ChatterGalleryPageController();
        Test.stopTest();
        
        System.assertEquals(3, controller.getImages().size());
    }
    
    private static void instantiateMockFeedItems(Boolean hasFeedItems){
        ConnectApi.FeedItemPage mockFeedItemPage = generateMockFeedItemPage(hasFeedItems);
        ConnectApi.ChatterFeeds.setTestGetFeedItemsFromFeed('internal', ConnectApi.FeedType.Files, UserInfo.getUserId(), mockFeedItemPage);
    }
    
    private static ConnectApi.FeedItemPage generateMockFeedItemPage(Boolean addFeedItems){
        ConnectApi.FeedItemPage mockFeedItemPage = new ConnectApi.FeedItemPage();
        mockFeedItemPage.items = new List<ConnectApi.FeedItem>();
        if(addFeedItems){
            mockFeedItemPage.items = generateMockFeedItems();
        }
        
        return mockFeedItemPage;
    }
    
    private static List<ConnectApi.FeedItem> generateMockFeedItems(){
        List<ConnectApi.FeedItem> mockItems = new List<ConnectApi.FeedItem>();
        mockItems.add(generateMockFeedItem(true, true));
        mockItems.add(generateMockFeedItem(true, false));
        mockItems.add(generateMockFeedItem(false, false));
        mockItems.add(generateMockFeedItem(true, true));
        mockItems.add(generateMockFeedItem(true, true));
        return mockItems;
    }
    
    private static ConnectApi.FeedItem generateMockFeedItem(Boolean hasAttachment, Boolean isImageType){
        ConnectApi.FeedItem mockFeedItem = new ConnectApi.FeedItem();
        if(hasAttachment){
            mockFeedItem.attachment = generateMockContentAttachment(isImageType);
        }
        
        return mockFeedItem;
    }
    
    private static ConnectApi.ContentAttachment generateMockContentAttachment(Boolean isImageType){
        ConnectApi.ContentAttachment mockContentAttachment = new ConnectApi.ContentAttachment();
        if(isImageType){
        	mockContentAttachment.mimeType = 'image';   
        }else{
            mockContentAttachment.mimeType = 'pdf';
        }
        
        return mockContentAttachment;
    }
}

public class ChatterGalleryPageController{
    public transient List<ConnectApi.ContentAttachment> images;
    
	public ChatterGalleryPageController(){
        instantiateImages();
        ConnectApi.FeedItemPage feedPage = getFirstFeedItemPageOfInternalPosts();
        generateImages(feedPage);
    }
    
    private void instantiateImages(){
        images = new List<ConnectApi.ContentAttachment>();
    }
    
    private ConnectApi.FeedItemPage getFirstFeedItemPageOfInternalPosts(){
        return ConnectApi.ChatterFeeds.getFeedItemsFromFeed('internal', ConnectApi.FeedType.Files, UserInfo.getUserId());
    }
    
    private void generateImages(ConnectApi.FeedItemPage feedPage){
        for(ConnectApi.FeedItem item:feedPage.items){
            if(doesFeedItemContainContentAttachment(item)){
                ifAttachmentIsImageAddToList((ConnectApi.ContentAttachment)item.attachment);
            }
        }
    }
    
    private Boolean doesFeedItemContainContentAttachment(ConnectApi.FeedItem item){
        return item.attachment != null && item.attachment instanceOf ConnectApi.ContentAttachment;
    }
    
    private void ifAttachmentIsImageAddToList(ConnectApi.ContentAttachment image){
        if(image.mimeType.contains('image')){
            images.add(image);
        }
    }
    
    public List<ConnectApi.ContentAttachment> getImages(){
        return images;
    }
}

A very awesome aspect of the ConnectApi is that it actually provides the ability to perform unit tests and not integration tests. The reason I say this is because the ConnectApi actually supports mocks! The platform will be much better off when mocks become the norm, but for now it is really nice to be able to mock out some of these API calls.

I won’t go into too much detail around the test class as most of it is just creating test data, but I would like to point out the ConnectApi.ChatterFeeds.setTestGetFeedItemsFromFeed method (line 25). This method registers a ConnectApi.FeedItemPage object to be returned when getFeedItems​FromFeed is called with matching parameters in a test context. You must use the get feed method with the same parameters or the code throws an exception. This exception looks like:

System.AssertException: Assertion Failed: No matching test result found for ChatterFeeds.getFeedItemsFromFeed(String communityId, ConnectApi.FeedType feedType, String subjectId). Before calling this, call ChatterFeeds.setTestGetFeedItemsFromFeed(String communityId, ConnectApi.FeedType feedType, String subjectId, ConnectApi.FeedItemPage result) to set the expected test result.

Both test methods start with instantiateMockFeedItems (lines 23-26) which essentially generates the ConnectApi.FeedItemPage to be set by the ConnectApi.ChatterFeeds.setTestGetFeedItemsFromFeed method. There is some logic to create both an empty feed and a feed with varying items and attachments. Both are used in different tests to exercise the different options available.

Now that we have our controller and test class written, let’s get something displaying on the page with our Visualforce!

Visualforce

The Visualforce is actually very simple for this. There isn’t really much to it to be honest. Let’s take a look.

<apex:page showHeader="true" sidebar="true" title="Chatter Gallery" controller="ChatterGalleryPageController">
    <apex:includeScript value="https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.js"/>
    <apex:includeScript value="{!URLFOR($Resource.Galleria,'galleria/galleria-1.3.5.min.js')}"/>
    <style>
        .galleria{ width: 700px; height: 400px; background: #000 }
    </style>
	<apex:sectionHeader title="View Chatter Images" subtitle="Chatter Gallery" />
    
    <div class="galleria">
        <apex:repeat value="{!images}" var="img">
            <apex:image url="{!img.downloadUrl}"/>
        </apex:repeat>
    </div>
    
    <script>
    	Galleria.loadTheme('{!URLFOR($Resource.Galleria,'galleria/themes/classic/galleria.classic.min.js')}');
        Galleria.run('.galleria');
    </script>
</apex:page>

All of the heavy lifting on the front end is actually done by the Javascript library I used for the gallery, Galleria. I chose Galleria by doing a Google Search for “simple javascript library”. I just arbitrarily picked something from the first page and went with it. All of the Visualforce was done to mimic Galleria’s Beginner’s Guide. I uploaded Galleria 1.3.5 as a Static Resource. I used the URLFOR function to get the script (line 3) and the theme (line 16). The only other difference from the Beginner’s Guide is that I used apex:repeat (line 10) to loop over the List of ConnectApi.ContentAttachment to use the downloadUrl to create an apex:image (line 11). This generated simple HTML with the img tags that the Javascript then picked up and did the rest of the work for it.

Finished Product

Our finished product looks like:

You can cycle through each image. The feed also contains other posts containing no images and some feed items containing PDFs. Notice how none of that effects the feed (as expected).

Looking Forward

In followup articles, I plan to expand this functionality by adding paging, likes, and comments. Look for this additional functionality to be coming out in the near future. Good luck working with the ConnectApi. There is a ton of cool things you can do and I would love to hear from others what they have come up with!

Trackbacks/Pingbacks

  1. Extending Chatter Image Gallery using ConnectApi | Jesse Altman - March 24, 2014

    […] post about using the ConnectApi to create an image gallery from Chatter image attachments entitled Chatter Image Gallery using ConnectApi. You must read this article as it is the starting point for this continuation. Some things may not […]

Leave a Comment