diff options
| -rw-r--r-- | assets/js/embed.js | 4 | ||||
| -rw-r--r-- | assets/js/notifications.js | 139 | ||||
| -rw-r--r-- | assets/js/sse.js | 200 | ||||
| -rw-r--r-- | assets/js/subscribe_widget.js | 12 | ||||
| -rw-r--r-- | locales/ar.json | 3 | ||||
| -rw-r--r-- | locales/de.json | 3 | ||||
| -rw-r--r-- | locales/el.json | 3 | ||||
| -rw-r--r-- | locales/en-US.json | 3 | ||||
| -rw-r--r-- | locales/eo.json | 3 | ||||
| -rw-r--r-- | locales/es.json | 5 | ||||
| -rw-r--r-- | locales/eu.json | 3 | ||||
| -rw-r--r-- | locales/fr.json | 3 | ||||
| -rw-r--r-- | locales/it.json | 3 | ||||
| -rw-r--r-- | locales/nb_NO.json | 3 | ||||
| -rw-r--r-- | locales/nl.json | 5 | ||||
| -rw-r--r-- | locales/pl.json | 3 | ||||
| -rw-r--r-- | locales/ru.json | 3 | ||||
| -rw-r--r-- | locales/uk.json | 3 | ||||
| -rw-r--r-- | src/invidious/views/embed.ecr | 36 | ||||
| -rw-r--r-- | src/invidious/views/licenses.ecr | 28 | ||||
| -rw-r--r-- | src/invidious/views/preferences.ecr | 7 | ||||
| -rw-r--r-- | src/invidious/views/template.ecr | 14 |
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> |
