noplanman / DiasPlus

// ==UserScript==
// @name        DiasPlus
// @namespace   diasplus
// @description Userscript that adds tweaks to diaspora*.
// @include     *
// @version     2.0.0
// @copyright   2016 Armando Lüscher
// @author      Armando Lüscher
// @oujs:author noplanman
// @grant       GM_addStyle
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       window
// @require     https://code.jquery.com/jquery-3.1.1.slim.min.js
// @homepageURL https://github.com/noplanman/DiasPlus
// @supportURL  https://github.com/noplanman/DiasPlus/issues
// ==/UserScript==

// Make sure we're on a diaspora* pod.
if (typeof unsafeWindow.Diaspora === 'undefined') {
  throw 'Not a diaspora* pod, move along...';
}

var DiasPlus = {};
DiasPlus.gon = unsafeWindow.gon;
DiasPlus.secure = true;
DiasPlus.domain = '';

/**
 * Get the pod URL (protocol + domain).
 *
 * @return {string} The pod URL.
 */
DiasPlus.getPodURL = function () {
  return 'http' + (DiasPlus.secure ? 's' : '') + '://' + DiasPlus.domain;
};


/**
 * Get the pod info from the GM settings.
 */
DiasPlus.loadPodInfo = function () {
  DiasPlus.setPodInfo(GM_getValue('dplus-pod-url', ''));
};

/**
 * Set the pod info and save it to the DiasPlus object and the GM settings.
 *
 * @param {string} podURL Pod URL to save.
 *
 * @return {boolean}
 */
DiasPlus.setPodInfo = function (podURL) {
  var info = podURL.split('://');
  if (info.length === 2) {
    DiasPlus.secure = info[0] === 'https';
    DiasPlus.domain = info[1];
    GM_setValue('dplus-pod-url', DiasPlus.getPodURL());

    return true
  }

  return false;
};

/**
 * Add the settings button to the top right navbar.
 */
DiasPlus.addSettingsButton = function () {
  // Add settings button.
  $('<li class="dplus-settings-button" title="DiasPlus Settings"><a><i class="entypo-cog"></i>D+</a></li>')
    .click(function () {
      var p = prompt('Modify your pod URL (eg. https://diasp.eu)', DiasPlus.getPodURL());
      DiasPlus.setPodInfo(p) && DiasPlus.addOompButton();
    })
    .appendTo('ul.navbar-right:first');
};

/**
 * Add the "Open on my pod" button to the top right navbar.
 */
DiasPlus.addOompButton = function () {
  // Remove the existing button if it already exists.
  $('.dplus-oomp-button').remove();

  // If we are not logged into this pod, it must be a foreign one.
  if (!('user' in DiasPlus.gon) && location.hostname !== DiasPlus.domain) {
    var $button = $('<li class="dplus-oomp-button" title="Open on my pod"><a target="_self"><i class="entypo-export"></i></a></li>')

    // Is this the first time we're setting the pod URL?
    if ('' === DiasPlus.domain) {
      $button.click(function () {
        var p = prompt('Your pod has not been defined yet!\n\nEnter your pod domain (eg. https://diasp.eu)', DiasPlus.getPodURL());
        DiasPlus.setPodInfo(p) && DiasPlus.addOompButton();
      });
    } else {
      var url = DiasPlus.getPodURL();

      if ('post' in DiasPlus.gon) {
        url += '/posts/' + DiasPlus.gon.post.guid;
      } else {
        url += location.pathname;
      }

      $('a', $button).attr('href', url);
    }

    $button.appendTo('ul.navbar-right');
  }
};

/**
 * Add the "Liked" and "Commented" links to the stream selection menu.
 */
DiasPlus.addExtraMenuLinks = function () {
  var $streamSelection = $('#stream_selection');
  $('li:nth-child(2)', $streamSelection).after(
    '<li><a class="hoverable" href="/liked">Liked</a></li>' +
    '<li><a class="hoverable" href="/commented">Commented</a></li>'
  );

  // Highlight the background of the active nav item's page.
  $streamSelection.find('li').each(function () {
    var navHref = $('a', this).attr('href');
    if (navHref === location.href.substring(location.href.length - navHref.length)) {
      $(this).addClass('selected');
    }
  });
};

/**
 * Add button that reverses the order of conversation messages.
 */
DiasPlus.addMessageSortingButton = function () {
  if ($('body').hasClass('page-conversations')) {
    var revMessages = function () {
      $('<a class="dplus-reverse-messages" title="Reverse message order"><i class="entypo-switch"></i></a>')
        .click(function () {
          $('#conversation-show .stream').html($('#conversation-show .stream-element').get().reverse());
        })
        .prependTo('.control-icons');
    };
    revMessages();
    DiasPlus.Observer.add('#conversation-show', revMessages);
  }
};

// Time when the mouse button was pressed, or false if not pressed.
var md = false;

/**
 * Initialise the "long click tags" feature.
 */
DiasPlus.initLongClickTags = function () {
  // MouseDown and MouseUp actions for the post entry field.
  $('#status_message_fake_text')
    .mousedown(function () {
      md = Date.now();
      DiasPlus.makeTag($(this));
    })
    .mouseup(function () {
      md = false;
    });
};

