Add support for Web notifications

This commit is contained in:
Omar Roth 2019-05-05 07:46:01 -05:00
parent b3788bc143
commit 0338fd42e1
No known key found for this signature in database
GPG key ID: B8254FB7EC3D37F2
22 changed files with 456 additions and 30 deletions

View file

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

139
assets/js/notifications.js Normal file
View file

@ -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
assets/js/sse.js Normal file
View file

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

View file

@ -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;
}
@ -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;
}
@ -71,6 +71,6 @@ function unsubscribe(timeouts = 0) {
xhr.ontimeout = function () {
console.log('Unsubscribing timed out.');
unsubscribe(timeouts + 1);
unsubscribe(timeouts++);
}
}