diff --git a/public/_locales b/public/_locales index 452c7f3..73ad30d 160000 --- a/public/_locales +++ b/public/_locales @@ -1 +1 @@ -Subproject commit 452c7f3ef6704340796ed0e506f694e84148c9f4 +Subproject commit 73ad30d9f66adc90c70752f9d0089774dc86393a diff --git a/public/content.css b/public/content.css index 5e5c3d3..4a70f9d 100644 --- a/public/content.css +++ b/public/content.css @@ -334,6 +334,10 @@ ytd-compact-playlist-renderer .ytd-compact-playlist-renderer #video-title:not(.c vertical-align: middle; } +.cbTitle { + position: relative; +} + .cbTitle:not(.cbTitleSelected, .cbTitlePreview) { cursor: pointer !important; } diff --git a/src/submission/SubmissionComponent.tsx b/src/submission/SubmissionComponent.tsx index 0cc1c34..3de15b0 100644 --- a/src/submission/SubmissionComponent.tsx +++ b/src/submission/SubmissionComponent.tsx @@ -24,6 +24,7 @@ import { Tooltip } from "../utils/tooltip"; import { LicenseComponent } from "../license/LicenseComponent"; import { ToggleOptionComponent } from "../popup/ToggleOptionComponent"; import { FormattedText } from "../popup/FormattedTextComponent"; +import { isAutoWarningShown } from "./autoWarning"; export interface SubmissionComponentProps { videoID: VideoID; @@ -33,7 +34,7 @@ export interface SubmissionComponentProps { submitClicked: (title: TitleSubmission | null, thumbnail: ThumbnailSubmission | null, actAsVip: boolean) => Promise; } -interface ChatDisplayName { +export interface ChatDisplayName { publicUserID: string; username: string | null; } @@ -51,10 +52,10 @@ export const SubmissionComponent = (props: SubmissionComponentProps) => { setChatDisplayName(displayName); const values = ["userName", "deArrowWarningReason"]; - const result = await sendRequestToServer("GET", "/api/userInfo", { - publicUserID: publicUserID, - values - }); + const result = await sendRequestToServer("GET", "/api/userInfo", { + publicUserID: publicUserID, + values + }); if (result.ok) { const userInfo = JSON.parse(result.responseText); @@ -341,6 +342,11 @@ export const SubmissionComponent = (props: SubmissionComponentProps) => { || (!selectedThumbnail.current && !selectedTitle) || (!!selectedTitle && selectedTitle.title.toLowerCase() === chrome.i18n.getMessage("OriginalTitle").toLowerCase())} onClick={async () => { + if (isAutoWarningShown()) { + alert(chrome.i18n.getMessage("resolveWarningFirst")); + return; + } + setCurrentlySubmitting(true); props.submitClicked(selectedTitle ? { @@ -479,7 +485,7 @@ function updateUnsubmitted(unsubmitted: UnsubmittedSubmission, } } -function getChatDisplayName(chatDisplayName: ChatDisplayName | null): string { +export function getChatDisplayName(chatDisplayName: ChatDisplayName | null): string { if (chatDisplayName) { if (chatDisplayName.username && chatDisplayName.username !== chatDisplayName.publicUserID) { return `${chatDisplayName.username} - ${chatDisplayName.publicUserID}`; diff --git a/src/submission/TitleComponent.tsx b/src/submission/TitleComponent.tsx index 22eb79d..a02f764 100644 --- a/src/submission/TitleComponent.tsx +++ b/src/submission/TitleComponent.tsx @@ -8,6 +8,7 @@ import { submitVideoBrandingAndHandleErrors } from "../dataFetching"; import { AnimationUtils } from "../../maze-utils/src/animationUtils"; import { VideoID } from "../../maze-utils/src/video"; import { shouldStoreVotes } from "../utils/configUtils"; +import { showAutoWarningIfRequired } from "./autoWarning"; export interface TitleComponentProps { submission: RenderedTitleSubmission; @@ -84,6 +85,8 @@ export const TitleComponent = (props: TitleComponentProps) => { setTitleChanged(newTitle !== props.submission.title); setFocused(true); + + showAutoWarningIfRequired(newTitle, e.target as HTMLElement); } }} onKeyDown={(e) => { diff --git a/src/submission/autoWarning.ts b/src/submission/autoWarning.ts new file mode 100644 index 0000000..29e99c7 --- /dev/null +++ b/src/submission/autoWarning.ts @@ -0,0 +1,199 @@ +import { objectToURI } from "../../maze-utils/src"; +import { getHash } from "../../maze-utils/src/hash"; +import Config from "../config/config"; +import { getCurrentPageTitle } from "../titles/titleData"; +import { cleanEmojis, cleanFancyText, cleanPunctuation } from "../titles/titleFormatter"; +import { sendRequestToServer } from "../utils/requests"; +import { Tooltip } from "../utils/tooltip"; +import { ChatDisplayName, getChatDisplayName } from "./SubmissionComponent"; + +interface AutoWarningCheck { + check: (title: string, originalTitle: string) => { + found: boolean; + match?: string | null; + }; + error: string; + id: string; +} + +let activeTooltip: Tooltip | null = null; +let currentWarningId: string | null = null; +let timeout: NodeJS.Timeout | null = null; + +const shownWarnings: string[] = []; +const autoWarningChecks: AutoWarningCheck[] = [ + { + error: chrome.i18n.getMessage("DeArrowDiscussingWarning"), + check: (title) => { + const match = title.match(/^(discussing|explaining|talking about|summarizing) .\S+ .\S+/i)?.[1]; + return { + found: !!match, + match, + }; + }, + id: "discussing" + }, { + error: chrome.i18n.getMessage("DeArrowStartLowerCaseWarning"), + check: (title) => { + return { + found: !!title.match(/^\p{Ll}\S+ \S+ \S+/u) + }; + }, + id: "startLowerCase" + }, { + error: chrome.i18n.getMessage("DeArrowEndWithPeriodWarning"), + check: (title) => { + return { + found: !!title.match(/\.$/u) + }; + }, + id: "endWithPeriod" + }, { + error: chrome.i18n.getMessage("DeArrowAddingAnswerWarning"), + check: (title, originalTitle) => { + // Only if ends with ? or ... and then optionally more symbols + return { + found: title.toLowerCase().startsWith(cleanPunctuation(cleanFancyText(cleanEmojis(originalTitle.toLowerCase())))) + && !!originalTitle.match(/(\?|\.\.\.)[^\p{L}]*$/u) + }; + }, + id: "addingAnswer" + }, { + error: chrome.i18n.getMessage("DeArrowClickbaitWarning"), + check: (title, originalTitle) => { + const regex = /clickbait|fake news|fake video|boring|yapping|yap|worth your time/i; + const match = title.match(regex)?.[0]; + const found = !!title.match(regex) && !originalTitle.match(regex); + + return { + found, + match: found ? match : null, + }; + }, + id: "clickbait" + }, { + error: chrome.i18n.getMessage("DeArrowKeepingBadOriginalWarning"), + check: (title, originalTitle) => { + const regex = /massive problem|you need|insane|crazy|wild|you won't believe this/i; + const match = title.match(regex)?.[0]; + const found = !!title.match(regex) && !!originalTitle.match(regex); + + return { + found, + match: found ? match : null, + }; + }, + id: "keepingBadOriginal" + }, { + error: chrome.i18n.getMessage("DeArrowEmojiWarning"), + check: (title) => { + return { + found: cleanEmojis(title) !== title.trim() + }; + }, + id: "emoji" + } +]; + +function getAutoWarning(title: string, originalTitle: string): { id: string; text: string } | null { + for (const check of autoWarningChecks) { + const { found, match } = check.check(title, originalTitle); + if (found && !shownWarnings.includes(check.id)) { + return { + id: check.id, + text: check.error + (match ? `\n\n${chrome.i18n.getMessage("DetectedWord")}${match}` : "") + }; + } + } + + return null; +} + +export function showAutoWarningIfRequired(title: string, element: HTMLElement): void { + // Wait until some time after typing stops + if (timeout) { + clearTimeout(timeout); + } + + timeout = setTimeout(() => { + showAutoWarningIfRequiredInternal(title, element); + }, 500) +} + +function showAutoWarningIfRequiredInternal(title: string, element: HTMLElement): void { + timeout = null; + + const originalTitle = getCurrentPageTitle() || ""; + console.log(originalTitle) + const warning = getAutoWarning(title, originalTitle); + if (warning && warning.id !== currentWarningId) { + activeTooltip?.close(); + + currentWarningId = warning.id; + activeTooltip = new Tooltip({ + textBoxes: warning.text.split("\n"), + referenceNode: element.parentElement!, + prependElement: element, + positionRealtive: false, + containerAbsolute: true, + bottomOffset: "35px", + rightOffset: "0", + leftOffset: "0", + displayTriangle: true, + extraClass: "centeredSBTriangle", + center: true, + showGotIt: false, + buttonsAtBottom: true, + textBoxMaxHeight: "350px", + opacity: 1, + buttons: [{ + name: chrome.i18n.getMessage("GotIt"), + listener: () => { + shownWarnings.push(warning.id); + activeTooltip?.close(); + activeTooltip = null; + currentWarningId = null; + } + }, { + name: chrome.i18n.getMessage("questionButton"), + // eslint-disable-next-line @typescript-eslint/no-misused-promises + listener: async () => { + const publicUserID = await getHash(Config.config!.userID!); + + const values = ["userName"]; + const result = await sendRequestToServer("GET", "/api/userInfo", { + publicUserID: publicUserID, + values + }); + + let name: ChatDisplayName | null = null; + + if (result.ok) { + const userInfo = JSON.parse(result.responseText); + name = { + publicUserID, + username: userInfo.userName + }; + } + + window.open(`https://chat.sponsor.ajay.app/#${objectToURI("", { + displayName: getChatDisplayName(name), + customDescription: `${chrome.i18n.getMessage("chatboxDescription")}\n\nhttps://discord.gg/SponsorBlock\nhttps://matrix.to/#/#sponsor:ajay.app?via=matrix.org`, + bigDescription: true + }, false)}`); + } + }], + }); + } +} + +export function resetShownWarnings(): void { + shownWarnings.length = 0; + activeTooltip?.close(); + activeTooltip = null; + currentWarningId = null; +} + +export function isAutoWarningShown(): boolean { + return !!activeTooltip; +} \ No newline at end of file diff --git a/src/video.ts b/src/video.ts index 3d1714b..3bed491 100644 --- a/src/video.ts +++ b/src/video.ts @@ -10,6 +10,7 @@ import { listenForBadges, listenForMiniPlayerTitleChange, listenForTitleChange } import { getPlaybackFormats } from "./thumbnails/thumbnailData"; import { replaceVideoPlayerSuggestionsBranding, setupMobileAutoplayHandler } from "./videoBranding/watchPageBrandingHandler"; import { onMobile } from "../maze-utils/src/pageInfo"; +import { resetShownWarnings } from "./submission/autoWarning"; export const submitButton = new SubmitButton(); @@ -48,6 +49,8 @@ function resetValues() { submitButton.close(); clearVideoBrandingInstances(); + + resetShownWarnings(); } // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars