Last active
August 2, 2024 12:34
-
-
Save ClementRoyer/e223f2adf80882de2f4384f1aaa4cfdd to your computer and use it in GitHub Desktop.
Sample of WebSocket connexion from FIORI (sapui5) to SAP using Abap Push Channel and Abap Messaging Channel (APC/AMC)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<core:FragmentDefinition | |
xmlns:core="sap.ui.core" | |
xmlns="sap.m"> | |
<VBox width="100%" height="100%" | |
renderType="Bare"> | |
<!-- Toolbar spacer --> | |
<VBox class="sapUiTinyMarginTop" | |
height="2.75rem" | |
width="100%"> | |
<Text text=" "/> | |
</VBox> | |
<!-- Header --> | |
<HBox class="sapUiSmallMarginTop sapUiSmallMarginBottom"> | |
<Button | |
icon="sap-icon://decline" | |
press=".ChatController.onCloseChat"/> | |
<VBox width="100%" | |
alignItems="Center"> | |
<Title text="{i18n>chat.title}" titleStyle="H3"/> | |
</VBox> | |
</HBox> | |
<!-- Seperator --> | |
<VBox class="sapMTBStandard"/> | |
<!-- Message --> | |
<ScrollContainer id="ScrollContainer" | |
class="sapUiSmallMarginEnd" | |
width="100%" | |
height="100%" | |
vertical="true"> | |
<VBox | |
width="100%" | |
alignItems="Center" | |
visible="{DisplayConfig>/Chat/EnableLoadMore}"> | |
<Button class="sapUiSmallMarginTopBottom" | |
icon="sap-icon://drill-up" | |
press=".ChatController.onPressLoadMore"/> | |
</VBox> | |
<VBox items="{Chat>/}" > | |
<VBox> | |
<VBox visible="{= ${Chat>SenderName} !== 'System' }"> | |
<FeedListItem | |
sender="{Chat>SenderName}" | |
iconDensityAware="false" | |
iconInitials="{= ${Chat>SenderName}.split(' ')[0][0] + ${Chat>SenderName}.split(' ')[1][0] }" | |
iconSize="XS" | |
timestamp="{Chat>TimeStamp}" | |
text="{Chat>Content}" | |
convertLinksToAnchorTags="All"/> | |
</VBox> | |
<!-- Systeme Text --> | |
<VBox visible="{= ${Chat>SenderName} === 'System' }"> | |
<Label | |
class="sapUiTinyMarginTop" | |
text="{i18n>chat.wss.info} {Chat>Content}" | |
visible="{= ${Chat>SenderName} === 'System' }"/> | |
</VBox> | |
</VBox> | |
</VBox> | |
</ScrollContainer> | |
<!-- Input Message --> | |
<FeedInput | |
post=".ChatController.onSendPress" | |
class="sapUiSmallMarginTopBottom" /> | |
</VBox> | |
</core:FragmentDefinition> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable no-console */ | |
sap.ui.define([ | |
"xx/xxx/xx/investmenthub/detail/controller/extension/popup/ErrorMessagePopover", | |
"sap/ui/core/mvc/ControllerExtension", | |
"sap/ui/core/ws/SapPcpWebSocket", | |
"sap/ui/model/json/JSONModel", | |
"sap/ca/ui/model/type/Time" | |
], function ( | |
ErrorMessagePopover, | |
ControllerExtension, | |
SapPcpWebSocket, | |
JSONModel, | |
Time | |
) { | |
"use strict"; | |
return ControllerExtension.extend("xx.xxx.xx.investmenthub.detail.controller.extension.popup.ChatController", { | |
/** @private **/ | |
ErrorHandler: ErrorMessagePopover, | |
/** | |
* @private | |
**/ | |
ownerController: undefined, | |
/** @private **/ | |
_chatMessagePageSize: 25, | |
/** | |
* ! Warning, WebSocket don't work in BAS, deploy to test/use it | |
* | |
* @private | |
* @type {sap.ui.core.ws.SapPcpWebSocket} | |
**/ | |
webSocket: undefined, | |
override: { | |
onInit: function () { | |
this.ownerController = this.base.getView().getController(); | |
this.ErrorHandler = this.ownerController.ErrorHandler; | |
} | |
}, | |
/* * * * * *\ | |
* PUBLIC * | |
\* * * * * */ | |
/** | |
* Start fetching messages and conntect to WebSocket | |
* | |
* @public | |
**/ | |
start: function() { | |
this.loadMessages().then(() => { | |
this.connectToLiveChat(); | |
}); | |
}, | |
/** | |
* Load Chat Messages | |
* | |
* @public | |
* @param {string} [top] - Number of messages to load | |
* @param {boolean} [keepPrevious] - Keep previously fetch messages | |
* @returns {Promise} - Http response | |
**/ | |
loadMessages: function(top = this._chatMessagePageSize, keepPrevious = false) { | |
/** @type {sap.ui.model.odata.v2.ODataModel} */ | |
const model = this.getModel(); | |
const chatModel = this.getChatModel(); | |
const project = this.getProject().Project; | |
const offset = keepPrevious ? chatModel.getProperty("/").length : 0; | |
return new Promise((resolve, reject) => { | |
model.metadataLoaded().then(() => { | |
model.read("/ZC_FIO_InvestmentHubChat", { | |
urlParameters: { | |
"$top": top, | |
"$skip": offset, | |
"$orderby": "TimeStamp desc" /** Use DESC to get last messages first **/ | |
}, | |
filters: [ new sap.ui.model.Filter("ProjectGUID", sap.ui.model.FilterOperator.EQ, project) ], | |
success: (oData) => { | |
var messages = oData.results; | |
if (messages.length < top) { | |
/** All messages haves been loaded */ | |
this.getDisplayConfigModel().setProperty("/Chat/EnableLoadMore", false); | |
} | |
if (keepPrevious) { | |
const currentMessages = chatModel.getProperty("/") || []; | |
messages = messages.concat(currentMessages); | |
} | |
/** Sort back to ascending **/ | |
messages.sort((a, b) => a.TimeStamp - b.TimeStamp); | |
chatModel.setProperty("/", messages); | |
resolve(messages); | |
}, | |
error: (oError) => { | |
this.ErrorHandler.showError(oError); | |
reject(oError); | |
} | |
}); | |
}); | |
}); | |
}, | |
/** | |
* Connect to Live Chat (WebSocket) | |
* | |
* @public | |
**/ | |
connectToLiveChat: function() { | |
this._connectWebSocket(); | |
}, | |
/* * * * * * *\ | |
* Formatter * | |
\* * * * * * */ | |
/* * * * *\ | |
* Event * | |
\* * * * */ | |
onSenderPress() { | |
}, | |
/** | |
* Event: Press send on a chat message | |
* | |
* @listens press | |
* @param {sap.ui.base.Event} oEvent - Event object | |
**/ | |
onSendPress: function(oEvent) { | |
const messageValue = oEvent.getParameter("value"); | |
this._sendMessage(messageValue); | |
}, | |
/** | |
* Event: Open Chat | |
* | |
* @listens press | |
* @param {sap.ui.base.Event} oEvent - Event object | |
**/ | |
onChatOpen: function (oEvent) { | |
var chatPanelId = this.ownerController.getChatId(); | |
var chatPanel = this.byId(chatPanelId); | |
var isVisible = chatPanel.getVisible(); | |
if (!isVisible) { | |
chatPanel.setVisible(true); | |
chatPanel.addStyleClass("open"); | |
} | |
this._scrollToBottom(); | |
}, | |
/** | |
* Event: Press close chat | |
* | |
* @listens press | |
* @param {sap.ui.base.Event} oEvent - Event object | |
*/ | |
onCloseChat: function (oEvent) { | |
var chatPanelId = this.ownerController.getChatId(); | |
var chatPanel = this.byId(chatPanelId); | |
var isVisible = chatPanel.getVisible(); | |
if (isVisible) { | |
chatPanel.setVisible(false); | |
chatPanel.removeStyleClass("open"); | |
} | |
}, | |
/** | |
* Event: On Press Load More in chat | |
* | |
* @listens press | |
* @param {sap.ui.base.Event} oEvent - Event object | |
**/ | |
onPressLoadMore: function(oEvent) { | |
this.loadMessages(this._chatMessagePageSize, true); | |
}, | |
/* * * * * *\ | |
* Private * | |
\* * * * * */ | |
/** | |
* Scroll chat to first element (down) | |
* | |
* @private | |
**/ | |
_scrollToBottom: function() { | |
// Ensure changes are applied before scrolling | |
sap.ui.getCore().applyChanges(); | |
// Scroll to the bottom after a delay to ensure rendering is complete | |
setTimeout(function() { | |
const scrollContainer = this.byId("ScrollContainer"); | |
scrollContainer.scrollTo(0, 9999999999999); | |
}.bind(this), 0); | |
}, | |
/** | |
* Start WebSocket Connection | |
* | |
* @private | |
**/ | |
_connectWebSocket: function() { | |
const project = this.getProject().Project; | |
/** SapPcpWebSocket doesn't work in BAS so change URL to use local sap URL **/ | |
const host = location.host.includes(".cloud.sap") ? "xxxxxx.xxx.com:xxxxx" : location.host; | |
const protocol = location.protocol === "https:" ? "wss" : "ws"; | |
const url = `${protocol}://${host}/sap/bc/apc/sap/zps_investment_hub_chat?Project=${project}`; | |
this.webSocket = new SapPcpWebSocket(url); | |
this.webSocket.attachOpen((oEvent) => { | |
console.info("WebSocket connection opened"); | |
}); | |
this.webSocket.attachClose((oEvent) => { | |
this.addSystemMessage(this.i18n("chat.wss.connection.closed")); | |
console.info("WebSocket connection closed"); | |
}); | |
this.webSocket.attachError((oEvent) => { | |
this.addSystemMessage(this.i18n("chat.wss.connection.error")); | |
console.error("WebSocket connection error", oEvent); | |
}); | |
this.webSocket.attachMessage((oEvent) => { | |
console.info("WebSocket message received", oEvent); | |
const params = oEvent.getParameters(); | |
// const user = params.pcpFields.User; | |
const userName = params.pcpFields.UserName; | |
const content = params.data; | |
if (!userName || userName === "") { | |
this.addSystemMessage(content); | |
} else { | |
this.addMessageToModel(userName, content); | |
} | |
}); | |
}, | |
/** | |
* Send a chat message | |
* | |
* @private | |
* @param {string} message - Message to send | |
**/ | |
_sendMessage: function(message) { | |
if (!message) { | |
return; | |
} | |
this.webSocket.send(message); | |
}, | |
/** | |
* Add a system message to message queue | |
* | |
* @private | |
* @param {string} message - Message to display | |
**/ | |
addSystemMessage: function(message) { | |
this.addMessageToModel("System", message); | |
}, | |
/** | |
* Add a message to the chat model | |
* | |
* @private | |
* @param {string} userName - UserName | |
* @param {string} message - Message Content | |
**/ | |
addMessageToModel: function(userName, message) { | |
const chatModel = this.getChatModel(); | |
const messages = chatModel.getProperty("/"); | |
messages.push({SenderName: userName, Content: message, TimeStamp: new Date()}); | |
chatModel.setProperty("/", messages); | |
this._scrollToBottom(); | |
}, | |
/* * * * *\ | |
* Utils * | |
\* * * * */ | |
/** | |
* Select by ID | |
* | |
* @private | |
* @param {string} objectID - The object you are looking for | |
* @return {Object} - your object | |
**/ | |
byId: function(objectID) { | |
return this.ownerController.byId(objectID); | |
}, | |
/** | |
* Create a new model | |
* | |
* @private | |
* @param {any} content - Your object | |
* @param {string} name - Your new model name | |
* @return {sap.ui.model.Model} - The new model | |
**/ | |
_createModel: function(content, name) { | |
const view = this.getView(); | |
view.setModel(new JSONModel(content), name); | |
return view.getModel(name); | |
}, | |
/** | |
* Create/update a model | |
* | |
* @private | |
* @param {any} oContent - the content of the new model | |
* @param {string} [sModel] - name of the model | |
* @return {sap.ui.model.odata.v2.ODataModel} - the model | |
**/ | |
setModel: function (oContent, sModel) { | |
return this.getView().setModel(oContent, sModel); | |
}, | |
/** | |
* Get a model | |
* | |
* @private | |
* @param {string} [sModel] - name of the model | |
* @return {sap.ui.model.odata.v2.ODataModel} - the model | |
**/ | |
getModel: function (sModel) { | |
return this.getView().getModel(sModel); | |
}, | |
/** | |
* Get Project Model | |
* | |
* @private | |
* @return {any} - Project model | |
**/ | |
getProject: function () { | |
return this.ownerController.getProject(); | |
}, | |
/** | |
* Get Chat Model | |
* | |
* @private | |
* @return {sap.ui.model.json.JSONModel} - the model | |
**/ | |
getChatModel: function () { | |
return this.getView().getModel("Chat"); | |
}, | |
/** | |
* Get Chat Model | |
* | |
* @private | |
* @return {sap.ui.model.json.JSONModel} - the model | |
**/ | |
getDisplayConfigModel: function () { | |
return this.ownerController.getDisplayConfigModel(); | |
}, | |
/** | |
* Get view from owner controller | |
* | |
* @private | |
* @return {sap.ui.core.mvc.View} View | |
**/ | |
getView() { | |
return this.ownerController.getView(); | |
}, | |
/** | |
* i18n base method encapsulation | |
* | |
* @private | |
* @param {string} sKey - key | |
* @param {string[]} [args] - parameters | |
* @returns {string} - i18n result | |
**/ | |
i18n: function (sKey, args = []) { | |
let _i18n = this.getModel("i18n").getResourceBundle(); | |
return _i18n ? _i18n.getText(sKey, args) : sKey; | |
} | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
CLASS zcl_apc_wsp_ext_zps_investment DEFINITION | |
PUBLIC | |
INHERITING FROM cl_apc_wsp_ext_stateless_base | |
FINAL | |
CREATE PUBLIC . | |
PUBLIC SECTION. | |
METHODS: | |
if_apc_wsp_extension~on_message REDEFINITION, | |
if_apc_wsp_extension~on_start REDEFINITION. | |
PROTECTED SECTION. | |
PRIVATE SECTION. | |
CONSTANTS: | |
c_application_id TYPE amc_application_id VALUE 'ZPS_INVESTMENT_HUB_CHAT', | |
c_channel_id TYPE amc_channel_id VALUE '/chat'. | |
DATA: message_manager TYPE REF TO if_apc_wsp_message_manager. | |
METHODS: | |
"! Subscribe to AMC sub channel | |
"! | |
"! @parameter i_context | Request Context | |
"! @parameter channel | Sub Channel to subscribe | |
subscribe_to_channel | |
IMPORTING VALUE(i_context) TYPE REF TO if_apc_wsp_server_context | |
VALUE(channel) TYPE if_abap_channel_types=>ty_amc_channel_extension_id, | |
"! Send message to any connected sessions | |
"! | |
"! @parameter message_text | Text Message to send | |
send_message_to_channel | |
IMPORTING VALUE(channel) TYPE if_abap_channel_types=>ty_amc_channel_extension_id | |
VALUE(message_text) TYPE string | |
VALUE(user) TYPE syst_uname DEFAULT sy-uname, | |
"! Send message back to single user | |
"! | |
"! @parameter message_text | Text Message to send | |
send_message_back | |
IMPORTING VALUE(message_text) TYPE string, | |
"! Retrieve username from user sap id | |
"! | |
"! @parameter user | User SAP ID | |
"! @parameter r_username | User full name | |
get_username | |
IMPORTING VALUE(user) TYPE syst_uname | |
RETURNING VALUE(r_username) TYPE string, | |
"! Save message to chat table | |
"! | |
"! @parameter raw_project_guid | Raw Project GUID (Fiori format) | |
"! @parameter message | Message content | |
save_message_to_db | |
IMPORTING VALUE(raw_project_guid) TYPE string | |
VALUE(message) TYPE string. | |
ENDCLASS. | |
CLASS zcl_apc_wsp_ext_zps_investment IMPLEMENTATION. | |
METHOD if_apc_wsp_extension~on_message. | |
"" Receive a new message "" | |
CONSTANTS: separator TYPE string VALUE ':'. | |
DATA(username) = me->get_username( sy-uname ). | |
me->message_manager = i_message_manager. | |
DATA(project) = i_context->get_initial_request( )->get_form_field( 'Project' ). | |
IF project IS INITIAL. | |
"" Message can't be processed without a project ID "" | |
RETURN. | |
ENDIF. | |
TRY. | |
DATA(received_message) = i_message->get_text( ). | |
DATA(pcp_message) = cl_amc_message_type_pcp=>deserialize_text( received_message ). | |
DATA(message_text) = pcp_message->get_text( ). | |
me->send_message_to_channel( channel = CONV #( project ) | |
message_text = |{ message_text }| ). | |
me->save_message_to_db( raw_project_guid = project | |
message = message_text ). | |
CATCH cx_apc_error. | |
"handle exception | |
ENDTRY. | |
ENDMETHOD. | |
METHOD if_apc_wsp_extension~on_start. | |
"" New Connection "" | |
DATA(username) = me->get_username( sy-uname ). | |
me->message_manager = i_message_manager. | |
DATA(project) = i_context->get_initial_request( )->get_form_field( 'Project' ). | |
me->send_message_to_channel( channel = CONV #( project ) | |
message_text = |{ username } a rejoint le chat.| | |
user = 'System' ). | |
me->subscribe_to_channel( i_context = i_context channel = CONV #( project ) ). | |
me->send_message_back( |Connexion au chat réussi.| ). | |
ENDMETHOD. | |
METHOD subscribe_to_channel. | |
"" Subscribe to sub channel "" | |
"" Bind the WebSocket connection to the AMC channel | |
TRY. | |
DATA(binding_manager) = i_context->get_binding_manager( ). | |
binding_manager->bind_amc_message_consumer( i_application_id = c_application_id | |
i_channel_id = c_channel_id | |
i_channel_extension_id = channel ). | |
CATCH cx_apc_error INTO DATA(lx_apc_error). | |
"" Error | |
ENDTRY. | |
ENDMETHOD. | |
METHOD send_message_to_channel. | |
"" Send Message to any connected session "" | |
DATA(user_name) = COND #( WHEN user <> 'System' THEN get_username( user ) ELSE 'System' ). | |
TRY. | |
DATA(message_channel) = CAST if_amc_message_producer_pcp( cl_amc_channel_manager=>create_message_producer( | |
i_application_id = c_application_id | |
i_channel_id = c_channel_id | |
i_channel_extension_id = channel ) ). | |
DATA(pcp_message) = cl_ac_message_type_pcp=>create( ). | |
"" Add header technical data | |
pcp_message->set_field( i_name = 'UserName' i_value = user_name ). | |
pcp_message->set_field( i_name = 'User' i_value = CONV #( user ) ). | |
"" Add Text | |
pcp_message->set_text( message_text ). | |
"" Send Message | |
message_channel->send( pcp_message ). | |
CATCH cx_amc_error. | |
"" Error | |
CATCH cx_ac_message_type_pcp_error. | |
"" Error | |
ENDTRY. | |
ENDMETHOD. | |
METHOD send_message_back. | |
"" Send Message to any connected session "" | |
CHECK message_manager IS BOUND. | |
CHECK strlen( message_text ) > 0. | |
TRY. | |
DATA(message) = message_manager->create_message( ). | |
message->set_text( message_text ). | |
message_manager->send( message ). | |
CATCH cx_apc_error. | |
"" Error | |
ENDTRY. | |
ENDMETHOD. | |
METHOD get_username. | |
"" Retrieve user full name "" | |
CHECK user IS NOT INITIAL. | |
SELECT SINGLE UserName | |
FROM ZI_User | |
WHERE UserID = @user | |
INTO @r_username. | |
ENDMETHOD. | |
METHOD save_message_to_db. | |
"" Save current message to DB "" | |
CHECK raw_project_guid IS NOT INITIAL | |
AND message IS NOT INITIAL. | |
DATA(project) = cl_soap_wsrmb_helper=>convert_uuid_hyphened_to_raw( raw_project_guid ). | |
DATA(chat_api) = NEW zcl_ps_investment_hub_chat( project ). | |
chat_api->add_message( CONV #( message ) ). | |
ENDMETHOD. | |
ENDCLASS. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment