Extending Chatter Image Gallery using ConnectApi

Last Monday, I wrote a 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 make sense without prior context. In this week’s article, I am extending this functionality to pull in Likes from Chatter. This information will be available in a dialog popup when the main image is clicked.

One thing that is important to remember when reading this article is that this was done to just test the ConnectApi. I am pulling in a bunch of Javascript libraries (Galleria, jQuery, & jQueryUI) to help with all of this, but they were arbitrarily picked to just mess around with. I also use some other Apex techniques such as Visualforce remoting and Inner Classes. I will touch on all of these briefly but the main piece to focus on is the ConnectApi functionality. Some of the other techniques were used simply to mess around with the technology. They may or may not be the absolute best way to do this (I am not a Javascript guru (not yet at least)).

Refactoring

You will notice that I did some major refactoring of my code base for my last post to accommodate some of the functionality I was looking to achieve here. I just wanted to call out that by having reliable unit tests, I was able to make these changes with a high level of confidence that I did not break any underlying functionality. Using TDD (Test Driven Development), I was able to quickly change while ensuring my code still functioned properly. With that said, let’s take a look at our new code.

Apex Controller

public class ChatterGalleryPageController{
    public transient List<ChatterImage> images;
    
	public ChatterGalleryPageController(){
        instantiateImages();
        ConnectApi.FeedItemPage feedPage = getFirstFeedItemPageOfInternalPosts();
        generateImages(feedPage);
    }
    
    private void instantiateImages(){
        images = new List<ChatterImage>();
    }
    
    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(item);
            }
        }
    }
    
    private Boolean doesFeedItemContainContentAttachment(ConnectApi.FeedItem item){
        return item.attachment != null && item.attachment instanceOf ConnectApi.ContentAttachment;
    }
    
    private void ifAttachmentIsImageAddToList(ConnectApi.FeedItem item){
        ConnectApi.ContentAttachment image = (ConnectApi.ContentAttachment)item.attachment;
        if(image.mimeType.contains('image')){
            images.add(new ChatterImage(item, image));
        }
    }
    
    public List<ChatterImage> getImages(){
        return images;
    }
    
    @RemoteAction
    public static List<ConnectApi.ChatterLike> getLikesForImage(String feedItemId){
        ConnectApi.ChatterLikePage likePage = ConnectApi.ChatterFeeds.getLikesForFeedItem('internal', feedItemId);
        return likePage.likes;
    }
    
    public class ChatterImage{
        public transient ConnectApi.FeedItem item;
        public transient ConnectApi.ContentAttachment image;
        public ChatterImage(ConnectApi.FeedItem item, ConnectApi.ContentAttachment image){
            this.item = item;
            this.image = image;
        }
        
        public ConnectApi.FeedItem getItem(){
            return item;
        }
        
        public ConnectApi.ContentAttachment getImage(){
            return image;
        }
    }
}

@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());
    }
    
    @IsTest(SeeAllData=true)
    static void testGetLikesForImage(){
        FeedItem item = [SELECT Id, LikeCount FROM FeedItem WHERE LikeCount > 0 LIMIT 1];
        
        Test.startTest();
        List<ConnectApi.ChatterLike> chatterLikes = ChatterGalleryPageController.getLikesForImage(item.Id);
        Test.stopTest();
        
        System.assertEquals(item.LikeCount, chatterLikes.size());
    }
    
    static testMethod void testGetCallsFromChatterImage(){
        ConnectApi.FeedItem mockFeedItem = generateMockFeedItem(true, true);
        ConnectApi.ContentAttachment mockContentAttachment = (ConnectApi.ContentAttachment)mockFeedItem.attachment;
		
        Test.startTest();
        ChatterGalleryPageController.ChatterImage chatterImage = new ChatterGalleryPageController.ChatterImage(mockFeedItem, mockContentAttachment);
    	Test.stopTest();
        
        System.assertEquals(mockFeedItem, chatterImage.getItem());
        System.assertEquals(mockContentAttachment, chatterImage.getImage());
    }
    
    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 very first thing that should pop out is that images (line 2) is now returning a List of ChatterImage, an inner class of the controller (lines 47-62). The reason this was changed to use an inner class is due to the fact that we need to be able to reference the ConnectApi.FeedItem as well as the ConnectApi.ContentAttachment in our Visualforce page. You will notice several of the methods have been refactored to properly generate the List of ChatterImage objects now.

