"use strict";
var notification_data = JSON.parse(
document.getElementById("notification_data").textContent,
);
/** Boolean meaning 'some tab have stream' */
const STORAGE_KEY_STREAM = "stream";
/** Number of notifications. May be increased or reset */
const STORAGE_KEY_NOTIF_COUNT = "notification_count";
var notifications, delivered;
var notifications_mock = { close: function () {} };
async function get_subscriptions_call() {
return new Promise((resolve) => {
helpers.xhr(
"GET",
"/api/v1/auth/subscriptions",
{
retries: 5,
entity_name: "subscriptions",
},
{
on200: function (subscriptions) {
create_notification_stream(subscriptions);
resolve(subscriptions);
},
},
);
});
}
// Start the retry mechanism
const get_subscriptions = exponential_backoff(get_subscriptions_call, 100, 1000);
function create_notification_stream(subscriptions) {
// sse.js can't be replaced to EventSource in place as it lack support of payload and headers
// see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/EventSource
notifications = new SSE("/api/v1/auth/notifications", {
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.info("Got notification:", notification);
// Ignore not actual and delivered notifications
if (
start_time > notification.published ||
delivered.includes(notification.videoId)
)
return;
delivered.push(notification.videoId);
let notification_count = helpers.storage.get(STORAGE_KEY_NOTIF_COUNT) || 0;
notification_count++;
helpers.storage.set(STORAGE_KEY_NOTIF_COUNT, notification_count);
update_ticker_count();
// permission for notifications handled on settings page. JS handler is in handlers.js
if (window.Notification && Notification.permission === "granted") {
var notification_text = notification.liveNow
? notification_data.live_now_text
: notification_data.upload_text;
notification_text = notification_text.replace("`x`", notification.author);
var system_notification = new Notification(notification_text, {
body: notification.title,
icon: "/ggpht" + new URL(notification.authorThumbnails[2].url).pathname,
img: "/ggpht" + new URL(notification.authorThumbnails[4].url).pathname,
});
system_notification.onclick = function (e) {
open("/watch?v=" + notification.videoId, "_blank");
};
}
};
notifications.addEventListener("error", function (e) {
console.warn(
"Something went wrong with notifications, trying to reconnect...",
);
notifications = notifications_mock;
});
notifications.stream();
}
function update_ticker_count() {
var notification_ticker = document.getElementById("notification_ticker");
const notification_count = helpers.storage.get(STORAGE_KEY_STREAM);
if (notification_count > 0) {
notification_ticker.innerHTML =
'' +
notification_count +
' ';
} else {
notification_ticker.innerHTML =
'';
}
}
function start_stream_if_needed() {
// random wait for other tabs set 'stream' flag
setTimeout(
function () {
if (!helpers.storage.get(STORAGE_KEY_STREAM)) {
// if no one set 'stream', set it by yourself and start stream
helpers.storage.set(STORAGE_KEY_STREAM, true);
notifications = notifications_mock;
get_subscriptions();
}
},
Math.random() * 1000 + 50,
); // [0.050 .. 1.050) second
}
addEventListener("storage", function (e) {
if (e.key === STORAGE_KEY_NOTIF_COUNT) update_ticker_count();
// if 'stream' key was removed
if (
e.key === STORAGE_KEY_STREAM &&
!helpers.storage.get(STORAGE_KEY_STREAM)
) {
if (notifications) {
// restore it if we have active stream
helpers.storage.set(STORAGE_KEY_STREAM, true);
} else {
start_stream_if_needed();
}
}
});
addEventListener("load", function () {
var notification_count_el = document.getElementById("notification_count");
var notification_count = notification_count_el
? parseInt(notification_count_el.textContent)
: 0;
helpers.storage.set(STORAGE_KEY_NOTIF_COUNT, notification_count);
if (helpers.storage.get(STORAGE_KEY_STREAM))
helpers.storage.remove(STORAGE_KEY_STREAM);
start_stream_if_needed();
});
addEventListener("unload", function () {
// let chance to other tabs to be a streamer via firing 'storage' event
if (notifications) helpers.storage.remove(STORAGE_KEY_STREAM);
});
function exponential_backoff(
fn,
maxRetries = 5,
initialDelay = 1000,
randomnessFactor = 0.5,
) {
let attempt = 0;
return function tryFunction() {
fn()
.then((response) => {
attempt = 0;
})
.catch((error) => {
if (attempt < maxRetries) {
attempt++;
let delay = initialDelay * Math.pow(2, attempt); // Exponential backoff
let randomMultiplier = 1 + Math.random() * randomnessFactor;
delay = delay * randomMultiplier;
console.log(
`Attempt ${attempt} failed. Retrying in ${(delay / 1000).toPrecision(2)} seconds...`,
);
setTimeout(tryFunction, delay); // Retry after delay
} else {
console.log("Max retries reached. Operation failed:", error);
}
});
}
}