FuturePost

Extension to 4chanX that adds support for delayed posts. Useful for delaying your posts when you want to keep a slow thread bumped while idle.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name     FuturePost
// @version  1.1
// @include      http://boards.4chan.org/*
// @include      https://boards.4chan.org/*
// @include      http://sys.4chan.org/*
// @include      https://sys.4chan.org/*
// @include      http://www.4chan.org/*
// @include      https://www.4chan.org/*
// @include      http://boards.4channel.org/*
// @include      https://boards.4channel.org/*
// @include      http://sys.4channel.org/*
// @include      https://sys.4channel.org/*
// @include      http://www.4channel.org/*
// @include      https://www.4channel.org/*
// @license MIT
// @require  https://code.jquery.com/jquery-3.5.1.min.js
// @namespace PyonScripts
// @description Extension to 4chanX that adds support for delayed posts. Useful for delaying your posts when you want to keep a slow thread bumped while idle.
// ==/UserScript==

// A separate queue for each page
let pageQueues = {
    '2': [],
    '3': [],
    '4': [],
    '5': [],
    '6': [],
    '7': [],
    '8': [],
    '9': [],
    '10': [],
    '11': []
};

function delay(time) {
  return new Promise(resolve => setTimeout(resolve, time));
}

// The main loop that will be responsible for posting the enqueued posts
setInterval(function() {
    const pageCountElement = $('#page-count');

    // if post-count has the class warning, don't post anything
    if (document.querySelector("#post-count").classList.contains('warning')) {
        return;
    }

    const currentPage = Number(pageCountElement.text());

    for (let i = 3; i <= currentPage; i++) {
        if (pageQueues[i.toString()] && pageQueues[i.toString()].length > 0) {
            const post = pageQueues[i.toString()].shift();

            document.querySelector('#shortcut-qr a').click()
            delay(1000).then(() =>  {
              if(post.file) {
                var detail = {file: post.file, name: post.fileName};
                if (typeof cloneInto === 'function') {
                  detail = cloneInto(detail, document.defaultView);
                }
                var event = new CustomEvent('QRSetFile', {bubbles: true, detail: detail});
                document.dispatchEvent(event);
              }
              document.querySelector('#qr textarea').value = post.message;

            });
            delay(2000).then(() =>  {
              document.querySelector("#file-n-submit input[type='submit']").click();
              // Attempt to remove the preview from the thread
              const fakePost = document.getElementById(`fp${post.mockNumber}`);
              if (fakePost) {
                fakePost.remove();
              }
            });
            console.log('Remaining queue', pageQueues);
            break;
        }
    }
}, 60000);

function removeFuturePost(postNumber) {
  // Iterate over each page queue in the pageQueues object
  for (const queueName in pageQueues) {
    const queue = pageQueues[queueName];
    // Find the index of the object with a matching postNumber in the queue
    const index = queue.findIndex(obj => obj.mockNumber === postNumber);
    // If the object is found, remove it from the queue
    if (index !== -1) {
      queue.splice(index, 1);
      removePostPreview(postNumber);
      break;
    }
  }
  console.log('Remaining queue', pageQueues);
}

function removePostPreview(postNumber) {
  // Attempt to remove the preview from the thread
  const fakePost = document.getElementById(`fp${postNumber}`);
  if (fakePost) {
    fakePost.remove();
  }
}

function initExtension() {
    var qrElement = document.querySelector('#qr.dialog');
    var futurePostEl = document.querySelector('#future-post');

    // Stop init if it can't find the quickly reply dialog, or if this extension
    // has already been initialized
    if (!qrElement || futurePostEl) {
        return;
    }

    const originalButton = qrElement.querySelector('input[type="submit"]');
    if (!originalButton) {
      return;
    }

    // Create and style your fake button
    var fakeButton = document.createElement('input');
    fakeButton.type = 'submit';
    fakeButton.value = "Submit";
    fakeButton.id = 'future-post';

    // Submit handler for the delayed post button
    fakeButton.addEventListener('click', (e) => {
      e.preventDefault();  // Prevents the default action
      e.stopPropagation(); // Prevents the event from propagating up the DOM tree, preventing any parent handlers from being notified of the event

      // Your custom submit behavior here
      const textArea = $('#qr textarea');
      const fileInput = document.querySelector('#qr input[type="file"]'); // replace with your actual file input ID

      const pageNum = $('#fp-page-num').val();

      let fileBlob;
      let fileName;

      document.addEventListener('QRFile', function(e) {
        if(e.detail) {
          fileName = e.detail.newName;
          fileBlob = new Blob([e.detail], { type: e.detail.type });
        }
      }, false);

      var event = new CustomEvent('QRGetFile', {bubbles: true, detail: null});
      document.dispatchEvent(event);

      // Generate a random post number
      const randomNumber = Math.floor(Math.random() * 1000000);
      let post = {
          mockNumber: randomNumber,
          message: textArea.val(),
          file: fileBlob, // this will be null if no file was selected
          fileName: fileName // this will be null if no file was selected
      };
      pageQueues[pageNum].push(post);

      // Try to reset the form, doings lots of extra stuff i prob don't have to
      $('#fp-checkbox').prop('checked', false).trigger('change');
      fileInput.value = '';
      textArea.val('');
      const closeAnchor = document.querySelector('#qr .close');
      closeAnchor.click();
      console.log(pageQueues);

      // Add a placeholder delayed post to the DOM
      const threadDiv = document.querySelector('.thread');

      // Create a new postContainer element
      const postContainer = document.createElement('div');
      postContainer.classList.add('replyContainer');
      postContainer.id = `fp${post.mockNumber}`

      let fileInfo = '';

      if(fileName) {
        const fileExtension = fileName.split('.').pop().toLowerCase();
        let imgThumb = '';
        if (['jpg', 'gif', 'png', 'jpeg'].includes(fileExtension)) {
          // Create a File blob with the fileName and set it as the img src
          const imgSrc = URL.createObjectURL(fileBlob);
          const fileSizeInBytes = fileBlob.size;
          const fileSizeInKB = Math.round(fileSizeInBytes / 1024);
          imgThumb = `<a class="fileThumb" href="javascript:void(0);" target="_blank">
            <img src="${imgSrc}" alt="${fileSizeInKB} KB" style="width: 125px;" loading="lazy">
          </a>`;
        }

        fileInfo = `<div class="file">
        <div class="fileText">
          <span class="file-info">
            <a href="javascript:void(0);" target="_blank">${fileName}</a>
          </span>
          </div>
          ${imgThumb}
        </div>`
      }

      // Set the innerHTML of the postContainer element with the provided HTML, replacing the message content
      postContainer.innerHTML = `
        <div class="post reply" style="border: 1px dotted yellow;">
          <div class="postInfoM mobile">
            <span class="nameBlock"><span class="name">Anonymous</span><br /></span>
          </div>
          <div class="postInfo desktop">
            <input type="checkbox" name="51464347" value="delete" /> <span class="nameBlock"><span class="name">Anonymous</span> </span> <span class="dateTime" title="This post will not be submitted until the page number that is shown" >
            Page ${pageNum}
            </span>
            <span class="postNum desktop"><a href="#fp${post.mockNumber}" title="Link to this post">No.</a><a href="javascript:void(0);" title="You can't reply to a future post">Pending</a></span>
            <a class="menu-button delete-future-post" href="javascript:void(0);"><i class="fa fa-times"></i></a>
          </div>
          ${fileInfo}
          <blockquote class="postMessage" style="min-width: 300px">
            ${post.message.replace(/\n/g, '<br>')}
            <br />
            <br />
            <strong style="color: red;">(Only visible to you)</strong>
          </blockquote>
        </div>
      `;

      // Append the new postContainer element to the thread div
      threadDiv.appendChild(postContainer);

      postContainer.querySelector('.delete-future-post').addEventListener('click', function(e) {
        e.preventDefault();
        const confirmation = confirm('Remove this future post?');
        if (confirmation) {
          removeFuturePost(post.mockNumber)
        }
      });
    });

    // Append checkbox to qrElement
    var checkbox = document.createElement('input');
    checkbox.type = 'checkbox';
    const futurePostHtml = `<div>
            Delay Post?
            <input type="checkbox" id="fp-checkbox" />
            Page:
            <select id="fp-page-num" required disabled>
              <option value="2">2</option>
              <option value="3">3</option>
              <option value="4">4</option>
              <option value="5">5</option>
              <option value="6" selected>6</option>
              <option value="7">7</option>
              <option value="8">8</option>
              <option value="9">9</option>
              <option value="10">10</option>
              <option value="11">11</option>
            </select>
      </div>
    `;
    $(qrElement).append(futurePostHtml);

    $(document).on('change', '#fp-checkbox', function() {
      if($(this).is(":checked")) {
        $('#fp-page-num').prop('disabled', false);
        // Check if fakeButton already exists in the DOM
        if (document.querySelector('#future-post')) {
          // If it does, simply show it
          fakeButton.style.display = "inline";
        } else {
          // Otherwise, add it to the DOM
          $(originalButton).after(fakeButton);
        }
        originalButton.style.display = 'none';
      } else {
        $('#fp-page-num').prop('disabled', true);
        // Replace the fake button with the original one
        fakeButton.style.display = "none";
        originalButton.style.display = "inline";
      }
    });

    startIdleStats();
}

