frontend: introduce rhodecode-app for more complex cross element wiring
This commit is contained in:
parent
6908a7915d
commit
73ba40da93
12 changed files with 774 additions and 265 deletions
|
|
@ -66,7 +66,6 @@
|
|||
"<%= dirs.js.src %>/rhodecode/utils/topics.js",
|
||||
"<%= dirs.js.src %>/rhodecode/widgets/multiselect.js",
|
||||
"<%= dirs.js.src %>/rhodecode/init.js",
|
||||
"<%= dirs.js.src %>/rhodecode/connection_controller.js",
|
||||
"<%= dirs.js.src %>/rhodecode/codemirror.js",
|
||||
"<%= dirs.js.src %>/rhodecode/comments.js",
|
||||
"<%= dirs.js.src %>/rhodecode/constants.js",
|
||||
|
|
@ -81,7 +80,6 @@
|
|||
"<%= dirs.js.src %>/rhodecode/select2_widgets.js",
|
||||
"<%= dirs.js.src %>/rhodecode/tooltips.js",
|
||||
"<%= dirs.js.src %>/rhodecode/users.js",
|
||||
"<%= dirs.js.src %>/rhodecode/utils/notifications.js",
|
||||
"<%= dirs.js.src %>/rhodecode/appenlight.js",
|
||||
"<%= dirs.js.src %>/rhodecode.js"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ from rhodecode.lib.ext_json import json
|
|||
def url_gen(request):
|
||||
urls = {
|
||||
'connect': request.route_url('channelstream_connect'),
|
||||
'subscribe': request.route_url('channelstream_subscribe')
|
||||
'subscribe': request.route_url('channelstream_subscribe'),
|
||||
'longpoll': request.registry.settings.get('channelstream.longpoll_url', ''),
|
||||
'ws': request.registry.settings.get('channelstream.ws_url', '')
|
||||
}
|
||||
return json.dumps(urls)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
<link rel="import" href="../../../../../../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../../../../../../bower_components/iron-ajax/iron-ajax.html">
|
||||
|
||||
<!--
|
||||
|
||||
`<channelstream-connection>` allows you to connect and interact with channelstream server
|
||||
abstracting websocket/long-polling connections from you.
|
||||
|
||||
In typical use, just slap some `<channelstream-connection>` at the top of your body:
|
||||
|
||||
<body>
|
||||
<channelstream-connection
|
||||
username="{{user.username}}"
|
||||
connect-url="http://127.0.0.1:8000/demo/connect"
|
||||
disconnect-url="http://127.0.0.1:8000/disconnect"
|
||||
subscribe-url="http://127.0.0.1:8000/demo/subscribe"
|
||||
message-url="http://127.0.0.1:8000/demo/message"
|
||||
long-poll-url="http://127.0.0.1:8000/listen"
|
||||
websocket-url="http://127.0.0.1:8000/ws"
|
||||
channels-url='["channel1", "channel2"]' />
|
||||
|
||||
Then you can do `channelstreamElem.connect()` to kick off your connection.
|
||||
This element also handles automatic reconnections.
|
||||
|
||||
## Default handlers
|
||||
|
||||
By default element has a listener attached that will fire `startListening()` handler on `channelstream-connected` event.
|
||||
|
||||
By default element has a listener attached that will fire `retryConnection()` handler on `channelstream-connect-error` event,
|
||||
this handler will forever try to re-establish connection to the server incrementing intervals between retries up to 1 minute.
|
||||
|
||||
-->
|
||||
<dom-module id="channelstream-connection">
|
||||
<template>
|
||||
|
||||
<iron-ajax
|
||||
id="ajaxConnect"
|
||||
url=""
|
||||
handle-as="json"
|
||||
method="post"
|
||||
content-type="application/json"
|
||||
loading="{{loadingConnect}}"
|
||||
last-response="{{connectLastResponse}}"
|
||||
on-response="_handleConnect"
|
||||
on-error="_handleConnectError"
|
||||
debounce-duration="100"></iron-ajax>
|
||||
|
||||
<iron-ajax
|
||||
id="ajaxDisconnect"
|
||||
url=""
|
||||
handle-as="json"
|
||||
method="post"
|
||||
content-type="application/json"
|
||||
loading="{{loadingDisconnect}}"
|
||||
last-response="{{_disconnectLastResponse}}"
|
||||
on-response="_handleDisconnect"
|
||||
debounce-duration="100"></iron-ajax>
|
||||
|
||||
<iron-ajax
|
||||
id="ajaxSubscribe"
|
||||
url=""
|
||||
handle-as="json"
|
||||
method="post"
|
||||
content-type="application/json"
|
||||
loading="{{loadingSubscribe}}"
|
||||
last-response="{{subscribeLastResponse}}"
|
||||
on-response="_handleSubscribe"
|
||||
debounce-duration="100"></iron-ajax>
|
||||
|
||||
<iron-ajax
|
||||
id="ajaxUnsubscribe"
|
||||
url=""
|
||||
handle-as="json"
|
||||
method="post"
|
||||
content-type="application/json"
|
||||
loading="{{loadingUnsubscribe}}"
|
||||
last-response="{{unsubscribeLastResponse}}"
|
||||
on-response="_handleUnsubscribe"
|
||||
debounce-duration="100"></iron-ajax>
|
||||
|
||||
<iron-ajax
|
||||
id="ajaxMessage"
|
||||
url=""
|
||||
handle-as="json"
|
||||
method="post"
|
||||
content-type="application/json"
|
||||
loading="{{loadingMessage}}"
|
||||
last-response="{{messageLastResponse}}"
|
||||
on-response="_handleMessage"
|
||||
on-error="_handleMessageError"
|
||||
debounce-duration="100"></iron-ajax>
|
||||
|
||||
<iron-ajax
|
||||
id="ajaxListen"
|
||||
url=""
|
||||
handle-as="text"
|
||||
loading="{{loadingListen}}"
|
||||
last-response="{{listenLastResponse}}"
|
||||
on-request="_handleListenOpen"
|
||||
on-error="_handleListenError"
|
||||
on-response="_handleListenMessageEvent"
|
||||
debounce-duration="100"></iron-ajax>
|
||||
|
||||
</template>
|
||||
<script src="channelstream-connection.js"></script>
|
||||
</dom-module>
|
||||
|
|
@ -0,0 +1,502 @@
|
|||
Polymer({
|
||||
is: 'channelstream-connection',
|
||||
|
||||
/**
|
||||
* Fired when `channels` array changes.
|
||||
*
|
||||
* @event channelstream-channels-changed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when `connect()` method succeeds.
|
||||
*
|
||||
* @event channelstream-connected
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when `connect` fails.
|
||||
*
|
||||
* @event channelstream-connect-error
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when `disconnect()` succeeds.
|
||||
*
|
||||
* @event channelstream-disconnected
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when `message()` succeeds.
|
||||
*
|
||||
* @event channelstream-message-sent
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when `message()` fails.
|
||||
*
|
||||
* @event channelstream-message-error
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when `subscribe()` succeeds.
|
||||
*
|
||||
* @event channelstream-subscribed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when `subscribe()` fails.
|
||||
*
|
||||
* @event channelstream-subscribe-error
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when `unsubscribe()` succeeds.
|
||||
*
|
||||
* @event channelstream-unsubscribed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when `unsubscribe()` fails.
|
||||
*
|
||||
* @event channelstream-unsubscribe-error
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when listening connection receives a message.
|
||||
*
|
||||
* @event channelstream-listen-message
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when listening connection is opened.
|
||||
*
|
||||
* @event channelstream-listen-opened
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when listening connection is closed.
|
||||
*
|
||||
* @event channelstream-listen-closed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when listening connection suffers an error.
|
||||
*
|
||||
* @event channelstream-listen-error
|
||||
*/
|
||||
|
||||
properties: {
|
||||
isReady: Boolean,
|
||||
/** List of channels user should be subscribed to. */
|
||||
channels: {
|
||||
type: Array,
|
||||
value: function () {
|
||||
return []
|
||||
},
|
||||
notify: true
|
||||
},
|
||||
/** Username of connecting user. */
|
||||
username: {
|
||||
type: String,
|
||||
value: 'Anonymous',
|
||||
reflectToAttribute: true
|
||||
},
|
||||
/** Connection identifier. */
|
||||
connectionId: {
|
||||
type: String,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
/** Websocket instance. */
|
||||
websocket: {
|
||||
type: Object,
|
||||
value: null
|
||||
},
|
||||
/** Websocket connection url. */
|
||||
websocketUrl: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
/** URL used in `connect()`. */
|
||||
connectUrl: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
/** URL used in `disconnect()`. */
|
||||
disconnectUrl: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
/** URL used in `subscribe()`. */
|
||||
subscribeUrl: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
/** URL used in `unsubscribe()`. */
|
||||
unsubscribeUrl: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
/** URL used in `message()`. */
|
||||
messageUrl: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
/** Long-polling connection url. */
|
||||
longPollUrl: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
/** Long-polling connection url. */
|
||||
shouldReconnect: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
/** Should send heartbeats. */
|
||||
heartbeats: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
/** How much should every retry interval increase (in milliseconds) */
|
||||
increaseBounceIv: {
|
||||
type: Number,
|
||||
value: 2000
|
||||
},
|
||||
_currentBounceIv: {
|
||||
type: Number,
|
||||
reflectToAttribute: true,
|
||||
value: 0
|
||||
},
|
||||
/** Should use websockets or long-polling by default */
|
||||
useWebsocket: {
|
||||
type: Boolean,
|
||||
reflectToAttribute: true,
|
||||
value: true
|
||||
},
|
||||
connected: {
|
||||
type: Boolean,
|
||||
reflectToAttribute: true,
|
||||
value: false
|
||||
}
|
||||
},
|
||||
|
||||
observers: [
|
||||
'_handleChannelsChange(channels.splices)'
|
||||
],
|
||||
|
||||
listeners: {
|
||||
'channelstream-connected': 'startListening',
|
||||
'channelstream-connect-error': 'retryConnection',
|
||||
},
|
||||
|
||||
/**
|
||||
* Mutators hold functions that you can set locally to change the data
|
||||
* that the client is sending to all endpoints
|
||||
* you can call it like `elem.mutators('connect', yourFunc())`
|
||||
* mutators will be executed in order they were pushed onto arrays
|
||||
*
|
||||
*/
|
||||
mutators: {
|
||||
connect: function () {
|
||||
return []
|
||||
}(),
|
||||
message: function () {
|
||||
return []
|
||||
}(),
|
||||
subscribe: function () {
|
||||
return []
|
||||
}(),
|
||||
unsubscribe: function () {
|
||||
return []
|
||||
}(),
|
||||
disconnect: function () {
|
||||
return []
|
||||
}()
|
||||
},
|
||||
ready: function () {
|
||||
this.isReady = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Connects user and fetches connection id from the server.
|
||||
*
|
||||
*/
|
||||
connect: function () {
|
||||
var request = this.$['ajaxConnect'];
|
||||
request.url = this.connectUrl;
|
||||
request.body = {
|
||||
username: this.username,
|
||||
channels: this.channels
|
||||
};
|
||||
for (var i = 0; i < this.mutators.connect.length; i++) {
|
||||
this.mutators.connect[i](request);
|
||||
}
|
||||
request.generateRequest()
|
||||
},
|
||||
/**
|
||||
* Overwrite with custom function that will
|
||||
*/
|
||||
addMutator: function (type, func) {
|
||||
this.mutators[type].push(func);
|
||||
},
|
||||
/**
|
||||
* Subscribes user to channels.
|
||||
*
|
||||
*/
|
||||
subscribe: function (channels) {
|
||||
var request = this.$['ajaxSubscribe'];
|
||||
request.url = this.subscribeUrl;
|
||||
request.body = {
|
||||
channels: channels,
|
||||
conn_id: this.connectionId
|
||||
};
|
||||
for (var i = 0; i < this.mutators.subscribe.length; i++) {
|
||||
this.mutators.subscribe[i](request);
|
||||
}
|
||||
if (request.body.channels.length) {
|
||||
request.generateRequest();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Unsubscribes user from channels.
|
||||
*
|
||||
*/
|
||||
unsubscribe: function (unsubscribe) {
|
||||
var request = this.$['ajaxUnsubscribe'];
|
||||
|
||||
request.url = this.unsubscribeUrl;
|
||||
request.body = {
|
||||
channels: unsubscribe,
|
||||
conn_id: this.connectionId
|
||||
};
|
||||
for (var i = 0; i < this.mutators.unsubscribe.length; i++) {
|
||||
this.mutators.unsubscribe[i](request);
|
||||
}
|
||||
request.generateRequest()
|
||||
},
|
||||
|
||||
/**
|
||||
* calculates list of channels we should add user to based on difference
|
||||
* between channels property and passed channel list
|
||||
*/
|
||||
calculateSubscribe: function (channels) {
|
||||
var currentlySubscribed = this.channels;
|
||||
var toSubscribe = [];
|
||||
for (var i = 0; i < channels.length; i++) {
|
||||
if (currentlySubscribed.indexOf(channels[i]) === -1) {
|
||||
toSubscribe.push(channels[i]);
|
||||
}
|
||||
}
|
||||
return toSubscribe
|
||||
},
|
||||
/**
|
||||
* calculates list of channels we should remove user from based difference
|
||||
* between channels property and passed channel list
|
||||
*/
|
||||
calculateUnsubscribe: function (channels) {
|
||||
var currentlySubscribed = this.channels;
|
||||
var toUnsubscribe = [];
|
||||
for (var i = 0; i < channels.length; i++) {
|
||||
if (currentlySubscribed.indexOf(channels[i]) !== -1) {
|
||||
toUnsubscribe.push(channels[i]);
|
||||
}
|
||||
}
|
||||
return toUnsubscribe
|
||||
},
|
||||
/**
|
||||
* Marks the connection as expired.
|
||||
*
|
||||
*/
|
||||
disconnect: function () {
|
||||
var request = this.$['ajaxDisconnect'];
|
||||
request.url = this.disconnectUrl;
|
||||
request.params = {
|
||||
conn_id: this.connectionId
|
||||
};
|
||||
for (var i = 0; i < this.mutators.disconnect.length; i++) {
|
||||
this.mutators.disconnect[i](request);
|
||||
}
|
||||
// mark connection as expired
|
||||
request.generateRequest();
|
||||
// disconnect existing connection
|
||||
this.closeConnection();
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends a message to the server.
|
||||
*
|
||||
*/
|
||||
message: function (message) {
|
||||
var request = this.$['ajaxMessage'];
|
||||
request.url = this.messageUrl;
|
||||
request.body = message;
|
||||
for (var i = 0; i < this.mutators.message.length; i++) {
|
||||
this.mutators.message[i](request)
|
||||
}
|
||||
request.generateRequest();
|
||||
},
|
||||
/**
|
||||
* Opens "long lived" (websocket/longpoll) connection to the channelstream server.
|
||||
*
|
||||
*/
|
||||
startListening: function (event) {
|
||||
this.fire('start-listening', {});
|
||||
if (this.useWebsocket) {
|
||||
this.useWebsocket = window.WebSocket ? true : false;
|
||||
}
|
||||
if (this.useWebsocket) {
|
||||
this.openWebsocket();
|
||||
}
|
||||
else {
|
||||
this.openLongPoll();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Opens websocket connection.
|
||||
*
|
||||
*/
|
||||
openWebsocket: function () {
|
||||
var url = this.websocketUrl + '?conn_id=' + this.connectionId;
|
||||
this.websocket = new WebSocket(url);
|
||||
this.websocket.onopen = this._handleListenOpen.bind(this);
|
||||
this.websocket.onclose = this._handleListenCloseEvent.bind(this);
|
||||
this.websocket.onerror = this._handleListenErrorEvent.bind(this);
|
||||
this.websocket.onmessage = this._handleListenMessageEvent.bind(this);
|
||||
},
|
||||
/**
|
||||
* Opens long-poll connection.
|
||||
*
|
||||
*/
|
||||
openLongPoll: function () {
|
||||
var request = this.$['ajaxListen'];
|
||||
request.url = this.longPollUrl + '?conn_id=' + this.connectionId;
|
||||
request.generateRequest()
|
||||
},
|
||||
/**
|
||||
* Retries `connect()` call while incrementing interval between tries up to 1 minute.
|
||||
*
|
||||
*/
|
||||
retryConnection: function () {
|
||||
if (!this.shouldReconnect) {
|
||||
return;
|
||||
}
|
||||
if (this._currentBounceIv < 60000) {
|
||||
this._currentBounceIv = this._currentBounceIv + this.increaseBounceIv;
|
||||
}
|
||||
else {
|
||||
this._currentBounceIv = 60000;
|
||||
}
|
||||
setTimeout(this.connect.bind(this), this._currentBounceIv);
|
||||
},
|
||||
/**
|
||||
* Closes listening connection.
|
||||
*
|
||||
*/
|
||||
closeConnection: function () {
|
||||
var request = this.$['ajaxListen'];
|
||||
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
|
||||
this.websocket.onclose = null;
|
||||
this.websocket.onerror = null;
|
||||
this.websocket.close();
|
||||
}
|
||||
if (request.loading) {
|
||||
request.lastRequest.abort();
|
||||
}
|
||||
this.connected = false;
|
||||
},
|
||||
|
||||
_handleChannelsChange: function (event) {
|
||||
// do not fire the event if set() didn't mutate anything
|
||||
// is this a reliable way to do it?
|
||||
if (!this.isReady || event === undefined) {
|
||||
return
|
||||
}
|
||||
this.fire('channelstream-channels-changed', event)
|
||||
},
|
||||
|
||||
_handleListenOpen: function (event) {
|
||||
this.connected = true;
|
||||
this.fire('channelstream-listen-opened', event);
|
||||
this.createHeartBeats();
|
||||
},
|
||||
|
||||
createHeartBeats: function () {
|
||||
if (typeof self._heartbeat === 'undefined' && this.websocket !== null
|
||||
&& this.heartbeats) {
|
||||
self._heartbeat = setInterval(this._sendHeartBeat.bind(this), 10000);
|
||||
}
|
||||
},
|
||||
|
||||
_sendHeartBeat: function () {
|
||||
if (this.websocket.readyState === WebSocket.OPEN && this.heartbeats) {
|
||||
this.websocket.send(JSON.stringify({type: 'heartbeat'}));
|
||||
}
|
||||
},
|
||||
|
||||
_handleListenError: function (event) {
|
||||
this.connected = false;
|
||||
this.retryConnection();
|
||||
},
|
||||
_handleConnectError: function (event) {
|
||||
this.connected = false;
|
||||
this.fire('channelstream-connect-error', event.detail);
|
||||
},
|
||||
|
||||
_handleListenMessageEvent: function (event) {
|
||||
var data = null;
|
||||
// comes from iron-ajax
|
||||
if (event.detail) {
|
||||
data = JSON.parse(event.detail.response)
|
||||
// comes from websocket
|
||||
setTimeout(this.openLongPoll.bind(this), 0);
|
||||
} else {
|
||||
data = JSON.parse(event.data)
|
||||
}
|
||||
this.fire('channelstream-listen-message', data);
|
||||
|
||||
},
|
||||
|
||||
_handleListenCloseEvent: function (event) {
|
||||
this.connected = false;
|
||||
this.fire('channelstream-listen-closed', event.detail);
|
||||
this.retryConnection();
|
||||
},
|
||||
|
||||
_handleListenErrorEvent: function (event) {
|
||||
this.connected = false;
|
||||
this.fire('channelstream-listen-error', {})
|
||||
},
|
||||
|
||||
_handleConnect: function (event) {
|
||||
this.currentBounceIv = 0;
|
||||
this.connectionId = event.detail.response.conn_id;
|
||||
this.fire('channelstream-connected', event.detail.response);
|
||||
},
|
||||
|
||||
_handleDisconnect: function (event) {
|
||||
this.connected = false;
|
||||
this.fire('channelstream-disconnected', {});
|
||||
},
|
||||
|
||||
_handleMessage: function (event) {
|
||||
this.fire('channelstream-message-sent', event.detail.response);
|
||||
},
|
||||
_handleMessageError: function (event) {
|
||||
this.fire('channelstream-message-error', event.detail);
|
||||
},
|
||||
|
||||
_handleSubscribe: function (event) {
|
||||
this.fire('channelstream-subscribed', event.detail.response);
|
||||
},
|
||||
|
||||
_handleSubscribeError: function (event) {
|
||||
this.fire('channelstream-subscribe-error', event.detail);
|
||||
},
|
||||
|
||||
_handleUnsubscribe: function (event) {
|
||||
this.fire('channelstream-unsubscribed', event.detail.response);
|
||||
},
|
||||
|
||||
_handleUnsubscribeError: function (event) {
|
||||
this.fire('channelstream-unsubscribe-error', event.detail);
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<link rel="import" href="../../../../../../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../channelstream-connection/channelstream-connection.html">
|
||||
|
||||
<dom-module id="rhodecode-app">
|
||||
<template>
|
||||
<rhodecode-toast id="notifications"></rhodecode-toast>
|
||||
<channelstream-connection
|
||||
id="channelstream-connection"
|
||||
on-channelstream-listen-message="receivedMessage"
|
||||
on-channelstream-connected="handleConnected"
|
||||
on-channelstream-subscribed="handleSubscribed">
|
||||
</channelstream-connection>
|
||||
</template>
|
||||
<script src="rhodecode-app.js"></script>
|
||||
</dom-module>
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
ccLog = Logger.get('RhodeCodeApp');
|
||||
ccLog.setLevel(Logger.OFF);
|
||||
|
||||
var rhodeCodeApp = Polymer({
|
||||
is: 'rhodecode-app',
|
||||
created: function () {
|
||||
ccLog.debug('rhodeCodeApp created');
|
||||
$.Topic('/notifications').subscribe(this.handleNotifications.bind(this));
|
||||
|
||||
$.Topic('/plugins/__REGISTER__').subscribe(
|
||||
this.kickoffChannelstreamPlugin.bind(this)
|
||||
);
|
||||
|
||||
$.Topic('/connection_controller/subscribe').subscribe(
|
||||
this.subscribeToChannelTopic.bind(this));
|
||||
},
|
||||
|
||||
/** proxy to channelstream connection */
|
||||
getChannelStreamConnection: function () {
|
||||
return this.$['channelstream-connection'];
|
||||
},
|
||||
|
||||
handleNotifications: function (data) {
|
||||
this.$['notifications'].handleNotification(data);
|
||||
},
|
||||
|
||||
/** opens connection to ws server */
|
||||
kickoffChannelstreamPlugin: function (data) {
|
||||
ccLog.debug('kickoffChannelstreamPlugin');
|
||||
var channels = ['broadcast'];
|
||||
var addChannels = this.checkViewChannels();
|
||||
for (var i = 0; i < addChannels.length; i++) {
|
||||
channels.push(addChannels[i]);
|
||||
}
|
||||
var channelstreamConnection = this.$['channelstream-connection'];
|
||||
channelstreamConnection.connectUrl = CHANNELSTREAM_URLS.connect;
|
||||
channelstreamConnection.subscribeUrl = CHANNELSTREAM_URLS.subscribe;
|
||||
channelstreamConnection.websocketUrl = CHANNELSTREAM_URLS.ws + '/ws';
|
||||
channelstreamConnection.longPollUrl = CHANNELSTREAM_URLS.longpoll + '/listen';
|
||||
// some channels might already be registered by topic
|
||||
for (var i = 0; i < channels.length; i++) {
|
||||
channelstreamConnection.push('channels', channels[i]);
|
||||
}
|
||||
// append any additional channels registered in other plugins
|
||||
$.Topic('/connection_controller/subscribe').processPrepared();
|
||||
channelstreamConnection.connect();
|
||||
},
|
||||
|
||||
checkViewChannels: function () {
|
||||
var channels = []
|
||||
// subscribe to PR repo channel for PR's'
|
||||
if (templateContext.pull_request_data.pull_request_id) {
|
||||
var channelName = '/repo$' + templateContext.repo_name + '$/pr/' +
|
||||
String(templateContext.pull_request_data.pull_request_id);
|
||||
channels.push(channelName);
|
||||
}
|
||||
return channels;
|
||||
},
|
||||
|
||||
/** subscribes users from channels in channelstream */
|
||||
subscribeToChannelTopic: function (channels) {
|
||||
var channelstreamConnection = this.$['channelstream-connection'];
|
||||
var toSubscribe = channelstreamConnection.calculateSubscribe(channels);
|
||||
ccLog.debug('subscribeToChannelTopic', toSubscribe);
|
||||
if (toSubscribe.length > 0) {
|
||||
// if we are connected then subscribe
|
||||
if (channelstreamConnection.connected) {
|
||||
channelstreamConnection.subscribe(toSubscribe);
|
||||
}
|
||||
// not connected? just push channels onto the stack
|
||||
else {
|
||||
for (var i = 0; i < toSubscribe.length; i++) {
|
||||
channelstreamConnection.push('channels', toSubscribe[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** publish received messages into correct topic */
|
||||
receivedMessage: function (event) {
|
||||
for (var i = 0; i < event.detail.length; i++) {
|
||||
var message = event.detail[i];
|
||||
if (message.message.topic) {
|
||||
ccLog.debug('publishing', message.message.topic);
|
||||
$.Topic(message.message.topic).publish(message);
|
||||
}
|
||||
else if (message.type === 'presence'){
|
||||
$.Topic('/connection_controller/presence').publish(message);
|
||||
}
|
||||
else {
|
||||
ccLog.warn('unhandled message', message);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handleConnected: function (event) {
|
||||
var channelstreamConnection = this.$['channelstream-connection'];
|
||||
channelstreamConnection.set('channelsState',
|
||||
event.detail.channels_info);
|
||||
channelstreamConnection.set('userState', event.detail.state);
|
||||
channelstreamConnection.set('channels', event.detail.channels);
|
||||
this.propagageChannelsState();
|
||||
},
|
||||
handleSubscribed: function (event) {
|
||||
var channelstreamConnection = this.$['channelstream-connection'];
|
||||
var channelInfo = event.detail.channels_info;
|
||||
var channelKeys = Object.keys(event.detail.channels_info);
|
||||
for (var i = 0; i < channelKeys.length; i++) {
|
||||
var key = channelKeys[i];
|
||||
channelstreamConnection.set(['channelsState', key], channelInfo[key]);
|
||||
}
|
||||
channelstreamConnection.set('channels', event.detail.channels);
|
||||
this.propagageChannelsState();
|
||||
},
|
||||
/** propagates channel states on topics */
|
||||
propagageChannelsState: function (event) {
|
||||
var channelstreamConnection = this.$['channelstream-connection'];
|
||||
var channel_data = channelstreamConnection.channelsState;
|
||||
var channels = channelstreamConnection.channels;
|
||||
for (var i = 0; i < channels.length; i++) {
|
||||
var key = channels[i];
|
||||
$.Topic('/connection_controller/channel_update').publish(
|
||||
{channel: key, state: channel_data[key]}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -11,10 +11,6 @@ Polymer({
|
|||
observers: [
|
||||
'_changedToasts(toasts.splices)'
|
||||
],
|
||||
ready: function(){
|
||||
|
||||
},
|
||||
|
||||
_changedToasts: function(newValue, oldValue){
|
||||
this.$['p-toast'].notifyResize();
|
||||
},
|
||||
|
|
@ -27,5 +23,16 @@ Polymer({
|
|||
open: function(){
|
||||
this.$['p-toast'].open();
|
||||
},
|
||||
handleNotification: function(data){
|
||||
if (!templateContext.rhodecode_user.notification_status && !data.message.force) {
|
||||
// do not act if notifications are disabled
|
||||
return
|
||||
}
|
||||
this.push('toasts',{
|
||||
level: data.message.level,
|
||||
message: data.message.message
|
||||
});
|
||||
this.open();
|
||||
},
|
||||
_gettext: _gettext
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<!-- required for stamped out templates that might use common elements -->
|
||||
<link rel="import" href="../../../../../bower_components/iron-ajax/iron-ajax.html">
|
||||
<link rel="import" href="shared-styles.html">
|
||||
<link rel="import" href="channelstream-connection/channelstream-connection.html">
|
||||
<link rel="import" href="rhodecode-app/rhodecode-app.html">
|
||||
<link rel="import" href="rhodecode-toast/rhodecode-toast.html">
|
||||
<link rel="import" href="rhodecode-toggle/rhodecode-toggle.html">
|
||||
<link rel="import" href="rhodecode-unsafe-html/rhodecode-unsafe-html.html">
|
||||
|
|
|
|||
|
|
@ -1,208 +0,0 @@
|
|||
"use strict";
|
||||
/** leak object to top level scope **/
|
||||
var ccLog = undefined;
|
||||
// global code-mirror logger;, to enable run
|
||||
// Logger.get('ConnectionController').setLevel(Logger.DEBUG)
|
||||
ccLog = Logger.get('ConnectionController');
|
||||
ccLog.setLevel(Logger.OFF);
|
||||
|
||||
var ConnectionController;
|
||||
var connCtrlr;
|
||||
var registerViewChannels;
|
||||
|
||||
(function () {
|
||||
ConnectionController = function (webappUrl, serverUrl, urls) {
|
||||
var self = this;
|
||||
|
||||
var channels = ['broadcast'];
|
||||
this.state = {
|
||||
open: false,
|
||||
webappUrl: webappUrl,
|
||||
serverUrl: serverUrl,
|
||||
connId: null,
|
||||
socket: null,
|
||||
channels: channels,
|
||||
heartbeat: null,
|
||||
channelsInfo: {},
|
||||
urls: urls
|
||||
};
|
||||
|
||||
this.listen = function () {
|
||||
if (window.WebSocket) {
|
||||
ccLog.debug('attempting to create socket');
|
||||
var socket_url = self.state.serverUrl + "/ws?conn_id=" + self.state.connId;
|
||||
var socket_conf = {
|
||||
url: socket_url,
|
||||
handleAs: 'json',
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
};
|
||||
self.state.socket = new WebSocket(socket_conf.url);
|
||||
|
||||
self.state.socket.onopen = function (event) {
|
||||
ccLog.debug('open event', event);
|
||||
if (self.state.heartbeat === null) {
|
||||
self.state.heartbeat = setInterval(function () {
|
||||
if (self.state.socket.readyState === WebSocket.OPEN) {
|
||||
self.state.socket.send('heartbeat');
|
||||
}
|
||||
}, 10000)
|
||||
}
|
||||
};
|
||||
self.state.socket.onmessage = function (event) {
|
||||
var data = $.parseJSON(event.data);
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
if (data[i].message.topic) {
|
||||
ccLog.debug('publishing',
|
||||
data[i].message.topic, data[i]);
|
||||
$.Topic(data[i].message.topic).publish(data[i])
|
||||
}
|
||||
else {
|
||||
ccLog.warn('unhandled message', data);
|
||||
}
|
||||
}
|
||||
};
|
||||
self.state.socket.onclose = function (event) {
|
||||
ccLog.debug('closed event', event);
|
||||
setTimeout(function () {
|
||||
self.connect(true);
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
self.state.socket.onerror = function (event) {
|
||||
ccLog.debug('error event', event);
|
||||
};
|
||||
}
|
||||
else {
|
||||
ccLog.debug('attempting to create long polling connection');
|
||||
var poolUrl = self.state.serverUrl + "/listen?conn_id=" + self.state.connId;
|
||||
self.state.socket = $.ajax({
|
||||
url: poolUrl
|
||||
}).done(function (data) {
|
||||
ccLog.debug('data', data);
|
||||
var data = $.parseJSON(data);
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
if (data[i].message.topic) {
|
||||
ccLog.info('publishing',
|
||||
data[i].message.topic, data[i]);
|
||||
$.Topic(data[i].message.topic).publish(data[i])
|
||||
}
|
||||
else {
|
||||
ccLog.warn('unhandled message', data);
|
||||
}
|
||||
}
|
||||
self.listen();
|
||||
}).fail(function () {
|
||||
ccLog.debug('longpoll error');
|
||||
setTimeout(function () {
|
||||
self.connect(true);
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
this.connect = function (create_new_socket) {
|
||||
var connReq = {'channels': self.state.channels};
|
||||
ccLog.debug('try obtaining connection info', connReq);
|
||||
$.ajax({
|
||||
url: self.state.urls.connect,
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify(connReq),
|
||||
dataType: "json"
|
||||
}).done(function (data) {
|
||||
ccLog.debug('Got connection:', data.conn_id);
|
||||
self.state.channels = data.channels;
|
||||
self.state.channelsInfo = data.channels_info;
|
||||
self.state.connId = data.conn_id;
|
||||
if (create_new_socket) {
|
||||
self.listen();
|
||||
}
|
||||
self.update();
|
||||
}).fail(function () {
|
||||
setTimeout(function () {
|
||||
self.connect(create_new_socket);
|
||||
}, 5000);
|
||||
});
|
||||
self.update();
|
||||
};
|
||||
|
||||
this.subscribeToChannels = function (channels) {
|
||||
var new_channels = [];
|
||||
for (var i = 0; i < channels.length; i++) {
|
||||
var channel = channels[i];
|
||||
if (self.state.channels.indexOf(channel)) {
|
||||
self.state.channels.push(channel);
|
||||
new_channels.push(channel)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* only execute the request if socket is present because subscribe
|
||||
* can actually add channels before initial app connection
|
||||
**/
|
||||
if (new_channels && self.state.socket !== null) {
|
||||
var connReq = {
|
||||
'channels': self.state.channels,
|
||||
'conn_id': self.state.connId
|
||||
};
|
||||
$.ajax({
|
||||
url: self.state.urls.subscribe,
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify(connReq),
|
||||
dataType: "json"
|
||||
}).done(function (data) {
|
||||
self.state.channels = data.channels;
|
||||
self.state.channelsInfo = data.channels_info;
|
||||
self.update();
|
||||
});
|
||||
}
|
||||
self.update();
|
||||
};
|
||||
|
||||
this.update = function () {
|
||||
for (var key in this.state.channelsInfo) {
|
||||
if (this.state.channelsInfo.hasOwnProperty(key)) {
|
||||
// update channels with latest info
|
||||
$.Topic('/connection_controller/channel_update').publish(
|
||||
{channel: key, state: this.state.channelsInfo[key]});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.run = function () {
|
||||
this.connect(true);
|
||||
};
|
||||
|
||||
$.Topic('/connection_controller/subscribe').subscribe(
|
||||
self.subscribeToChannels);
|
||||
};
|
||||
|
||||
$.Topic('/plugins/__REGISTER__').subscribe(function (data) {
|
||||
if (window.CHANNELSTREAM_SETTINGS && window.CHANNELSTREAM_SETTINGS.enabled) {
|
||||
connCtrlr = new ConnectionController(
|
||||
CHANNELSTREAM_SETTINGS.webapp_location,
|
||||
CHANNELSTREAM_SETTINGS.ws_location,
|
||||
CHANNELSTREAM_URLS
|
||||
);
|
||||
registerViewChannels();
|
||||
|
||||
$(document).ready(function () {
|
||||
connCtrlr.run();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
registerViewChannels = function (){
|
||||
// subscribe to PR repo channel for PR's'
|
||||
if (templateContext.pull_request_data.pull_request_id) {
|
||||
var channelName = '/repo$' + templateContext.repo_name + '$/pr/' +
|
||||
String(templateContext.pull_request_data.pull_request_id);
|
||||
connCtrlr.state.channels.push(channelName);
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
|
||||
function notifySystem(data) {
|
||||
var notification = new Notification(data.message.level + ': ' + data.message.message);
|
||||
};
|
||||
|
||||
function notifyToaster(data){
|
||||
var notifications = document.getElementById('notifications');
|
||||
notifications.push('toasts',
|
||||
{ level: data.message.level,
|
||||
message: data.message.message
|
||||
});
|
||||
notifications.open();
|
||||
}
|
||||
|
||||
function handleNotifications(data) {
|
||||
if (!templateContext.rhodecode_user.notification_status && !data.message.force) {
|
||||
// do not act if notifications are disabled
|
||||
return
|
||||
}
|
||||
// use only js notifications for now
|
||||
var onlyJS = true;
|
||||
if (!("Notification" in window) || onlyJS) {
|
||||
// use legacy notificartion
|
||||
notifyToaster(data);
|
||||
}
|
||||
else {
|
||||
// Let's check whether notification permissions have already been granted
|
||||
if (Notification.permission === "granted") {
|
||||
notifySystem(data);
|
||||
}
|
||||
// Otherwise, we need to ask the user for permission
|
||||
else if (Notification.permission !== 'denied') {
|
||||
Notification.requestPermission(function (permission) {
|
||||
if (permission === "granted") {
|
||||
notifySystem(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
else{
|
||||
notifyToaster(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$.Topic('/notifications').subscribe(handleNotifications);
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
/plugins/__REGISTER__ - launched after the onDomReady() code from rhodecode.js is executed
|
||||
/ui/plugins/code/anchor_focus - launched when rc starts to scroll on load to anchor on PR/Codeview
|
||||
/ui/plugins/code/comment_form_built - launched when injectInlineForm() is executed and the form object is created
|
||||
/notifications - shows new event notifications
|
||||
/notifications - shows new event notifications
|
||||
/connection_controller/subscribe - subscribes user to new channels
|
||||
/connection_controller/presence - receives presence change messages
|
||||
/connection_controller/channel_update - receives channel states
|
||||
|
|
|
|||
|
|
@ -146,7 +146,9 @@ c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default
|
|||
<%def name="head_extra()"></%def>
|
||||
${self.head_extra()}
|
||||
<script>
|
||||
$.Topic('/plugins/__REGISTER__').prepareOrPublish({});
|
||||
window.addEventListener("load", function(event) {
|
||||
$.Topic('/plugins/__REGISTER__').prepareOrPublish({});
|
||||
});
|
||||
</script>
|
||||
## extra stuff
|
||||
%if c.pre_code:
|
||||
|
|
@ -174,6 +176,6 @@ c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default
|
|||
%if c.post_code:
|
||||
${c.post_code|n}
|
||||
%endif
|
||||
<rhodecode-toast id="notifications"></rhodecode-toast>
|
||||
<rhodecode-app></rhodecode-app>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue