Skip to content

Commit

Permalink
Database-backed news page (#306)
Browse files Browse the repository at this point in the history
  • Loading branch information
NikkelM authored Jun 30, 2024
1 parent 8cd8b71 commit 807b993
Show file tree
Hide file tree
Showing 15 changed files with 187 additions and 50 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# Changelog

## v3.1.7-beta
## v3.1.7

<!--Releasenotes start-->
- The extensions's news page can now be updated with breaking changes or other important information without the need to update the extension itself.
- Fixed some dynamic content on the News page.
- Added a hint in the popup if no channel has yet been visited.
- Removed some unneeded scripts from the extension's pages.
<!--Releasenotes end-->

## v3.1.6
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "random-youtube-video",
"version": "3.1.6",
"version": "3.1.7",
"description": "Customize, shuffle and play random videos from any YouTube channel.",
"scripts": {
"dev": "concurrently \"npm run dev:chromium\" \"npm run dev:firefox\"",
Expand Down
75 changes: 53 additions & 22 deletions src/background.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
// Background service worker for the extension, which is run ("started") on extension initialization
// Handles communication between the extension and the content script as well as Firebase interactions
import { configSync, setSyncStorageValue } from "./chromeStorage.js";
import { configSync, setSyncStorageValue, setSessionStorageValue } from "./chromeStorage.js";
import { isFirefox, firebaseConfig } from "./config.js";
import { getApp, getApps, initializeApp } from "firebase/app";
import { getDatabase, ref, child, update, get, remove } from "firebase/database";
import { getFirestore, query, collection, getDocs, orderBy, limit, where } from "firebase/firestore";
// We need to import utils.js to get the console re-routing function
import { } from "./utils.js";

// ---------- Initialization/Chrome event listeners ----------
const isFirefox = typeof browser !== "undefined";
// ---------- Firebase ----------
const app = !getApps().length ? initializeApp(firebaseConfig) : getApp();
const firebase = getDatabase(app);
const firestore = getFirestore(app);

await initExtension();

// Check whether a new version was installed
Expand All @@ -26,7 +34,8 @@ async function initExtension() {
await handleExtensionUpdate(manifestData, configSync.previousVersion);
}

checkLocalStorageCapacity();
await checkLocalStorageCapacity();
await checkForAndShowNews();
}

// Make sure we are not using too much local storage
Expand Down Expand Up @@ -201,21 +210,43 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
});

// ---------- Firebase ----------
import { initializeApp } from 'firebase/app';
import { getDatabase, ref, child, update, get, remove } from 'firebase/database';

const firebaseConfig = {
apiKey: "AIzaSyA6d7Ahi7fMB4Ey8xXM8f9C9Iya97IGs-c",
authDomain: "random--video-ex-chrome.firebaseapp.com",
projectId: "random-youtube-video-ex-chrome",
storageBucket: "random-youtube-video-ex-chrome.appspot.com",
messagingSenderId: "141257152664",
appId: "1:141257152664:web:f70e46e35d02921a8818ed",
databaseURL: "https://random-youtube-video-ex-chrome-default-rtdb.europe-west1.firebasedatabase.app"
};

const app = initializeApp(firebaseConfig);
const db = getDatabase(app);
// Interact with Firestore and get the latest news
// createdAt is a custom field
async function checkForAndShowNews() {
if (configSync.nextNewsCheckTime >= Date.now()) {
console.log(`Skipping news check until ${new Date(configSync.nextNewsCheckTime).toLocaleString()}`);
return;
}

const q = query(
collection(firestore, "news"),
where("published", "==", true),
orderBy("createdAt", "desc"),
limit(1)
);

const querySnapshot = await getDocs(q);
if (querySnapshot.empty) {
console.log("No published news articles found in the database.");
return;
}

const doc = querySnapshot.docs[0];
const news = {
id: doc.id,
...doc.data()
};

// Set the next time to check for news to tomorrow
await setSyncStorageValue("nextNewsCheckTime", new Date(new Date().setHours(24, 0, 0, 0)).getTime());

// Check if the published flag is true, and if the user has not viewed this news article yet, indicated through the lastViewedNewsId
if (news && news.published && news.id !== configSync.lastViewedNewsId) {
setSyncStorageValue("lastViewedNewsId", news.id);
await setSessionStorageValue("news", news);
chrome.tabs.create({ url: "html/breakingNews.html" });
}
}