function startIdleStats() {
  let lastUpdateTime = Date.now();
  let changeDurations = []; // keep track of all durations between changes

  // Create a callback function to be executed whenever a mutation is observed
  const callback = function(mutationsList, observer) {
      for(let mutation of mutationsList) {
          if (mutation.type === 'childList') {
              let currentTime = Date.now();
              let duration = currentTime - lastUpdateTime;
              lastUpdateTime = currentTime;
              changeDurations.push(duration);
              dispatchIdleMetricsUpdate(); // dispatch event when metrics change
          }
      }
  };

  // Create an observer instance linked to the callback function
  let observer = new MutationObserver(callback);

  // Start observing the 'page-count' node for configured mutations
  let targetNode = document.getElementById('page-count');
  observer.observe(targetNode, { childList: true, subtree: true });

  // Calculate the weighted average change duration
  function calculateWeightedAverageDuration() {
      let weightedSum = 0;
      let totalWeight = 0;
      for(let i = 0; i < changeDurations.length; i++) {
          let weight = i + 1; // more recent changes have a larger weight
          weightedSum += changeDurations[i] * weight;
          totalWeight += weight;
      }
      let average = weightedSum / totalWeight;
      return average / 60000; // convert from ms to minutes
  }

  // Calculate the approximate time before the thread hits page 11
  function calculateIdleTimeBeforePageEleven() {
      let weightedAvg = calculateWeightedAverageDuration();
      let totalBumpTime = 0;
      for (let page in pageQueues) {
          totalBumpTime += pageQueues[page].length * page * weightedAvg;
      }
      let totalPageChanges = 11;
      let approximateTime = totalPageChanges * weightedAvg + totalBumpTime;
      return approximateTime;
  }

  // Dispatch an event with the updated idle metrics
  function dispatchIdleMetricsUpdate() {
      let event = new CustomEvent('idleMetricsUpdate', {
          detail: {
              idleTimeBeforePageEleven: calculateIdleTimeBeforePageEleven(),
              weightedAvgDuration: calculateWeightedAverageDuration(),
          }
      });
      window.dispatchEvent(event);
  }

  window.addEventListener('idleMetricsUpdate', function(e) {
      let idleTimeBeforePageEleven = e.detail.idleTimeBeforePageEleven;
      let hours = Math.floor(idleTimeBeforePageEleven / 60);
      let minutes = Math.floor(idleTimeBeforePageEleven % 60);
      let weightedAvgDuration = e.detail.weightedAvgDuration.toFixed(2); // rounded to 2 decimal places

      // Display the report in the console with a label for easy filtering
      console.log(
          `%cIdle Metrics Report:\n\n` +
          `Idle time before page 11: ${hours} hour(s) and ${minutes} minute(s)\n` +
          `Weighted average duration: ${weightedAvgDuration} minute(s)`,
          'color: green; font-weight: bold;'
      );
  });
}

// Initialize the extension
document.addEventListener('QRDialogCreation', function(e) {
  initExtension();
}, false);