5.4.2.2. Creating an activity composer

The Activity Stream portlet features a Composer container that contains some built-in composers, like the File composer to share a document, or the Link composer to share any external media resource. In general, a composer is a UI form/dialog that binds to a Java class to compose and save an activity.

The container is extensible, so you can add your own composer. In this tutorial, it is assumed that you will add a LocationComposer that functions as below:

Your project involves a Java class, a Groovy template, JavaScript and some other resources.

  1. Create a Maven project with 2 modules:

  2. Edit the pom.xml file:

    
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <parent>
        <artifactId>social</artifactId>
        <groupId>org.exoplatform.social</groupId>
        <version>4.0.4</version>
      </parent>
      <artifactId>social-location-composer</artifactId>
      <version>4.0.x</version>
      <packaging>pom</packaging>
      <name>eXo Social - Location Composer</name>
      <description>eXo Social - Location Composer</description>
      <modules>
        <module>resources</module>
        <module>composer-plugin</module>
      </modules>
    </project>
  3. Create folders and files for the composer-plugin folder, as follows:

  4. Edit the composer-plugin/pom.xml file:

    
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <parent>
        <artifactId>social-location-composer</artifactId>
        <groupId>org.exoplatform.social</groupId>
        <version>4.0.x</version>
      </parent>
      <groupId>com.acme.samples</groupId>
      <artifactId>acme-location-composer-plugin</artifactId>
      <packaging>jar</packaging>
      <name>Sample activity composer plugin</name>
      <description>Sample activity composer plugin</description>
      <dependencies>
        <dependency>
          <groupId>org.exoplatform.social</groupId>
          <artifactId>social-component-common</artifactId>
          <version>4.0.4</version>
          <scope>provided</scope>
        </dependency>
        <dependency>
          <groupId>org.exoplatform.social</groupId>
          <artifactId>social-component-core</artifactId>
          <version>4.0.4</version>
          <scope>provided</scope>
        </dependency>
         <dependency>
          <groupId>org.exoplatform.kernel</groupId>
          <artifactId>exo.kernel.commons</artifactId>
          <version>2.4.7-GA</version>
          <scope>provided</scope>
        </dependency>
        <dependency>
          <groupId>org.exoplatform.social</groupId>
          <artifactId>social-component-webui</artifactId>
          <version>4.0.4</version>
          <scope>provided</scope>
        </dependency>
        <dependency>
          <groupId>org.exoplatform.platform-ui</groupId>
          <artifactId>platform-ui-webui-core</artifactId>
          <version>4.0.4</version>
          <scope>provided</scope>
        </dependency>
        <dependency>
          <groupId>org.gatein.portal</groupId>
          <artifactId>exo.portal.component.web.controller</artifactId>
          <version>3.5.8-PLF</version>
          <scope>provided</scope>
        </dependency>
        <dependency>
          <groupId>org.gatein.portal</groupId>
          <artifactId>exo.portal.webui.framework</artifactId>
          <version>3.5.8-PLF</version>
          <scope>provided</scope>
        </dependency>
      </dependencies>
    </project>
  5. Edit the SampleActivityComposer.java file:

    package com.acme.samples;
    
    
    import java.util.LinkedHashMap;
    import java.util.Map;
    import java.util.ResourceBundle;
    import org.exoplatform.social.core.activity.model.ExoSocialActivity;
    import org.exoplatform.social.core.activity.model.ExoSocialActivityImpl;
    import org.exoplatform.social.core.application.PeopleService;
    import org.exoplatform.social.core.identity.model.Identity;
    import org.exoplatform.social.core.identity.provider.OrganizationIdentityProvider;
    import org.exoplatform.social.core.identity.provider.SpaceIdentityProvider;
    import org.exoplatform.social.core.space.model.Space;
    import org.exoplatform.social.core.space.spi.SpaceService;
    import org.exoplatform.social.webui.Utils;
    import org.exoplatform.social.webui.activity.UIDefaultActivity;
    import org.exoplatform.social.webui.composer.UIActivityComposer;
    import org.exoplatform.social.webui.composer.UIComposer;
    import org.exoplatform.social.webui.composer.UIComposer.PostContext;
    import org.exoplatform.social.webui.profile.UIUserActivitiesDisplay;
    import org.exoplatform.social.webui.profile.UIUserActivitiesDisplay.DisplayMode;
    import org.exoplatform.social.webui.space.UISpaceActivitiesDisplay;
    import org.exoplatform.web.application.ApplicationMessage;
    import org.exoplatform.webui.application.WebuiRequestContext;
    import org.exoplatform.webui.config.annotation.ComponentConfig;
    import org.exoplatform.webui.config.annotation.EventConfig;
    import org.exoplatform.webui.core.UIApplication;
    import org.exoplatform.webui.core.UIComponent;
    import org.exoplatform.webui.event.Event;
    import org.exoplatform.webui.event.EventListener;
    import org.exoplatform.webui.form.UIFormStringInput;
    import org.exoplatform.webui.form.UIFormTextAreaInput;
    @ComponentConfig(template = "war:/groovy/com/acme/samples/SampleActivityComposer.gtmpl", events = {
        @EventConfig(listeners = SampleActivityComposer.CheckinActionListener.class),
        @EventConfig(listeners = UIActivityComposer.CloseActionListener.class),
        @EventConfig(listeners = UIActivityComposer.SubmitContentActionListener.class),
        @EventConfig(listeners = UIActivityComposer.ActivateActionListener.class) })
    public class SampleActivityComposer extends UIActivityComposer {
      public static final String LOCATION = "location";
      
      private String location_ = "";
      private boolean isLocationValid_ = false;
      private Map<String, String> templateParams;
      public SampleActivityComposer() {
        setReadyForPostingActivity(false);
        UIFormStringInput inputLocation = new UIFormStringInput("InputLocation", "InputLocation", null);
        addChild(inputLocation);
      }
      public void setLocationValid(boolean isValid) {
        isLocationValid_ = isValid;
      }
      public boolean isLocationValid() {
        return isLocationValid_;
      }
      public void setTemplateParams(Map<String, String> tempParams) {
        templateParams = tempParams;
      }
      public Map<String, String> getTemplateParams() {
        return templateParams;
      }
      public void clearLocation() {
        location_ = "";
      }
      public String getLocation() {
        return location_;
      }
      private void setLocation(String city, WebuiRequestContext requestContext) {
        location_ = city;
        if (location_ == null || location_ == "") {
          UIApplication uiApp = requestContext.getUIApplication();
          uiApp.addMessage(new ApplicationMessage("Invalid location!", null, ApplicationMessage.ERROR));
          return;
        }
        templateParams = new LinkedHashMap<String, String>();
        templateParams.put(LOCATION, location_);
        setLocationValid(true);
      }
      @Override
      public void onActivate(Event<UIActivityComposer> uiActivityComposer) {
      }
      @Override
      public void onSubmit(Event<UIActivityComposer> uiActivityComposer) {
      }
      @Override
      public void onClose(Event<UIActivityComposer> uiActivityComposer) {
      }
      /* called when user clicks "Share" button.
       * create and save activity.
       */
      @Override
      public void onPostActivity(PostContext postContext,
                                 UIComponent uiComponent,
                                 WebuiRequestContext requestContext,
                                 String postedMessage) throws Exception {
        if (postContext == UIComposer.PostContext.SPACE){
          UISpaceActivitiesDisplay uiDisplaySpaceActivities = (UISpaceActivitiesDisplay) getActivityDisplay();
          Space space = uiDisplaySpaceActivities.getSpace();
          Identity spaceIdentity = Utils.getIdentityManager().getOrCreateIdentity(SpaceIdentityProvider.NAME,
                                                                   space.getPrettyName(),
                                                                   false);
          ExoSocialActivity activity = new ExoSocialActivityImpl(Utils.getViewerIdentity().getId(),
                                       SpaceService.SPACES_APP_ID,
                                       postedMessage,
                                       null);
          activity.setType(UIDefaultActivity.ACTIVITY_TYPE);
          Utils.getActivityManager().saveActivityNoReturn(spaceIdentity, activity);
          uiDisplaySpaceActivities.init();
        } else if (postContext == PostContext.USER) {
          UIUserActivitiesDisplay uiUserActivitiesDisplay = (UIUserActivitiesDisplay) getActivityDisplay();
          Identity ownerIdentity = Utils.getIdentityManager().getOrCreateIdentity(OrganizationIdentityProvider.NAME,
                                                                       uiUserActivitiesDisplay.getOwnerName(), false);
          if (postedMessage.length() > 0) {
            postedMessage += "<br>";
          }
          
          if (this.getLocation() != null && this.getLocation().length() > 0) {
            postedMessage += String.format("%s checked in at %s.", ownerIdentity.getProfile().getFullName(), this.getLocation());
          } else {
            postedMessage += String.format("%s checked in at Nowhere.", ownerIdentity.getProfile().getFullName());
          }
          ExoSocialActivity activity = new ExoSocialActivityImpl(Utils.getViewerIdentity().getId(),
                                           PeopleService.PEOPLE_APP_ID,
                                           postedMessage,
                                           null);
          activity.setType(UIDefaultActivity.ACTIVITY_TYPE);
          activity.setTemplateParams(templateParams);
          this.clearLocation();
          Utils.getActivityManager().saveActivityNoReturn(ownerIdentity, activity);
          this.setLocationValid(false);
          if (uiUserActivitiesDisplay.getSelectedDisplayMode() == DisplayMode.MY_SPACE) {
            uiUserActivitiesDisplay.setSelectedDisplayMode(DisplayMode.ALL_ACTIVITIES);
          }
        }
      }
      public static class CheckinActionListener extends EventListener<SampleActivityComposer> {
        
        // this is called on event "Checkin" (when users clicks Check-in button).
        @Override
        public void execute(Event<SampleActivityComposer> event) throws Exception {
          WebuiRequestContext requestContext = event.getRequestContext();
          SampleActivityComposer sampleActivityComposer = event.getSource();
          String city;
          try {
            city = requestContext.getRequestParameter(OBJECTID).trim();
          } catch (Exception e) {
            System.out.println("Exception when getting OBJECTID!");
            return;
          }
          if (city != null && city.length() > 0) {
            sampleActivityComposer.setLocationValid(true);
          } else {
            sampleActivityComposer.setLocationValid(false);
          }
          
          sampleActivityComposer.setLocation(city, requestContext);
          if (sampleActivityComposer.location_ != null && sampleActivityComposer.location_.length() > 0) {
            requestContext.addUIComponentToUpdateByAjax(sampleActivityComposer);
            event.getSource().setReadyForPostingActivity(true);
          }
        }
      }
    }

    Some remarks:

    • The groovy template (groovy/com/acme/SampleActivityComposer.gtmpl) is configured in this class to be rendered when the composer is activated.

    • The inner class (CheckinActionListener) listens to the "Checkin" events (when the user clicks the Check-in button). The class name is bound to the event name.

  6. Edit the composer-plugin/src/main/resources/conf/configuration.xml file to register the extension:

    
    <configuration
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd http://www.exoplatform.org/xml/ns/kernel_1_2.xsd"
        xmlns="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd">

      <external-component-plugins>
        <target-component>org.exoplatform.container.definition.PortalContainerConfig</target-component>
        <component-plugin>
          <name>Add PortalContainer Definitions</name>
          <set-method>registerChangePlugin</set-method>
          <type>org.exoplatform.container.definition.PortalContainerDefinitionChangePlugin</type>
          <priority>101</priority>
          <init-params>
            <values-param>
              <name>apply.specific</name>
              <value>portal</value>
            </values-param>
            <object-param>
              <name>addDependencies</name>
              <object type="org.exoplatform.container.definition.PortalContainerDefinitionChange$AddDependencies">
                <field name="dependencies">
                  <collection type="java.util.ArrayList">
                    <value>
                      <string>acme-extension</string>
                    </value>
                  </collection>
                </field>
              </object>
            </object-param>
          </init-params>
        </component-plugin>
      </external-component-plugins>
    </configuration>
  7. Edit the composer-plugin/src/main/resources/conf/portal/configuration.xml file to configure UIExtensionManager and ResourceBundleService:

    
    <configuration
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.exoplatform.org/xml/ns/kernel_1_1.xsd http://www.exoplatform.org/xml/ns/kernel_1_1.xsd"
       xmlns="http://www.exoplatform.org/xml/ns/kernel_1_1.xsd">
      <external-component-plugins>
        <target-component>org.exoplatform.webui.ext.UIExtensionManager</target-component>
        <component-plugin>
          <name>add.action</name>
          <set-method>registerUIExtensionPlugin</set-method>
          <type>org.exoplatform.webui.ext.UIExtensionPlugin</type>
          <init-params>
            <object-param>
              <name>Sample Activity Composer</name>
              <object type="org.exoplatform.webui.ext.UIExtension">
                <field name="type"><string>org.exoplatform.social.webui.composer.UIActivityComposer</string></field>
                <field name="name"><string>SampleActivityComposer</string></field>
                <field name="component"><string>com.acme.samples.SampleActivityComposer</string></field>
                <field name="rank"><int>1</int></field>
              </object>
            </object-param>
          </init-params>
        </component-plugin>
      </external-component-plugins>

      <external-component-plugins>
        <target-component>org.exoplatform.services.resources.ResourceBundleService</target-component>
        <component-plugin>
          <name>Location Activity Composer Plugin</name>
          <set-method>addResourceBundle</set-method>
          <type>org.exoplatform.services.resources.impl.BaseResourceBundlePlugin</type>
          <init-params>
            <values-param>
              <name>classpath.resources</name>
              <description></description>
              <value>locale.com.acme.LocationComposer</value>
            </values-param>
            <values-param>
              <name>portal.resource.names</name>
              <description></description>
              <value>locale.com.acme.LocationComposer</value>
            </values-param>
          </init-params>
        </component-plugin>
      </external-component-plugins>
    </configuration>
  8. Edit the resources in the locale/com/acme/LocationComposer_en.properties file (that is configured as locale.com.acme.LocationComposer in the previous step):

    UIActivityComposer.label.SampleActivityComposer=Check-in
    com.acme.LocationComposer.CheckinBtn=Check-in

    The first line is for the tooltip of the composer icon, it is looked up by the composer container so you must use the property name as it is. The second property is for the label of the button, it is handled by yourself in the SampleActivityComposer.gtmpl so name it as you want.

  9. Create folders and files of the resources module. It will be built into acme-extension.war that you have registered in the conf/configuration.xml file.

  10. Edit the resources/pom.xml file:

    
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <parent>
        <artifactId>social-location-composer</artifactId>
        <groupId>org.exoplatform.social</groupId>
        <version>4.0.x</version>
      </parent>
      <groupId>com.acme.samples</groupId>
      <artifactId>acme-extension</artifactId>
      <packaging>war</packaging>
      <name>eXo Social Location Composer Resources</name>
      <description>eXo Social Location Composer Resources</description>
      <build>
        <finalName>acme-extension</finalName>
      </build>
    </project>
  11. Edit the web.xml file:

    
    <web-app>
      <display-name>acme-extension</display-name>
      <listener>
        <listener-class>org.exoplatform.container.web.PortalContainerConfigOwner</listener-class>
      </listener>
      <filter>
        <filter-name>ResourceRequestFilter</filter-name>
        <filter-class>org.exoplatform.portal.application.ResourceRequestFilter</filter-class>
      </filter>

      <filter-mapping>
        <filter-name>ResourceRequestFilter</filter-name>
        <url-pattern>/*</url-pattern>
      </filter-mapping>

      <servlet>
        <servlet-name>GateInServlet</servlet-name>
        <servlet-class>org.gatein.wci.api.GateInServlet</servlet-class>
        <load-on-startup>0</load-on-startup>
      </servlet>
      <servlet-mapping>
        <servlet-name>GateInServlet</servlet-name>
        <url-pattern>/gateinservlet</url-pattern>
      </servlet-mapping>

    </web-app>
  12. Edit the gatein-resources.xml file to register JavaScript and CSS resources:

    
    <gatein-resources
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.gatein.org/xml/ns/gatein_resources_1_3 http://www.gatein.org/xml/ns/gatein_resources_1_3"
      xmlns="http://www.gatein.org/xml/ns/gatein_resources_1_3">
      
      <portal-skin>
        <skin-name>Default</skin-name>
        <skin-module>acme.samples</skin-module>
        <css-path>/skin/DefaultSkin/Stylesheet.css</css-path>
      </portal-skin>
      <module>
        <name>location-activity-composer</name>
        <script>
          <path>/javascript/acme/samples/LocationComposer.js</path>
        </script>
        <depends>
          <module>socialUtil</module>
        </depends>
        <depends>
          <module>jquery</module>
          <as>jq</as>
        </depends>
        <depends>
           <module>mentionsPlugin</module>
        </depends>
        <depends>
          <module>mentionsLib</module>
          <as>mentions</as>
        </depends>
        <depends>
          <module>webui</module>
        </depends>
      </module>
    </gatein-resources>
  13. Edit the javascript/acme/samples/LocationComposer.js file:

    (function($) {
      var LocationComposer = {
        ENTER_KEY_CODE: 13,
        
        onLoad: function(params) {
          LocationComposer.configure(params);
          LocationComposer.init();
        },
        
        configure: function(params) {
          this.locationValid = params.locationValid || false;
          this.inputLocationId = params.inputLocationId || 'InputLocation';
          this.checkinButtonId = params.checkinButtonId || 'CheckinButton';
          this.checkinUrl = decodeURI(params.checkinUrl || "");
          this.location = params.location || '';
        },
        
        init: function() {
          LocationComposer = this;
          
          if (this.locationValid === "false") {
            this.inputLocation = $('#' + this.inputLocationId);
            this.checkinButton = $('#' + this.checkinButtonId);
            
            var LocationComposer = this;
            var inputLocation = this.inputLocation;
            var checkinBtn = this.checkinButton;
            inputLocation.on('focus', function(evt) {
              if (inputLocation.val() === '') {
                inputLocation.val('');
              }
            });
            this.inputLocation.on('keypress', function(evt) {
              if (LocationComposer.ENTER_KEY_CODE == (evt.which ? evt.which : evt.keyCode)) {
                $(checkinBtn).click();
              } 
            });
            this.checkinButton.removeAttr('disabled');
            this.checkinButton.on('click', function(evt) {
              if (inputLocation.val() === '') {
                return;
              }
              var url = LocationComposer.checkinUrl.replace(/&amp;/g, "&") + '&objectId=' + encodeURI(inputLocation.val()) + '&ajaxRequest=true';
              ajaxGet(url, function() {
                try {
                  $('textarea#composerInput').exoMentions('showButton', function() {});
                } catch (e) {
                  console.log(e);
                }
              });
            });
          }
          
          var closeButton = $('#UIActivityComposerContainer').find('a.uiIconClose:first');
          if (closeButton.length > 0) {
            closeButton.on('click', function() {
              $('textarea#composerInput').exoMentions('clearLink', function() { });
            });
          }
        }
      };
      return LocationComposer;
    })(jq);
  14. Edit the SampleActivityComposer.gtmpl file:

    
    <%
      import org.exoplatform.webui.form.UIFormStringInput;
      
      def uicomponentId = uicomponent.id;
      def labelCheckin = _ctx.appRes("com.acme.LocationComposer.CheckinBtn");
      
      def locationValid = uicomponent.isLocationValid();
      uicomponent.setLocationValid(false);
      def location = uicomponent.getLocation();
      
      def params = "{" +
                      "locationValid: '" + locationValid + "'," +
                      "inputLocationId: 'InputLocation'," +
                      "checkinButtonId: 'CheckinButton'," +
                      "checkinUrl: encodeURI('" + uicomponent.url("Checkin") + "')," +
                      "location: '" + location + "'" +
                      "}";
       
      def requestContext = _ctx.getRequestContext();
      def jsManager = requestContext.getJavascriptManager();
      jsManager.require("SHARED/jquery", "jq").require("SHARED/location-activity-composer", "locComposer").addScripts("locComposer.onLoad($params);");
      
    %>
    <div id="$uicomponentId">
      <div id="LocationComposerContainer" class="uiComposerLink clearfix">
        <button id="CheckinButton" class="btn pull-right">$labelCheckin</button>
        <div class="Title Editable">
          <%if (locationValid) {%>
            <span class="tabName">Location: $location</span>
          <%} else {
            uicomponent.renderChild(UIFormStringInput.class);
          }%>
        </div>
      </div>
    </div>

    This code calls the location-activity-composer JavaScript module that you registered in the gatein-resources.xml file.

  15. Edit the CSS resources in the skin/Default/Stylesheet.css file:

    
    .sampleactivitycomposer .uiIconSocSampleActivityComposer {
      
    background: url('/social-resources/skin/ShareImages/activity/SOCIntranetBG.png') no-repeat left -388px;
    }
    a.sampleactivitycomposer:hover .uiIconSocSampleActivityComposer  {
      
    background: url('/social-resources/skin/ShareImages/activity/SOCIntranetBG.png') no-repeat left -388px;
    }

    Here you re-use the background image that is packaged in social-resources.war. You can create your own icon.

  16. Build the project, then deploy composer-plugin/target/acme-location-composer-plugin-4.0.x.jar into $PLATFORM_TOMCAT_HOME/lib, and resources/target/acme-extension.war into $PLATFORM_TOMCAT_HOME/webapps.

Testing

Click the icon (with the "Check-in" tooltip) to bring up the location input. Type something, click Check-in, then click Share. An activity will display like you see at the beginning of this page.

Copyright ©. All rights reserved. eXo Platform SAS
blog comments powered byDisqus