Reddit - Quick RES user tagging

Quickly tag multiple users with the same tag and open the tag popup using the keyboard

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name           Reddit - Quick RES user tagging
// @description    Quickly tag multiple users with the same tag and open the tag popup using the keyboard
// @author         James Skinner <[email protected]> (http://github.com/spiralx)
// @namespace      http://spiralx.org/
// @version        2.2.0
// @license        MIT
// @icon           data:image/png;base64,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
// @supportURL     https://greasyfork.org/en/scripts/370256-reddit-quick-user-tagging/feedback
// @match          *://*.reddit.com/r/*
// @match          *://*.reddit.com/user/*
// @grant          none
// @run-at         document-end
// @require        https://unpkg.com/jquery@3/dist/jquery.min.js
// @require        https://greasyfork.org/scripts/389748-console-message-v2/code/console-message-v2.js?version=730537
// ==/UserScript==

/* jshint asi: true, esnext: true, laxbreak: true */
/* global jQuery, message */

/**

==== 2.2.0 (2022.07.24) ====
* Change name and description
* Add Q keyboard shortcut to open tag dialog
* Removed references to unused Watcher object
* Tidy up some code

==== 2.1.2 (2022.07.01) ====
* Update icon and add license metadata field

==== 2.1.1 (2022.04.16) ====
* Clicking 'Clear tag' closes tag popup

==== 2.1.0 (2021.11.05) ====
* Fix when ConsoleMessage not available

==== 2.0.5 (2021.06.08) ====
* Update icons

==== 2.0.4 (2021.06.02) ====
* Set width of tag dialog to 800px

==== 2.0.0 (2019.09.03) ====
* Rename script and update version to 2.0.0

==== 1.0.0 (2018.10.09) ====
* Rewrite code base
* Set previous tag on preset tag clicked

==== 0.9.0 (2018.10.09) ====
* Made tag modal wider and text smaller to accomodate more preset tags
* Added clear tag link to the right of the colour drop-down

==== 0.8.0 (2018.07.13) ====
* Changed console.message require to use GreasyFork

==== 0.7.0 (2018.02.13) ====
* Changed the rendering of the tag preview to match how RES does it
* Moved output to all use console.message

==== 0.6.0 (2018.02.11) ====
* Use unpkg.com for jQuery
* Add console.message for logging
* Use localstorage to save tags
* Use new Tag class to store tag info
* Updated ID for text field in tag popup

==== 0.5.1 (2018.02.11) ====
* Update icons to match other Reddit script

==== 0.5.0 (26.08.2017) ====
* Change to simple use of GM_getValue and GM_setValue for storage

==== 0.4.0 (21.08.2017) ====
* Update all other tags correctly when changing a user's tag
* Handle removing all other tags when clearing a user's tag

==== 0.3.0 (13.07.2017) ====

* Checks tag link to see if tag set, always overwrites if not
* Updates other tags for same user on current page

==== 0.2.1 (27.06.2017) ====
* Changed timeout of field set function to 250ms

==== 0.2.0 (31.05.2017) ====
* Updated jQuery to v3.2.1
* Added timeout before overriding tag/colour fields
* Update preview when setting tag/colour

*/

