Skip to content

Commit

Permalink
Add support for Web notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
omarroth committed Jun 1, 2019
1 parent b3788bc commit 0338fd4
Show file tree
Hide file tree
Showing 22 changed files with 456 additions and 30 deletions.
4 changes: 2 additions & 2 deletions assets/js/embed.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function get_playlist(plid, timeouts = 0) {
function get_playlist(plid, timeouts) {
if (timeouts > 10) {
console.log('Failed to pull playlist');
return;
Expand Down Expand Up @@ -53,7 +53,7 @@ function get_playlist(plid, timeouts = 0) {

xhr.ontimeout = function () {
console.log('Pulling playlist timed out.');
get_playlist(plid, timeouts + 1);
get_playlist(plid, timeouts++);
}
}

Expand Down
139 changes: 139 additions & 0 deletions assets/js/notifications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
var notifications, delivered;

function get_subscriptions(callback, failures) {
if (failures >= 10) {
return
}

var xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.timeout = 20000;
xhr.open('GET', '/api/v1/auth/subscriptions', true);
xhr.send(null);

xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
subscriptions = xhr.response;
callback(subscriptions);
} else {
console.log('Pulling subscriptions failed... ' + failures + '/10');
get_subscriptions(callback, failures++)
}
}
}

xhr.ontimeout = function () {
console.log('Pulling subscriptions failed... ' + failures + '/10');
get_subscriptions(callback, failures++);
}
}