async function updatePlaylistInfoInDB(playlistId, playlistInfo, overwriteVideos) {
// Find out if the playlist already exists in the database
Expand All @@ -228,25 +259,25 @@ async function updatePlaylistInfoInDB(playlistId, playlistInfo, overwriteVideos)
if (overwriteVideos || !playlistExists) {
console.log("Setting playlistInfo in the database...");
// Update the entire object. Due to the way Firebase works, this will overwrite the existing 'videos' object, as it is nested within the playlist
update(ref(db, playlistId), playlistInfo);
update(ref(firebase, playlistId), playlistInfo);
} else {
console.log("Updating playlistInfo in the database...");
// Contains all properties except the videos
const playlistInfoWithoutVideos = Object.fromEntries(Object.entries(playlistInfo).filter(([key, value]) => (key !== "videos")));

// Upload the 'metadata'
update(ref(db, playlistId), playlistInfoWithoutVideos);
update(ref(firebase, playlistId), playlistInfoWithoutVideos);

// Update the videos separately to not overwrite existing videos
update(ref(db, playlistId + "/videos"), playlistInfo.videos);
update(ref(firebase, playlistId + "/videos"), playlistInfo.videos);
}

return "PlaylistInfo was sent to database.";
}

async function updateDBPlaylistToV1_0_0(playlistId) {
// Remove all videos from the database
remove(ref(db, playlistId + '/videos'));
remove(ref(firebase, playlistId + '/videos'));

return "Videos were removed from the database playlist.";
}
Expand Down
13 changes: 12 additions & 1 deletion src/chromeStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ await validateConfigSync();

export let configSync = await chrome.storage.sync.get();

