From e722ded58a928989bb583ae266acf765c129592c Mon Sep 17 00:00:00 2001 From: Ajay Date: Wed, 8 Nov 2023 16:07:59 -0500 Subject: [PATCH] Add dearrow promo based on title and remove old one Also refactor requests out to seperate file --- maze-utils | 2 +- public/_locales | 2 +- public/content.css | 7 ++ src/components/SponsorTimeEditComponent.tsx | 6 +- src/config.ts | 4 +- src/content.ts | 57 ++++------------- src/dearrowPromotion.ts | 71 +++++++++++++++++++++ src/options.ts | 3 +- src/popup.ts | 5 +- src/utils.ts | 46 ------------- src/utils/requests.ts | 47 ++++++++++++++ src/utils/videoLabels.ts | 3 +- src/utils/warnings.ts | 9 ++- 13 files changed, 155 insertions(+), 107 deletions(-) create mode 100644 src/dearrowPromotion.ts create mode 100644 src/utils/requests.ts diff --git a/maze-utils b/maze-utils index 92d368b0..6bdc9402 160000 --- a/maze-utils +++ b/maze-utils @@ -1 +1 @@ -Subproject commit 92d368b051c1af360ab9db45215df274b71de8f5 +Subproject commit 6bdc9402c6cf2a8c97e53e1c360744e12afd919f diff --git a/public/_locales b/public/_locales index 6ff5f86e..9ba877c0 160000 --- a/public/_locales +++ b/public/_locales @@ -1 +1 @@ -Subproject commit 6ff5f86e9aca64c2ad51aa13e39db1a040506f9d +Subproject commit 9ba877c006a9a78774e56713cc170f355d81a4ab diff --git a/public/content.css b/public/content.css index 39051417..dc2b1ba1 100644 --- a/public/content.css +++ b/public/content.css @@ -742,6 +742,7 @@ input::-webkit-inner-spin-button { color: white; font-size: 12px; z-index: 10000; + font-weight: normal; } .sponsorBlockTooltip a { @@ -764,6 +765,12 @@ input::-webkit-inner-spin-button { right: 50%; } +.sponsorBlockTooltip.sbTriangle.sbTopTriangle::after { + bottom: 100%; + top: unset; + border-color: transparent transparent rgba(28, 28, 28, 0.7) transparent; +} + .sponsorBlockLockedColor { color: #ffc83d !important; } diff --git a/src/components/SponsorTimeEditComponent.tsx b/src/components/SponsorTimeEditComponent.tsx index 191f8ce7..1b470a78 100644 --- a/src/components/SponsorTimeEditComponent.tsx +++ b/src/components/SponsorTimeEditComponent.tsx @@ -2,14 +2,12 @@ import * as React from "react"; import * as CompileConfig from "../../config.json"; import Config from "../config"; import { ActionType, Category, ChannelIDStatus, ContentContainer, SponsorTime } from "../types"; -import Utils from "../utils"; import SubmissionNoticeComponent from "./SubmissionNoticeComponent"; import { RectangleTooltip } from "../render/RectangleTooltip"; import SelectorComponent, { SelectorOption } from "./SelectorComponent"; import { DEFAULT_CATEGORY } from "../utils/categoryUtils"; import { getFormattedTime, getFormattedTimeToSeconds } from "../../maze-utils/src/formating"; - -const utils = new Utils(); +import { asyncRequestToServer } from "../utils/requests"; export interface SponsorTimeEditProps { index: number; @@ -727,7 +725,7 @@ class SponsorTimeEditComponent extends React.Component Config.isReady(), 5000, 10).then(() => { addCSS(); setCategoryColorCSSVariables(); - // DeArrow promotion - setTimeout(async () => { - if (document.URL === "https://www.youtube.com/" - && Config.config.showDeArrowPromotion - && Config.config.showUpsells - && Config.config.showNewFeaturePopups - && (Config.config.skipCount > 30 || !Config.config.trackViewCount) - && Math.random() < 0.05) { - - if (!await isDeArrowInstalled()) { - const element = await waitForElement("#contents") as HTMLElement; - if (element) { - Config.config.showDeArrowPromotion = false; - - new Tooltip({ - text: chrome.i18n.getMessage("DeArrowPromotionMessage2"), - linkOnClick: () => window.open("https://dearrow.ajay.app"), - referenceNode: element, - prependElement: element.firstElementChild as HTMLElement, - timeout: 15000, - positionRealtive: false, - containerAbsolute: true, - bottomOffset: "inherit", - topOffset: "-82px", - leftOffset: "0", - rightOffset: "0", - displayTriangle: false, - center: true, - opacity: 1 - }); - } - } else { - Config.config.showDeArrowPromotion = false; - } - } - }, 5000); - runCompatibilityChecks(); }); @@ -440,6 +403,8 @@ function resetValues() { for (let i = 0; i < skipNotices.length; i++) { skipNotices.pop()?.close(); } + + hideDeArrowPromotion(); } function videoIDChange(): void { @@ -480,6 +445,8 @@ function videoIDChange(): void { // Clear unsubmitted segments from the previous video sponsorTimesSubmitting = []; updateSponsorTimesSubmitting(); + + tryShowingDeArrowPromotion().catch(logWarn); } function handleMobileControlsMutations(): void { @@ -1112,7 +1079,7 @@ async function sponsorsLookup(keepOldSubmissions = true) { if (hashParams.requiredSegment) extraRequestData.requiredSegment = hashParams.requiredSegment; const hashPrefix = (await getHash(getVideoID(), 1)).slice(0, 4) as VideoID & HashedValue; - const response = await utils.asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, { + const response = await asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, { categories, actionTypes: getEnabledActionTypes(), userAgent: `${chrome.runtime.id}`, @@ -1252,7 +1219,7 @@ function getEnabledActionTypes(forceFullVideo = false): ActionType[] { async function lockedCategoriesLookup(): Promise { const hashPrefix = (await getHash(getVideoID(), 1)).slice(0, 4); - const response = await utils.asyncRequestToServer("GET", "/api/lockCategories/" + hashPrefix); + const response = await asyncRequestToServer("GET", "/api/lockCategories/" + hashPrefix); if (response.ok) { try { @@ -1646,7 +1613,7 @@ function sendTelemetryAndCount(skippingSegments: SponsorTime[], secondsSkipped: counted = true; } - if (fullSkip) utils.asyncRequestToServer("POST", "/api/viewedVideoSponsorTime?UUID=" + segment.UUID); + if (fullSkip) asyncRequestToServer("POST", "/api/viewedVideoSponsorTime?UUID=" + segment.UUID); } } } @@ -2282,7 +2249,7 @@ async function sendSubmitMessage() { } } - const response = await utils.asyncRequestToServer("POST", "/api/skipSegments", { + const response = await asyncRequestToServer("POST", "/api/skipSegments", { videoID: getVideoID(), userID: Config.config.userID, segments: sponsorTimesSubmitting, diff --git a/src/dearrowPromotion.ts b/src/dearrowPromotion.ts new file mode 100644 index 00000000..208c627f --- /dev/null +++ b/src/dearrowPromotion.ts @@ -0,0 +1,71 @@ +import { waitFor } from "../maze-utils/src"; +import { getYouTubeTitleNode } from "../maze-utils/src/elements"; +import { getHash } from "../maze-utils/src/hash"; +import { getVideoID, isOnInvidious, isOnMobileYouTube } from "../maze-utils/src/video"; +import Config from "./config"; +import { Tooltip } from "./render/Tooltip"; +import { isDeArrowInstalled } from "./utils/crossExtension"; +import { isVisible } from "./utils/pageUtils"; +import { asyncRequestToServer } from "./utils/requests"; + +let tooltip: Tooltip = null; +export async function tryShowingDeArrowPromotion() { + if (Config.config.showDeArrowPromotion + && !isOnMobileYouTube() + && !isOnInvidious() + && document.URL.includes("watch") + && Config.config.showUpsells + && Config.config.showNewFeaturePopups + && (Config.config.skipCount > 30 || !Config.config.trackViewCount)) { + + if (!await isDeArrowInstalled()) { + try { + const element = await waitFor(() => getYouTubeTitleNode(), 5000, 500, (e) => isVisible(e)) as HTMLElement; + if (element && element.innerText && badTitle(element.innerText)) { + const hashPrefix = (await getHash(getVideoID(), 1)).slice(0, 4); + const deArrowData = await asyncRequestToServer("GET", "/api/branding/" + hashPrefix); + if (!deArrowData.ok) return; + + const deArrowDataJson = JSON.parse(deArrowData.responseText); + const title = deArrowDataJson?.[getVideoID()]?.titles?.[0]; + if (title && title.title && (title.locked || title.votes > 0)) { + Config.config.showDeArrowPromotion = false; + + tooltip = new Tooltip({ + text: chrome.i18n.getMessage("DeArrowTitleReplacementSuggestion") + "\n\n" + title.title, + linkOnClick: () => { + window.open("https://dearrow.ajay.app"); + Config.config.shownDeArrowPromotion = true; + }, + referenceNode: element, + prependElement: element.firstElementChild as HTMLElement, + timeout: 15000, + positionRealtive: false, + containerAbsolute: true, + bottomOffset: "inherit", + topOffset: "55px", + leftOffset: "0", + rightOffset: "0", + topTriangle: true, + center: true, + opacity: 1 + }); + } + } + } catch { } // eslint-disable-line no-empty + } else { + Config.config.showDeArrowPromotion = false; + } + } +} + +/** + * Two upper case words (at least 2 letters long) + */ +function badTitle(title: string): boolean { + return !!title.match(/\p{Lu}{2,} \p{Lu}{2,}[.!? ]/u); +} + +export function hideDeArrowPromotion(): void { + if (tooltip) tooltip.close(); +} \ No newline at end of file diff --git a/src/options.ts b/src/options.ts index 8acdd86e..6b088538 100644 --- a/src/options.ts +++ b/src/options.ts @@ -18,6 +18,7 @@ import { StorageChangesObject } from "../maze-utils/src/config"; import { getHash } from "../maze-utils/src/hash"; import { isFirefoxOrSafari } from "../maze-utils/src"; import { isDeArrowInstalled } from "./utils/crossExtension"; +import { asyncRequestToServer } from "./utils/requests"; const utils = new Utils(); let embed = false; @@ -567,7 +568,7 @@ function activatePrivateTextChange(element: HTMLElement) { switch (option) { case "userID": if (Config.config[option]) { - utils.asyncRequestToServer("GET", "/api/userInfo", { + asyncRequestToServer("GET", "/api/userInfo", { publicUserID: getHash(Config.config[option]), values: ["warnings", "banned"] }).then((result) => { diff --git a/src/popup.ts b/src/popup.ts index 41fb9210..21267afe 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -27,6 +27,7 @@ import GenericNotice from "./render/GenericNotice"; import { getErrorMessage, getFormattedTime } from "../maze-utils/src/formating"; import { StorageChangesObject } from "../maze-utils/src/config"; import { getHash } from "../maze-utils/src/hash"; +import { asyncRequestToServer, sendRequestToServer } from "./utils/requests"; const utils = new Utils(); @@ -295,7 +296,7 @@ async function runThePopup(messageListener?: MessageListener): Promise { const values = ["userName", "viewCount", "minutesSaved", "vip", "permissions"]; - utils.asyncRequestToServer("GET", "/api/userInfo", { + asyncRequestToServer("GET", "/api/userInfo", { publicUserID: await getHash(Config.config.userID), values }).then((res) => { @@ -818,7 +819,7 @@ async function runThePopup(messageListener?: MessageListener): Promise { PageElements.setUsernameStatus.style.display = "unset"; PageElements.setUsernameStatus.innerText = chrome.i18n.getMessage("Loading"); - utils.sendRequestToServer("POST", "/api/setUsername?userID=" + Config.config.userID + "&username=" + PageElements.usernameInput.value, function (response) { + sendRequestToServer("POST", "/api/setUsername?userID=" + Config.config.userID + "&username=" + PageElements.usernameInput.value, function (response) { if (response.status == 200) { //submitted PageElements.submitUsername.style.display = "none"; diff --git a/src/utils.ts b/src/utils.ts index 7b3a3841..c96a7772 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,10 +2,8 @@ import Config, { VideoDownvotes } from "./config"; import { CategorySelection, SponsorTime, BackgroundScriptContainer, Registration, VideoID, SponsorHideType, CategorySkipOption } from "./types"; import { getHash, HashedValue } from "../maze-utils/src/hash"; -import * as CompileConfig from "../config.json"; import { isFirefoxOrSafari, waitFor } from "../maze-utils/src"; import { findValidElementFromSelector } from "../maze-utils/src/dom"; -import { FetchResponse, sendRequestToCustomServer } from "../maze-utils/src/background-request-proxy" import { isSafari } from "../maze-utils/src/config"; export default class Utils { @@ -240,50 +238,6 @@ export default class Utils { return permissionRegex; } - /** - * Sends a request to a custom server - * - * @param type The request type. "GET", "POST", etc. - * @param address The address to add to the SponsorBlock server address - * @param callback - */ - asyncRequestToCustomServer(type: string, url: string, data = {}): Promise { - return sendRequestToCustomServer(type, url, data); - } - - /** - * Sends a request to the SponsorBlock server with address added as a query - * - * @param type The request type. "GET", "POST", etc. - * @param address The address to add to the SponsorBlock server address - * @param callback - */ - async asyncRequestToServer(type: string, address: string, data = {}): Promise { - const serverAddress = Config.config.testingServer ? CompileConfig.testingServerAddress : Config.config.serverAddress; - - return await (this.asyncRequestToCustomServer(type, serverAddress + address, data)); - } - - /** - * Sends a request to the SponsorBlock server with address added as a query - * - * @param type The request type. "GET", "POST", etc. - * @param address The address to add to the SponsorBlock server address - * @param callback - */ - sendRequestToServer(type: string, address: string, callback?: (response: FetchResponse) => void): void { - const serverAddress = Config.config.testingServer ? CompileConfig.testingServerAddress : Config.config.serverAddress; - - // Ask the background script to do the work - chrome.runtime.sendMessage({ - message: "sendRequest", - type, - url: serverAddress + address - }, (response) => { - callback(response); - }); - } - findReferenceNode(): HTMLElement { const selectors = [ "#player-container-id", // Mobile YouTube diff --git a/src/utils/requests.ts b/src/utils/requests.ts new file mode 100644 index 00000000..8c160eb0 --- /dev/null +++ b/src/utils/requests.ts @@ -0,0 +1,47 @@ +import Config from "../config"; +import * as CompileConfig from "../../config.json"; +import { FetchResponse, sendRequestToCustomServer } from "../../maze-utils/src/background-request-proxy"; + +/** + * Sends a request to a custom server + * + * @param type The request type. "GET", "POST", etc. + * @param address The address to add to the SponsorBlock server address + * @param callback + */ +export function asyncRequestToCustomServer(type: string, url: string, data = {}): Promise { + return sendRequestToCustomServer(type, url, data); +} + +/** + * Sends a request to the SponsorBlock server with address added as a query + * + * @param type The request type. "GET", "POST", etc. + * @param address The address to add to the SponsorBlock server address + * @param callback + */ +export async function asyncRequestToServer(type: string, address: string, data = {}): Promise { + const serverAddress = Config.config.testingServer ? CompileConfig.testingServerAddress : Config.config.serverAddress; + + return await (asyncRequestToCustomServer(type, serverAddress + address, data)); +} + +/** + * Sends a request to the SponsorBlock server with address added as a query + * + * @param type The request type. "GET", "POST", etc. + * @param address The address to add to the SponsorBlock server address + * @param callback + */ +export function sendRequestToServer(type: string, address: string, callback?: (response: FetchResponse) => void): void { + const serverAddress = Config.config.testingServer ? CompileConfig.testingServerAddress : Config.config.serverAddress; + + // Ask the background script to do the work + chrome.runtime.sendMessage({ + message: "sendRequest", + type, + url: serverAddress + address + }, (response) => { + callback(response); + }); +} \ No newline at end of file diff --git a/src/utils/videoLabels.ts b/src/utils/videoLabels.ts index 731bfd33..82af788c 100644 --- a/src/utils/videoLabels.ts +++ b/src/utils/videoLabels.ts @@ -2,6 +2,7 @@ import { Category, CategorySkipOption, VideoID } from "../types"; import { getHash } from "../../maze-utils/src/hash"; import Utils from "../utils"; import { logWarn } from "./logger"; +import { asyncRequestToServer } from "./requests"; const utils = new Utils(); @@ -20,7 +21,7 @@ async function getLabelHashBlock(hashPrefix: string): Promise { - const userInfo = await utils.asyncRequestToServer("GET", "/api/userInfo", { + const userInfo = await asyncRequestToServer("GET", "/api/userInfo", { publicUserID: await getHash(Config.config.userID), values: ["warningReason"] }); if (userInfo.ok) { const warningReason = JSON.parse(userInfo.responseText)?.warningReason; - const userNameData = await utils.asyncRequestToServer("GET", "/api/getUsername?userID=" + Config.config.userID); + const userNameData = await asyncRequestToServer("GET", "/api/getUsername?userID=" + Config.config.userID); const userName = userNameData.ok ? JSON.parse(userNameData.responseText).userName : ""; const publicUserID = await getHash(Config.config.userID); @@ -43,7 +42,7 @@ export async function openWarningDialog(contentContainer: ContentContainer): Pro { name: chrome.i18n.getMessage("warningConfirmButton"), listener: async () => { - const result = await utils.asyncRequestToServer("POST", "/api/warnUser", { + const result = await asyncRequestToServer("POST", "/api/warnUser", { userID: Config.config.userID, enabled: false });