The next thing to take notice of is the inclusion of a new method, getLikesForImage (lines 41-45). An important thing to note about this method is the @RemoteAction annotation. This annotation provides support for Apex methods used in Visualforce to be called via JavaScript, aka Visualforce Remoting. A second important thing to note is that this method is also static. In order for it to be called via Visualforce Remoting and even have the @RemoteAction annotation, a method must be static and either global or in this case public.

The getLikesForImage method’s main purpose is to call into the ConnectApi to get the like information for the clicked image. In order to do that, we need to call ConnectApi.ChatterFeeds.getLikesForFeedItem(String communityId, String feedItemId) method. This method returns the first page of likes for the specified feed item. The page contains the default number of items. This method returns a ConnectApi.​Chatter​Like​Page. This page contains the information about the current page of likes, but all we are really interested in is the List of ConnectApi.​Chatter​Like. Eventually, on the Visualforce page, we will loop over this list and grab the user. The user will return a a ConnectApi.​User​Summary, a subclass of ConnectApi.User, which is a subclass of ConnectApi.​ActorWithId, which is a subclass of ConnectApi.Actor. The reason this class structure is relevant is due to the fact that eventually we will want to get the name which is part of the ConnectApi.Actor class.

It is important to note that the ConnectApi.ChatterFeeds.getLikesForFeedItem(String communityId, String feedItemId) method only returns the very first page of likes. In order to get every single like, there is potential you may have to call the overridden version of this method, getLikesForFeedItem(String communityId, String feedItemId, Integer pageParam, Integer pageSize). Note how this method allows parameters for paging while the method we used did not. For our use case, this is just to test the ConnectApi so we left paging out for now.

Test Class

@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());
    }
    
    @IsTest(SeeAllData=true)
    static void testGetLikesForImage(){
        FeedItem item = generateFeedItemWithLikes();
        
        Test.startTest();
        List<ConnectApi.ChatterLike> chatterLikes = ChatterGalleryPageController.getLikesForImage(item.Id);
        Test.stopTest();
        
        System.assertEquals(1, chatterLikes.size());
    }
    
    static testMethod void testGetCallsFromChatterImage(){
        ConnectApi.FeedItem mockFeedItem = generateMockFeedItem(true, true);
        ConnectApi.ContentAttachment mockContentAttachment = (ConnectApi.ContentAttachment)mockFeedItem.attachment;
		
        Test.startTest();
        ChatterGalleryPageController.ChatterImage chatterImage = new ChatterGalleryPageController.ChatterImage(mockFeedItem, mockContentAttachment);
    	Test.stopTest();
        
        System.assertEquals(mockFeedItem, chatterImage.getItem());
        System.assertEquals(mockContentAttachment, chatterImage.getImage());
    }
    
    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;
    }
    
    private static FeedItem generateFeedItemWithLikes(){
        FeedItem item = new FeedItem();
        item.Body = 'Test';
        item.ParentId = UserInfo.getUserId();
        insert item;
        
        generateFeedLike(item);
        
        return item;
    }
    
    private static void generateFeedLike(FeedItem item){
        FeedLike fdLike = new FeedLike();
        fdLike.FeedItemId = item.Id;
        insert fdLike;
    }
}

public class ChatterGalleryPageController{
    public transient List<ChatterImage> images;
    
	public ChatterGalleryPageController(){
        instantiateImages();
        ConnectApi.FeedItemPage feedPage = getFirstFeedItemPageOfInternalPosts();
        generateImages(feedPage);
    }
    
    private void instantiateImages(){
        images = new List<ChatterImage>();
    }
    
    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(item);
            }
        }
    }
    
    private Boolean doesFeedItemContainContentAttachment(ConnectApi.FeedItem item){
        return item.attachment != null && item.attachment instanceOf ConnectApi.ContentAttachment;
    }
    
    private void ifAttachmentIsImageAddToList(ConnectApi.FeedItem item){
        ConnectApi.ContentAttachment image = (ConnectApi.ContentAttachment)item.attachment;
        if(image.mimeType.contains('image')){
            images.add(new ChatterImage(item, image));
        }
    }
    
    public List<ChatterImage> getImages(){
        return images;
    }
    
    @RemoteAction
    public static List<ConnectApi.ChatterLike> getLikesForImage(String feedItemId){
        ConnectApi.ChatterLikePage likePage = ConnectApi.ChatterFeeds.getLikesForFeedItem('internal', feedItemId);
        return likePage.likes;
    }
    
    public class ChatterImage{
        public transient ConnectApi.FeedItem item;
        public transient ConnectApi.ContentAttachment image;
        public ChatterImage(ConnectApi.FeedItem item, ConnectApi.ContentAttachment image){
            this.item = item;
            this.image = image;
        }
        
        public ConnectApi.FeedItem getItem(){
            return item;
        }
        
        public ConnectApi.ContentAttachment getImage(){
            return image;
        }
    }
}