function create_notification_stream(subscriptions) {
notifications = new SSE(
'/api/v1/auth/notifications?fields=videoId,title,author,authorId,publishedText,published,authorThumbnails,liveNow', {
withCredentials: true,
payload: 'topics=' + subscriptions.map(function (subscription) { return subscription.authorId }).join(','),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
delivered = [];

var start_time = Math.round(new Date() / 1000);

notifications.onmessage = function (event) {
if (!event.id) {
return
}

var notification = JSON.parse(event.data);
console.log('Got notification:', notification);

if (start_time < notification.published && !delivered.includes(notification.videoId)) {
if (Notification.permission === 'granted') {
var system_notification =
new Notification((notification.liveNow ? notification_data.live_now_text : notification_data.upload_text).replace('`x`', notification.author), {
body: notification.title,
icon: '/ggpht' + new URL(notification.authorThumbnails[2].url).pathname,
img: '/ggpht' + new URL(notification.authorThumbnails[4].url).pathname,
tag: notification.videoId
});

system_notification.onclick = function (event) {
window.open('/watch?v=' + event.currentTarget.tag, '_blank');
}
}

delivered.push(notification.videoId);
localStorage.setItem('notification_count', parseInt(localStorage.getItem('notification_count') || '0') + 1);
var notification_ticker = document.getElementById('notification_ticker');

if (parseInt(localStorage.getItem('notification_count')) > 0) {
notification_ticker.innerHTML =
'<span id="notification_count">' + localStorage.getItem('notification_count') + '</span> <i class="icon ion-ios-notifications"></i>';
} else {
notification_ticker.innerHTML =
'<i class="icon ion-ios-notifications-outline"></i>';
}
}
}

notifications.onerror = function (event) {
console.log('Something went wrong with notifications, trying to reconnect...');
notifications.close();
get_subscriptions(create_notification_stream);
}

notifications.ontimeout = function (event) {
console.log('Something went wrong with notifications, trying to reconnect...');
notifications.close();
get_subscriptions(create_notification_stream);
}

notifications.stream();
}

window.addEventListener('storage', function (e) {
if (e.key === 'stream' && !e.newValue) {
if (notifications) {
localStorage.setItem('stream', true);
} else {
setTimeout(function () {
if (!localStorage.getItem('stream')) {
get_subscriptions(create_notification_stream);
localStorage.setItem('stream', true);
}
}, Math.random() * 1000 + 10);
}
} else if (e.key === 'notification_count') {
var notification_ticker = document.getElementById('notification_ticker');

if (parseInt(e.newValue) > 0) {
notification_ticker.innerHTML =
'<span id="notification_count">' + e.newValue + '</span> <i class="icon ion-ios-notifications"></i>';
} else {
notification_ticker.innerHTML =
'<i class="icon ion-ios-notifications-outline"></i>';
}
}
});

window.addEventListener('load', function (e) {
localStorage.setItem('notification_count', document.getElementById('notification_count') ? document.getElementById('notification_count').innerText : '0');

if (localStorage.getItem('stream')) {
localStorage.removeItem('stream');
} else {
setTimeout(function () {
if (!localStorage.getItem('stream')) {
get_subscriptions(create_notification_stream);
localStorage.setItem('stream', true);
}
}, Math.random() * 1000 + 10);
}
});

window.addEventListener('unload', function (e) {
if (notifications) {
localStorage.removeItem('stream');
}
});
200 changes: 200 additions & 0 deletions assets/js/sse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/**
* Copyright (C) 2016 Maxime Petazzoni <maxime.petazzoni@bulix.org>.
* All rights reserved.
*/

var SSE = function (url, options) {
if (!(this instanceof SSE)) {
return new SSE(url, options);
}

this.INITIALIZING = -1;
this.CONNECTING = 0;
this.OPEN = 1;
this.CLOSED = 2;

this.url = url;

options = options || {};
this.headers = options.headers || {};
this.payload = options.payload !== undefined ? options.payload : '';
this.method = options.method || (this.payload && 'POST' || 'GET');

this.FIELD_SEPARATOR = ':';
this.listeners = {};

this.xhr = null;
this.readyState = this.INITIALIZING;
this.progress = 0;
this.chunk = '';

this.addEventListener = function(type, listener) {
if (this.listeners[type] === undefined) {
this.listeners[type] = [];
}

if (this.listeners[type].indexOf(listener) === -1) {
this.listeners[type].push(listener);
}
};

this.removeEventListener = function(type, listener) {
if (this.listeners[type] === undefined) {
return;
}

var filtered = [];
this.listeners[type].forEach(function(element) {
if (element !== listener) {
filtered.push(element);
}
});
if (filtered.length === 0) {
delete this.listeners[type];
} else {
this.listeners[type] = filtered;
}
};

this.dispatchEvent = function(e) {
if (!e) {
return true;
}

e.source = this;

var onHandler = 'on' + e.type;
if (this.hasOwnProperty(onHandler)) {
this[onHandler].call(this, e);
if (e.defaultPrevented) {
return false;
}
}

if (this.listeners[e.type]) {
return this.listeners[e.type].every(function(callback) {
callback(e);
return !e.defaultPrevented;
});
}

return true;
};

this._setReadyState = function (state) {
var event = new CustomEvent('readystatechange');
event.readyState = state;
this.readyState = state;
this.dispatchEvent(event);
};

this._onStreamFailure = function(e) {
this.dispatchEvent(new CustomEvent('error'));
this.close();
}

this._onStreamProgress = function(e) {
if (this.xhr.status !== 200) {
this._onStreamFailure(e);
return;
}

if (this.readyState == this.CONNECTING) {
this.dispatchEvent(new CustomEvent('open'));
this._setReadyState(this.OPEN);
}

var data = this.xhr.responseText.substring(this.progress);
this.progress += data.length;
data.split(/(\r\n|\r|\n){2}/g).forEach(function(part) {
if (part.trim().length === 0) {
this.dispatchEvent(this._parseEventChunk(this.chunk.trim()));
this.chunk = '';
} else {
this.chunk += part;
}
}.bind(this));
};

this._onStreamLoaded = function(e) {
this._onStreamProgress(e);

// Parse the last chunk.
this.dispatchEvent(this._parseEventChunk(this.chunk));
this.chunk = '';
};

/**
* Parse a received SSE event chunk into a constructed event object.
*/
this._parseEventChunk = function(chunk) {
if (!chunk || chunk.length === 0) {
return null;
}

var e = {'id': null, 'retry': null, 'data': '', 'event': 'message'};
chunk.split(/\n|\r\n|\r/).forEach(function(line) {
line = line.trimRight();
var index = line.indexOf(this.FIELD_SEPARATOR);
if (index <= 0) {
// Line was either empty, or started with a separator and is a comment.
// Either way, ignore.
return;
}

var field = line.substring(0, index);
if (!(field in e)) {
return;
}

var value = line.substring(index + 1).trimLeft();
if (field === 'data') {
e[field] += value;
} else {
e[field] = value;
}
}.bind(this));

var event = new CustomEvent(e.event);
event.data = e.data;
event.id = e.id;
return event;
};

this._checkStreamClosed = function() {
if (this.xhr.readyState === XMLHttpRequest.DONE) {
this._setReadyState(this.CLOSED);
}
};

this.stream = function() {
this._setReadyState(this.CONNECTING);

this.xhr = new XMLHttpRequest();
this.xhr.addEventListener('progress', this._onStreamProgress.bind(this));
this.xhr.addEventListener('load', this._onStreamLoaded.bind(this));
this.xhr.addEventListener('readystatechange', this._checkStreamClosed.bind(this));
this.xhr.addEventListener('error', this._onStreamFailure.bind(this));
this.xhr.addEventListener('abort', this._onStreamFailure.bind(this));
this.xhr.open(this.method, this.url);
for (var header in this.headers) {
this.xhr.setRequestHeader(header, this.headers[header]);
}
this.xhr.send(this.payload);
};

this.close = function() {
if (this.readyState === this.CLOSED) {
return;
}

this.xhr.abort();
this.xhr = null;
this._setReadyState(this.CLOSED);
};
};

// Export our SSE module for npm.js
if (typeof exports !== 'undefined') {
exports.SSE = SSE;
}
12 changes: 6 additions & 6 deletions assets/js/subscribe_widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ if (subscribe_button.getAttribute('data-type') === 'subscribe') {
subscribe_button.onclick = unsubscribe;
}

function subscribe(timeouts = 0) {
if (timeouts > 10) {
function subscribe(timeouts) {
if (timeouts >= 10) {
console.log('Failed to subscribe.');
return;
}
Expand Down Expand Up @@ -37,12 +37,12 @@ function subscribe(timeouts = 0) {

xhr.ontimeout = function () {
console.log('Subscribing timed out.');
subscribe(timeouts + 1);
subscribe(timeouts++);
}
}

function unsubscribe(timeouts = 0) {
if (timeouts > 10) {
function unsubscribe(timeouts) {
if (timeouts >= 10) {
console.log('Failed to subscribe');
return;
}
Expand Down Expand Up @@ -71,6 +71,6 @@ function unsubscribe(timeouts = 0) {

xhr.ontimeout = function () {
console.log('Unsubscribing timed out.');
unsubscribe(timeouts + 1);
unsubscribe(timeouts++);
}
}
Loading

0 comments on commit 0338fd4

Please sign in to comment.