summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOmar Roth <omarroth@protonmail.com>2019-05-05 07:46:01 -0500
committerOmar Roth <omarroth@protonmail.com>2019-06-01 16:09:17 -0500
commit0338fd42e15ee9803068e6d6eeb04d78b94f321c (patch)
tree44794e8e271b25105fbd79d60643f229940ce653
parentb3788bc1431aea47b7a9ffb325984f4a58c21125 (diff)
downloadinvidious-0338fd42e15ee9803068e6d6eeb04d78b94f321c.tar.gz
invidious-0338fd42e15ee9803068e6d6eeb04d78b94f321c.tar.bz2
invidious-0338fd42e15ee9803068e6d6eeb04d78b94f321c.zip
Add support for Web notifications
-rw-r--r--assets/js/embed.js4
-rw-r--r--assets/js/notifications.js139
-rw-r--r--assets/js/sse.js200
-rw-r--r--assets/js/subscribe_widget.js12
-rw-r--r--locales/ar.json3
-rw-r--r--locales/de.json3
-rw-r--r--locales/el.json3
-rw-r--r--locales/en-US.json3
-rw-r--r--locales/eo.json3
-rw-r--r--locales/es.json5
-rw-r--r--locales/eu.json3
-rw-r--r--locales/fr.json3
-rw-r--r--locales/it.json3
-rw-r--r--locales/nb_NO.json3
-rw-r--r--locales/nl.json5
-rw-r--r--locales/pl.json3
-rw-r--r--locales/ru.json3
-rw-r--r--locales/uk.json3
-rw-r--r--src/invidious/views/embed.ecr36
-rw-r--r--src/invidious/views/licenses.ecr28
-rw-r--r--src/invidious/views/preferences.ecr7
-rw-r--r--src/invidious/views/template.ecr14
22 files changed, 456 insertions, 30 deletions
diff --git a/assets/js/embed.js b/assets/js/embed.js
index cbf21a58..12530fbe 100644
--- a/assets/js/embed.js
+++ b/assets/js/embed.js
@@ -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++);
}
}
diff --git a/assets/js/notifications.js b/assets/js/notifications.js
new file mode 100644
index 00000000..5c9847de
--- /dev/null
+++ b/assets/js/notifications.js
@@ -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');
+ }
+});
diff --git a/assets/js/sse.js b/assets/js/sse.js
new file mode 100644
index 00000000..3601b5af
--- /dev/null
+++ b/assets/js/sse.js
@@ -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;
+}
diff --git a/assets/js/subscribe_widget.js b/assets/js/subscribe_widget.js
index 25c5f2a6..f875d505 100644
--- a/assets/js/subscribe_widget.js
+++ b/assets/js/subscribe_widget.js
@@ -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++);
}
}
diff --git a/locales/ar.json b/locales/ar.json
index 9e077242..9619043d 100644
--- a/locales/ar.json
+++ b/locales/ar.json
@@ -85,6 +85,9 @@
"Only show latest unwatched video from channel: ": "فقط اظهر اخر فيديو لم يتم رؤيتة من القناة: ",
"Only show unwatched: ": "فقط اظهر الذى لم يتم رؤيتة: ",
"Only show notifications (if there are any): ": "إظهار الإشعارات فقط (إذا كان هناك أي): ",
+ "Enable web notifications": "",
+ "`x` uploaded a video": "",
+ "`x` is live": "",
"Data preferences": "إعدادات التفضيلات",
"Clear watch history": "حذف سجل المشاهدة",
"Import/export data": "إضافة\\إستخراج البيانات",
diff --git a/locales/de.json b/locales/de.json
index 4e243c4e..d1b2601e 100644
--- a/locales/de.json
+++ b/locales/de.json
@@ -85,6 +85,9 @@
"Only show latest unwatched video from channel: ": "Nur neueste ungesehene Videos des Kanals anzeigen: ",
"Only show unwatched: ": "Nur ungesehene anzeigen: ",
"Only show notifications (if there are any): ": "Nur Benachrichtigungen anzeigen (wenn es welche gibt): ",
+ "Enable web notifications": "",
+ "`x` uploaded a video": "",
+ "`x` is live": "",
"Data preferences": "Dateneinstellungen",
"Clear watch history": "Verlauf löschen",
"Import/export data": "Daten im- exportieren",
diff --git a/locales/el.json b/locales/el.json
index 7a12d2df..54d514cb 100644
--- a/locales/el.json
+++ b/locales/el.json
@@ -91,6 +91,9 @@
"Only show latest unwatched video from channel: ": "Προβολή μόνο του τελευταίου μη-προβεβλημένου βίντεο του καναλιού: ",
"Only show unwatched: ": "Προβολή μόνο μη-προβεβλημένων: ",
"Only show notifications (if there are any): ": "Προβολή μόνο ειδοποιήσεων (αν υπάρχουν): ",
+ "Enable web notifications": "",
+ "`x` uploaded a video": "",
+ "`x` is live": "",
"Data preferences": "Προτιμήσεις δεδομένων",
"Clear watch history": "Εκκαθάριση ιστορικού προβολής",
"Import/export data": "Εισαγωγή/εξαγωγή δεδομένων",
diff --git a/locales/en-US.json b/locales/en-US.json
index 5f6245f5..1ca2b970 100644
--- a/locales/en-US.json
+++ b/locales/en-US.json
@@ -91,6 +91,9 @@
"Only show latest unwatched video from channel: ": "Only show latest unwatched video from channel: ",
"Only show unwatched: ": "Only show unwatched: ",
"Only show notifications (if there are any): ": "Only show notifications (if there are any): ",
+ "Enable web notifications": "Enable web notifications",
+ "`x` uploaded a video": "`x` uploaded a video",
+ "`x` is live": "`x` is live",
"Data preferences": "Data preferences",
"Clear watch history": "Clear watch history",
"Import/export data": "Import/export data",
diff --git a/locales/eo.json b/locales/eo.json
index 3f06c790..f14ef466 100644
--- a/locales/eo.json
+++ b/locales/eo.json
@@ -85,6 +85,9 @@
"Only show latest unwatched video from channel: ": "Nur montri pli novan malviditan videon el kanalo: ",
"Only show unwatched: ": "Nur montri malviditajn: ",
"Only show notifications (if there are any): ": "Nur montri sciigojn (se estas): ",
+ "Enable web notifications": "",
+ "`x` uploaded a video": "",
+ "`x` is live": "",
"Data preferences": "Datumagordoj",
"Clear watch history": "Forigi vidohistorion",
"Import/export data": "Importi/Eksporti datumojn",
diff --git a/locales/es.json b/locales/es.json
index 2f6d8560..e0fac8a2 100644
--- a/locales/es.json
+++ b/locales/es.json
@@ -85,6 +85,9 @@
"Only show latest unwatched video from channel: ": "Mostrar solo el último vídeo sin ver del canal: ",
"Only show unwatched: ": "Mostrar solo los no vistos: ",
"Only show notifications (if there are any): ": "Mostrar solo notificaciones (si hay alguna): ",
+ "Enable web notifications": "",
+ "`x` uploaded a video": "",
+ "`x` is live": "",
"Data preferences": "Preferencias de los datos",
"Clear watch history": "Borrar el historial de reproducción",
"Import/export data": "Importar/Exportar datos",
@@ -312,4 +315,4 @@
"Videos": "Vídeos",
"Playlists": "Listas de reproducción",
"Current version: ": "Versión actual: "
-}
+} \ No newline at end of file
diff --git a/locales/eu.json b/locales/eu.json
index aae43603..60fa6f6d 100644
--- a/locales/eu.json
+++ b/locales/eu.json
@@ -85,6 +85,9 @@
"Only show latest unwatched video from channel: ": "",
"Only show unwatched: ": "",
"Only show notifications (if there are any): ": "",
+ "Enable web notifications": "",
+ "`x` uploaded a video": "",
+ "`x` is live": "",
"Data preferences": "",
"Clear watch history": "",
"Import/export data": "",
diff --git a/locales/fr.json b/locales/fr.json
index 9f042480..e2d586ae 100644
--- a/locales/fr.json
+++ b/locales/fr.json
@@ -85,6 +85,9 @@
"Only show latest unwatched video from channel: ": "Afficher uniquement la dernière vidéo de la chaîne non regardée : ",
"Only show unwatched: ": "Afficher uniquement les vidéos non regardées : ",
"Only show notifications (if there are any): ": "Afficher uniquement les notifications (s'il y en a) : ",
+ "Enable web notifications": "",
+ "`x` uploaded a video": "",
+ "`x` is live": "",
"Data preferences": "Préférences liées aux données",
"Clear watch history": "Supprimer l'historique des vidéos regardées",
"Import/export data": "Importer/exporter les données",
diff --git a/locales/it.json b/locales/it.json
index 10527f9f..ce7800c3 100644
--- a/locales/it.json
+++ b/locales/it.json
@@ -85,6 +85,9 @@
"Only show latest unwatched video from channel: ": "Mostra solo il video più recente non guardato del canale: ",
"Only show unwatched: ": "Mostra solo i video non guardati: ",
"Only show notifications (if there are any): ": "Mostra solo le notifiche (se presenti): ",
+ "Enable web notifications": "",
+ "`x` uploaded a video": "",
+ "`x` is live": "",
"Data preferences": "Preferenze dati",
"Clear watch history": "Cancella la cronologia dei video guardati",
"Import/export data": "Importazione/esportazione dati",
diff --git a/locales/nb_NO.json b/locales/nb_NO.json
index 63492245..83f97570 100644
--- a/locales/nb_NO.json
+++ b/locales/nb_NO.json
@@ -85,6 +85,9 @@
"Only show latest unwatched video from channel: ": "Kun vis siste usette video fra kanal: ",
"Only show unwatched: ": "Kun vis usette: ",
"Only show notifications (if there are any): ": "Kun vis merknader (hvis det er noen): ",
+ "Enable web notifications": "",
+ "`x` uploaded a video": "",
+ "`x` is live": "",
"Data preferences": "Datainnstillinger",
"Clear watch history": "Tøm visningshistorikk",
"Import/export data": "Importer/eksporter data",
diff --git a/locales/nl.json b/locales/nl.json
index 5da8548a..50fe85d8 100644
--- a/locales/nl.json
+++ b/locales/nl.json
@@ -85,6 +85,9 @@
"Only show latest unwatched video from channel: ": "Alleen nieuwste niet-bekeken video van kanaal tonen: ",
"Only show unwatched: ": "Alleen niet-bekeken videos tonen: ",
"Only show notifications (if there are any): ": "Alleen meldingen tonen (als die er zijn): ",
+ "Enable web notifications": "",
+ "`x` uploaded a video": "",
+ "`x` is live": "",
"Data preferences": "Gegevensinstellingen",
"Clear watch history": "Kijkgeschiedenis wissen",
"Import/export data": "Gegevens im-/exporteren",
@@ -312,4 +315,4 @@
"Videos": "Video's",
"Playlists": "Afspeellijsten",
"Current version: ": "Huidige versie: "
-}
+} \ No newline at end of file
diff --git a/locales/pl.json b/locales/pl.json
index 621bdd76..fa4ec965 100644
--- a/locales/pl.json
+++ b/locales/pl.json
@@ -85,6 +85,9 @@
"Only show latest unwatched video from channel: ": "Pokazuj tylko najnowszy nie obejrzany film z kanału: ",
"Only show unwatched: ": "Pokazuj tylko nie obejrzane: ",
"Only show notifications (if there are any): ": "Pokazuj tylko powiadomienia (jeśli są): ",
+ "Enable web notifications": "",
+ "`x` uploaded a video": "",
+ "`x` is live": "",
"Data preferences": "Preferencje danych",
"Clear watch history": "Wyczyść historię",
"Import/export data": "Import/Eksport danych",
diff --git a/locales/ru.json b/locales/ru.json
index f9c56204..e603b98f 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -85,6 +85,9 @@
"Only show latest unwatched video from channel: ": "Показывать только непросмотренные видео с каналов: ",
"Only show unwatched: ": "Показывать только непросмотренные видео: ",
"Only show notifications (if there are any): ": "Показывать только оповещения, если они есть: ",
+ "Enable web notifications": "",
+ "`x` uploaded a video": "",
+ "`x` is live": "",
"Data preferences": "Настройки данных",
"Clear watch history": "Очистить историю просмотров",
"Import/export data": "Импорт/Экспорт данных",
diff --git a/locales/uk.json b/locales/uk.json
index e666e280..319f22d7 100644
--- a/locales/uk.json
+++ b/locales/uk.json
@@ -85,6 +85,9 @@
"Only show latest unwatched video from channel: ": "Показувати тільки непереглянуті відео з каналів: ",
"Only show unwatched: ": "Показувати тільки непереглянуті відео: ",
"Only show notifications (if there are any): ": "Показувати лише сповіщення, якщо вони є: ",
+ "Enable web notifications": "",
+ "`x` uploaded a video": "",
+ "`x` is live": "",
"Data preferences": "Налаштування даних",
"Clear watch history": "Очистити історію переглядів",
"Import/export data": "Імпорт і експорт даних",
diff --git a/src/invidious/views/embed.ecr b/src/invidious/views/embed.ecr
index 32abd626..b6307b9c 100644
--- a/src/invidious/views/embed.ecr
+++ b/src/invidious/views/embed.ecr
@@ -2,24 +2,24 @@
<html lang="<%= env.get("preferences").as(Preferences).locale %>">
<head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <meta name="thumbnail" content="<%= thumbnail %>">
- <%= rendered "components/player_sources" %>
- <link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>">
- <title><%= HTML.escape(video.title) %> - Invidious</title>
- <style>
- #player {
- position: fixed;
- right: 0;
- bottom: 0;
- min-width: 100%;
- min-height: 100%;
- width: auto;
- height: auto;
- z-index: -100;
- }
- </style>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta name="thumbnail" content="<%= thumbnail %>">
+ <%= rendered "components/player_sources" %>
+ <link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>">
+ <title><%= HTML.escape(video.title) %> - Invidious</title>
+ <style>
+ #player {
+ position: fixed;
+ right: 0;
+ bottom: 0;
+ min-width: 100%;
+ min-height: 100%;
+ width: auto;
+ height: auto;
+ z-index: -100;
+ }
+ </style>
</head>
<body>
diff --git a/src/invidious/views/licenses.ecr b/src/invidious/views/licenses.ecr
index 6b10fb99..0f92d86e 100644
--- a/src/invidious/views/licenses.ecr
+++ b/src/invidious/views/licenses.ecr
@@ -25,6 +25,20 @@
<tr>
<td>
+ <a href="/js/notifications.js?v=<%= ASSET_COMMIT %>">notifications.js</a>
+ </td>
+
+ <td>
+ <a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0</a>
+ </td>
+
+ <td>
+ <a href="/js/notifications.js?v=<%= ASSET_COMMIT %>"><%= translate(locale, "source") %></a>
+ </td>
+ </tr>
+
+ <tr>
+ <td>
<a href="/js/player.js?v=<%= ASSET_COMMIT %>">player.js</a>
</td>
@@ -53,6 +67,20 @@
<tr>
<td>
+ <a href="/js/sse.js?v=<%= ASSET_COMMIT %>">sse.js</a>
+ </td>
+
+ <td>
+ <a href="http://www.jclark.com/xml/copying.txt">Expat</a>
+ </td>
+
+ <td>
+ <a href="https://github.com/mpetazzoni/sse.js"><%= translate(locale, "source") %></a>
+ </td>
+ </tr>
+
+ <tr>
+ <td>
<a href="/js/subscribe_widget.js?v=<%= ASSET_COMMIT %>">subscribe_widget.js</a>
</td>
diff --git a/src/invidious/views/preferences.ecr b/src/invidious/views/preferences.ecr
index d0747b59..e9d2d84c 100644
--- a/src/invidious/views/preferences.ecr
+++ b/src/invidious/views/preferences.ecr
@@ -165,6 +165,13 @@ function update_value(element) {
<label for="notifications_only"><%= translate(locale, "Only show notifications (if there are any): ") %></label>
<input name="notifications_only" id="notifications_only" type="checkbox" <% if preferences.notifications_only %>checked<% end %>>
</div>
+
+ <% # Conditions for supporting web notifications %>
+ <% if CONFIG.use_pubsub_feeds && (Kemal.config.ssl || config.https_only) %>
+ <div class="pure-control-group">
+ <a href="#" onclick="Notification.requestPermission()"><%= translate(locale, "Enable web notifications") %></a>
+ </div>
+ <% end %>
<% end %>
<% if env.get?("user") && config.admins.includes? env.get?("user").as(User).email %>
diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr
index 6b6f74fa..0d8c9924 100644
--- a/src/invidious/views/template.ecr
+++ b/src/invidious/views/template.ecr
@@ -51,10 +51,10 @@
</a>
</div>
<div class="pure-u-1-4">
- <a title="<%= translate(locale, "Subscriptions") %>" href="/feed/subscriptions" class="pure-menu-heading">
+ <a id="notification_ticker" title="<%= translate(locale, "Subscriptions") %>" href="/feed/subscriptions" class="pure-menu-heading">
<% notification_count = env.get("user").as(User).notifications.size %>
<% if notification_count > 0 %>
- <%= notification_count %> <i class="icon ion-ios-notifications"></i>
+ <span id="notification_count"><%= notification_count %></span> <i class="icon ion-ios-notifications"></i>
<% else %>
<i class="icon ion-ios-notifications-outline"></i>
<% end %>
@@ -151,6 +151,16 @@
<div class="pure-u-1 pure-u-md-2-24"></div>
</div>
<script src="/js/themes.js?v=<%= ASSET_COMMIT %>"></script>
+ <% if env.get? "user" %>
+ <script src="/js/sse.js?v=<%= ASSET_COMMIT %>"></script>
+ <script>
+ var notification_data = {
+ upload_text: '<%= HTML.escape(translate(locale, "`x` uploaded a video")) %>',
+ live_upload_text: '<%= HTML.escape(translate(locale, "`x` is live")) %>',
+ }
+ </script>
+ <script src="/js/notifications.js?v=<%= ASSET_COMMIT %>"></script>
+ <% end %>
</body>
</html>