/**
 * Check if the passed character is not a space or new line character.
 *
 * @param {string} c The character to check.
 *
 * @return {boolean} True if not a space or new line, else False.
 */
DiasPlus.isValidChar = function (c) {
  return undefined !== c && !/\s/.test(c);
};

/**
 * Convert the currently selected word of the passed text area to a tag.
 *
 * @param {jQuery} $textArea The text area to be handled.
 */
DiasPlus.makeTag = function ($textArea) {
  try {
    // Mouse has been released early.
    if (!md) {
      return;
    }

    // Mouse button down long enough? Loop with timeouts until yes.
    if (md + 500 > Date.now()) {
      setTimeout(function () {
        DiasPlus.makeTag($textArea);
      }, 50);
    } else if ($textArea instanceof jQuery && $textArea.is('textarea')) {
      // Make sure we have been passed a text area.
      var textAreaText = $textArea.val();
      var cPos1 = $textArea[0].selectionStart;
      var cPos2 = $textArea[0].selectionEnd;

      // Only if there is no selection.
      if (cPos1 === cPos2) {

        // Search for the word end backwards.
        while (--cPos1 >= 0 && DiasPlus.isValidChar(textAreaText[cPos1]));
        cPos1++;

        // Let's handle the tag.
        if (DiasPlus.isValidChar(textAreaText[cPos1])) {
          if (textAreaText[cPos1] === '#') {
            // Looks like we're removing the tag.
            if (DiasPlus.isValidChar(textAreaText[cPos1 + 1]) && textAreaText[cPos1 + 1] !== '#') {
              $textArea.val(textAreaText.substring(0, cPos1) + textAreaText.substring(cPos1 + 1));
              // If we're removing the tag from the left, compensate for the # character.
              (textAreaText[cPos2] === '#') || cPos2--;
            }
          } else {
            // Looks like we're adding the tag.
            $textArea.val(textAreaText.substring(0, cPos1) + '#' + textAreaText.substring(cPos1));
            cPos2++;
          }
          // Set new caret positions.
          $textArea[0].selectionStart = $textArea[0].selectionEnd = cPos2;
        }
      }
      md = false;
    }
  } catch (e) {
    DiasPlus.doLog('Error while making tag.', 'e', false, e);
    md = false;
  }
};

/**
 * Make a log entry.
 *
 * @param {string}  logMessage Message to write to the log console.
 * @param {string}  logLevel   Level to log ([l]og,[i]nfo,[w]arning,[e]rror).
 * @param {boolean} alsoAlert  Also echo the message in an alert box.
 * @param {Error}   e          If an exception is passed too, add that info.
 */
DiasPlus.doLog = function (logMessage, logLevel, alsoAlert, e) {
  // Default to "log" if nothing is provided.
  logLevel = logLevel || 'l';

  // Add exception details if available.
  if (e instanceof Error) {
    logMessage += ' (' + e.name + ': ' + e.message + ')';
  }

  logLevel === 'l' && console.log(logMessage);
  logLevel === 'i' && console.info(logMessage);
  logLevel === 'w' && console.warn(logMessage);
  logLevel === 'e' && console.error(logMessage);

  alsoAlert && alert(logMessage);
};

/**
 * Start the party.
 */
DiasPlus.init = function () {
  // Add the global CSS rules.
  GM_addStyle(
    '.dplus-settings-button, .dplus-oomp-button { cursor: pointer; }' +
    '.dplus-oomp-button { background: #0c0; }' +
    '.dplus-oomp-button a { color: #fff !important; }' +
    '.page-conversations .control-icons a { cursor: pointer; display: inline-block !important; }' +
    '.page-conversations .control-icons .dplus-reverse-messages i { font-size: 20px; }'
  );

  // Load the pod infos from the GM settings.
  DiasPlus.loadPodInfo();

  // Load all the features.
  DiasPlus.addSettingsButton();
  DiasPlus.initLongClickTags();
  DiasPlus.addExtraMenuLinks();
  DiasPlus.addOompButton();
  DiasPlus.addMessageSortingButton();
};

// source: https://muffinresearch.co.uk/does-settimeout-solve-the-domcontentloaded-problem/
if (/(?!.*?compatible|.*?webkit)^mozilla|opera/i.test(navigator.userAgent)) { // Feeling dirty yet?
  document.addEventListener('DOMContentLoaded', DiasPlus.init, false);
} else {
  window.setTimeout(DiasPlus.init, 0);
}

/**
 * The MutationObserver to detect page changes.
 */
DiasPlus.Observer = {
  // The mutation observer objects.
  observers: [],

  /**
   * Add an observer to observe for DOM changes.
   *
   * @param {string}   queryToObserve Query string of elements to observe.
   * @param {function} cb             Callback function for the observer.
   */
  add: function (queryToObserve, cb) {
    // Check if we can use the MutationObserver.
    if ('MutationObserver' in window) {
      var toObserve = document.querySelector(queryToObserve);
      if (toObserve) {
        var mo = new MutationObserver(cb);
        DiasPlus.Observer.observers.push(mo);

        // Observe child changes.
        mo.observe(toObserve, {
          childList: true
        });
      }
    }
  }
};