; (($, message) => {

  const STYLES = {
    func: { color: '#c41', 'font-weight': 'bold' },
    attr: { color: '#1a2', 'font-weight': 'bold' },
    value: { color: '#05f' },
    punc: { 'font-weight': 'bold' },
    // comment: { color: 'c1007f' },
    // error: { color: '#f4f', 'font-weight': 'bold' },
    // link: { color: '#05f', 'text-decoration': 'underline' },
  }

  // --------------------------------------------------------------------------

  if (message) {
    message().extend({
      tag({ text, colour }) {
        return this.text(text, {
          fontSize: '0.9em',
          padding: '0 4px',
          border: 'solid 1px rgb(199, 199, 199)',
          borderRadius: 3,
          ...getStyleForColour(colour)
        })
      }
    })
  }

  // --------------------------------------------------------------------------

  const TAG_STORAGE_KEY = 'resPreviousUserTag'

  const TAG_DIALOG_OPEN_TIMEOUT = 200

  const BACKGROUND_TO_TEXT_COLOUR_MAP = {
    none: 'inherit',
    aqua: 'black',
    lime: 'black',
    pink: 'black',
    silver: 'black',
    white: 'black',
    yellow: 'black'
  }

  const IGNORE_TAGS = new Set([ 'A', 'BUTTON', 'INPUT', 'TEXTAREA' ])

  // --------------------------------------------------------------------

  const CLEAR_TAG_LINK = `
    <a href="javascript:void(0)"
      title="Clear tag">
      clear tag
    </a>
  `

  const NO_PREVIEW_TAG = `
    <span class="RESUserTag">
      <a href="javascript:void 0"
         title="Set a tag"
         class="RESUserTagImage userTagLink truncateTag"
      >&nbsp;</a>
    </span>
  `

  const QUICK_REPEAT_TAG = `
    <span class="RESUserTag" style="filter: hue-rotate(90deg) saturate(0.5);">
      <a href="javascript:void 0"
         title="Repeat previous tag"
         class="RESUserTagImage userTagLink truncateTag"
      >&nbsp;</a>
    </span>
  `

  // --------------------------------------------------------------------------

  function getStyleForColour (colour) {
    return {
      color: BACKGROUND_TO_TEXT_COLOUR_MAP[ colour ] || 'white',
      backgroundColor: colour === 'none'
        ? 'transparent'
        : colour
    }
  }

  // --------------------------------------------------------------------------

  const getPreviewForTag = ({ text, colour }) => {
    const { color, backgroundColor } = getStyleForColour(colour)

    return `
      <span class="RESUserTag">
        <a href="javascript:void 0"
           title="${text}"
           class="userTagLink hasTag truncateTag"
           style="color: ${color}; background-color: ${backgroundColor};"
        >${text}</a>
      </span>
    `
  }

  // --------------------------------------------------------------------------

  const EMPTY_TAG = Object.freeze({
    text: '',
    colour: 'none'
  })

  const isEmptyTag = ({ text, colour } = EMPTY_TAG) => text === '' && colour === 'none'

  const areEqualTags = (a, b) => a.text === b.text && a.colour === b.colour

  // --------------------------------------------------------------------------

  let previousUserTag = EMPTY_TAG
  let ctx = null

  if (localStorage[ TAG_STORAGE_KEY ]) {
    previousUserTag = JSON.parse(localStorage[ TAG_STORAGE_KEY ])
  }

  if (message) {
    let msg = message()
      .text('reddit-save-res-tag.init', STYLES.func)
      .text(': ', STYLES.punc)

    if (!isEmptyTag(previousUserTag)) {
      msg = msg
        .text('previousUserTag', STYLES.attr)
        .text(' = ', STYLES.punc)
        .tag(previousUserTag)
    } else {
      msg = msg.text('No previous tag found')
    }

    msg.print()
  } else if (previousUserTag !== EMPTY_TAG) {
    console.log(`previousUserTag = %o`, previousUserTag)
  }

  // --------------------------------------------------------------------------

  class TagContext {
    constructor ($thingElem) {
      this.$thingElem = $thingElem

      this.authorId = $thingElem.data('author-fullname')

      this.$dialog = $('.userTagger-dialog-head').closest('.RESHover')

      this.user = $('.RESHover .RESHoverTitle .res-icon + span').text() || null

      this.$textField = $('#userTaggerText')
      this.$colourField = $('#userTaggerColor')
      this.$previewElem = $('#userTaggerPreview')
      this.$presetTags = $('#userTaggerPresetTags')
    }

    clearFields () {
      this.$textField.val('').focus()
      this.$colourField.val('none')
      this.$previewElem.html(NO_PREVIEW_TAG)
    }

    setFields ({ text, colour }) {
      this.$textField.val(text)
      this.$colourField.val(colour)
      this.$previewElem.html(getPreviewForTag({ text, colour }))
    }

    getTag () {
      return {
        text: this.$textField.val().trim(),
        colour: this.$colourField.val()
      }
    }
  }

  // --------------------------------------------------------------------------

  function onTagSelected (tag) {
    // console.info(`onTagSelected: tag = %o, ctx = %o, prevTag = %o`, tag, ctx, previousUserTag)

    if (message) {
      message()
        .text('onTagSelected', STYLES.func)
        .text('(', STYLES.punc)
        .text('tag', STYLES.attr)
        .text(': ', STYLES.punc)
        .tag(tag)
        .text('): ', STYLES.punc)
        .text('previousUserTag', STYLES.attr)
        .text(' = ', STYLES.punc)
        .tag(previousUserTag)
        .text(', ', STYLES.punc)
        .text('ctx', STYLES.attr)
        .text(' = ', STYLES.punc)
        .object(ctx)
        .print()
    }

    const $tagLinks = $(`.id-${ctx.authorId}`)
      .next()
      .children(0)

    if (isEmptyTag(tag)) {
      $tagLinks
        .html('&nbsp;')
        .css('background-color', 'transparent')
        .removeClass('hasTag')
        .addClass('RESUserTagImage')
    } else if (!areEqualTags(tag, previousUserTag)) {
      previousUserTag = tag
      localStorage[TAG_STORAGE_KEY] = JSON.stringify(previousUserTag)

      $tagLinks
        .text(tag.text)
        .css(getStyleForColour(tag.colour))
        .addClass('hasTag')
        .removeClass('RESUserTagImage')
    }

    ctx = null
  }

  // --------------------------------------------------------------------------

  function onTagClicked ($tagLink) {
      const $thing = $tagLink.closest('.thing')

      // console.info(`a.userTagLink.click`, this, $thing)

      setTimeout(() => {
        ctx = new TagContext($thing)
        ctx.$dialog.width(800)

        const authorHasTag = $tagLink.hasClass('hasTag')

        if (!authorHasTag && !isEmptyTag(previousUserTag)) {
          ctx.setFields(previousUserTag)
        }

        $(CLEAR_TAG_LINK)
          .click(function () {
            ctx.clearFields()
            $('#userTaggerSave').click()
            return false
          })
          .css({ marginLeft: 'auto' })
          .insertAfter(ctx.$colourField)

        const $srcField = ctx.$previewElem.parent().next()

        ctx.$presetTags
          .one('click.resrem', '.userTagLink', function () {
            const $clicked = $(this)
            const clickedTag = {
              text: $clicked.text(),
              colour: $clicked.css('background-color')
            }

            // console.info(`preset .userTagLink.click`, this, clickedTag, ctx)
            onTagSelected(clickedTag)
          })
          .parent()
            .insertBefore($srcField)

        $srcField.next().css({ float: 'left', width: '50%' })
      }, TAG_DIALOG_OPEN_TIMEOUT)
  }

  // --------------------------------------------------------------------------

  $('body')
    .on('click.resrem', '.RESUserTag', function () {
      const $tagLink = $(this).children('a.userTagLink')
      onTagClicked($tagLink)
    })
    .on('click.resrem', '#userTaggerSave', function () {
      if (ctx) {
        onTagSelected(ctx.getTag())
      }
    })
    .on('keypress.resrem', event => {
      if (ctx || event.key !== 'q' || IGNORE_TAGS.has(event.target.tagName)) {
        return
      }

      $('div.thing.comment.noncollapsed.res-selected .entry.res-selected .RESUserTag').click()

      return false
    })

  // --------------------------------------------------------------------------

  /*
  <div class="RESHover RESHoverInfoCard RESDialogSmall" style="top: 387.918px; left: 215.767px; width: 350px; display: block; opacity: 1;">
    <h3 class="RESHoverTitle" data-hover-element="0">
      <div>
        <span class="res-icon"></span>&nbsp;<span>Plan-Six</span>
      </div>
    </h3>

    <div class="RESCloseButton">x</div>

    <div class="RESHoverBody RESDialogContents" data-hover-element="1">
      <form id="userTaggerToolTip">
        <div class="fieldPair">
          <label class="fieldPair-label" for="userTaggerText">Text</label>

          <input class="fieldPair-text" type="text" id="userTaggerText">
        </div>

        <div class="fieldPair">
          <label class="fieldPair-label" for="userTaggerColor">Color</label>

          <select id="userTaggerColor">
            <option style="color: inherit; background-color: none" value="none">none</option>
            <option style="color: black; background-color: aqua" value="aqua">aqua</option>
            <option style="color: white; background-color: black" value="black">black</option>
            <option style="color: white; background-color: blue" value="blue">blue</option>
            <option style="color: white; background-color: cornflowerblue" value="cornflowerblue">cornflowerblue</option>
            <option style="color: white; background-color: fuchsia" value="fuchsia">fuchsia</option>
            <option style="color: white; background-color: gray" value="gray">gray</option>
            <option style="color: white; background-color: green" value="green">green</option>
            <option style="color: black; background-color: lime" value="lime">lime</option>
            <option style="color: white; background-color: maroon" value="maroon">maroon</option>
            <option style="color: white; background-color: navy" value="navy">navy</option>
            <option style="color: white; background-color: olive" value="olive">olive</option>
            <option style="color: white; background-color: orange" value="orange">orange</option>
            <option style="color: white; background-color: orangered" value="orangered">orangered</option>
            <option style="color: black; background-color: pink" value="pink">pink</option>
            <option style="color: white; background-color: purple" value="purple">purple</option>
            <option style="color: white; background-color: red" value="red">red</option>
            <option style="color: black; background-color: silver" value="silver">silver</option>
            <option style="color: white; background-color: teal" value="teal">teal</option>
            <option style="color: black; background-color: white" value="white">white</option>
            <option style="color: black; background-color: yellow" value="yellow">yellow</option>
          </select>
        </div>

        <div class="fieldPair">
          <label class="fieldPair-label" for="userTaggerPreview">Preview</label>

          <span id="userTaggerPreview" style="color: white; background-color: olive;">
            <span class="RESUserTag">
              <a class="userTagLink hasTag truncateTag" style="background-color: olive; color: white !important;" title="Sexist" href="javascript:void 0">Sexist</a>
            </span>
          </span>
        </div>
        <a class="userTagLink hasTag truncateTag" style="background-color: olive; color: white !important;" title="Feminist" href="javascript:void 0">Feminist</a>

        <div class="fieldPair res-usertag-ignore">
          <label class="fieldPair-label" for="userTaggerIgnore">Ignore</label>

          <div id="userTaggerIgnoreContainer" class="toggleButton ">
            <span class="toggleThumb"></span>
            <div class="toggleLabel res-icon" data-enabled-text="" data-disabled-text=""></div>
            <input id="userTaggerIgnore" name="userTaggerIgnore" type="checkbox">
          </div>

          <a class="gearIcon" href="#res:settings/userTagger/hardIgnore" title="RES Settings > User Tagger > hardIgnore"> configure </a>
        </div>

        <div class="fieldPair">
          <label class="fieldPair-label" for="userTaggerLink">
            <span class="userTaggerOpenLink">
              <a title="open link" href="javascript:void 0">Source URL</a>
            </span>
          </label>

          <input class="fieldPair-text" type="text" id="userTaggerLink" value="https://www.reddit.com/r/giantbomb/comments/7x251d/all_systems_goku_02/du5fpwr/">
        </div>

        <div class="fieldPair">
          <label class="fieldPair-label" for="userTaggerVotesUp" title="Upvotes you have given this redditor">Upvotes</label>

          <input type="number" style="width: 50px;" id="userTaggerVotesUp" value="0">
        </div>

        <div class="fieldPair">
          <label class="fieldPair-label" for="userTaggerVotesDown" title="Downvotes you have given this redditor">Downvotes</label>

          <input type="number" style="width: 50px;" id="userTaggerVotesDown" value="0">
        </div>

        <div class="res-usertagger-footer">
          <a href="/r/dashboard#userTaggerContents" target="_blank" rel="noopener noreferer">View tagged users</a>

          <input type="submit" id="userTaggerSave" value="✓ save tag">
        </div>
      </form>
    </div>
  </div>

  */

})(jQuery, typeof message !== 'undefined' ? message : null)

jQuery.noConflict(true)