In my last article, I discussed how awesome it was to be able to use mocks. Unfortunately, the same can not be said in this post. The biggest thing you may notice is the addition of the testGetLikesForImage method. One thing that will pop out instantly is that it actually has the @IsTest(SeeAllData=true) annotation set. You will notice that I call the generateFeedItemWithLikes() method which actually generates the needed FeedItem and FeedLike objects. I never have to actually call already existing data. Unfortunately, if you remove that annotation, you receive the following error:

system.UnsupportedOperationException: ConnectApi methods are not supported in data siloed tests. Please use @IsTest(SeeAllData=true).

With the annotation, the tests run fine and there are no issues. This behavior is more detailed in their documentation.

The other main change is the addition of the testGetCallsFromChatterImage method. It is there to test the ChatterImage inner class’ public methods.

Visualforce

<apex:page showHeader="true" sidebar="true" title="Chatter Gallery" controller="ChatterGalleryPageController">
    <apex:includeScript value="//ajax.googleapis.com/ajax/libs/jquery/1/jquery.js"/>
    <apex:includeScript value="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/jquery-ui.min.js"/>
    <apex:stylesheet value="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/themes/smoothness/jquery-ui.css"/>
    <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">
            <img src="{!img.image.downloadUrl}" data-title="{!img.item.Id}"/>
        </apex:repeat>
    </div>
    
    <div id="dialog" title="Like Info">
    	
	</div>
    
    <script>
    	Galleria.loadTheme('{!URLFOR($Resource.Galleria,'galleria/themes/classic/galleria.classic.min.js')}');
        Galleria.run('.galleria', {
        	extend: function(options) {
                this.bind('image', function(e) {
                    $(e.imageTarget).click(this.proxy(function() {
                    	imageClicked(e);
                    }));
                });
            }
        });
    	function imageClicked(e){
            Visualforce.remoting.Manager.invokeAction(
                '{!$RemoteAction.ChatterGalleryPageController.getLikesForImage}',
                e.galleriaData.title, 
                renderDialog
            );
        }
    	function renderDialog(result, event){
        	var dialog = $('#dialog');
            dialog.empty();
            setLikeCount(dialog, result);
            setWhoLiked(dialog, result);
            dialog.dialog();
        }
    	function setLikeCount(dialog, result){
        	if(result.length == 0){
                dialog.append('<p><strong>No likes:</strong></p>');
            }else if(result.length == 1){
            	dialog.append('<p><strong>1 like:</strong></p>');
            }else{
                dialog.append('<p><strong>' + result.length + ' likes:</strong></p>');
            }
        }
    	function setWhoLiked(dialog, result){
            if(result.length == 0){
                dialog.append('<p>Sorry! No one liked this yet!</p>');
            }else{    
                for (i=0; i<result.length; i++){
                    dialog.append('<p>' + result[i].user.name + ' liked this</p>');
                }
            }
        }
    </script>
</apex:page>

The Visualforce changes are relatively minor, but you will notice an enhanced script between lines 21-64. I extended the Galleria library on lines 23-31 by adding an extend function. This section of code binds the imageClicked function to be run when the main image is clicked. This function uses Visualforce remoting to call the getLikesForImage on the ChatterGalleryPageController.

The rest of the script is essentially formatting the jQueryUI Dialog to add the Like information to it. This is all done in the renderDialog function and the functions it calls (lines 39-63).

Finished Product

With all of this running, we can now click on each image and see the Like information associated with that post. Each variation looks like:

No likes

One like

Multiple likes

Looking Forward

Next week, we will look to extend this even further to potentially pull in comments or even add paging. 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!

One Response to “Extending Chatter Image Gallery using ConnectApi”

  1. March 26, 2014 at 7:50 pm #

    Very informative article, Jesse. ++Bonus points for providing the test class! 🙂

Leave a Comment