Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ copy_common_files() {
"$PROJECT_ROOT/src/content-info.js" \
"$PROJECT_ROOT/src/content-thumbnails.js" \
"$PROJECT_ROOT/src/content-ai-labels.js" \
"$PROJECT_ROOT/src/content-announcement.js" \
"$PROJECT_ROOT/src/content-ai-labels-page.js" \
"$PROJECT_ROOT/src/content-messages.js" \
"$PROJECT_ROOT/src/popup.html" \
Expand Down
6 changes: 5 additions & 1 deletion src/_locales/bg/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,9 @@
"settings_ai_labeled_video_dim": { "message": "Затъмни", "description": "AI setting option" },
"settings_ai_labeled_video_hide": { "message": "Скрий", "description": "AI setting option" },
"settings_ai_labeled_video_help": { "message": "Проверява само карти в отворени страници на YouTube. Изгледите на re:Watch показват кеширани етикети и не стартират проверки.", "description": "AI setting help" },
"settings_ai_labeled_video_info": { "message": "Проверките се извършват само за карти в отворени страници на YouTube. Изгледите на re:Watch не стартират проверки; те показват само кеширани резултати и запазват локалните видеа видими. Функцията използва етикета Made with AI на YouTube, не общо AI разпознаване, и може да пропусне AI видеа.", "description": "AI setting information" }
"settings_ai_labeled_video_info": { "message": "Проверките се извършват само за карти в отворени страници на YouTube. Изгледите на re:Watch не стартират проверки; те показват само кеширани резултати и запазват локалните видеа видими. Функцията използва етикета Made with AI на YouTube, не общо AI разпознаване, и може да пропусне AI видеа.", "description": "AI setting information" },
"ai_label_announcement_title": { "message": "Нови контроли за AI етикети", "description": "Заглавие на еднократното съобщение за функцията за AI етикети" },
"ai_label_announcement_body": { "message": "Изберете дали видеата с AI етикет да се показват, затъмняват или скриват в re:Watch.", "description": "Текст на еднократното съобщение за функцията за AI етикети" },
"announcement_dismiss": { "message": "Отказ", "description": "Скриване на съобщение за функция на разширението" },
"ai_label_announcement_settings": { "message": "Виж в настройките", "description": "Отваряне на настройките за Feed от съобщението" }
}
6 changes: 5 additions & 1 deletion src/_locales/de/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,9 @@
"settings_ai_labeled_video_dim": { "message": "Abdunkeln", "description": "AI setting option" },
"settings_ai_labeled_video_hide": { "message": "Ausblenden", "description": "AI setting option" },
"settings_ai_labeled_video_help": { "message": "Prüft nur Karten auf geöffneten YouTube-Seiten. re:Watch-Ansichten zeigen zwischengespeicherte Kennzeichnungen und starten keine Prüfungen.", "description": "AI setting help" },
"settings_ai_labeled_video_info": { "message": "Prüfungen laufen nur für Karten auf geöffneten YouTube-Seiten. re:Watch-Ansichten starten keine Prüfungen; sie zeigen nur zwischengespeicherte Ergebnisse und lassen lokale Videos sichtbar. Die Funktion nutzt YouTubes Made-with-AI-Hinweis, keine allgemeine KI-Erkennung, und kann KI-Videos übersehen.", "description": "AI setting information" }
"settings_ai_labeled_video_info": { "message": "Prüfungen laufen nur für Karten auf geöffneten YouTube-Seiten. re:Watch-Ansichten starten keine Prüfungen; sie zeigen nur zwischengespeicherte Ergebnisse und lassen lokale Videos sichtbar. Die Funktion nutzt YouTubes Made-with-AI-Hinweis, keine allgemeine KI-Erkennung, und kann KI-Videos übersehen.", "description": "AI setting information" },
"ai_label_announcement_title": { "message": "Neue Steuerelemente für KI-Kennzeichnungen", "description": "Titel der einmaligen Ankündigung der KI-Kennzeichnungsfunktion" },
"ai_label_announcement_body": { "message": "Lege fest, ob KI-gekennzeichnete Videos in re:Watch angezeigt, abgeblendet oder ausgeblendet werden.", "description": "Text der einmaligen Ankündigung der KI-Kennzeichnungsfunktion" },
"announcement_dismiss": { "message": "Schließen", "description": "Eine Funktionsankündigung schließen" },
"ai_label_announcement_settings": { "message": "In Einstellungen ansehen", "description": "Feed-Einstellungen über die Ankündigung öffnen" }
}
6 changes: 5 additions & 1 deletion src/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,5 +146,9 @@
"settings_ai_labeled_video_info": {
"message": "Checks run only for cards on open YouTube pages. re:Watch views never start checks; they only show cached results and keep local videos visible. The feature uses YouTube's Made with AI disclosure, not general AI detection, and can miss AI-made videos.",
"description": "Privacy and reliability information for AI-label handling"
}
},
"ai_label_announcement_title": { "message": "New AI-label controls", "description": "Title of the one-time AI-label feature announcement" },
"ai_label_announcement_body": { "message": "Choose whether AI-labeled videos are shown, dimmed, or hidden in re:Watch.", "description": "Body of the one-time AI-label feature announcement" },
"announcement_dismiss": { "message": "Dismiss", "description": "Dismiss an extension feature announcement" },
"ai_label_announcement_settings": { "message": "See it in Settings", "description": "Open Feed settings from the AI-label feature announcement" }
}
6 changes: 5 additions & 1 deletion src/_locales/es/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,9 @@
"settings_ai_labeled_video_dim": { "message": "Atenuar", "description": "AI setting option" },
"settings_ai_labeled_video_hide": { "message": "Ocultar", "description": "AI setting option" },
"settings_ai_labeled_video_help": { "message": "Solo comprueba tarjetas de páginas de YouTube abiertas. Las vistas de re:Watch muestran etiquetas almacenadas y nunca inician comprobaciones.", "description": "AI setting help" },
"settings_ai_labeled_video_info": { "message": "Las comprobaciones solo se ejecutan para tarjetas de páginas de YouTube abiertas. Las vistas de re:Watch no inician comprobaciones; solo muestran resultados almacenados y mantienen visibles los vídeos locales. La función usa la divulgación Made with AI de YouTube, no una detección general de IA, y puede omitir vídeos creados con IA.", "description": "AI setting information" }
"settings_ai_labeled_video_info": { "message": "Las comprobaciones solo se ejecutan para tarjetas de páginas de YouTube abiertas. Las vistas de re:Watch no inician comprobaciones; solo muestran resultados almacenados y mantienen visibles los vídeos locales. La función usa la divulgación Made with AI de YouTube, no una detección general de IA, y puede omitir vídeos creados con IA.", "description": "AI setting information" },
"ai_label_announcement_title": { "message": "Nuevos controles para etiquetas de IA", "description": "Título del anuncio único de la función de etiquetas de IA" },
"ai_label_announcement_body": { "message": "Elige si los vídeos con etiqueta de IA se muestran, atenúan u ocultan en re:Watch.", "description": "Texto del anuncio único de la función de etiquetas de IA" },
"announcement_dismiss": { "message": "Descartar", "description": "Descartar un anuncio de función de la extensión" },
"ai_label_announcement_settings": { "message": "Ver en Configuración", "description": "Abrir la configuración del Feed desde el anuncio" }
}
6 changes: 5 additions & 1 deletion src/_locales/fr/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,9 @@
"settings_ai_labeled_video_dim": { "message": "Atténuer", "description": "AI setting option" },
"settings_ai_labeled_video_hide": { "message": "Masquer", "description": "AI setting option" },
"settings_ai_labeled_video_help": { "message": "Ne vérifie que les cartes des pages YouTube ouvertes. Les vues re:Watch affichent les étiquettes en cache et ne lancent jamais de vérification.", "description": "AI setting help" },
"settings_ai_labeled_video_info": { "message": "Les vérifications ne concernent que les cartes des pages YouTube ouvertes. Les vues re:Watch ne lancent aucune vérification ; elles affichent uniquement les résultats en cache et gardent les vidéos locales visibles. La fonction utilise la divulgation Made with AI de YouTube, pas une détection générale d’IA, et peut manquer des vidéos créées avec IA.", "description": "AI setting information" }
"settings_ai_labeled_video_info": { "message": "Les vérifications ne concernent que les cartes des pages YouTube ouvertes. Les vues re:Watch ne lancent aucune vérification ; elles affichent uniquement les résultats en cache et gardent les vidéos locales visibles. La fonction utilise la divulgation Made with AI de YouTube, pas une détection générale d’IA, et peut manquer des vidéos créées avec IA.", "description": "AI setting information" },
"ai_label_announcement_title": { "message": "Nouveaux contrôles des libellés IA", "description": "Titre de l’annonce unique de la fonctionnalité de libellés IA" },
"ai_label_announcement_body": { "message": "Choisissez si les vidéos libellées IA sont affichées, atténuées ou masquées dans re:Watch.", "description": "Texte de l’annonce unique de la fonctionnalité de libellés IA" },
"announcement_dismiss": { "message": "Fermer", "description": "Fermer une annonce de fonctionnalité de l’extension" },
"ai_label_announcement_settings": { "message": "Voir dans les paramètres", "description": "Ouvrir les paramètres du Feed depuis l’annonce" }
}
137 changes: 136 additions & 1 deletion src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,54 @@ const stateManager = {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
(async () => {
console.log('Background script received message:', message.type, 'from sender:', sender.tab ? 'content' : 'popup');

if (message.type === 'getAnnouncements') {
if (!isLocalSubscriptionSender(sender) || !Number.isInteger(sender.tab?.id)) {
sendResponse({ announcements: [] });
return;
}
sendResponse({ announcements: Object.values(ANNOUNCEMENTS) });
return;
}

if (message.type === 'claimAnnouncement') {
if (!isLocalSubscriptionSender(sender) || !Number.isInteger(sender.tab?.id)) {
sendResponse({ show: false });
return;
}
if (!ANNOUNCEMENTS[message.announcementId]) {
sendResponse({ show: false });
return;
}
try {
sendResponse({ show: await claimAnnouncement(message.announcementId, sender.tab.id) });
} catch (error) {
console.warn('[Announcements] could not claim an announcement', error);
sendResponse({ show: false });
}
return;
}

if (message.type === 'releaseAnnouncement' || message.type === 'runAnnouncementAction') {
if (!isLocalSubscriptionSender(sender)) {
sendResponse({ ok: false });
return;
}
const announcement = ANNOUNCEMENTS[message.announcementId];
if (!announcement) {
sendResponse({ ok: false });
return;
}
try {
if (message.type === 'runAnnouncementAction') await runAnnouncementAction(announcement);
await releaseAnnouncement(message.announcementId);
sendResponse({ ok: true });
} catch (error) {
console.warn('[Announcements] could not complete an announcement', error);
sendResponse({ ok: false });
}
return;
}

if (message.type === 'openPopup') {
const popupId = await stateManager.get('activePopupWindowId');
Expand Down Expand Up @@ -224,6 +272,18 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
return true; // Indicates async response
});

chrome.tabs?.onRemoved?.addListener((tabId) => {
stateManager.get(ANNOUNCEMENT_OWNERS_STATE_KEY).then((owners) => {
owners = owners || {};
const remainingOwners = Object.fromEntries(
Object.entries(owners).filter(([, ownerTabId]) => ownerTabId !== tabId)
);
if (Object.keys(remainingOwners).length !== Object.keys(owners).length) {
return stateManager.set({ [ANNOUNCEMENT_OWNERS_STATE_KEY]: remainingOwners });
}
}).catch(() => {});
});

function isLocalSubscriptionSender(sender) {
const senderUrl = sender && sender.url || '';
// The loopback origin is injected only by the Firefox E2E build to replay
Expand Down Expand Up @@ -302,7 +362,82 @@ function createContextMenus() {

// MV3 service workers are torn down and restarted; re-create the menu on both
// install/update and browser startup so it's always present.
chrome.runtime.onInstalled.addListener(createContextMenus);
const ANNOUNCEMENT_OWNERS_STATE_KEY = 'announcementOwners';
let announcementClaimQueue = Promise.resolve();
const ANNOUNCEMENTS = {
'ai-label-controls-v1': {
id: 'ai-label-controls-v1',
titleKey: 'ai_label_announcement_title',
title: 'New AI-label controls',
bodyKey: 'ai_label_announcement_body',
body: 'Choose whether AI-labeled videos are shown, dimmed, or hidden in re:Watch.',
actionLabelKey: 'ai_label_announcement_settings',
actionLabel: 'See it in Settings',
storageKey: '__rwui_notice_7',
action: { type: 'open-extension-page', path: 'feed.html#settings' }
}
};

async function announcementOwnerIsLive(tabId) {
if (!Number.isInteger(tabId) || !chrome.tabs?.get) return false;
try {
await chrome.tabs.get(tabId);
return true;
} catch (_) {
return false;
}
}

function claimAnnouncement(announcementId, tabId) {
const claim = announcementClaimQueue.then(() => claimAnnouncementUnlocked(announcementId, tabId));
// Keep the queue usable after a storage/API failure while returning the
// failure to this caller for normal message handling.
announcementClaimQueue = claim.catch(() => {});
return claim;
}

async function claimAnnouncementUnlocked(announcementId, tabId) {
const owners = (await stateManager.get(ANNOUNCEMENT_OWNERS_STATE_KEY)) || {};
const ownerTabId = owners[announcementId];
if (ownerTabId !== tabId && await announcementOwnerIsLive(ownerTabId)) return false;
await stateManager.set({
[ANNOUNCEMENT_OWNERS_STATE_KEY]: { ...owners, [announcementId]: tabId }
});
return true;
}

async function releaseAnnouncement(announcementId) {
const owners = (await stateManager.get(ANNOUNCEMENT_OWNERS_STATE_KEY)) || {};
const nextOwners = { ...owners };
delete nextOwners[announcementId];
await stateManager.set({ [ANNOUNCEMENT_OWNERS_STATE_KEY]: nextOwners });
}

async function runAnnouncementAction(announcement) {
if (announcement.action?.type === 'open-extension-page') {
await chrome.tabs.create({ url: chrome.runtime.getURL(announcement.action.path) });
return;
}
throw new Error(`Unsupported announcement action: ${announcement.action?.type || 'none'}`);
}

async function injectAnnouncementsIntoOpenYouTubeTabs() {
if (!chrome.tabs?.query || !chrome.scripting?.executeScript) return;
const tabs = await chrome.tabs.query({ url: ['https://www.youtube.com/*'] });
await Promise.all(tabs.filter((tab) => Number.isInteger(tab.id)).map((tab) =>
chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['content-announcement.js']
}).catch(() => {})
));
}

chrome.runtime.onInstalled.addListener((details) => {
createContextMenus();
Promise.resolve(details.reason === 'install' || details.reason === 'update')
.then((shouldInject) => shouldInject && injectAnnouncementsIntoOpenYouTubeTabs())
.catch((error) => console.warn('[Announcements] setup failed', error));
});
if (chrome.runtime.onStartup) chrome.runtime.onStartup.addListener(createContextMenus);

// Extract a YouTube video id (and shorts flag) from any link/page URL.
Expand Down
9 changes: 9 additions & 0 deletions src/content-ai-labels.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,15 @@
cardsById.get(id).add(card);
if (resolvedStatuses.has(id)) apply(id, resolvedStatuses.get(id));
observer.observe(card);
// Firefox can omit the initial IntersectionObserver callback for
// an already-visible card during document startup. Queue such a
// card directly; later cards still use observer re-entry.
const bounds = card.getBoundingClientRect();
if (bounds.bottom > 0 && bounds.top < window.innerHeight &&
bounds.right > 0 && bounds.left < window.innerWidth) {
queuedIds.add(id);
drain();
}
}

function scan(root = document) {
Expand Down
Loading