/* c8 ignore start - This event listener cannot really be tested */
/* c8 ignore start - This event listener cannot be tested */
// Whenever someone changes a value in sync storage, we need to be notified to update the global configSync object
chrome.storage.onChanged.addListener(async function (changes, namespace) {
// We only care about changes to the sync storage
Expand All @@ -32,6 +32,17 @@ export async function removeSyncStorageValue(key) {
await chrome.storage.sync.remove(key);
}

/* c8 ignore start - session storage cannot be mocked like the others, but under the hood behaves the same as sync storage, which we do test */
export async function setSessionStorageValue(key, value) {
await chrome.storage.session.set({ [key]: value });
}

export async function getSessionStorageValue(key) {
// await is needed here, contrary to what intellisense says!
return (await chrome.storage.session.get(key))[key];
}
/* c8 ignore stop */

// Returns the number of requests the user can still make to the Youtube API today
export async function getUserQuotaRemainingToday() {
// The quota gets reset at midnight
Expand Down
17 changes: 17 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,23 @@ export const configSyncDefaults = {
"reviewMessageShown": false,
// If the message asking for a donation has been shown yet
"donationMessageShown": false,
// The id/date of the last viewed news article
"lastViewedNewsId": null,
// The next time we should check for news (once per day)
// We delay the first check by 24 hours to not immediately show the news after a user has installed the extension
"nextNewsCheckTime": new Date(new Date().setHours(24, 0, 0, 0)).getTime()
};

export const isFirefox = typeof browser !== "undefined";

export const firebaseConfig = {
apiKey: "AIzaSyA6d7Ahi7fMB4Ey8xXM8f9C9Iya97IGs-c",
authDomain: "random--video-ex-chrome.firebaseapp.com",
projectId: "random-youtube-video-ex-chrome",
storageBucket: "random-youtube-video-ex-chrome.appspot.com",
messagingSenderId: "141257152664",
appId: "1:141257152664:web:f70e46e35d02921a8818ed",
databaseURL: "https://random-youtube-video-ex-chrome-default-rtdb.europe-west1.firebasedatabase.app"
};

export const shufflingHints = [
Expand Down
74 changes: 74 additions & 0 deletions src/html/breakingNews.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Checks Firestore for new news and opens the news page if there are
import { configSync, getSessionStorageValue, setSyncStorageValue } from "../chromeStorage.js";
import { buildShufflingHints, tryFocusingTab } from "./htmlUtils.js";

const domElements = getDomElements();
await setDomElementValues(domElements);
await buildShufflingHints(domElements);
await setDomElementEventListeners(domElements);

function getDomElements() {
return {
newsHeading: document.getElementById("newsHeading"),
newsContent: document.getElementById("newsContent"),
publishTime: document.getElementById("publishTime"),
// The p element containing the shuffle hint
shufflingHintP: document.getElementById("shufflingHintP"),
// The button that displays the next shuffle hint
nextHintButton: document.getElementById("nextHintButton"),

// FOOTER
// View changelog button
viewChangelogButton: document.getElementById("viewChangelogButton"),
}
}

// Set default values from configSync == user preferences
async function setDomElementValues(domElements) {
await buildNews();

// If the current extension version is newer than configSync.lastViewedChangelogVersion, highlight the changelog button
if (configSync.lastViewedChangelogVersion !== chrome.runtime.getManifest().version) {
domElements.viewChangelogButton.classList.add("highlight-green");
}
}

// Set event listeners for DOM elements
async function setDomElementEventListeners(domElements) {
// View changelog button
domElements.viewChangelogButton.addEventListener("click", async function () {
await setSyncStorageValue("lastViewedChangelogVersion", chrome.runtime.getManifest().version);

const changelogPage = chrome.runtime.getURL("html/changelog.html");
let mustOpenTab = await tryFocusingTab(changelogPage);
if (mustOpenTab) {
await chrome.tabs.create({ url: changelogPage });
}

domElements.viewChangelogButton.classList.remove("highlight-green");
});
}

async function buildNews() {
const news = await getSessionStorageValue("news");

// This normally shouldn't happen, as the news page is only opened if news have been added to session storage before
if (!news) {
console.log("No news found in session storage, even though there should be some!");
return;
}

domElements.newsHeading.textContent = news.heading;
domElements.newsContent.innerHTML = news.htmlContent;

const createdAtDate = new Date(news.createdAt.seconds * 1000);
const differenceInTime = new Date().getTime() - createdAtDate.getTime();
const differenceInDays = Math.floor(differenceInTime / (1000 * 3600 * 24));

let dateString = `Published on ${createdAtDate.toLocaleDateString()}`;
if (differenceInDays > 1) {
dateString += ` (${differenceInDays} days ago)`;
}

domElements.publishTime.textContent = dateString;
}
26 changes: 17 additions & 9 deletions static/html/breakingNews.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,27 @@ <h1> News - Random YouTube Video</h1>
<br />

<div id="newsDiv">
<h1 id="newsHeading">Playlists are broken!</h1>
<p id="newsContent">
A recent change to YouTube has broken the extension's ability to automatically create playlists.
<br />
<br />
The affected option in the popup is "<i>Open a playlist</i>". It is possible that this feature still works for you, but it likely will not.
<br />
I am actively monitoring the situation and looking for alternative ways of getting this functionality working again.
</p>
<h1 id="newsHeading">News will be displayed here</h1>
<div id="newsContent">
<p>If you're seeing this text, it means you either manually opened this page (in which case it won't work!),
<br />
or there are news but they couldn't be displayed correctly - please report this as a bug on GitHub!</p>
</div>
<br />
<br />
<p id="publishTime"></p>
</div>

<br />
<br />
<br />
<br />
<div id="shufflingHint">
<h3><i>Did you know...</i></h3>
<p id="shufflingHintP" class="thirdWidth">The hints could not be loaded - please report this bug on GitHub!</p>
<button id="nextHintButton" class="randomYoutubeVideoButton" style="margin-top: 6px">New Hint</button>
</div>

<br />
<br />
<footer id="randomYoutubeVideoFooter">
Expand Down Expand Up @@ -56,5 +62,7 @@ <h1 id="newsHeading">Playlists are broken!</h1>
<a class="buttonLink" href="https://ko-fi.com/nikkelm" target="_blank" rel="noopener" title="Show your appreciation and support the development of the extension">Donate</a>
</footer>
</div>

<script src="../breakingNews.js" type="module"></script>
</body>
</html>
3 changes: 0 additions & 3 deletions static/html/changelog.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,11 @@ <h3><i>Did you know...</i></h3>
>
<br />
<a class="buttonLink" href="https://github.com/NikkelM/Random-YouTube-Video" target="_blank" rel="noopener" title="View the source code or get into contact with the developer">GitHub</a>
<a class="buttonLink" href="https://github.com/NikkelM/Random-YouTube-Video/blob/main/CHANGELOG.md" target="_blank" rel="noopener">Full Changelog</a>
<a class="buttonLink" href="https://ko-fi.com/nikkelm" target="_blank" rel="noopener" title="Show your appreciation and support the development of the extension">Donate</a>
</footer>
</div>
</div>

<script src="../utils.js" type="text/javascript"></script>
<script src="../htmlUtils.js" type="text/javascript"></script>
<script src="../changelog.js" type="module"></script>
</body>
</html>
4 changes: 0 additions & 4 deletions static/html/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -232,10 +232,6 @@ <h3 id="channelCustomOptionsHeader">Channel settings</h3>
</footer>
</div>

<script src="../utils.js" type="text/javascript"></script>
<script src="../shuffleVideo.js" type="text/javascript"></script>
<script src="../htmlUtils.js" type="text/javascript"></script>
<script src="../popupUtils.js" type="text/javascript"></script>
<script src="../popup.js" type="module"></script>
</body>
</html>
3 changes: 0 additions & 3 deletions static/html/shufflingPage.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,6 @@ <h3><i>Did you know...</i></h3>
</footer>
</div>

<script src="../utils.js" type="text/javascript"></script>
<script src="../shuffleVideo.js" type="text/javascript"></script>
<script src="../htmlUtils.js" type="text/javascript"></script>
<script src="../shufflingPage.js" type="module"></script>
</body>
</html>
2 changes: 0 additions & 2 deletions static/html/welcome.html
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,6 @@ <h3><i>Did you know...</i></h3>
</footer>
</div>

<script src="../utils.js" type="text/javascript"></script>
<script src="../htmlUtils.js" type="text/javascript"></script>
<script src="../welcome.js" type="module"></script>
</body>
</html>
4 changes: 2 additions & 2 deletions static/manifest.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "Random YouTube Video",
"description": "Customize, shuffle and play random videos from any YouTube channel.",
"version": "3.1.6",
"version_name": "3.1.7-beta",
"version": "3.1.7",
"version_name": "3.1.7",
"manifest_version": 3,
"content_scripts": [
{
Expand Down
4 changes: 4 additions & 0 deletions test/testSetup.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ chrome.storage.local.set.callsFake((obj) => {
Object.assign(mockedLocalStorage, deepCopy(obj));
return Promise.resolve();
});
chrome.storage.local.remove.callsFake((key) => {
delete mockedLocalStorage[key];
return Promise.resolve();
});
chrome.storage.local.clear.callsFake(() => {
for (const key in mockedLocalStorage) {
delete mockedLocalStorage[key];
Expand Down
Loading

0 comments on commit 807b993

Please sign in to comment.