frontend: introduce rhodecode-app for more complex cross element wiring

This commit is contained in:
Marcin Lulek 2016-09-13 12:25:42 +02:00
parent 6908a7915d
commit 73ba40da93
12 changed files with 774 additions and 265 deletions

View file

@ -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"
],

View file

@ -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)

View file

@ -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>

View file

@ -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);
}
});

View file

@ -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>

View file

@ -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]}
);
}
}
});

View file

@ -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
});

View file

@ -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">

View file

@ -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);
}
}
})();

View file

@ -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);

View file

@ -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

View file

@ -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>