This blog is the third installment in the series of blogs “Becoming a Ninja Web IDE Developer”.
Link to the first blog can be founder here: WebIDE Ninja #1: Consume 3rd Party services with UI5
Link to the second blog can be found here: WebIDE Ninja #2: Create template which generate Right Pane WebIDE plugin
For those of you who cannot wait or simply want to use slack in WebIDE.
To use the fast forward, please go through the following steps:
In the previous blog (#2) we showed how to develop WebIDE template which generate right pane plugin.
Generating/Creating a plugin is only the first step of the plugin development lifecycle, the second step is the plugin implementation (both UI and business logic).
In this blog post we will cover the second step and will show how to implement a "real" plugin.
What is slack (from their website):
Slack is a messaging app for teams. It brings all your team’s communication and files in one place, where they’re instantly searchable and available wherever you go. Come explore some of our main features and see why thousands of customers love us.
Since it launch, Slack has become one of the favorite tools for development team around the world. We've piloted this integration a year ago in TechEd Barcelona and got very good responses, so we figured its about time to show you how to do this on your own :smile:
For us Slack brings collaboration capabilities into WebIDE. By integrating slack in WebIDE we can allow developers to chat with each other in real time, get automatic updates via slack bots (will not be covered in this blog post), integrate with other 3rd party solution via slack integrations and more.
In this blog post we will leverage slack real time messaging capabilities and show how you can chat with your team members in real time without leaving WebIDE.
The first thing that we need is a slack team that we are part of. If you are not a member in any team or this is the first time you use slack you can create a new team. In order to do so, go through the following steps:
In order to import the right pane plugin, go through the following steps:
In this section we will implement the view part of our chat. In this view we would like to show the following data:
In additional to the fields we will need also to add some css (custom style sheets) that will make our plugin look nice.
At the end our plugin should look like the following:
The Pane.view.js is where we will create all the controls of our view. Our view will be built from various UI5 controls like: BorderLayout, RowRepeater, TextArea,Image and more.
In order to implement the chat view, go through the following steps:
this.oLayout = new sap.ui.commons.layout.BorderLayout({
width: "100%",
height: "100%",
begin: new sap.ui.commons.layout.BorderLayoutArea({size: "0px"}),
end: new sap.ui.commons.layout.BorderLayoutArea({size: "0px"}),
center: this.getCenter(oController),
top: this.getTop(oController),
bottom: this.getBottom(oController)
});
return this.oLayout;
this function create the main layout of our chat, next we will need to create the content for all the different areas for our layout (center,top,bottom)
getCenter: function(oController) {
this.oRowRepeater = new sap.ui.commons.RowRepeater("messages_row_repeater",{
// noData: new sap.ui.commons.TextView({text: "No Messages..."}),
design: sap.ui.commons.RowRepeaterDesign.BareShell,
numberOfRows: 0,
noData: new sap.ui.commons.TextView()
}).addStyleClass("row-repeater");
var oCreatedByImage = new sap.ui.commons.Image({
width: "36px",
height: "36px",
src: "{image_72}"
}).addStyleClass('user-profile-picture');
var oCreatedByName = new sap.ui.commons.TextView({
design: sap.ui.commons.TextViewDesign.Bold,
text: "{createdByName}"
}).addStyleClass('created-by-name-tv');
var oMessage = new sap.ui.commons.TextView({
text: "{message}",
width: "80%"
}).addStyleClass('message-content');
var oCreatedAt = new sap.ui.commons.TextView({
text: "{createdAt}"
}).addStyleClass("created-at-tv");
var oContentLayout = new sap.ui.layout.VerticalLayout({
content: [oCreatedByName, oCreatedAt,oMessage]
});
this.oRowLayout = new sap.ui.layout.HorizontalLayout({
width: "100%",
content: [oCreatedByImage,oContentLayout]
}).addStyleClass('row-layout');
this.oRowRepeater.bindRows("/data", this.oRowLayout);
var oLayout = new sap.ui.commons.layout.BorderLayoutArea("messages-row-layout",{
size: "75%",
width: "100%",
contentAlign: "left",
visible: true,
content: [this.oRowRepeater]
}).addStyleClass('bl-center-content');
return oLayout;
},
getBottom: function(oController) {
this.oPostTextArea = new sap.ui.commons.TextArea({
rows: 1,
width: "100%",
placeholder: "Write something nice...",
liveChange: [oController.onPostLiveChange,oController]
}).addStyleClass('new-post-ta');
var sImagePath = require.toUrl('slack') + "/img/send.png";
this.oPostButton = new sap.ui.commons.Image({
src: sImagePath,
width: "25px",
height: "25px",
visible: false,
press: [oController.onPostButtonPressed,oController]
});
var oAbsLayout = new sap.ui.commons.layout.AbsoluteLayout({
width: "100%"
}).addStyleClass('post-abs-layout');
oAbsLayout.addContent(this.oPostTextArea,{left: "0px",top: "0px"});
oAbsLayout.addContent(this.oPostButton,{right: "20px",top: "12px"});
var oLayout = new sap.ui.commons.layout.BorderLayoutArea({
size: "10%",
contentAlign: "center",
visible: true,
overflowY: 'hidden',
overflowX: 'hidden',
content: [oAbsLayout]
}).addStyleClass('bl-bottom-content');
return oLayout;
},
getTop: function(oController) {
var sImagePath = require.toUrl('slack') + "/image/slack.png";
var oIcon = new sap.ui.commons.Image({
width: "32px",
height: "32px",
src: "{user_image}"
}).addStyleClass('slack-icon');
var oTitleTextView = new sap.ui.commons.TextView({
text: "Logged in as {user_fname}"
}).addStyleClass('slack-title-tv');
this.oChannelsComboBox = new sap.ui.commons.ComboBox({
width: "90%",
change: [oController.onChannelChanged,oController]
}).addStyleClass('slack-channels-cb');
var oChannelListItem = new sap.ui.core.ListItem({
"text" : "{name}",
"key" : "{id}"
});
this.oChannelsComboBox.bindItems("/channels",oChannelListItem);
var oSettingsIcon = new sap.ui.commons.Button({
text: "Settings",
lite: true,
width: "auto",
press: [oController.onSettingsButtonClicked,oController]
}).addStyleClass('settings-button');
this.oSettingsDialog = new sap.ui.commons.Dialog({
title: "Slack Configurations",
content: this.getSlackConfigurationsContent(oController),
modal: true,
buttons : [
new sap.ui.commons.Button({
text : "Close",
press: [oController.onConfigurationSaveChangesButtonClicked,oController]
}),
new sap.ui.commons.Button({
text : "Save Changes",
press: [oController.onConfigurationSaveChangesButtonClicked,oController]
})
]
});
var oHLayout = new sap.ui.layout.HorizontalLayout({
width: "100%",
content: [oIcon,oTitleTextView]
});
var oVLayout = new sap.ui.layout.VerticalLayout({
width: "100%",
content: [oHLayout,this.oChannelsComboBox]
}).addStyleClass('top-layout');
var oLayout = new sap.ui.commons.layout.BorderLayoutArea({
size: "15%",
contentAlign: "left",
visible: true,
overflowY: 'hidden',
overflowX: 'hidden',
content: [oVLayout,oSettingsIcon]
}).addStyleClass('bl-top-content');
return oLayout;
},
getSlackConfigurationsContent: function(oController) {
var oAuthTokenLabel = new sap.ui.commons.Label({
text: "Authentication Token"
});
this.oAuthTokenTextField = new sap.ui.commons.TextField({
placeholder: "paste your slack authentication token here",
required: true,
change: [oController.onAuthTokenValueChanged,oController]
}).addStyleClass('auth-token-text-field');
var oRemoveTokenButton = new sap.ui.commons.Button({
text : "Remove",
lite: true,
press: [oController.onRemoveTokenButtonClicked,oController]
}).addStyleClass('remove-token-button');
// var oTokenHLayout = new sap.ui.layout.HorizontalLayout({
// width: "100%",
// content: [oRemoveTokenButton,this.oAuthTokenTextField]
// }).addStyleClass('token-h-layout');
var oAuthGenLink = new sap.ui.commons.Link({
text :"Generate slack authentication token",
target: "_blank",
href: "https://api.slack.com/web"
}).addStyleClass('auth-token-link');
var oVLayout = new sap.ui.layout.VerticalLayout({
width: "100%",
content: [oAuthTokenLabel,this.oAuthTokenTextField,oAuthGenLink]
});
return oVLayout;
}
.sc-main-layout {
background-color: #3f5161;
}
.row-repeater {
margin-top: 10px;
height: 100%;
}
.bl-top-content {
background-color: #3f5161;
}
.bl-center-content {
background-color: white;
}
.bl-bottom-content {
background-color: white;
}
.slack-icon {
/*position: absolute;*/
/*top: 50%;*/
/*transform: translateY(-50%);*/
/*margin: 0 10px;*/
}
.slack-title-tv {
color: white;
font-size: 12px;
line-height: 32px;
margin-left: 15px;
}
.top-layout {
padding: 10px;
width: "100%";
}
.slack-channels-cb {
background-color: transparent;
border: 1px solid white;
color: #ab9ba9;
margin-top: 10px;
}
.slack-channel-listitem {
}
.row-layout {
margin-bottom: 2px;
padding: 5px;
margin-left: 2px;
margin-right: 10px;
border-bottom: 1px solid #DDD;
width: 100%;
}
.message-content {
height: auto !important;
margin-right: 5px !important;
color: #3d3c40;
font-size: 13px;
word-wrap: break-word !important;
display: inline-block;
}
.new-post-ta {
border: 2px solid #E0E0E0;
font-size: 13px;
line-height: 1.0rem;
overflow-y: hidden !important;
overflow-x: hidden;
color: #3d3c40;
max-height: 45px !important;
width: 95% !important;
margin-top: 10px !important;
padding-right: 40px !important;
outline: 0 !important;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
background-color: transparent;
}
.post-abs-layout {
width: 100% !important;
}
.user-profile-picture {
margin-right: 10px;
border-radius: 5px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
box-shadow: 0 0 1px rgba(255, 255, 255, .8);
-webkit-box-shadow: 0 0 1px rgba(255, 255, 255, .8);
-moz-box-shadow: 0 0 1px rgba(255, 255, 255, .8);
transition: opacity .1s ease-out 0s;
opacity: .8;
}
.created-by-name-tv {
margin-top: -3px;
color: #3d3c40;
font-size: 14px;
text-transform: capitalize;
}
.created-at-tv {
color: #babbbf;
font-size: 11px;
margin-top: -5px;
}
.settings-button {
position: absolute;
right: 20px;
top: 10px;
color: white;
font-size: 13px;
}
.auth-token-link {
font-size: 13px;
margin-top: 5px;
}
.auth-token-text-field {
width: 250px !important;
}
.token-h-layout {
width: 100% !important;
}
.remove-token-button {
margin-left: 10px;
font-color: red !important;
}
.get-started-button {
color: #2a80b9;
font-weight: 600;
font-size: 14px;
}
This file will contain the code of the chat controller. The chat controller will leverage the following capabilities from slack.com api:
The code of the chat controller is:
jQuery.sap.require("jquery.sap.storage");
sap.ui.controller("slack.view.Pane", {
_oSocket: null,
_vToken: null,
_vChannel: null,
_bConnected: false,
_bAutoGrowthSet: false,
_oStorage: null,
_oUserInfo: null,
/**
* Called when a controller is instantiated and its View controls (if available) are already created.
* Can be used to modify the View before it is displayed, to bind event handlers and do other one-time initialization.
* @memberOf view.Pane
*/
onInit: function() {
/** get the slack authentication token from local storag
* Notice! this is not a best practice to store tokens in the local storage
* we are doing it for testing only scenarios
* */
this._oStorage = jQuery.sap.storage(jQuery.sap.storage.Type.local);
this._vToken = this._oStorage.get("slackAuthenticationToken");
// Create the data model skeleton
var oModel = new sap.ui.model.json.JSONModel({
user: {},
data: [],
channels: [],
users: []
});
sap.ui.getCore().setModel(oModel); // Set model skeleton to the global model
/**
* If the user not enter his/her token yet display no data message
* */
if (!this._vToken) {
this.displayNoData();
} else {
var that = this;
// set the token into the dialog input field
this.getView().oAuthTokenTextField.setValue(this._vToken);
// test if the token is valid with slack.com api
this.testAuthenticationToken(this._vToken, function(authResponse) {
// if the token is valid, fetch logged in user details from api
if (authResponse.ok === true) {
var oUser = {
user_id: authResponse.user_id,
team_id: authResponse.team_id,
username: authResponse.user,
url: authResponse.url,
profile: {}
};
// execute request to get user info
that.getUserInfo(authResponse.user_id, function(userInfoResponse) {
if (userInfoResponse.ok === true) {
// bind user info to data model
oModel.oData.user.profile = userInfoResponse.user.profile;
oModel.oData.user_image = userInfoResponse.user.profile.image_72;
oModel.oData.user_fname = userInfoResponse.user.real_name;
oModel.oData.user = oUser;
that.getView().bindElement("/");
// connect to slack.com socket API's and fetch chat history
that.connectAndLoadData();
}
});
} else {
// if the token is invalid, present no data
this.displayNoData();
}
});
}
},
displayNoData: function() {
this.getView().oRowRepeater.setNoData(new sap.ui.commons.Button({
width: "100%",
lite: true,
text: "Get started here",
press: [this.onSettingsButtonClicked, this]
}).addStyleClass('get-started-button'));
},
connectAndLoadData: function() {
this.getView().oRowRepeater.setBusy(true); // display activity indicator while loading data --> it's better to do it with model binding..
this.connentToSlack(); // connect to slack WebSocket API
},
onAfterRendering: function() {},
/**
*
* */
connentToSlack: function() {
var that = this;
// execute reques to rtm (real time messaging) WebSocket api
$.get("https://slack.com/api/rtm.start", {
token: this._vToken
}, function(data) {
// create the WebSocket native object
that._oSocket = new WebSocket(data.url);
that._oSocket.onopen = function() {
// implement on connetion opened here...
};
// on recieve new message implementation
that._oSocket.onmessage = function(evt) {
that.getView().oRowRepeater.setBusy(false);
var parsedData = JSON.parse(evt.data);
// message of type "hello" means that the user has been connected to slack WebSocket API
if (parsedData.type === "hello") {
if (!that._bConnected) {
that._bConnected = true;
// Load channels and chat history of the deafult channel
that.fetchInitialData();
}
}
if (parsedData.type === "message") {
if (parsedData.channel !== that._vChannel) {
return;
}
var oModel = sap.ui.getCore().getModel();
if (parsedData.subtype === "message_deleted") {
that.removeMessageById(parsedData.deleted_ts, oModel);
} else if (parsedData.subtype === "bot_message") {
var bot = that.findBotByName(parsedData.username);
if (bot) {
oModel.oData.data.push({
createdByName: bot.profile.real_name,
message: parsedData.text,
image_72: bot.profile.image_72,
createdAt: moment.unix(parsedData.ts).fromNow(),
ts: parsedData.ts
});
}
} else {
var user = that.findUserById(parsedData.user);
if (user) {
oModel.oData.data.push({
createdByName: user.real_name !== undefined && user.real_name.length > 0 ? user.real_name : user.name,
message: parsedData.text,
image_72: user.profile.image_72,
createdAt: moment.unix(parsedData.ts).fromNow(),
ts: parsedData.ts
});
}
}
that.getView().oRowRepeater.setNumberOfRows(oModel.oData.data.length);
that.scrollToBottom("messages-row-layout");
oModel.refresh(false);
}
};
});
},
listUsers: function(callback) {
$.get("https://slack.com/api/users.list", {
token: this._vToken
}, callback);
},
removeMessageById: function(vMessageId, oModel) {
var aMessages = oModel.oData.data;
for (var i = 0; i < aMessages.length; i++) {
if (aMessages[i].ts === vMessageId) {
aMessages.splice(i, 1);
return true;
}
}
return false;
},
findUserById: function(vUserId) {
if (!vUserId) {
return;
}
var user = null;
var oModel = sap.ui.getCore().getModel();
for (var i = 0; i < oModel.oData.users.length; i++) {
user = oModel.oData.users[i];
if (user.id === vUserId) {
break;
}
}
return user;
},
findBotByName: function(vBotName) {
var oModel = sap.ui.getCore().getModel();
for (var i = 0; i < oModel.oData.users.length; i++) {
var user = oModel.oData.users[i];
if (user.name && user.name === vBotName) {
return user;
}
}
},
fetchHistory: function(vChannelId, callback) {
$.get("https://slack.com/api/channels.history", {
token: this._vToken,
count: 30,
channel: vChannelId
// inclusive: 1
}, callback);
},
fetchAvailabelChannels: function(callback) {
$.get("https://slack.com/api/channels.list", {
token: this._vToken,
exclude_archived: true
}, callback);
},
selectDefaultChannel: function(oModel) {
for (var i = 0; i < oModel.oData.channels.length; i++) {
var oChannel = oModel.oData.channels[i];
if (oChannel.is_general) {
this.oView.oChannelsComboBox.setSelectedKey(oChannel.id);
this._vChannel = oChannel.id;
break;
}
}
},
addMessageToList: function(oSlackMessage, oModel) {
var user = this.findUserById(oSlackMessage.user);
if (oSlackMessage.subtype !== undefined && oSlackMessage.subtype === "bot_message") {
user = this.findBotByName(oSlackMessage.username);
if (user) {
console.log(user.profile.image_72);
}
} else {
user = this.findUserById(oSlackMessage.user);
}
if (user) {
oModel.oData.data.unshift({
createdByName: user.profile.real_name !== undefined && user.profile.real_name.length > 0 ? user.profile.real_name : user.name,
message: oSlackMessage.text,
image_72: user.profile.image_72,
createdAt: moment.unix(oSlackMessage.ts).fromNow(),
ts: oSlackMessage.ts
});
}
},
onChannelChanged: function(oEvent) {
oEvent.preventDefault();
var oModel = sap.ui.getCore().getModel();
oModel.oData.data = [];
oModel.refresh(false);
this._vChannel = oEvent.oSource.getSelectedKey();
this.getView().oRowRepeater.setBusy(true);
var that = this;
this.fetchHistory(this._vChannel, function(historyData) {
for (var i = 0; i < historyData.messages.length; i++) {
that.addMessageToList(historyData.messages[i], oModel);
}
that.getView().oRowRepeater.setBusy(false);
that.getView().oRowRepeater.setNumberOfRows(oModel.oData.data.length);
oModel.refresh(false);
that.scrollToBottom("messages-row-layout");
});
},
onPostLiveChange: function(oEvent) {
if (!this._bAutoGrowthSet) {
this._bAutoGrowthSet = true;
$(".new-post-ta").autogrow({
onInitialize: false,
fixMinHeight: true
});
}
if (oEvent.oSource.getLiveValue().length === 0) {
this.getView().oPostButton.setVisible(false);
} else {
this.getView().oPostButton.setVisible(true);
}
},
onPostButtonPressed: function(oEvent) {
var vMessage = this.getView().oPostTextArea.getValue();
var that = this;
$.post("https://slack.com/api/chat.postMessage", {
token: that._vToken,
channel: that._vChannel,
text: vMessage,
// username: "ranhsd",
as_user: true
}, function(response) {});
this.getView().oPostTextArea.setValue("");
},
fetchInitialData: function() {
var oModel = sap.ui.getCore().getModel();
var that = this;
// fetch available chat channels for logged in user
this.fetchAvailabelChannels(function(data) {
// store list of channels in global model and select the default channel
oModel.oData.channels = data.channels;
that.selectDefaultChannel(oModel);
// fetch the list of users in this channel
that.listUsers(function(responseData) {
oModel.oData.users = responseData.members; // store the list of users in the global model
// fetch chat history of the selected channel
that.fetchHistory(that._vChannel, function(historyData) {
// map slack api message object to data model object
for (var i = 0; i < historyData.messages.length; i++) {
that.addMessageToList(historyData.messages[i], oModel);
}
that.getView().oRowRepeater.setBusy(false);
that.getView().oRowRepeater.setNumberOfRows(oModel.oData.data.length);
that.scrollToBottom("messages-row-layout");
// bind the results to the UI
oModel.refresh(false);
});
});
});
},
fetchLoggedInUser: function() {},
onSettingsButtonClicked: function(oEvent) {
var oSettingsToolPopup = this.getView().oSettingsDialog;
if (oSettingsToolPopup.isOpen()) {
oSettingsToolPopup.close();
} else {
oSettingsToolPopup.open();
}
},
onConfigurationSaveChangesButtonClicked: function(oEvent) {
// Parent of the button is the dialog
// so we can use get parent
oEvent.oSource.getParent().close();
},
onAuthTokenValueChanged: function(oEvent) {
var sValue = oEvent.oSource.getValue();
var that = this;
if (sValue.length > 0 && sValue !== this._vToken) {
this.testAuthenticationToken(sValue, function(data) {
if (data.ok === true) {
that._oStorage.put("slackAuthenticationToken", sValue);
that._vToken = sValue;
that.connectAndLoadData();
} else {
alert('Invalid Authentication token plese use another one');
that.getView().oAuthTokenTextField.setValue("");
}
});
// xoxp-3704342075-5154985101-5155004893-9964df
} else if (sValue.length === 0) {
if (this._oStorage.get("slackAuthenticationToken")) {
this._oStorage.remove("slackAuthenticationToken");
}
}
},
onCloseConfigDialogClicked: function(oEvent) {
oEvent.oSource.getParent().close();
},
testAuthenticationToken: function(vToken, callback) {
$.get("https://slack.com/api/auth.test", {
token: vToken
}, callback);
},
getUserInfo: function(vUserId, callback) {
$.get("https://slack.com/api/users.info", {
token: this._vToken,
user: vUserId
}, callback);
},
scrollToBottom: function(sDivId) {
setTimeout(function() {
var oDiv = $("#" + sDivId);
if (oDiv[0] !== undefined) {
oDiv.scrollTop(oDiv[0].scrollHeight);
}
}, 0);
}
});
After implementing the view and the controller it's time to run and see that everything is working as expected.
In order to run your plugin, do the following:
video which shows how to run and test your plugin:
The code above is in "quick and dirty" style, if you have some time and you want to write it according to JS best practices we suggest you to do the following:
The full source code is available on github: GitHub - ranhsd/WebIDE-Slack
If you like to contribute by adding more features, change to architecture (according to JavaScript best practices) or fix some bugs please create a pull request. For those of you who are not familiar with pull request there is a great video tutorial in here: Git &amp; GitHub: Pull requests (10/11) - YouTube
That's it :smile:
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
36 | |
25 | |
16 | |
13 | |
7 | |
7 | |
6 | |
6 | |
6 | |
6 |