Lemmy post utilities - Filter posts by title

Filters posts on any lemmy instance by text in the title. It can also auto-open image posts, unblur thumbnails, and other things.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Lemmy post utilities - Filter posts by title
// @namespace   Violentmonkey Scripts
// @description Filters posts on any lemmy instance by text in the title. It can also auto-open image posts, unblur thumbnails, and other things.
// @match       https://*lemmy*.*/*
// @include     https://lemy.nl/*
// @include     https://burggit.moe/*
// @include     https://lemmit.online/*
// @include     https://yiffit.net/*
// @include     https://reddthat.com/*
// @include     https://sh.itjust.works/*
// @exclude     https://lemmyverse.net/*
// @exclude     https://lemmy-status.org/*
// @exclude     https://search-lemmy.com/*
// @exclude     https://join-lemmy.org/*
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_setClipboard
// @version     2.2.10
// @icon        data:image/png;base64,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
// @author      Xynoth
// @license     GPT-3
// @date        14/7/2023, 20:15:03
// ==/UserScript==

//----------------------------------------
// CONSTANTS
//----------------------------------------

// Key constants
const BLOCKED_TAGS_KEY = "blockedTags";
const BLOCKED_CASE_SENSITIVE_TAGS_KEY = "blockedCaseSensitiveTags";
const AUTO_OPEN_MEDIA_POSTS_KEY = "autoOpenMediaPosts";
const UNBLUR_THUMBNAILS_KEY = "unblurThumbs";
const SHOW_FILTERED_STUBS_KEY = "showFilteredStubs";
const COMMAS_AS_SEPARATORS_KEY = "useCommasAsSeparators";
const CASE_SENSITIVE_KEY = "caseSensitiveTag";
const WIDGET_HEIGHT_KEY = "widgetHeight";
const FIX_BROKEN_VIDEO_PREVIEWS_KEY = "fixBrokenVideoPreviews";
const MARK_POSTS_AS_NSFW_KEY = "markNewPostsAsNSFW";

// Constant selectors
const postsContainerClass = "post-listings";
const profileContainerClass = "person-details";
const searchContainerClass = "search"
const postContainer = "post-listing";
const loadingSpinnerSelector = ".icon.spin"; // This is the "creators" select in the search page
const postCommunityContainer = ".community-link";
const postCommunityNameContainer = `${postCommunityContainer} > span`;
const postSrcLink = `div:nth-of-type(2) p a`;
const fixedPreviewContainerClass = "fixed-preview";
const fixedPreviewVideoClass = "fixed-preview-video";
const postAsNSFW = "#post-nsfw[type='checkbox']";
const createPostContainerId = "createPostForm";
const editPostClass = "post-form";

// GUI main elements
const settingsWidgetId = "settings-widget";
const blockedTagsDialogId = "tags-dialog";
const dialogBgId = "dialog-bg";
const settingsWidgetContainerId = "widget-container";
const blockTagListId = "blocked-insensitive-tag-list";
const csBlockTagListId = "blocked-sensitive-tags-list";
const openedPostChecker = "already-opened";
const filteredPostChecker = "filter-checked";
const processedPageChecker = "processed-page";

// CSS Color constants
const caseInsensitiveTagColor = "#dd2222";
const caseSensitiveTagColor = "#2052b3";
const primaryBtnColor = "#0052cc";
const primaryBtnHoverColor = "#0066ff";
const primaryBtnActiveColor = "#0047b3";
const widgetBgColor = "#1a1a1b";
const errorToastColor = "#f94b4b";
const errorToastBgColor = "#2f0808";

// Other constants
const logoImg = GM_info.script.icon;
const namedRegex = "regex\\((.*?)\\):";

// HTML content constants
const bottomWidgetContent = `
<div id="${settingsWidgetContainerId}">
  <h1><span id="widget-logo"></span>Lemmy post utilities</h1>
  <div class="form-entry">
    <label>Blocked tags: </label>
    <div class="btn-container">
      <button id="block-tag-btn" type="button">🖊</button>
    </div>
  </div>
  <div class="form-entry">
    <label>
      Show stubs on filter:
    </label>
    <div class="btn-container">
      <span class="switch" id="show-stub-swt">
        <input type="checkbox">
        <span class="slider round"></span>
      </span>
    </div>
  </div>
  <div class="form-entry">
    <label>
      Fix broken video previews:
    </label>
    <div class="btn-container">
      <span class="switch" id="fix-video-previews-swt">
        <input type="checkbox">
        <span class="slider round"></span>
      </span>
    </div>
  </div>
  <div class="form-entry">
    <label>
      Auto open media previews:
    </label>
    <div class="btn-container">
      <span class="switch" id="auto-open-swt">
        <input type="checkbox">
        <span class="slider round"></span>
      </span>
    </div>
  </div>
  <div class="form-entry">
    <label>
      Unblur NSFW thumbnails:
    </label>
    <div class="btn-container">
      <span class="switch" id="unblur-swt">
        <input type="checkbox">
        <span class="slider round"></span>
      </span>
    </div>
  </div>
  <div class="form-entry">
    <label>
      Mark new posts as NSFW:
    </label>
    <div class="btn-container">
      <span class="switch" id="default-nsfw-posts-swt">
        <input type="checkbox">
        <span class="slider round"></span>
      </span>
    </div>
  </div>
</div>
`;

const tagsDialogContent = `
<div id="blocked-tags-dialog-container">
  <div id ="blocked-tags-dialog-head">
    <h1>Blocked tags editor</h1>
    <button type="button" class="close-dialog-btn">⨯</button>
  </div>
  <div>
    <p>Any tag added here will hide any post that contains the word in its title.</p>
    <p>You can also use some advanced filtering options like the following:</p>
    <ul>
      <li>Use <a href="https://regex101.com/">regex</a> starting a tag with <code>regex:</code></li>
      <li>Create a named regex starting a tag with <code>regex(your-regex-name):</code></li>
      <li>Filter by linked source instead of title starting a tag with <code>source:</code></li>
      <li>Combine <code>source:</code> with any variant of <code>regex:</code></li>
      <li>You can click any tag to copy it's raw value.</li>
    </ul>
    <p>These are the blocked tags you have for this instance:</p>
    <div id="blocked-tags-field-container">
      <div id="blocked-tags-field">
        <p id="empty-blocked-tags">You haven't blocked anything yet.</p>
        <ul id="${blockTagListId}" class="blocked-tags-list" hidden>
        </ul>
        <hr id="blocked-tags-separator" hidden>
        <ul id="${csBlockTagListId}" class="blocked-tags-list" hidden>
        </ul>
      </div>
      <div id="blocked-tags-field-legend">
        <span id="tag-case-insensitive-legend" class="tag-legend">
          <span class="tag-color-legend"></span>
          <small class="tag-label-legend">Case insensitive tag</small>
        </span>
        <span id="tag-case-sensitive-legend" class="tag-legend">
          <span class="tag-color-legend"></span>
          <small class="tag-label-legend">Case sensitive tag</small>
        </span>
      </div>
    </div>
    <p id="clipboard-notice" hidden>Tag value copied to the clipboard!</p>
    <div class="switch-container">
      <label>
        Use commas as tag separators:
      </label>
      <div class="btn-container">
        <span class="switch" id="use-commas-swt">
          <input type="checkbox">
          <span class="slider round"></span>
        </span>
      </div>
    </div>
    <div class="switch-container">
      <label>
        Add tags as case sensitive:
      </label>
      <div class="btn-container">
        <span class="switch" id="case-sensitive-swt">
          <input type="checkbox">
          <span class="slider round"></span>
        </span>
      </div>
    </div>
    <div id="tag-input-container">
      <input type="text" id="tag-input" placeholder="Add your tags">
      <button type="button" id="tag-save-btn">Save</button>
    </div>
    <label id="blocked-tags-input-error" hidden>Tag must be longer than 1 non-whitespace character!</label>
  </div>
</div>
`;

const tagContent = `
<span title="TOOLTIP-CONTENT">
  <label>TAG-NAME</label>
  <button type="button">⨯</button>
</span>
`;

const filteredPostStubContent = `
<div class="hidden-post-stub-meta-container">
  <p>This post was hidden because it contained the tag 'TAG'.</p>
  <span class="hidden-post-stub-btn-container">
    <button class="show-hidden-post-title-btn" type="button">Show title</button>
    <button class="show-hidden-post-btn" type="button">Show post</button>
  </span>
</div>
<span class="stub-hidden-post-title" hidden>
  <br>
  <p>Post title was '<b>POST-TITLE</b>' from <a class="stub-hidden-post-community-link">POST-COMMUNITY</a> community.</p>
</span>
`;

const videoSourceContent = `
<source src="VIDEO-SOURCE" type="video/VIDEO-TYPE">
`;

// CSS to add to style elements
const initialCSS = getInitialCSS();

const guiCSS = () => `

/* Fix for stubs leaving empty space at the bottom of the page for some reason */
html {
  overflow-y: auto;
}

body {
  overflow-y: clip;
  scrollbar-width: none;
}

/* The settings widget */
#${settingsWidgetId} {
  display: flex;
  float: right;
  bottom: 0;
  right: 0.8rem;
  max-width: 15rem;
  position: fixed;
  transform: translateY(${getData(WIDGET_HEIGHT_KEY) ?? storeData(WIDGET_HEIGHT_KEY, "21rem")});
  transition: 200ms ease-in-out transform;
}

#${settingsWidgetId}:hover {
  transform: translateY(1px);
}

/* The container for the widget */
#widget-container {
  flex-direction: column;
  border: 1px solid #333;
  font-size: 0.9rem;
  background-color: ${widgetBgColor};
  width: 100%;
  padding: 1rem;
  border-radius: .5rem .5rem 0 0;
  margin-top: 50px; /* This is the space that will allow showing the popup when hovering over it */
}

#widget-container:hover {
  margin-top: 0;
}

/* Widget title */
#widget-container > h1 {
  font-size: 1rem;
  font-weight: bold;
  height: 1rem;
}

#widget-logo {
  display: inline-block;
  background-image: url('${logoImg}');
  height: 1rem;
  width: 1rem;
  background-size: contain;
  background-repeat: no-repeat;
  margin-right: 0.5rem;
}

/* Widget block button */
#block-tag-btn {
  appearance: none;
  color: #ddd;
  background: rgba(255,255,255,0.1);
  border: none;
  border-radius: 2rem;
  padding: .3rem .5rem;
}

/* The layout for each label + form control */
.form-entry {
  display: inline-flex;
}

.form-entry:not(:first-child) {
  margin-top: 0.8rem;
}

.form-entry > label {
  display: flex;
	align-items: center;
	justify-content: left;
  width: 10rem;
}

.btn-container {
  display: grid;
	place-items: center;
}

/* The dialog to filter tags */
#${blockedTagsDialogId} {
  appearance: none;
  border: none;
  background-color: ${widgetBgColor};
  padding: 1rem;
  border-radius: 0.5rem;
  box-shadow: 0 0 20px 5px rgba(255,255,255,0.2);
  position: fixed;
  top: 50%;
  bottom: 50%;
  z-index: 1001;
  max-width: 36.5rem;
}

#dialog-bg {
  display: none;
  background-color: rgba(0,0,0,.5); /* For browsers that don't support backdrop-filter */
  position: fixed;
  height: 100vh;
  width: 100vw;
  backdrop-filter: blur(2px);
  top: 0;
  left: 0;
  z-index: 1000;
}

#blocked-tags-separator {
  width: 100%;
  margin-left: auto;
  margin-right: auto;
  border-top: 2px solid #666;
  margin-top: .5rem;
  margin-bottom: .5rem;
}

#blocked-tags-dialog-head {
  display: flex;
}

#blocked-tags-dialog-head > h1 {
  font-size: 1.5rem;
  margin-bottom: 1rem;
  color: #fff;
}

#tag-input-container {
  margin-top: 1rem;
  margin-bottom: 1rem;
  display: flex;
}

#blocked-tags-input-error {
  color: ${errorToastColor};
  font-size: 0.8rem;
  background-color: ${errorToastBgColor};
  padding: .4rem;
  border-radius: .5rem;
  font-weight: bold;
}

#blocked-tags-field-container {
  margin-bottom: 1rem;
}

#blocked-tags-field {
  display: grid;
  padding: 1rem;
  max-height: 20rem;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: thin;
  place-items: center;
  border: 1px solid #555;
  border-radius: 0.3rem;
}

#empty-blocked-tags {
  color: #666;
  margin: 0;
  padding: 1rem;
}

#clipboard-notice {
  color: #2f2;
  font-size: 0.8rem;
  width: 100%;
  text-align: center;
}

#tag-input {
  border: none;
  flex-grow: 100;
  background-color: #333;
  border-radius: .5rem;
  padding: .5rem 1rem;
  width: 80%;
  margin-right: 1rem;
  color: #ddd;
}

#tag-save-btn {
  appearance: none;
  border: none;
  background-color: ${primaryBtnColor};
  color: #ddd;
  padding: .5rem 1rem;
  border-radius: 0.3rem;
}

#tag-save-btn:hover {
  background-color: ${primaryBtnHoverColor};
  color: #fff;
}

#tag-save-btn:active {
  background-color: ${primaryBtnActiveColor};
  color: #fff;
}

#tag-case-insensitive-legend > span {
  background-color: ${caseInsensitiveTagColor};
}

#tag-case-sensitive-legend > span {
  background-color: ${caseSensitiveTagColor};
}

#blocked-tags-field-legend {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
}

.close-dialog-btn {
  appearance: none;
  color: #fff;
  font-weight: bold;
  background-color: transparent;
  border: none;
  padding: 0.2rem 0.6rem;
  position: absolute;
  right: 0;
  top: 0;
  transition: 200ms all;
}

.close-dialog-btn:hover {
  border-radius: 6rem;
  background-color: rgba(255,255,255,0.2);
}

.switch-container {
  width: 100%;
  display: flex;
}

.switch-container > label {
  width: 16rem;
  margin-right: 1rem;
}

.blocked-tags-list {
  display: flex;
  flex-wrap: wrap;
  max-width: 35.5rem;
  margin-bottom: 0;
  padding: 0;
}

.blocked-tags-list > li {
  display: inline-flex;
  list-style-type: none;
}

.tag-legend {
  margin-right: 1rem;
}

.tag-color-legend {
  display: inline-block;
  width: 7px;
  height: 7px;
}

.tag-label-legend {
  font-size: 0.6rem;
}

.blocked-tag {
  border-radius: 0.4rem;
  font-size: 0.8rem;
  color: #fff;
  margin: .2rem;
  cursor: grab;
}

.blocked-tag[data-dragged-item] {
  cursor: grabbing;
  opacity: 0.7;
}

.blocked-tag label {
  padding-left: .5rem;
  white-space: pre-wrap;
  cursor: grab;
}

.blocked-tag button {
  appearance: none;
  color: #fff;
  background: transparent;
  border: none;
  border-radius: .4rem;
}

.blocked-tag button:hover {
  background-color: rgba(255,255,255,0.2);
}

.case-insensitive-tag {
  background-color: ${caseInsensitiveTagColor};
}

.case-sensitive-tag {
  background-color: ${caseSensitiveTagColor};
}

/* The filtered post stubs */
.filtered-post-stub p {
  margin-bottom: 0;
}

.hidden-post-stub-meta-container {
  display: flex;
}

.hidden-post-stub-btn-container {
  margin-left: auto;
}

.hidden-post-stub-btn-container > button {
  appearance: none;
  background-color: transparent;
  border: none;
  cursor: pointer;
  color: #3498db;
  margin-right: 1rem;
}

.hide-post-btn {
  appearance: none;
  color: #f22;
  font-weight: bold;
  font-size: 2rem;
  line-height: 1rem;
  width: 1.8rem;
  background-color: transparent;
  border: none;
  padding: 0.2rem 0.6rem;
  height: 0;
  float: right;
  transition: 200ms all;
}

/* The switch - the box around the slider */
.switch {
  position: relative;
  display: inline-block;
  width: 30px;
  height: 16px;
}

/* Hide default HTML checkbox */
.switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

/* The slider */
.slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: #777;
  -webkit-transition: .4s;
  transition: .4s;
}

.slider:before {
  position: absolute;
  content: "";
  height: 12px;
  width: 12px;
  left: 2px;
  bottom: 2px;
  background-color: white;
  -webkit-transition: .4s;
  transition: .4s;
}

input:checked + .slider {
  background-color: #2196F3;
}

input:focus + .slider {
  box-shadow: 0 0 1px #2196F3;
}

input:checked + .slider:before {
  -webkit-transform: translateX(12px);
  -ms-transform: translateX(12px);
  transform: translateX(12px);
}

/* Rounded sliders */
.slider.round {
  border-radius: 34px;
}

.slider.round:before {
  border-radius: 50%;
}

`;

const unblurCSS = `

/* Unblurs thumbnails */
.img-blur {
    filter: none !important;
}

`

//----------------------------------------
// GUI AND INITIAL SETUP
//----------------------------------------

// Load initial settings and store defaults if they didn't exist
console.info(`Loading data for domain '${document.location.host}'.`);

const blockedTitleTags = getData(BLOCKED_TAGS_KEY) ?? storeData(BLOCKED_TAGS_KEY, []);
const csBlockedTitleTags = getData(BLOCKED_CASE_SENSITIVE_TAGS_KEY) ?? storeData(BLOCKED_CASE_SENSITIVE_TAGS_KEY, []);
const expandMediaPosts = getData(AUTO_OPEN_MEDIA_POSTS_KEY) ?? storeData(AUTO_OPEN_MEDIA_POSTS_KEY, false);
const unblurThumbnails = getData(UNBLUR_THUMBNAILS_KEY) ?? storeData(UNBLUR_THUMBNAILS_KEY, false);
const useCommasAsSeparators = getData(COMMAS_AS_SEPARATORS_KEY) ?? storeData(COMMAS_AS_SEPARATORS_KEY, true);
const showStubsForFilteredPosts = getData(SHOW_FILTERED_STUBS_KEY) ?? storeData(SHOW_FILTERED_STUBS_KEY, true);
const fixPostVideoPreviews = getData(FIX_BROKEN_VIDEO_PREVIEWS_KEY) ?? storeData(FIX_BROKEN_VIDEO_PREVIEWS_KEY, true);
const caseSensitiveTags = getData(CASE_SENSITIVE_KEY) ?? storeData(CASE_SENSITIVE_KEY, false);
const markNewPostsAsNSFW = getData(MARK_POSTS_AS_NSFW_KEY) ?? storeData(MARK_POSTS_AS_NSFW_KEY, false);

if (blockedTitleTags.length > 0 || csBlockedTitleTags.length > 0)
  console.info("Waiting for page to fully load to block tags: ", blockedTitleTags, csBlockedTitleTags);

// Add the GUI to control the settings to the document
const settingWidget = addElement(document.body, "DIV", settingsWidgetId, bottomWidgetContent);
const tagsDialog = addElement(document.body, "DIALOG", blockedTagsDialogId, tagsDialogContent);
const dialogBg = addElement(document.body, "DIV", dialogBgId);

// Add initial CSS changes
updateCSS();

// Load tags into the dialog window
updateVisibleTags();

// Reflect boolean values of settings on GUI
document.querySelector("#show-stub-swt > input").checked = showStubsForFilteredPosts;
document.querySelector("#auto-open-swt > input").checked = expandMediaPosts;
document.querySelector("#unblur-swt > input").checked = unblurThumbnails;
document.querySelector("#use-commas-swt > input").checked = useCommasAsSeparators;
document.querySelector("#case-sensitive-swt > input").checked = caseSensitiveTags;
document.querySelector("#fix-video-previews-swt > input").checked = fixPostVideoPreviews;
document.querySelector("#default-nsfw-posts-swt > input").checked = markNewPostsAsNSFW;

// --------- Add event listeners ---------

// Blocked list dialog button
document.getElementById("block-tag-btn").onclick = () => {
  openDialog(tagsDialog);
}

// Close dialog button
tagsDialog.getElementsByClassName("close-dialog-btn")[0].onclick = () => {
  closeDialog(tagsDialog);
}

// Close dialog on clicking outside the dialog
dialogBg.onclick = (event) => {
  event.stopPropagation();
  closeDialog(tagsDialog);
}

// Show stubs on filtering posts
document.getElementById("show-stub-swt").onclick = (event) => {
  const showStubs = storeData(SHOW_FILTERED_STUBS_KEY, toggleCheckbox(event));
  const postsContainer = getPostContainer();

  if (postsContainer)
    postsContainer.getElementsByClassName(postContainer).forEach(post => {
      let siblingNode = post.nextElementSibling;
      if (showStubs && post.hasAttribute("hidden")) {
        // Show sibling separator as well
        if (siblingNode && siblingNode.tagName == "HR")
          siblingNode.removeAttribute("hidden");

        // Show post with the stub
        post.removeAttribute("hidden");
      } else if (post.getElementsByClassName("filtered-post-stub")[0]) {

        // Remove separator if it exists
        if (siblingNode && siblingNode.tagName == "HR")
          siblingNode.setAttribute("hidden", true);

        // Hide post completelly
        post.setAttribute("hidden", true);
      }
    })
}

// Fix some broken video previews
document.getElementById("fix-video-previews-swt").onclick = (event) => {
  const fixVideoPreviews = storeData(FIX_BROKEN_VIDEO_PREVIEWS_KEY, toggleCheckbox(event));
  const fixedPreviewContainers = document.getElementsByClassName(fixedPreviewContainerClass);
  const fixedPreviewVideos = document.getElementsByClassName(fixedPreviewVideoClass);
  const postsContainer = getPostContainer();
  const wasProcessed = postsContainer.id === processedPageChecker;

  if (!postsContainer)
    return;

  if (!wasProcessed)
    postsContainer.setAttribute("id", processedPageChecker);

  if (fixVideoPreviews) {
    if (fixedPreviewContainers.length > 0 || fixedPreviewVideos.length > 0) {
      for(let i = 0; i < fixedPreviewContainers.length; i++) {
        fixedPreviewContainers[i].querySelector("picture").setAttribute("hidden", true);
        fixedPreviewVideos[i].removeAttribute("hidden");
      }
    } else {
      fixBrokenVideoPreviews(postsContainer);
    }
  } else if (fixedPreviewContainers.length > 0 || fixedPreviewVideos.length > 0) {
      for(let i = 0; i < fixedPreviewContainers.length; i++) {
        fixedPreviewContainers[i].querySelector("picture").removeAttribute("hidden");
        fixedPreviewVideos[i].setAttribute("hidden", true);
      }
  }
}

// Auto-open tags on page reload
document.getElementById("auto-open-swt").onclick = (event) => {
  const openMediaPosts = storeData(AUTO_OPEN_MEDIA_POSTS_KEY, toggleCheckbox(event));
  const postsContainer = getPostContainer();
  const wasProcessed = postsContainer.id === processedPageChecker;

  if (!postsContainer)
    return;

  if (!wasProcessed)
    postsContainer.setAttribute("id", processedPageChecker);

  // Open posts if they weren't already
  if (openMediaPosts) {
    openPosts(postsContainer);
  }
}

// Unblur setting
document.getElementById("unblur-swt").onclick = (event) => {
  storeData(UNBLUR_THUMBNAILS_KEY, toggleCheckbox(event));

  // Update CSS of site after having changed the setting
  updateCSS();
}

// Mark new posts as NSFW by default
document.getElementById("default-nsfw-posts-swt").onclick = (event) => {
  const markAsNSFW = storeData(MARK_POSTS_AS_NSFW_KEY, toggleCheckbox(event));
  const NSFWCheckbox = document.querySelector(postAsNSFW);
  const createPostForm = document.getElementById(createPostContainerId);

  if (NSFWCheckbox && !NSFWCheckbox.checked && markAsNSFW && createPostForm)
    NSFWCheckbox.click();
}

// Auto-open tags on page reload
document.getElementById("use-commas-swt").onclick = (event) => {
  storeData(COMMAS_AS_SEPARATORS_KEY, toggleCheckbox(event));
}

// Auto-open tags on page reload
document.getElementById("case-sensitive-swt").onclick = (event) => {
  storeData(CASE_SENSITIVE_KEY, toggleCheckbox(event));
}

// Accept pressing enter while in some input to send the data
document.getElementById("tag-input").onkeydown = (event) => {
  if(event.keyCode === 13){
    document.getElementById("tag-save-btn").click();
  }
}

// Event for tag save on button click or enter on input of tag blocking
document.getElementById("tag-save-btn").onclick = () => {
  const tagInput = document.getElementById("tag-input");
  const errorEl = document.getElementById("blocked-tags-input-error");
  const clipboardNoticeEl = document.getElementById("clipboard-notice");
  const tagsToSubmit = document.getElementById("tag-input").value;
  clipboardNoticeEl.setAttribute("hidden", true);

  if (tagsToSubmit.trim().length > 1) {
    const isCaseSensitive = getData(CASE_SENSITIVE_KEY);
    const splitOnCommas = getData(COMMAS_AS_SEPARATORS_KEY);
    let tagsAsArray = getTagsAsArray(tagsToSubmit, splitOnCommas);
    let tagsKey;

    if (isCaseSensitive)
      tagsKey = BLOCKED_CASE_SENSITIVE_TAGS_KEY;
    else
      tagsKey = BLOCKED_TAGS_KEY;

    const oldTags = getData(tagsKey);

    tagsAsArray = tagsAsArray.filter(tag => !oldTags.includes(tag));

    for(let i = 0; i < tagsAsArray.length; i++) {
      const keywordStart = /^(regex:|source:(regex(\((.*?)\):|:))*|regex\((.*?)\):)/ig;
      console.log(tagsAsArray[i].replace(keywordStart, ""));
      if (tagsAsArray[i].replace(keywordStart, "").trim().length === 0) {
        errorEl.removeAttribute("hidden");
        return;
      }
    }

    // Hide the error message if it was visible
    if (errorEl.hasAttribute("hidden"))
      errorEl.setAttribute("hidden", true);

    if (tagsAsArray.length > 0) {
      // Get the old array of tags and concatenate the new one
      const allTags = oldTags.concat(tagsAsArray);

      // Store the new array of tags and update the tags to show
      storeData(tagsKey, allTags);
      addTagsToDialog(tagsAsArray, tagsKey);

      // Hide empty tags element if it was visible and show the tags
      const blockedTagsListContainer = document.getElementById(blockTagListId);
      const csBlockedTagsListContainer = document.getElementById(csBlockTagListId);

      // Update tag section visibility if required
      checkForEmptyTags(getData(BLOCKED_TAGS_KEY), getData(BLOCKED_CASE_SENSITIVE_TAGS_KEY));
    }

    // Clear input
    tagInput.value = "";
  } else {
    errorEl.removeAttribute("hidden");
  }
}

//----------------------------------------
// MAIN METHODS
//----------------------------------------

// Wait for page to fully load to start doing things
window.onload = () => {
  const baseContainer = document.getElementById("app");
  let NSFWCheckbox = document.querySelector(postAsNSFW);
  let createPostForm = document.getElementById(createPostContainerId);
  let searchContainer = document.getElementsByClassName(searchContainerClass)[0];
  let postsContainer = getPostContainer();

  if (NSFWCheckbox && !NSFWCheckbox.checked && markNewPostsAsNSFW && createPostForm)
    NSFWCheckbox.click();

  // Make sure that the widget height is correct
  updateWidgetHeightCSS();

  // If there is a posts container in the page
  if (postsContainer) {

     // Perform first filter
    if (blockedTitleTags.length > 0 || csBlockedTitleTags.length > 0) {
      console.info("Page loaded, filtering tags...");
      filterPosts(postsContainer);
    }

    // Open remaining posts if enabled
    if (expandMediaPosts)
      openPosts(postsContainer);

    // Fix video previews if there was any post
    if (fixPostVideoPreviews)
      fixBrokenVideoPreviews(postsContainer);

    document.getElementsByClassName(postsContainer.className)[0].setAttribute("id", processedPageChecker);
  }

  // Observe the changes of the page to know when to rethrow the filter method when the user changes the page
  const observer = new MutationObserver((e) => {
    if (document.getElementById(processedPageChecker))
      return;

    const blockedTags = getData(BLOCKED_TAGS_KEY);
    const csBlockedTags = getData(BLOCKED_CASE_SENSITIVE_TAGS_KEY);
    const autoOpenMedia = getData(AUTO_OPEN_MEDIA_POSTS_KEY);
    const fixVideoPreviews = getData(FIX_BROKEN_VIDEO_PREVIEWS_KEY);
    const markAsNSFW = getData(MARK_POSTS_AS_NSFW_KEY);

    searchContainer = document.getElementsByClassName(searchContainerClass)[0];
    NSFWCheckbox = document.querySelector(postAsNSFW);
    createPostForm = document.getElementById(createPostContainerId);
    postsContainer = getPostContainer();

    // If on the creation post page, mark as NSFW the post
    if (NSFWCheckbox && !NSFWCheckbox.checked && markAsNSFW && createPostForm)
      NSFWCheckbox.click();

    for (let i = 0; i < e.length; i++) {
      if (e[i].target.getElementsByClassName(editPostClass)[0])
        return;

      let postEdit = e[i].target.getElementsByClassName("form-control")[0];
      let filteredPostStub = e[i].target.getElementsByClassName("filtered-post-stub")[0];
      let filteredPostBtn = e[i].target.getElementsByClassName("hide-post-btn")[0];

      // Prevent filtering to retrigger if editting a post
      if (postEdit && searchContainer)
        return;

      // If a post was already filtered don't add stubs again
      if (filteredPostBtn || filteredPostStub)
        continue;

      // Perform actions if on one of the pages
      if (postsContainer) {

        if (blockedTags.length > 0 || csBlockedTags.length > 0) {
          console.info("Page reloaded, filtering new posts...");
          filterPosts(postsContainer);
        }

        if (autoOpenMedia)
          openPosts(postsContainer);

        if (fixVideoPreviews)
          fixBrokenVideoPreviews(postsContainer);

        // Mark page as already processed
        document.getElementsByClassName(postsContainer.className)[0].setAttribute("id", processedPageChecker);
        break;
      }
    }
  });

  observer.observe(baseContainer, {subtree: true, childList: true});
}

// Gets initial CSS content
function getInitialCSS() {
  let style = document.head.getElementsByTagName("style")[0];
  if (!style) {
    style = document.createElement('style');
    document.head.appendChild(style);
    return "";
  }

  return style.innerHTML;
}

// Gets the CSS from the script to be in effect
function getEffectiveCSS() {
  let fullCSS = guiCSS();

  // Check each setting that changes CSS apart from the GUI
  if (getData(UNBLUR_THUMBNAILS_KEY))
    fullCSS += unblurCSS;

  return fullCSS;
}

// Splits tags on commas if necessary
function getTagsAsArray(tagsString, splitOnCommas=true) {
  if (splitOnCommas) {
    return tagsString.split(",");
  }
  return [tagsString];
}

// Gets the current page posts container
function getPostContainer() {
  return document.getElementsByClassName(postsContainerClass)[0] ||
      document.getElementsByClassName(profileContainerClass)[0] ||
      document.getElementsByClassName(searchContainerClass)[0] ||
      document.querySelector(".post");
}

// Adds an element to the document
function addElement(parentEl, elementTag, elementId, html="", idAsClass=false) {
  let p = parentEl;
  let newElement = document.createElement(elementTag);
  if (idAsClass)
    newElement.className = elementId;
  else
    newElement.setAttribute('id', elementId);
  newElement.innerHTML = html;
  p.appendChild(newElement);
  return newElement;
}

// Adds the new tags to the dialog
function addTagsToDialog(tagArray, tagsType) {
  let tagsContainerSelector = csBlockTagListId;

  if (tagsType === BLOCKED_TAGS_KEY)
    tagsContainerSelector = blockTagListId;

  const blockedTagsListEl = document.getElementById(tagsContainerSelector);

  tagArray.forEach(tag => {
    addTagElement(tag, tagsType, blockedTagsListEl);
  });
}

// Adds a tag element to the dialog
function addTagElement(tag, tagType, tagListElement) {

  // Get the specific css class for tag type
  let tagSpecificClass = "case-sensitive-tag";

  if (tagType === BLOCKED_TAGS_KEY)
    tagSpecificClass = "case-insensitive-tag";

  // Add the element to the dialog and bind the button event
  const newTag = addElement(tagListElement,
               "LI",
               `blocked-tag ${tagSpecificClass}`,
               tagContent
                 .replace("TAG-NAME", parseTagName(tag))
                 .replace("TAG-TYPE", tagType)
                 .replace("TOOLTIP-CONTENT", `'${tag}' (${tagSpecificClass})`),
               true);

  newTag.getElementsByTagName("button")[0].onclick = () => {
    onRemoveBlockedTag(tagType, tag, newTag);
  }

  newTag.getElementsByTagName("label")[0].onclick = () => {
    GM_setClipboard(tag);
    document.getElementById("clipboard-notice").removeAttribute("hidden");
  }

  // Mark element as draggable
  newTag.setAttribute("draggable", true);

  // Handle drag & drop events
  newTag.ondragstart = (e) => {
    newTag.setAttribute("data-dragged-item", true);
  }

  newTag.ondragover = (e) => {
    e.preventDefault();
  }

  newTag.ondragend = () => {
    newTag.removeAttribute("data-dragged-item");
  }

  newTag.ondrop = (e) => {
    const target = e.target;
    const targetTag = target.nodeName === "LABEL" || target.nodeName == "BUTTON" ?
          target.parentNode.parentNode : target.parentNode;
    const draggedItem = document.querySelector('li[data-dragged-item]');
    const nextTagName = targetTag.getElementsByTagName("label")[0].innerHTML;
    const movedTagName = draggedItem.getElementsByTagName("label")[0].innerHTML;
    let movedTagParentId = targetTag.parentNode.id;
    let draggedTagType = BLOCKED_TAGS_KEY;

    if (draggedItem.className.includes("case-sensitive-tag"))
      draggedTagType = BLOCKED_CASE_SENSITIVE_TAGS_KEY;

    // Only accept dropping it in the same area
    if (draggedItem.parentNode.id === movedTagParentId) {

      // Update order of tags
      let tags = getData(draggedTagType);

      // We need to change a bit the logic depending on the position of the element
      if (tags.indexOf(nextTagName) > tags.indexOf(movedTagName)) {
        tags.splice(tags.indexOf(nextTagName) + 1, 0, null);
        targetTag.parentNode.insertBefore(draggedItem, targetTag.nextSibling);
      } else {
        tags.splice(tags.indexOf(nextTagName), 0, null);
        targetTag.parentNode.insertBefore(draggedItem, targetTag);
      }

      tags.splice(tags.indexOf(movedTagName), 1);
      tags[tags.indexOf(null)] = movedTagName;

      storeData(draggedTagType, tags);
    }
    newTag.removeAttribute('data-dragged-item');
  }
}

