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:
In the container, a Check-in icon is added (see the screenshot). A click on it will expand an input field and a Check-in button.
The user inputs his location and clicks the button. The input is validated (for simplification, the sample code just checks that it is empty or not), then the Share button is enabled. By clicking Share, the user posts to the activity stream a message saying he "checked in at" the location.
Your project involves a Java class, a Groovy template, JavaScript and some other resources.
Create a Maven project with 2 modules:
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>
Create folders and files for the composer-plugin folder, as follows:
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>
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.
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>
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>
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.
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.
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>
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>
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>
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(/&/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);
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.
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.
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.