aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorAjay <[email protected]>2022-08-19 23:16:45 -0400
committerAjay <[email protected]>2022-08-19 23:16:45 -0400
commit42d76cf2579968116473cdc24b70d18f84277b66 (patch)
tree503e347a52680399d8363472ec5414457a2ec38b /src
parentd06b7411dc828b1afcc100b439ff483106c2ab74 (diff)
parent780ea4a9d0b12762d10e787786816e3e8a6f9b26 (diff)
downloadSponsorBlock-42d76cf2579968116473cdc24b70d18f84277b66.tar.gz
SponsorBlock-42d76cf2579968116473cdc24b70d18f84277b66.zip
Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters
Diffstat (limited to 'src')
-rw-r--r--src/background.ts10
-rw-r--r--src/components/NoticeTextSectionComponent.tsx21
-rw-r--r--src/components/SponsorTimeEditComponent.tsx14
-rw-r--r--src/components/options/CategorySkipOptionsComponent.tsx37
-rw-r--r--src/content.ts219
-rw-r--r--src/js-components/chat.ts47
-rw-r--r--src/messageTypes.ts3
-rw-r--r--src/options.ts33
-rw-r--r--src/permissions.ts20
-rw-r--r--src/popup.ts6
-rw-r--r--src/render/GenericNotice.tsx10
-rw-r--r--src/utils.ts123
-rw-r--r--src/utils/genericUtils.ts25
-rw-r--r--src/utils/warnings.ts66
14 files changed, 397 insertions, 237 deletions
diff --git a/src/background.ts b/src/background.ts
index 15b4c15d..80f0cd8d 100644
--- a/src/background.ts
+++ b/src/background.ts
@@ -125,7 +125,7 @@ chrome.runtime.onConnect.addListener((port) => {
chrome.runtime.onInstalled.addListener(function () {
// This let's the config sync to run fully before checking.
// This is required on Firefox
- setTimeout(function() {
+ setTimeout(async () => {
const userID = Config.config.userID;
// If there is no userID, then it is the first install.
@@ -141,6 +141,12 @@ chrome.runtime.onInstalled.addListener(function () {
// Don't show update notification
Config.config.categoryPillUpdate = true;
}
+
+ if (Config.config.supportInvidious) {
+ if (!(await utils.containsInvidiousPermission())) {
+ chrome.tabs.create({url: chrome.extension.getURL("/permissions/index.html")});
+ }
+ }
}, 1500);
});
@@ -224,7 +230,7 @@ async function asyncRequestToServer(type: string, address: string, data = {}) {
async function sendRequestToCustomServer(type: string, url: string, data = {}) {
// If GET, convert JSON to parameters
if (type.toLowerCase() === "get") {
- url = utils.objectToURI(url, data, true);
+ url = GenericUtils.objectToURI(url, data, true);
data = null;
}
diff --git a/src/components/NoticeTextSectionComponent.tsx b/src/components/NoticeTextSectionComponent.tsx
index 71fcb263..122d0fde 100644
--- a/src/components/NoticeTextSectionComponent.tsx
+++ b/src/components/NoticeTextSectionComponent.tsx
@@ -36,12 +36,31 @@ class NoticeTextSelectionComponent extends React.Component<NoticeTextSelectionPr
: null}
<span>
- {this.props.text}
+ {this.getTextElements(this.props.text)}
</span>
</td>
</tr>
);
}
+
+ private getTextElements(text: string): Array<string | React.ReactElement> {
+ const elements: Array<string | React.ReactElement> = [];
+ const textParts = text.split(/(?=\s+)/);
+ for (const textPart of textParts) {
+ if (textPart.match(/^\s*http/)) {
+ elements.push(
+ <a href={textPart} target="_blank" rel="noreferrer">
+ {textPart}
+ </a>
+ );
+ } else {
+ elements.push(textPart);
+ }
+
+ }
+
+ return elements;
+ }
}
export default NoticeTextSelectionComponent; \ No newline at end of file
diff --git a/src/components/SponsorTimeEditComponent.tsx b/src/components/SponsorTimeEditComponent.tsx
index 17014882..1515644a 100644
--- a/src/components/SponsorTimeEditComponent.tsx
+++ b/src/components/SponsorTimeEditComponent.tsx
@@ -115,14 +115,6 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
style.marginTop = "15px";
}
- // This method is required to get !important
- // https://stackoverflow.com/a/45669262/1985387
- const oldYouTubeDarkStyles = (node) => {
- if (node) {
- node.style.setProperty("color", "black", "important");
- node.style.setProperty("text-shadow", "none", "important");
- }
- };
// Create time display
let timeDisplay: JSX.Element;
const timeDisplayStyle: React.CSSProperties = {};
@@ -142,8 +134,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
</span>
<input id={"submittingTime0" + this.idSuffix}
className="sponsorTimeEdit sponsorTimeEditInput"
- ref={oldYouTubeDarkStyles}
type="text"
+ style={{color: "inherit", backgroundColor: "inherit"}}
value={this.state.sponsorTimeEdits[0]}
onChange={(e) => this.handleOnChange(0, e, sponsorTime, e.target.value)}
onWheel={(e) => this.changeTimesWhenScrolling(0, e, sponsorTime)}>
@@ -157,8 +149,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
<input id={"submittingTime1" + this.idSuffix}
className="sponsorTimeEdit sponsorTimeEditInput"
- ref={oldYouTubeDarkStyles}
type="text"
+ style={{color: "inherit", backgroundColor: "inherit"}}
value={this.state.sponsorTimeEdits[1]}
onChange={(e) => this.handleOnChange(1, e, sponsorTime, e.target.value)}
onWheel={(e) => this.changeTimesWhenScrolling(1, e, sponsorTime)}>
@@ -204,6 +196,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
className="sponsorTimeEditSelector sponsorTimeCategories"
defaultValue={sponsorTime.category}
ref={this.categoryOptionRef}
+ style={{color: "inherit", backgroundColor: "inherit"}}
onChange={(event) => this.categorySelectionChange(event)}>
{this.getCategoryOptions()}
</select>
@@ -227,6 +220,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
<select id={"sponsorTimeActionTypes" + this.idSuffix}
className="sponsorTimeEditSelector sponsorTimeActionTypes"
defaultValue={sponsorTime.actionType}
+ style={{color: "inherit", backgroundColor: "inherit"}}
ref={this.actionTypeOptionRef}
onChange={(e) => this.actionTypeSelectionChange(e)}>
{this.getActionTypeOptions(sponsorTime)}
diff --git a/src/components/options/CategorySkipOptionsComponent.tsx b/src/components/options/CategorySkipOptionsComponent.tsx
index 32083ccb..b298347e 100644
--- a/src/components/options/CategorySkipOptionsComponent.tsx
+++ b/src/components/options/CategorySkipOptionsComponent.tsx
@@ -116,10 +116,10 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
skipOptionSelected(event: React.ChangeEvent<HTMLSelectElement>): void {
let option: CategorySkipOption;
- this.removeCurrentCategorySelection();
-
switch (event.target.value) {
- case "disable":
+ case "disable":
+ Config.config.categorySelections = Config.config.categorySelections.filter(
+ categorySelection => categorySelection.name !== this.props.category);
return;
case "showOverlay":
option = CategorySkipOption.ShowOverlay;
@@ -135,28 +135,17 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
break;
}
- Config.config.categorySelections.push({
- name: this.props.category,
- option: option
- });
-
- // Forces the Proxy to send this to the chrome storage API
- Config.config.categorySelections = Config.config.categorySelections;
- }
-
- /** Removes this category from the config list of category selections */
- removeCurrentCategorySelection(): void {
- // Remove it if it exists
- for (let i = 0; i < Config.config.categorySelections.length; i++) {
- if (Config.config.categorySelections[i].name === this.props.category) {
- Config.config.categorySelections.splice(i, 1);
-
- // Forces the Proxy to send this to the chrome storage API
- Config.config.categorySelections = Config.config.categorySelections;
-
- break;
- }
+ const existingSelection = Config.config.categorySelections.find(selection => selection.name === this.props.category);
+ if (existingSelection) {
+ existingSelection.option = option;
+ } else {
+ Config.config.categorySelections.push({
+ name: this.props.category,
+ option: option
+ });
}
+
+ Config.forceSyncUpdate("categorySelections");
}
getCategorySkipOptions(): JSX.Element[] {
diff --git a/src/content.ts b/src/content.ts
index f30d5760..fc611a89 100644
--- a/src/content.ts
+++ b/src/content.ts
@@ -10,7 +10,6 @@ import SkipNotice from "./render/SkipNotice";
import SkipNoticeComponent from "./components/SkipNoticeComponent";
import SubmissionNotice from "./render/SubmissionNotice";
import { Message, MessageResponse, VoteResponse } from "./messageTypes";
-import * as Chat from "./js-components/chat";
import { SkipButtonControlBar } from "./js-components/skipButtonControlBar";
import { getStartTimeFromUrl } from "./utils/urlParser";
import { findValidElement, getControls, getExistingChapters, getHashParams, isVisible } from "./utils/pageUtils";
@@ -20,6 +19,7 @@ import { AnimationUtils } from "./utils/animationUtils";
import { GenericUtils } from "./utils/genericUtils";
import { logDebug } from "./utils/logger";
import { importTimes } from "./utils/exporter";
+import { openWarningDialog } from "./utils/warnings";
// Hack to get the CSS loaded on permission-based sites (Invidious)
utils.wait(() => Config.config !== null, 5000, 10).then(addCSS);
@@ -62,13 +62,14 @@ let sponsorSkipped: boolean[] = [];
let video: HTMLVideoElement;
let videoMuted = false; // Has it been attempted to be muted
let videoMutationObserver: MutationObserver = null;
+let waitingForNewVideo = false;
// List of videos that have had event listeners added to them
const videosWithEventListeners: HTMLVideoElement[] = [];
const controlsWithEventListeners: HTMLElement[] = []
// This misleading variable name will be fixed soon
-let onInvidious;
-let onMobileYouTube;
+let onInvidious: boolean;
+let onMobileYouTube: boolean;
//the video id of the last preview bar update
let lastPreviewBarUpdate;
@@ -76,9 +77,6 @@ let lastPreviewBarUpdate;
// Is the video currently being switched
let switchingVideos = null;
-// Made true every videoID change
-let firstEvent = false;
-
// Used by the play and playing listeners to make sure two aren't
// called at the same time
let lastCheckTime = 0;
@@ -102,7 +100,8 @@ const playerButtons: Record<string, {button: HTMLButtonElement, image: HTMLImage
// Direct Links after the config is loaded
utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYouTubeVideoID(document)));
// wait for hover preview to appear, and refresh attachments if ever found
-window.addEventListener("DOMContentLoaded", () => utils.waitForElement(".ytp-inline-preview-ui").then(() => refreshVideoAttachments()));
+utils.waitForElement(".ytp-inline-preview-ui").then(() => refreshVideoAttachments())
+utils.waitForElement("a.ytp-title-link[data-sessionlink='feature=player-title']").then(() => videoIDChange(getYouTubeVideoID(document)).then())
addPageListeners();
addHotkeyListener();
@@ -119,6 +118,9 @@ let submissionNotice: SubmissionNotice = null;
// If there is an advert playing (or about to be played), this is true
let isAdPlaying = false;
+let lastResponseStatus: number;
+let retryCount = 0;
+
// Contains all of the functions and variables needed by the skip notice
const skipNoticeContentContainer: ContentContainer = () => ({
vote,
@@ -166,6 +168,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
//send the sponsor times along with if it's found
sendResponse({
found: sponsorDataFound,
+ status: lastResponseStatus,
sponsorTimes: sponsorTimes,
time: video.currentTime,
onMobileYouTube
@@ -206,8 +209,12 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
submitSponsorTimes();
break;
case "refreshSegments":
+ // update video on refresh if videoID invalid
+ if (!sponsorVideoID) videoIDChange(getYouTubeVideoID(document));
+ // fetch segments
sponsorsLookup(false).then(() => sendResponse({
found: sponsorDataFound,
+ status: lastResponseStatus,
sponsorTimes: sponsorTimes,
time: video.currentTime,
onMobileYouTube
@@ -301,6 +308,7 @@ if (!Config.configSyncListeners.includes(contentConfigUpdateListener)) {
function resetValues() {
lastCheckTime = 0;
lastCheckVideoTime = -1;
+ retryCount = 0;
sponsorTimes = [];
existingChaptersImported = false;
@@ -330,8 +338,6 @@ function resetValues() {
logDebug("Setting switching videos to true (reset data)");
}
- firstEvent = true;
-
// Reset advert playing flag
isAdPlaying = false;
@@ -343,7 +349,7 @@ function resetValues() {
categoryPill?.setVisibility(false);
}
-async function videoIDChange(id) {
+async function videoIDChange(id): Promise<void> {
//if the id has not changed return unless the video element has changed
if (sponsorVideoID === id && (isVisible(video) || !video)) return;
@@ -447,7 +453,7 @@ function createPreviewBar(): void {
isVisibleCheck: true
}, {
// For new mobile YouTube (#1287)
- selector: ".ytm-progress-bar",
+ selector: ".progress-bar-line",
isVisibleCheck: true
}, {
// For Desktop YouTube
@@ -527,6 +533,13 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
return;
}
+ // ensure we are on the correct video
+ const newVideoID = getYouTubeVideoID(document);
+ if (newVideoID !== sponsorVideoID) {
+ videoIDChange(newVideoID);
+ return;
+ }
+
logDebug(`Considering to start skipping: ${!video}, ${video?.paused}`);
if (!video) return;
if (currentTime === undefined || currentTime === null) {
@@ -537,7 +550,16 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
updateActiveSegment(currentTime);
if (video.paused) return;
- if (videoMuted && !inMuteSegment(currentTime)) {
+ const skipInfo = getNextSkipIndex(currentTime, includeIntersectingSegments, includeNonIntersectingSegments);
+
+ const currentSkip = skipInfo.array[skipInfo.index];
+ const skipTime: number[] = [currentSkip?.scheduledTime, skipInfo.array[skipInfo.endIndex]?.segment[1]];
+ const timeUntilSponsor = skipTime?.[0] - currentTime;
+ const videoID = sponsorVideoID;
+ const skipBuffer = 0.003;
+
+ if (videoMuted && !inMuteSegment(currentTime, skipInfo.index !== -1
+ && timeUntilSponsor < skipBuffer && shouldAutoSkip(currentSkip))) {
video.muted = false;
videoMuted = false;
@@ -547,22 +569,15 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
}
}
+ logDebug(`Ready to start skipping: ${skipInfo.index} at ${currentTime}`);
+ if (skipInfo.index === -1) return;
+
if (Config.config.disableSkipping || channelWhitelisted || (channelIDInfo.status === ChannelIDStatus.Fetching && Config.config.forceChannelCheck)){
return;
}
if (incorrectVideoCheck()) return;
- const skipInfo = getNextSkipIndex(currentTime, includeIntersectingSegments, includeNonIntersectingSegments);
-
- logDebug(`Ready to start skipping: ${skipInfo.index} at ${currentTime}`);
- if (skipInfo.index === -1) return;
-
- const currentSkip = skipInfo.array[skipInfo.index];
- const skipTime: number[] = [currentSkip.scheduledTime, skipInfo.array[skipInfo.endIndex].segment[1]];
- const timeUntilSponsor = skipTime[0] - currentTime;
- const videoID = sponsorVideoID;
-
// Find all indexes in between the start and end
let skippingSegments = [skipInfo.array[skipInfo.index]];
if (skipInfo.index !== skipInfo.endIndex) {
@@ -576,7 +591,11 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
}
}
- const skipBuffer = 0.003;
+ logDebug(`Next step in starting skipping: ${!shouldSkip(currentSkip)}, ${!sponsorTimesSubmitting?.some((segment) => segment.segment === currentSkip.segment)}`);
+
+ // Don't skip if this category should not be skipped
+ if (!shouldSkip(currentSkip) && !sponsorTimesSubmitting?.some((segment) => segment.segment === currentSkip.segment)) return;
+
const skippingFunction = (forceVideoTime?: number) => {
let forcedSkipTime: number = null;
let forcedIncludeIntersectingSegments = false;
@@ -593,6 +612,19 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
skippingSegments,
openNotice: skipInfo.openNotice
});
+
+ // These are segments that start at the exact same time but need seperate notices
+ for (const extra of skipInfo.extraIndexes) {
+ const extraSkip = skipInfo.array[extra];
+ if (shouldSkip(extraSkip)) {
+ skipToTime({
+ v: video,
+ skipTime: [extraSkip.scheduledTime, extraSkip.segment[1]],
+ skippingSegments: [extraSkip],
+ openNotice: skipInfo.openNotice
+ });
+ }
+ }
if (utils.getCategorySelection(currentSkip.category)?.option === CategorySkipOption.ManualSkip
|| currentSkip.actionType === ActionType.Mute) {
@@ -649,15 +681,17 @@ function getVirtualTime(): number {
(performance.now() - lastKnownVideoTime.preciseTime) / 1000 + lastKnownVideoTime.videoTime : null);
if ((lastTimeFromWaitingEvent || !utils.isFirefox())
- && !isSafari() && virtualTime && Math.abs(virtualTime - video.currentTime) < 0.6) {
+ && !isSafari() && virtualTime && Math.abs(virtualTime - video.currentTime) < 0.6 && video.currentTime !== 0) {
return virtualTime;
} else {
return video.currentTime;
}
}
-function inMuteSegment(currentTime: number): boolean {
- const checkFunction = (segment) => segment.actionType === ActionType.Mute && segment.segment[0] <= currentTime && segment.segment[1] > currentTime;
+function inMuteSegment(currentTime: number, includeOverlap: boolean): boolean {
+ const checkFunction = (segment) => segment.actionType === ActionType.Mute
+ && segment.segment[0] <= currentTime
+ && (segment.segment[1] > currentTime || (includeOverlap && segment.segment[1] + 0.02 > currentTime));
return sponsorTimes?.some(checkFunction) || sponsorTimesSubmitting.some(checkFunction);
}
@@ -695,27 +729,30 @@ function setupVideoMutationListener() {
});
}
-function refreshVideoAttachments() {
- const newVideo = findValidElement(document.querySelectorAll('video')) as HTMLVideoElement;
- if (newVideo && newVideo !== video) {
- video = newVideo;
+async function refreshVideoAttachments(): Promise<void> {
+ if (waitingForNewVideo) return;
- if (!videosWithEventListeners.includes(video)) {
- videosWithEventListeners.push(video);
+ waitingForNewVideo = true;
+ const newVideo = await utils.waitForElement("video", true) as HTMLVideoElement;
+ waitingForNewVideo = false;
- setupVideoListeners();
- setupSkipButtonControlBar();
- setupCategoryPill();
- }
+ video = newVideo;
+ if (!videosWithEventListeners.includes(video)) {
+ videosWithEventListeners.push(video);
- // Create a new bar in the new video element
- if (previewBar && !utils.findReferenceNode()?.contains(previewBar.container)) {
- previewBar.remove();
- previewBar = null;
+ setupVideoListeners();
+ setupSkipButtonControlBar();
+ setupCategoryPill();
+ }
- createPreviewBar();
- }
+ if (previewBar && !utils.findReferenceNode()?.contains(previewBar.container)) {
+ previewBar.remove();
+ previewBar = null;
+
+ createPreviewBar();
}
+
+ videoIDChange(getYouTubeVideoID(document));
}
function setupVideoListeners() {
@@ -727,17 +764,18 @@ function setupVideoListeners() {
switchingVideos = false;
let startedWaiting = false;
+ let lastPausedAtZero = true;
video.addEventListener('play', () => {
// If it is not the first event, then the only way to get to 0 is if there is a seek event
// This check makes sure that changing the video resolution doesn't cause the extension to think it
// gone back to the begining
- if (!firstEvent && video.currentTime === 0) return;
- firstEvent = false;
+ if (video.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA
+ && video.currentTime === 0) return;
updateVirtualTime();
- if (switchingVideos) {
+ if (switchingVideos || lastPausedAtZero) {
switchingVideos = false;
logDebug("Setting switching videos to false");
@@ -745,6 +783,8 @@ function setupVideoListeners() {
if (sponsorTimes) startSkipScheduleCheckingForStartSponsors();
}
+ lastPausedAtZero = false;
+
// Check if an ad is playing
updateAdFlag();
@@ -760,6 +800,7 @@ function setupVideoListeners() {
});
video.addEventListener('playing', () => {
updateVirtualTime();
+ lastPausedAtZero = false;
if (startedWaiting) {
startedWaiting = false;
@@ -767,6 +808,14 @@ function setupVideoListeners() {
|| (lastCheckVideoTime !== video.currentTime && Date.now() - lastCheckTime > 2000)}`);
}
+ if (switchingVideos) {
+ switchingVideos = false;
+ logDebug("Setting switching videos to false");
+
+ // If already segments loaded before video, retry to skip starting segments
+ if (sponsorTimes) startSkipScheduleCheckingForStartSponsors();
+ }
+
// Make sure it doesn't get double called with the play event
if (Math.abs(lastCheckVideoTime - video.currentTime) > 0.3
|| (lastCheckVideoTime !== video.currentTime && Date.now() - lastCheckTime > 2000)) {
@@ -788,6 +837,10 @@ function setupVideoListeners() {
startSponsorSchedule();
} else {
updateActiveSegment(video.currentTime);
+
+ if (video.currentTime === 0) {
+ lastPausedAtZero = true;
+ }
}
});
video.addEventListener('ratechange', () => startSponsorSchedule());
@@ -867,7 +920,6 @@ async function sponsorsLookup(keepOldSubmissions = true) {
const hashParams = getHashParams();
if (hashParams.requiredSegment) extraRequestData.requiredSegment = hashParams.requiredSegment;
- // Check for hashPrefix setting
const hashPrefix = (await utils.getHash(sponsorVideoID, 1)).slice(0, 4) as VideoID & HashedValue;
const response = await utils.asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, {
categories,
@@ -876,6 +928,9 @@ async function sponsorsLookup(keepOldSubmissions = true) {
...extraRequestData
});
+ // store last response status
+ lastResponseStatus = response?.status;
+
if (response?.ok) {
const recievedSegments: SponsorTime[] = JSON.parse(response.responseText)
?.filter((video) => video.videoID === sponsorVideoID)
@@ -887,7 +942,7 @@ async function sponsorsLookup(keepOldSubmissions = true) {
?.sort((a, b) => a.segment[0] - b.segment[0]);
if (!recievedSegments || !recievedSegments.length) {
// return if no video found
- retryFetch();
+ retryFetch(404);
return;
}
@@ -949,8 +1004,8 @@ async function sponsorsLookup(keepOldSubmissions = true) {
//otherwise the listener can handle it
updatePreviewBar();
}
- } else if (response?.status === 404) {
- retryFetch();
+ } else {
+ retryFetch(lastResponseStatus);
}
importExistingChapters(true);
@@ -999,17 +1054,24 @@ async function lockedCategoriesLookup(): Promise<void> {
}
}
-function retryFetch(): void {
+function retryFetch(errorCode: number): void {
if (!Config.config.refetchWhenNotFound) return;
-
sponsorDataFound = false;
+ if (errorCode !== 404 && retryCount > 1) {
+ // Too many errors (50x), give up
+ return;
+ }
+
+ retryCount++;
+
+ const delay = errorCode === 404 ? (10000 + Math.random() * 30000) : (2000 + Math.random() * 10000);
setTimeout(() => {
if (sponsorVideoID && sponsorTimes?.length === 0
|| sponsorTimes.every((segment) => segment.source !== SponsorSourceType.Server)) {
sponsorsLookup();
}
- }, 10000 + Math.random() * 30000);
+ }, delay);
}
/**
@@ -1073,28 +1135,29 @@ function startSkipScheduleCheckingForStartSponsors() {
}
}
-function getYouTubeVideoID(document: Document): string | boolean {
- const url = document.URL;
+function getYouTubeVideoID(document: Document, url?: string): string | boolean {
+ url ||= document.URL;
// clips should never skip, going from clip to full video has no indications.
if (url.includes("youtube.com/clip/")) return false;
// skip to document and don't hide if on /embed/
- if (url.includes("/embed/") && url.includes("youtube.com")) return getYouTubeVideoIDFromDocument(document, false);
+ if (url.includes("/embed/") && url.includes("youtube.com")) return getYouTubeVideoIDFromDocument(false);
// skip to URL if matches youtube watch or invidious or matches youtube pattern
if ((!url.includes("youtube.com")) || url.includes("/watch") || url.includes("/shorts/") || url.includes("playlist")) return getYouTubeVideoIDFromURL(url);
// skip to document if matches pattern
- if (url.includes("/channel/") || url.includes("/user/") || url.includes("/c/")) return getYouTubeVideoIDFromDocument(document);
+ if (url.includes("/channel/") || url.includes("/user/") || url.includes("/c/")) return getYouTubeVideoIDFromDocument();
// not sure, try URL then document
- return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(document, false);
+ return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(false);
}
-function getYouTubeVideoIDFromDocument(document: Document, hideIcon = true): string | boolean {
+function getYouTubeVideoIDFromDocument(hideIcon = true): string | boolean {
// get ID from document (channel trailer / embedded playlist)
- const videoURL = document.querySelector("[data-sessionlink='feature=player-title']")?.getAttribute("href");
+ const element = video?.parentElement?.parentElement?.querySelector("a.ytp-title-link[data-sessionlink='feature=player-title']");
+ const videoURL = element?.getAttribute("href");
if (videoURL) {
onInvidious = hideIcon;
return getYouTubeVideoIDFromURL(videoURL);
} else {
- return false
+ return false;
}
}
@@ -1241,13 +1304,33 @@ async function whitelistCheck() {
* Returns info about the next upcoming sponsor skip
*/
function getNextSkipIndex(currentTime: number, includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean):
- {array: ScheduledTime[], index: number, endIndex: number, openNotice: boolean} {
+ {array: ScheduledTime[], index: number, endIndex: number, extraIndexes: number[], openNotice: boolean} {
+
+ const autoSkipSorter = (segment: ScheduledTime) => {
+ const skipOption = utils.getCategorySelection(segment.category)?.option;
+ if ((skipOption === CategorySkipOption.AutoSkip || shouldAutoSkip(segment))
+ && segment.actionType === ActionType.Skip) {
+ return 0;
+ } else if (skipOption !== CategorySkipOption.ShowOverlay) {
+ return 1;
+ } else {
+ return 2;
+ }
+ }
const { includedTimes: submittedArray, scheduledTimes: sponsorStartTimes } =
getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments);
const { scheduledTimes: sponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, true);
- const minSponsorTimeIndex = sponsorStartTimes.indexOf(Math.min(...sponsorStartTimesAfterCurrentTime));
+ // This is an array in-case multiple segments have the exact same start time
+ const minSponsorTimeIndexes = GenericUtils.indexesOf(sponsorStartTimes, Math.min(...sponsorStartTimesAfterCurrentTime));
+ // Find auto skipping segments if possible, sort by duration otherwise
+ const minSponsorTimeIndex = minSponsorTimeIndexes.sort(
+ (a, b) => ((autoSkipSorter(submittedArray[a]) - autoSkipSorter(submittedArray[b]))
+ || (submittedArray[a].segment[1] - submittedArray[a].segment[0]) - (submittedArray[b].segment[1] - submittedArray[b].segment[0])))[0] ?? -1;
+ // Store extra indexes for the non-auto skipping segments if others occur at the exact same start time
+ const extraIndexes = minSponsorTimeIndexes.filter((i) => i !== minSponsorTimeIndex && autoSkipSorter(submittedArray[i]) !== 0);
+
const endTimeIndex = getLatestEndTimeIndex(submittedArray, minSponsorTimeIndex);
const { includedTimes: unsubmittedArray, scheduledTimes: unsubmittedSponsorStartTimes } =
@@ -1263,6 +1346,7 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool
array: submittedArray,
index: minSponsorTimeIndex,
endIndex: endTimeIndex,
+ extraIndexes, // Segments at same time that need seperate notices
openNotice: true
};
} else {
@@ -1270,6 +1354,7 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool
array: unsubmittedArray,
index: minUnsubmittedSponsorTimeIndex,
endIndex: previewEndTimeIndex,
+ extraIndexes: [], // No manual things for unsubmitted
openNotice: false
};
}
@@ -1855,10 +1940,7 @@ async function vote(type: number, UUID: SegmentUUID, category?: Category, skipNo
skipNotice.afterVote.bind(skipNotice)(utils.getSponsorTimeFromUUID(sponsorTimes, UUID), type, category);
} else if (response.successType == -1) {
if (response.statusCode === 403 && response.responseText.startsWith("Vote rejected due to a warning from a moderator.")) {
- skipNotice.setNoticeInfoMessageWithOnClick.bind(skipNotice)(() => {
- Chat.openWarningChat(response.responseText);
- skipNotice.closeListener.call(skipNotice);
- }, chrome.i18n.getMessage("voteRejectedWarning"));
+ openWarningDialog(skipNoticeContentContainer);
} else {
skipNotice.setNoticeInfoMessage.bind(skipNotice)(GenericUtils.getErrorMessage(response.statusCode, response.responseText))
}
@@ -2046,7 +2128,7 @@ async function sendSubmitMessage() {
playerButtons.submit.image.src = chrome.extension.getURL("icons/PlayerUploadFailedIconSponsorBlocker.svg");
if (response.status === 403 && response.responseText.startsWith("Submission rejected due to a warning from a moderator.")) {
- Chat.openWarningChat(response.responseText);
+ openWarningDialog(skipNoticeContentContainer);
} else {
alert(GenericUtils.getErrorMessage(response.status, response.responseText));
}
@@ -2272,7 +2354,8 @@ function checkForPreloadedSegment() {
const navigationApiAvailable = "navigation" in window;
if (navigationApiAvailable) {
// TODO: Remove type cast once type declarations are updated
- (window as unknown as { navigation: EventTarget }).navigation.addEventListener("navigate", () => videoIDChange(getYouTubeVideoID(document)));
+ (window as unknown as { navigation: EventTarget }).navigation.addEventListener("navigate", (e) =>
+ videoIDChange(getYouTubeVideoID(document, (e as unknown as Record<string, Record<string, string>>).destination.url)));
}
// Record availability of Navigation API
diff --git a/src/js-components/chat.ts b/src/js-components/chat.ts
deleted file mode 100644
index 9ff30af7..00000000
--- a/src/js-components/chat.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import Config from "../config";
-import Utils from "../utils";
-const utils = new Utils();
-
-export interface ChatConfig {
- displayName: string,
- composerInitialValue?: string,
- customDescription?: string
-}
-
-export function openChat(config: ChatConfig): void {
- const chat = document.createElement("div");
- chat.classList.add("sbChatNotice");
- chat.style.zIndex = "2000";
-
- const iframe= document.createElement("iframe");
- iframe.src = "https://chat.sponsor.ajay.app/#" + utils.objectToURI("", config, false);
- chat.appendChild(iframe);
-
- const closeButton = document.createElement("img");
- closeButton.classList.add("sbChatClose");
- closeButton.src = chrome.extension.getURL("icons/close.png");
- closeButton.addEventListener("click", () => {
- chat.remove();
- closeButton.remove();
- });
- chat.appendChild(closeButton);
-
- const referenceNode = utils.findReferenceNode();
- referenceNode.prepend(chat);
-}
-
-export async function openWarningChat(warningMessage: string): Promise<void> {
- const warningReasonMatch = warningMessage.match(/Warning reason: '(.+)'/);
- alert(chrome.i18n.getMessage("warningChatInfo") + `\n\n${warningReasonMatch ? ` Warning reason: ${warningReasonMatch[1]}` : ``}`);
-
- const userNameData = await utils.asyncRequestToServer("GET", "/api/getUsername?userID=" + Config.config.userID);
- const userName = userNameData.ok ? JSON.parse(userNameData.responseText).userName : "";
- const publicUserID = await utils.getHash(Config.config.userID);
-
- openChat({
- displayName: `${userName ? userName : ``}${userName !== publicUserID ? ` | ${publicUserID}` : ``}`,
- composerInitialValue: `I got a warning and confirm I [REMOVE THIS CAPITAL TEXT TO CONFIRM] reread the guidelines.` +
- warningReasonMatch ? ` Warning reason: ${warningReasonMatch[1]}` : ``,
- customDescription: chrome.i18n.getMessage("warningChatInfo")
- });
-} \ No newline at end of file
diff --git a/src/messageTypes.ts b/src/messageTypes.ts
index cdc49011..c48aba34 100644
--- a/src/messageTypes.ts
+++ b/src/messageTypes.ts
@@ -73,6 +73,7 @@ export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | IsInfoF
export interface IsInfoFoundMessageResponse {
found: boolean;
+ status: number;
sponsorTimes: SponsorTime[];
time: number;
onMobileYouTube: boolean;
@@ -100,7 +101,7 @@ export type MessageResponse =
| GetChannelIDResponse
| SponsorStartResponse
| IsChannelWhitelistedResponse
- | Record<string, never>
+ | Record<string, never> // empty object response {}
| VoteResponse
| ImportSegmentsResponse;
diff --git a/src/options.ts b/src/options.ts
index dc3314cf..1a522c9b 100644
--- a/src/options.ts
+++ b/src/options.ts
@@ -452,13 +452,7 @@ function invidiousInstanceAddInit(element: HTMLElement, option: string) {
* @param option
*/
function invidiousInit(checkbox: HTMLInputElement, option: string) {
- let permissions = ["declarativeContent"];
- if (utils.isFirefox()) permissions = [];
-
- chrome.permissions.contains({
- origins: utils.getPermissionRegex(),
- permissions: permissions
- }, function (result) {
+ utils.containsInvidiousPermission().then((result) => {
if (result != checkbox.checked) {
Config.config[option] = result;
@@ -474,22 +468,8 @@ function invidiousInit(checkbox: HTMLInputElement, option: string) {
* @param option
*/
async function invidiousOnClick(checkbox: HTMLInputElement, option: string): Promise<void> {
- return new Promise((resolve) => {
- if (checkbox.checked) {
- utils.setupExtraSitePermissions(function (granted) {
- if (!granted) {
- Config.config[option] = false;
- checkbox.checked = false;
- } else {
- checkbox.checked = true;
- }
-
- resolve();
- });
- } else {
- utils.removeExtraSiteRegistration();
- }
- });
+ const enabled = await utils.applyInvidiousPermissions(checkbox.checked, option);
+ checkbox.checked = enabled;
}
/**
@@ -598,8 +578,9 @@ async function setTextOption(option: string, element: HTMLElement, value: string
function downloadConfig() {
const file = document.createElement("a");
const jsonData = JSON.parse(JSON.stringify(Config.cachedSyncConfig));
- file.setAttribute("href", "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(jsonData)));
- file.setAttribute("download", "SponsorBlockConfig.json");
+ const dateTimeString = new Date().toJSON().replace("T", "_").replace(/:/g, ".").replace(/.\d+Z/g, "")
+ file.setAttribute("href", `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(jsonData))}`);
+ file.setAttribute("download", `SponsorBlockConfig_${dateTimeString}.json`);
document.body.append(file);
file.click();
file.remove();
@@ -673,4 +654,4 @@ function copyDebugOutputToClipboard() {
function isIncognitoAllowed(): Promise<boolean> {
return new Promise((resolve) => chrome.extension.isAllowedIncognitoAccess(resolve));
-} \ No newline at end of file
+}
diff --git a/src/permissions.ts b/src/permissions.ts
index 05da496e..1e2c1119 100644
--- a/src/permissions.ts
+++ b/src/permissions.ts
@@ -12,25 +12,17 @@ window.addEventListener('DOMContentLoaded', init);
async function init() {
localizeHtmlPage();
- const domains = document.location.hash.replace("#", "").split(",");
-
const acceptButton = document.getElementById("acceptPermissionButton");
acceptButton.addEventListener("click", () => {
- chrome.permissions.request({
- origins: utils.getPermissionRegex(domains),
- permissions: []
- }, (granted) => {
- if (granted) {
- alert(chrome.i18n.getMessage("permissionRequestSuccess"));
+ utils.applyInvidiousPermissions(Config.config.supportInvidious).then((enabled) => {
+ Config.config.supportInvidious = enabled;
- Config.config.ytInfoPermissionGranted = true;
-
- chrome.tabs.getCurrent((tab) => {
- chrome.tabs.remove(tab.id);
- });
+ if (enabled) {
+ alert(chrome.i18n.getMessage("permissionRequestSuccess"));
+ window.close();
} else {
alert(chrome.i18n.getMessage("permissionRequestFailed"));
}
- });
+ })
});
} \ No newline at end of file
diff --git a/src/popup.ts b/src/popup.ts
index 061634c2..9d60b058 100644
--- a/src/popup.ts
+++ b/src/popup.ts
@@ -415,8 +415,10 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
if (request.sponsorTimes) {
displayDownloadedSponsorTimes(request.sponsorTimes, request.time);
}
- } else {
+ } else if (request.status == 404 || request.status == 200) {
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsor404");
+ } else {
+ PageElements.videoFound.innerHTML = chrome.i18n.getMessage("connectionError") + request.status;
}
}
@@ -664,7 +666,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
voteButtonsContainer.appendChild(upvoteButton);
voteButtonsContainer.appendChild(downvoteButton);
voteButtonsContainer.appendChild(uuidButton);
- if (downloadedTimes[i].actionType === ActionType.Skip
+ if (downloadedTimes[i].actionType === ActionType.Skip || downloadedTimes[i].actionType === ActionType.Mute
&& [SponsorHideType.Visible, SponsorHideType.Hidden].includes(downloadedTimes[i].hidden)) {
voteButtonsContainer.appendChild(hideButton);
}
diff --git a/src/render/GenericNotice.tsx b/src/render/GenericNotice.tsx
index 32f0ad50..639edb86 100644
--- a/src/render/GenericNotice.tsx
+++ b/src/render/GenericNotice.tsx
@@ -73,7 +73,13 @@ export default class GenericNotice {
hideRightInfo={options.hideRightInfo}
closeListener={() => this.close()} >
- {this.getMessageBox(this.idSuffix, options.textBoxes)}
+ <tr id={"sponsorSkipNoticeMiddleRow" + this.idSuffix}
+ className="sponsorTimeMessagesRow"
+ style={{maxHeight: (this.contentContainer().v.offsetHeight - 200) + "px"}}>
+ <td style={{width: "100%"}}>
+ {this.getMessageBoxes(this.idSuffix, options.textBoxes)}
+ </td>
+ </tr>
<tr id={"sponsorSkipNoticeSpacer" + this.idSuffix}
className="sponsorBlockSpacer">
@@ -90,7 +96,7 @@ export default class GenericNotice {
);
}
- getMessageBox(idSuffix: string, textBoxes: TextBox[]): JSX.Element[] {
+ getMessageBoxes(idSuffix: string, textBoxes: TextBox[]): JSX.Element[] {
if (textBoxes) {
const result = [];
for (let i = 0; i < textBoxes.length; i++) {
diff --git a/src/utils.ts b/src/utils.ts
index d7e6fcf8..081e014e 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -2,7 +2,7 @@ import Config, { VideoDownvotes } from "./config";
import { CategorySelection, SponsorTime, FetchResponse, BackgroundScriptContainer, Registration, HashedValue, VideoID, SponsorHideType } from "./types";
import * as CompileConfig from "../config.json";
-import { findValidElementFromSelector } from "./utils/pageUtils";
+import { findValidElement, findValidElementFromSelector } from "./utils/pageUtils";
import { GenericUtils } from "./utils/genericUtils";
export default class Utils {
@@ -22,8 +22,9 @@ export default class Utils {
];
/* Used for waitForElement */
- waitingMutationObserver:MutationObserver = null;
- waitingElements: { selector: string, callback: (element: Element) => void }[] = [];
+ creatingWaitingMutationObserver = false;
+ waitingMutationObserver: MutationObserver = null;
+ waitingElements: { selector: string, visibleCheck: boolean, callback: (element: Element) => void }[] = [];
constructor(backgroundScriptContainer: BackgroundScriptContainer = null) {
this.backgroundScriptContainer = backgroundScriptContainer;
@@ -34,40 +35,66 @@ export default class Utils {
}
/* Uses a mutation observer to wait asynchronously */
- async waitForElement(selector: string): Promise<Element> {
+ async waitForElement(selector: string, visibleCheck = false): Promise<Element> {
return await new Promise((resolve) => {
+ const initialElement = this.getElement(selector, visibleCheck);
+ if (initialElement) {
+ resolve(initialElement);
+ return;
+ }
+
this.waitingElements.push({
selector,
+ visibleCheck,
callback: resolve
});
- if (!this.waitingMutationObserver) {
- this.waitingMutationObserver = new MutationObserver(() => {
- const foundSelectors = [];
- for (const { selector, callback } of this.waitingElements) {
- const element = document.querySelector(selector);
- if (element) {
- callback(element);
- foundSelectors.push(selector);
- }
- }
-
- this.waitingElements = this.waitingElements.filter((element) => !foundSelectors.includes(element.selector));
-
- if (this.waitingElements.length === 0) {
- this.waitingMutationObserver.disconnect();
- this.waitingMutationObserver = null;
- }
- });
+ if (!this.creatingWaitingMutationObserver) {
+ this.creatingWaitingMutationObserver = true;
- this.waitingMutationObserver.observe(document.body, {
- childList: true,
- subtree: true
- });
+ if (document.body) {
+ this.setupWaitingMutationListener();
+ } else {
+ window.addEventListener("DOMContentLoaded", () => {
+ this.setupWaitingMutationListener();
+ });
+ }
}
});
}
+ private setupWaitingMutationListener(): void {
+ if (!this.waitingMutationObserver) {
+ this.waitingMutationObserver = new MutationObserver(() => {
+ const foundSelectors = [];
+ for (const { selector, visibleCheck, callback } of this.waitingElements) {
+ const element = this.getElement(selector, visibleCheck);
+ if (element) {
+ callback(element);
+ foundSelectors.push(selector);
+ }
+ }
+
+ this.waitingElements = this.waitingElements.filter((element) => !foundSelectors.includes(element.selector));
+
+ if (this.waitingElements.length === 0) {
+ this.waitingMutationObserver.disconnect();
+ this.waitingMutationObserver = null;
+ this.creatingWaitingMutationObserver = false;
+ }
+ });
+
+ this.waitingMutationObserver.observe(document.body, {
+ childList: true,
+ subtree: true
+ });
+ }
+ }
+
+ private getElement(selector: string, visibleCheck: boolean) {
+ return visibleCheck ? findValidElement(document.querySelectorAll(selector)) : document.querySelector(selector);
+ }
+
containsPermission(permissions: chrome.permissions.Permissions): Promise<boolean> {
return new Promise((resolve) => {
chrome.permissions.contains(permissions, resolve)
@@ -183,6 +210,37 @@ export default class Utils {
});
}
+ applyInvidiousPermissions(enable: boolean, option = "supportInvidious"): Promise<boolean> {
+ return new Promise((resolve) => {
+ if (enable) {
+ this.setupExtraSitePermissions((granted) => {
+ if (!granted) {
+ Config.config[option] = false;
+ }
+
+ resolve(granted);
+ });
+ } else {
+ this.removeExtraSiteRegistration();
+ resolve(false);
+ }
+ });
+ }
+
+ containsInvidiousPermission(): Promise<boolean> {
+ return new Promise((resolve) => {
+ let permissions = ["declarativeContent"];
+ if (this.isFirefox()) permissions = [];
+
+ chrome.permissions.contains({
+ origins: this.getPermissionRegex(),
+ permissions: permissions
+ }, function (result) {
+ resolve(result);
+ });
+ })
+ }
+
/**
* Merges any overlapping timestamp ranges into single segments and returns them as a new array.
*/
@@ -358,19 +416,6 @@ export default class Utils {
return referenceNode;
}
- objectToURI<T>(url: string, data: T, includeQuestionMark: boolean): string {
- let counter = 0;
- for (const key in data) {
- const seperator = (url.includes("?") || counter > 0) ? "&" : (includeQuestionMark ? "?" : "");
- const value = (typeof(data[key]) === "string") ? data[key] as unknown as string : JSON.stringify(data[key]);
- url += seperator + encodeURIComponent(key) + "=" + encodeURIComponent(value);
-
- counter++;
- }
-
- return url;
- }
-
isContentScript(): boolean {
return window.location.protocol === "http:" || window.location.protocol === "https:";
}
diff --git a/src/utils/genericUtils.ts b/src/utils/genericUtils.ts
index 90a6b5e2..0f8555d0 100644
--- a/src/utils/genericUtils.ts
+++ b/src/utils/genericUtils.ts
@@ -108,6 +108,27 @@ function hexToRgb(hex: string): {r: number, g: number, b: number} {
} : null;
}
+/**
+ * List of all indexes that have the specified value
+ * https://stackoverflow.com/a/54954694/1985387
+ */
+function indexesOf<T>(array: T[], value: T): number[] {
+ return array.map((v, i) => v === value ? i : -1).filter(i => i !== -1);
+}
+
+function objectToURI<T>(url: string, data: T, includeQuestionMark: boolean): string {
+ let counter = 0;
+ for (const key in data) {
+ const seperator = (url.includes("?") || counter > 0) ? "&" : (includeQuestionMark ? "?" : "");
+ const value = (typeof(data[key]) === "string") ? data[key] as unknown as string : JSON.stringify(data[key]);
+ url += seperator + encodeURIComponent(key) + "=" + encodeURIComponent(value);
+
+ counter++;
+ }
+
+ return url;
+}
+
function generateUserID(length = 36): string {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
@@ -132,5 +153,7 @@ export const GenericUtils = {
getFormattedTimeToSeconds,
getErrorMessage,
getLuminance,
- generateUserID
+ generateUserID,
+ indexesOf,
+ objectToURI
} \ No newline at end of file
diff --git a/src/utils/warnings.ts b/src/utils/warnings.ts
new file mode 100644
index 00000000..09423e03
--- /dev/null
+++ b/src/utils/warnings.ts
@@ -0,0 +1,66 @@
+import Config from "../config";
+import GenericNotice, { NoticeOptions } from "../render/GenericNotice";
+import { ContentContainer } from "../types";
+import Utils from "../utils";
+import { GenericUtils } from "./genericUtils";
+const utils = new Utils();
+
+export interface ChatConfig {
+ displayName: string,
+ composerInitialValue?: string,
+ customDescription?: string
+}
+
+export async function openWarningDialog(contentContainer: ContentContainer): Promise<void> {
+ const userInfo = await utils.asyncRequestToServer("GET", "/api/userInfo", {
+ userID: 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 userName = userNameData.ok ? JSON.parse(userNameData.responseText).userName : "";
+ const publicUserID = await utils.getHash(Config.config.userID);
+
+ let notice: GenericNotice = null;
+ const options: NoticeOptions = {
+ title: chrome.i18n.getMessage("warningTitle"),
+ textBoxes: [{
+ text: chrome.i18n.getMessage("warningChatInfo"),
+ icon: null
+ }, ...warningReason.split("\n").map((reason) => ({
+ text: reason,
+ icon: null
+ }))],
+ buttons: [{
+ name: chrome.i18n.getMessage("questionButton"),
+ listener: () => openChat({
+ displayName: `${userName ? userName : ``}${userName !== publicUserID ? ` | ${publicUserID}` : ``}`
+ })
+ },
+ {
+ name: chrome.i18n.getMessage("warningConfirmButton"),
+ listener: async () => {
+ const result = await utils.asyncRequestToServer("POST", "/api/warnUser", {
+ userID: Config.config.userID,
+ enabled: false
+ });
+
+ if (result.ok) {
+ notice?.close();
+ } else {
+ alert(`${chrome.i18n.getMessage("warningError")} ${result.status}`);
+ }
+ }
+ }],
+ timed: false
+ };
+
+ notice = new GenericNotice(contentContainer, "warningNotice", options);
+ }
+}
+
+export function openChat(config: ChatConfig): void {
+ window.open("https://chat.sponsor.ajay.app/#" + GenericUtils.objectToURI("", config, false));
+} \ No newline at end of file