// Updates the CSS of the site
function updateCSS() {
  let style = document.head.getElementsByTagName("style")[0];
  if (!style) {
    style = document.createElement('style');
    style.innerHTML = css;
    document.head.appendChild(style);
    return;
  }

  // Update the CSS with the initial one + the settings CSS
  style.innerHTML = initialCSS + getEffectiveCSS();
}

// Update the visible part of the settings widget
function updateWidgetHeightCSS() {
  const widget = document.getElementById(settingsWidgetContainerId);
  const widgetHeight = widget.getBoundingClientRect().height;
  const visibleTopHeight = 10;
  if (widgetHeight > 0 && widgetHeight - visibleTopHeight > 0) {
    const oldHeight = getData(WIDGET_HEIGHT_KEY);
    let updatedHeight = (widgetHeight - visibleTopHeight) + "px";
    if (oldHeight != updatedHeight) {
      storeData(WIDGET_HEIGHT_KEY, (widgetHeight - visibleTopHeight) + "px");
      updateCSS();
    }
  }
}

// Updates the visible tags in the blocked tags dialog
function updateVisibleTags() {
  const csBlockedTags = getData(BLOCKED_CASE_SENSITIVE_TAGS_KEY);
  const blockedTags = getData(BLOCKED_TAGS_KEY);
  const blockedTagsListEl = document.getElementById(blockTagListId);
  const csBlockedTagsListEl = document.getElementById(csBlockTagListId);

  // Reset list
  blockedTagsListEl.innerHTML = "";
  csBlockedTagsListEl.innerHTML = "";

  blockedTags.forEach(tag => {
    addTagElement(tag, BLOCKED_TAGS_KEY, blockedTagsListEl);
  });

  csBlockedTags.forEach(tag => {
    addTagElement(tag, BLOCKED_CASE_SENSITIVE_TAGS_KEY, csBlockedTagsListEl);
  });

  checkForEmptyTags(blockedTags, csBlockedTags);
}

// Parses the tag name if it's a named regex
function parseTagName(tag) {
  if ((tag.toLowerCase().startsWith("regex(") || tag.toLowerCase().startsWith("source:regex(")) && new RegExp(namedRegex, "i").test(tag)) {
    let regexName = getRegexNameFromTag(tag);
    if (regexName != null) {
      return tag.replace(new RegExp(namedRegex + ".*", "i"), `regex: ${regexName}`);
    }
    // If the named regex wasn't correctly formatted we just return the regular tag however it was typed
    return tag;
  }
  return tag;
}

// Gets the regex name from a named regex tab
function getRegexNameFromTag(tag) {
  let regexMatch = new RegExp(namedRegex).exec(tag);
  if (regexMatch != null) {
    return regexMatch[1];
  }
  return null;
}

// Toggles checkboxes on sliders inside the onclick event
function toggleCheckbox(event) {
  let checkbox = event.target.parentNode.getElementsByTagName("input")[0];
  checkbox.checked = !checkbox.checked;
  return checkbox.checked;
}

// Opens a dialog box
function openDialog(dialogSelector) {
  dialogSelector.show();
  dialogBg.style.display = "block";
}

// Closes the dialog box
function closeDialog(dialogSelector) {
  dialogSelector.close();
  dialogBg.style.display = "none";
  document.getElementById("blocked-tags-input-error").setAttribute("hidden", true);
  document.getElementById("clipboard-notice").setAttribute("hidden", true);
}

// Adds the notice for no blocked tag if necessary
function checkForEmptyTags(blockedTags, csBlockedTags) {
  const emptyTagsEl = document.getElementById("empty-blocked-tags");
  const blockedTagsListEl = document.getElementById(blockTagListId);
  const csBlockedTagsListEl = document.getElementById(csBlockTagListId);
  const tagsSeparatorEl = document.getElementById("blocked-tags-separator");
  const hasAnyBlockedTag = blockedTags.length > 0;
  const hasAnyCsBlockedTag = csBlockedTags.length > 0;
  const hasAnyTag = hasAnyBlockedTag || hasAnyCsBlockedTag;
  const isBlockedTagsListHidden = blockedTagsListEl.hasAttribute("hidden");
  const isCsBlockedTagsListHidden = csBlockedTagsListEl.hasAttribute("hidden");

  // Check if any has tags
  if (hasAnyTag) {
    emptyTagsEl.setAttribute("hidden", true);
    if (hasAnyBlockedTag)
      blockedTagsListEl.removeAttribute("hidden");
    else
      blockedTagsListEl.setAttribute("hidden", true);

    if (hasAnyCsBlockedTag)
      csBlockedTagsListEl.removeAttribute("hidden");
    else
      csBlockedTagsListEl.setAttribute("hidden", true);

    // Either both have tags or at least one has tags
    if (hasAnyBlockedTag && hasAnyCsBlockedTag)
      tagsSeparatorEl.removeAttribute("hidden");
    else
      tagsSeparatorEl.setAttribute("hidden", true);

  // If it has no tags
  } else if (!hasAnyTag) {
    emptyTagsEl.removeAttribute("hidden");
    blockedTagsListEl.setAttribute("hidden", true);
    tagsSeparatorEl.setAttribute("hidden", true);
    csBlockedTagsListEl.setAttribute("hidden", true);
  }
}

// Checks if the page is loading
function checkIfPageIsLoading(postsContainer, posts) {
  const searchContainer = document.getElementsByClassName(searchContainerClass)[0];
  const emptyResultsContainer = document.querySelector(loadingSpinnerSelector);
  return posts.length === 0 && postsContainer === searchContainer && emptyResultsContainer;
}

// Removes a tag from the view and the storeData
function onRemoveBlockedTag(tagType, tagToRemove, tagElement) {
  // Remove the actual tag element
  tagElement.parentNode.removeChild(tagElement);

  // Update the storage
  let allTags = getData(tagType);
  let tagIndex = allTags.indexOf(tagToRemove);

  if (tagIndex != -1)
    allTags.splice(tagIndex, 1);

  storeData(tagType, allTags);

  // Toggle visibility of the elements where the tag was removed if they are empty
  checkForEmptyTags(getData(BLOCKED_TAGS_KEY), getData(BLOCKED_CASE_SENSITIVE_TAGS_KEY));
}

// Filters posts by words from the title as specified by the user
function filterPosts(postsContainer) {
  let posts = postsContainer.getElementsByClassName(postContainer);

  if (checkIfPageIsLoading(postsContainer, posts)) {
    let intervalCount = 0;

    // Check for posts in 1 second intervals
    const checkInterval = setInterval(() => {
      posts = document.getElementsByClassName(postContainer);
      // Finish interval checking the search finished or 30 seconds passed
      if (posts.length > 0 || intervalCount > 30 || !document.querySelector(loadingSpinnerSelector)) {
        clearInterval(checkInterval);
        filterPostsInPage(posts);
      }
      intervalCount++;
    }, 1000);

    return;
  }

  filterPostsInPage(posts);
}

// Filters the posts in the current page
function filterPostsInPage(postsList) {
  const showStubs = getData(SHOW_FILTERED_STUBS_KEY);
  const blockedTags = getData(BLOCKED_TAGS_KEY);
  const csBlockedTags = getData(BLOCKED_CASE_SENSITIVE_TAGS_KEY);

  const blockedTagsByType = {
    byTitle: [],
    bySource: []
  }

  const csBlockedTagsByType = {
    byTitle: [],
    bySource: []
  }

  // Get type of filter for each tag first to avoid unnecesary iterations
  blockedTags.forEach(tag => {
    if (tag.startsWith("source:"))
      blockedTagsByType.bySource.push(tag);
    else
      blockedTagsByType.byTitle.push(tag);
  })

  csBlockedTags.forEach(tag => {
    if (tag.startsWith("source:"))
      csBlockedTagsByType.bySource.push(tag);
    else
      csBlockedTagsByType.byTitle.push(tag);
  })

  // Filter every post
  Array.from(postsList).forEach(post => {
    if (post.getElementsByClassName(editPostClass)[0])
      return;

    post.setAttribute(filteredPostChecker, true);
    const communityEl = post.querySelector(postCommunityNameContainer);
    const communityLinkEl = post.querySelector(postCommunityContainer);
    const sourceEl = post.querySelector(postSrcLink)
    let titleEl = post.querySelector(".post-title h1 span");
    let source = null;
    let community;
    let communityLink;
    let title;

    // Make sure we get a valid title selector
    if (!titleEl)
      titleEl = post.querySelector(".post-title h1 a");
    title = titleEl.innerHTML;

    // Make sure community info is in post, otherwise get from page assuming the user
    // is in the community view instead, where the community isn't in the posts but the page
    if (!communityEl) {
      community = document.querySelector(postCommunityNameContainer).innerHTML;
      communityLink = document.querySelector(postCommunityContainer).href;
    } else {
      community = communityEl.innerHTML;
      communityLink = communityLinkEl.href;
    }

    if (sourceEl)
      source = sourceEl.href;

    const postInfo = {
      post: post,
      title: title,
      communityName: community,
      communityLink: communityLink,
      source: source
    }

    // Filter posts by title first
    const wasFiltered = filterPost(blockedTagsByType.byTitle, csBlockedTagsByType.byTitle, showStubs, postInfo);

    // Filter posts by source after if it wasn't already filtered by title
    if (!wasFiltered && source)
      filterPost(blockedTagsByType.bySource, csBlockedTagsByType.bySource, showStubs, postInfo, true);
  })
}

function filterPost(blockedTags, csBlockedTags, showStubs, postInfo, filterBySource=false) {
  // Filter posts using case insensitive tags
  if (blockedTags.length > 0) {
    for(let i = 0; i < blockedTags.length; i++) {
      let tag = blockedTags[i];

      if (filterBySource)
        tag = tag.substring(7);

      const regex = new RegExp(escapeRegex(tag), "i");
      if (regex.test(filterBySource ? postInfo.source : postInfo.title)) {
        removePost(postInfo, blockedTags[i], showStubs);

        return true;
      }
    }
  }

  // Filter posts using case sensitive tags
  if (csBlockedTags.length > 0) {
    for(let i = 0; i < csBlockedTags.length; i++) {
      const tag = csBlockedTags[i];

      if (filterBySource)
        tag = tag.substring(7);

      const regex = new RegExp(escapeRegex(tag));
      if (regex.test(filterBySource ? postInfo.source : postInfo.title)) {
        removePost(postInfo, csBlockedTags[i], showStubs);
        return true;
      }
    }
  }
}

// Escapes the special characters entered by a user
function escapeRegex(regex) {
  // Escape \'s in regex with double \\ if it's a regex to be used in "new RegExp()" method
  if (regex.toLowerCase().startsWith("regex:"))
    return regex.substring(6).replace("\\", "\\");

  // If it's a named regex
  else if (regex.toLowerCase().startsWith("regex(") && new RegExp(namedRegex, "i").test(regex)) {
    const regStart = `regex(${getRegexNameFromTag(regex)}):`;
    return regex.substring(regStart.length).replace("\\", "\\");
  }
  return regex.replace(/([()[{*+.$^\\|?])/g, '\\$1');

}

// Filters out a post
function removePost(postInfo, tag, showStubs) {
  const post = postInfo.post;

  // Hide the content of the children and add a stub by default
  post.style.display = "none";
  addFilterStubToPost(postInfo, tag)

  // If the setting to show stubs is disabled we hide the post completelly
  if (!showStubs) {
    // Removes the posts from the body completelly and logs it to the console
    console.info(`Removing post with title ${postInfo.title} from community ${postInfo.communityName} because the post contained the tag ${parseTagName(tag)}.`);
    post.setAttribute("hidden", true);

    let siblingNode = post.nextElementSibling;

    // Remove separator if it exists
    if (siblingNode && siblingNode.tagName == "HR")
      siblingNode.setAttribute("hidden", true);
  }
}

// Adds a filter stub after filtering a post
function addFilterStubToPost(postInfo, tag) {
  let stub = addElement(postInfo.post, "DIV", "filtered-post-stub",
                          filteredPostStubContent.replace("TAG", parseTagName(tag))
                            .replace("POST-TITLE", postInfo.title)
                            .replace("POST-COMMUNITY", postInfo.communityName), true);
  stub.getElementsByClassName("stub-hidden-post-community-link")[0].href = postInfo.communityLink;

  // Add actions for the buttons
  stub.getElementsByClassName("show-hidden-post-title-btn")[0].onclick = () => {
    stub.getElementsByClassName("stub-hidden-post-title")[0].removeAttribute("hidden");
    stub.getElementsByClassName("show-hidden-post-title-btn")[0].setAttribute("hidden", true);
  }

  // Add theming and add it before the filtered post
  stub.classList.add("post-listing");
  postInfo.post.parentNode.insertBefore(stub, postInfo.post);

  stub.getElementsByClassName("show-hidden-post-btn")[0].onclick = () => {
    stub.setAttribute("hidden", true);
    postInfo.post.style= "";

    // Make sure the title button element is hidden
    stub.getElementsByClassName("stub-hidden-post-title")[0].setAttribute("hidden", true);
    stub.getElementsByClassName("show-hidden-post-title-btn")[0].removeAttribute("hidden");

    // Prepend the post re-remover if it didn't exist yet
    let postRemoveEl = postInfo.post.getElementsByClassName("hide-post-btn")[0];
    if (!postInfo.post.getElementsByClassName("hide-post-btn")[0]) {
      postRemoveEl = document.createElement("BUTTON");
      postRemoveEl.className = "hide-post-btn";
      postRemoveEl.innerHTML = "🗶";
      postRemoveEl.setAttribute("type", "button");
      postInfo.post.prepend(postRemoveEl);

      postRemoveEl.onclick = () => {
        stub.removeAttribute("hidden");
        postRemoveEl.setAttribute("hidden", true);
        postInfo.post.style.display = "none";
      }
    } else
      postRemoveEl.removeAttribute("hidden");


    for (let i = 0; i < postInfo.post.children.length; i++) {
      postInfo.post.children[i].style = "";
    }
  }
}

// Opens media posts so that the image or video is shown for all posts in the current page of the timeline
function openPosts(postsContainer) {
  let posts = postsContainer.getElementsByClassName(postContainer);

  if (checkIfPageIsLoading(postsContainer, posts)) {
    let intervalCount = 0;

    // Check for posts in 1 second intervals
    const checkInterval = setInterval(() => {
      posts = document.getElementsByClassName(postContainer);

      // Finish interval checking the search finished or 30 seconds passed
      if (posts.length > 0 || intervalCount > 30 || !document.querySelector(loadingSpinnerSelector)) {
        clearInterval(checkInterval);
        clickPostsThumbnail(posts);
      }
      intervalCount++;
    }, 1000);

    return;
  }

  clickPostsThumbnail(posts)
}

// Clicks the thumbnail of the post to open it
function clickPostsThumbnail(posts) {
   Array.from(posts).forEach(post => {

    const postHasMedia = post.querySelector("picture, video");
    let clickableThumbnail = post.querySelector("button.thumbnail");
    if (postHasMedia) {
      if (!clickableThumbnail)
        clickableThumbnail = post.querySelector("a.text-body[href$='.mp4'] .thumbnail, a.text-body[href$='.webm'] .thumbnail, a.text-body[href^='https://www.redgifs.com/watch'] .thumbnail")

      if (clickableThumbnail && !post.getElementsByClassName("filtered-post-stub")[0]){
        let intervalCount = 0;
        const checkInterval = setInterval(() => {
          const postChildren = post.children;
          clickableThumbnail.click();

          // Check that the post opened within 5 tries
          if (postChildren.length > 2 || intervalCount > 5)
            clearInterval(checkInterval);

          intervalCount++;
        }, 1000);
      }

      post.setAttribute(openedPostChecker, true);
    }
  })
}

// Fixes imgur previews showing as jpg instead of the actual video
function fixBrokenVideoPreviews(postsContainer) {
  let posts = postsContainer.getElementsByClassName(postContainer);

  if (checkIfPageIsLoading(postsContainer, posts)) {
    let intervalCount = 0;

    // Check for posts in 1 second intervals
    const checkInterval = setInterval(() => {
      posts = document.getElementsByClassName(postContainer);
      // Finish interval checking the search finished or 30 seconds passed
      if (posts.length > 0 || intervalCount > 30 || !document.querySelector(loadingSpinnerSelector)) {
        clearInterval(checkInterval);
        checkVideoPreviews(postsContainer);
      }
      intervalCount++;
    }, 1000);

    return;
  }

  checkVideoPreviews(postsContainer);
}

// Updates the video previews when needed so that they work again
function checkVideoPreviews(postsContainer) {
  setTimeout(() => {
    const posts = postsContainer.getElementsByClassName(postContainer);
    updateVideoPreviews(posts);
  }, 500);
}

// Updates the video previews
function updateVideoPreviews(posts) {
  Array.from(posts).forEach(post => {
    const postSourceLinkEl = post.querySelector(postSrcLink);
    const imageLinkContainer = post.querySelectorAll("div:nth-of-type(3) > a:not(.btn)");

    if (postSourceLinkEl && imageLinkContainer.length > 0) {
      const postSourceLink = postSourceLinkEl.href;
      let newSrc = postSourceLink;

      if (postSourceLink.includes("i.imgur.com") && postSourceLink.endsWith(".gifv"))
        newSrc = postSourceLink.replace(".gifv", ".mp4");
      else if (!postSourceLink.includes("i.imgur.com") && postSourceLink.includes("imgur.com"))
        newSrc = postSourceLink.replace("imgur.com", "i.imgur.com") + ".mp4";
      else if (imageLinkContainer.length >= 1 && (postSourceLink != imageLinkContainer[0].href) && imageLinkContainer[0].href.endsWith(".webm") || imageLinkContainer[0].href.endsWith(".mp4")) {
        newSrc = imageLinkContainer[0].href;
      }

      // Only apply changes if the src was suposed to be different
      if (newSrc != postSourceLink) {

        imageLinkContainer.forEach(imageContainer => {
          let pictureContainer = imageContainer.querySelector("picture");
          if (pictureContainer) {
            pictureContainer.setAttribute("hidden", true);
            let videoElement = addElement(post.querySelector("div:nth-child(3).my-2"), "VIDEO",
                                          `embed-responsive-item ${fixedPreviewVideoClass} col-12`,
                                          videoSourceContent.replace("VIDEO-SOURCE", newSrc)
                                            .replace("VIDEO-TYPE", newSrc.split(".").at(-1)),
                                          true)
            videoElement.setAttribute("controls", "");
            videoElement.setAttribute("loop", "");
            videoElement.parentNode.classList.add("embed-responsive", fixedPreviewContainerClass);
          }
        });
      }
    }
  });
}
//----------------------------------------
// STORAGE METHODS
//----------------------------------------

// Composes the key with the current instance name to store data per-instance
function composeInstanceKey(key) {
  return document.location.host + "->" + key;
}

// Saves data to the storage of the userscript
function storeData(key, value) {
  const instanceKey = composeInstanceKey(key);
  let treatedValue = value;
  if (typeof value === "array" || typeof value === "object")
    treatedValue = JSON.stringify(value);
  GM_setValue(instanceKey, treatedValue);
  return treatedValue;
}

// Gets data from the userscript storage
function getData(key) {
  const instanceKey = composeInstanceKey(key);
  let value = GM_getValue(instanceKey);

  if (value === null || value === undefined)
    return null;

  if (typeof value === "string") {
    let isValueArray = value.startsWith("[") && value.endsWith("]");
    let isValueObject = value.startsWith("{") && value.endsWith("}");
    if (isValueArray || isValueObject)
        return JSON.parse(value);
  }
  return value;
}