Long Comment: Imgur

Allows for longer comments on imgur

Click here to install Browse More Scripts
// ==UserScript==
// @name         Long Comment: Imgur
// @namespace    9e1cfcee9335690743c0d228475ec98d966d935f
// @version      1.1
// @description  Allows for longer comments on imgur
// @author       longcomment.com
// @match        https://imgur.com/*
// @grant        none
// @run-at       document-body
// ==/UserScript==

//Version history
//1.0	Initial Version
//1.1	An issue with the old design was addressed

(function () {
    'use strict';
	//Sensible defaults. These are later set by the API
    var minlen = 100;
    var maxlen = 10000;
    var root = "longcomment.com";
    var $$ = document.querySelectorAll.bind(document);
    var $ = document.querySelector.bind(document);
    var isNewDesign = null;

	//Gets the UTF-8 byte length of a text
    var getByteLength = function (str) {
        if (typeof(str) === typeof("")) {
            var te = new TextEncoder();
            return te.encode(str).length;
        }
        return undefined;
    };

	//Finds a node by going up the DOM tree until `querySelector()` hits.
    var findNode = function (base, selector) {
        var node;
        while (base) {
            if (node = base.querySelector(selector)) {
                return node;
            }
            base = base.parentNode;
        }
        return null;
    };

    //Adds styles depending on old or new design
    var addStyle = function () {
        var style = document.createElement("style");
        var rules;
        if (isNewDesign) {
            rules = [
                "#long-comment{height:26px;margin-left:1em;padding-left:12px;box-shadow:0 3px 4px rgba(0,0,0,.12);border-radius:3px;border:none;font-size:14px;line-height:14px;color:#fff;text-transform:capitalize;text-align:center;outline-style:none;cursor:pointer;font-family:Proxima Nova ExtraBold,Helvetica Neue,Helvetica,Arial,sans-serif}",
                "#long-comment[disabled]{color:#b4b9c2;background:#464b57;cursor:default}",
                ".via-lc,.lc-pending{font-style:italic;font-size:small;}"
            ];
        } else {
            rules = [
                "#long-comment{padding:10px 25px;margin-left:1em;font-weight:700;float:right;}",
                ".via-lc,.lc-pending{font-style:italic;font-size:small;}"
            ];
        }
        style.textContent = rules.join("\r\n");
        $("head").appendChild(style);
		//Prevent multiple calls
        addStyle = function () {
            console.warn("LC API: Duplicate call to addStyles()");
        };
    };

    //Triggers a builtin event
    var triggerDomEvent = function (ele, eventName) {
        var e = new Event(eventName, {
            bubbles: true,
            cancelable: true
        });
        return ele.dispatchEvent(e);
    };

    //Triggers a custom event
    var triggerCustomEvent = function (ele, eventName) {
        //== This used to be necessary in the past but is no longer done this way. ==//
        //var e = document.createEvent('CustomEvent');
        //e.initEvent(eventName, true, false);
        //ele.dispatchEvent(e);
        return triggerDomEvent(ele, eventName);
    };

    //Sets the value of a textbox and its neighbors.
    //This is for the old design because it uses a hidden textbox that throws "querySelector" off.
    var setMultiText = function (tb, value) {
        var boxes = tb.parentNode.querySelectorAll("textarea");
        boxes.forEach(function (v) {
            v.value = value;
            if (v._valueTracker) {
                v._valueTracker.setValue(value);
            }
            var react = Object.getOwnPropertyNames(v).filter(function (prop) {
                return prop.indexOf("__reactInternalInstance$") === 0;
            })[0];
            if (react) {
                //Get root react component
                console.log("Trying to set react value in internal property");
                var component = v[react].return;
                while (component && typeof(component.type) === typeof("")) {
                    component = component.return;
                }
				if(component){
					component.stateNode.setState({
						value: value
					});
				}
            }
        });
        //Call all sorts of events
        boxes.forEach(function (v) {
            triggerCustomEvent(v, "onKeyDown");
            triggerDomEvent(v, "input");
            triggerDomEvent(v, "change");
            triggerCustomEvent(v, "onChange");
        });
    };

    //Creates the "Long Comment" button with all properties and events set.
    //You need to add it to the DOM manually.
    var createLongBtn = function () {
        var btn = document.createElement("input");
        btn.type = "button";
        btn.value = "Long Comment";
        btn.id = "long-comment";
        //The comment box is empty by default
        btn.disabled = true;
        if (isNewDesign) {
            btn.classList.add("Button");
        } else {
            btn.classList.add("btn");
            btn.classList.add("btn-main");
        }
        btn.addEventListener("click", function () {
            var box = findNode(this, "textarea");
            var form = box.form;
            var btn = form.querySelector("#submit-comment") || form.querySelector(".Create-submitBtn");
            if (!box || !btn) {
                console.warn("Invalid form state. Did imgur change something?");
                return false;
            }
            //This is already a long comment
            if (box.value.match(/^LC:[\S]+/)) {
                triggerDomEvent(form, "submit");
                return true;
            }
            var bl = getByteLength(box.value);
            if (bl < minlen) {
                alert("Text too short. Needs to be at least " + minlen + " bytes. Yours is " + bl);
                return false;
            }
            if (bl > maxlen) {
                alert("Text too long. Needs to be at most " + minlen + " bytes. Yours is " + bl);
                return false;
            }
            console.log("Longcomment API: Storing comment");
            var xhr = new XMLHttpRequest();
            xhr.open("POST", "https://" + root + "/add");
            xhr.addEventListener("load", function () {
                var data = {
                    success: false,
                    msg: "Invalid JSON response"
                };
                try {
                    data = JSON.parse(xhr.responseText);
                } catch (e) {
                    console.error("Invalid response from longcomment API:", xhr.responseText);
                }
                if (data.success) {
                    //ID is contained in the field "data.id" if we want to use it directly
                    setMultiText(box, data.placeholder);
                    console.debug("LC API: Set text:", box.value);
                    if (isNewDesign) {
                        btn.disabled = false;
                        btn.classList.remove("disabled");
                        triggerDomEvent(btn, "click");
                        triggerCustomEvent(btn, "onClick");
                    } else {
                        triggerDomEvent(form, "submit");
                    }
                } else {
                    console.error("Longcomment API error message:", data.msg);
                    if (data.msg) {
                        alert("There was an error creating your longcomment:\r\n" + data.msg);
                    }
                }
            });
            xhr.addEventListener("error", function () {
                console.error("Longcomment API error:", arguments);
            });
            var fd = new FormData();
            fd.append("comment", box.value);
            xhr.send(fd);
            return true;
        });
        return btn;
    };

    //Gets a longcomment text from the API
    var getLongComment = function (id, callback) {
        if (typeof(callback) !== typeof(function () {})) {
            callback = function () {
                console.warn("Longcomment API: No callback supplied");
            };
        }
        console.log("Longcomment API: Obtaining id", id);
        var xhr = new XMLHttpRequest();
        xhr.open("GET", "https://" + root + "/comment/" + encodeURIComponent(id));
        xhr.addEventListener("load", function () {
            var data = {
                success: false,
                msg: "Invalid JSON response"
            };
            try {
                data = JSON.parse(xhr.responseText);
            } catch (e) {
                console.error("Invalid response from longcomment API:", xhr.responseText);
            }
            callback(data);
        });
        xhr.addEventListener("error", function () {
            console.error("Longcomment API error:", arguments);
            callback(null);
        });
        xhr.send(null);
        return xhr;
    };

    //Obtain API limits from the LC api.
    //This will cache the response for a day.
    var getInfo = function (callback) {
        if (typeof(callback) !== typeof(function () {})) {
            callback = function () {
                console.warn("Longcomment API: No callback supplied");
            };
        }

        var cache = localStorage.getItem("lc-properties");
        if (cache) {
            try {
                cache = JSON.parse(cache);
                //Update at most once per day
                if (cache.cachedate > Date.now() - 86400 * 1000) {
                    minlen = cache.minlen | 0;
                    maxlen = cache.maxlen | 0;
                    //sanity check
                    if (maxlen === 0 || maxlen <= minlen) {
                        minlen = 100;
                        maxlen = 10000;
                    }
                    //Exit early
                    console.debug("Longcomment: Taking cached copy of info:", cache);
                    callback(cache);
                    return true;
                }
            } catch (e) {
                //Item is invalid. Discard and continue
                localStorage.removeItem("lc-properties");
            }
        }

        var xhr = new XMLHttpRequest();
        xhr.open("GET", "https://" + root + "/info");
        xhr.addEventListener("load", function () {
            var data = {
                msg: "Invalid JSON response"
            };
            try {
                data = JSON.parse(xhr.responseText);
            } catch (e) {
                console.error("Invalid response from longcomment API:", xhr.responseText);
            }
            callback(data);
        });
        xhr.addEventListener("error", function () {
            console.error("Longcomment API error:", arguments);
            callback(null);
        });
        xhr.send(null);
        return xhr;
    };

    //Replaces the LC id with the text content
    var replaceLongComment = function (element) {
        var id = getLongCommentId(element.textContent);
        if (typeof(id) === typeof("")) {
            //Replacing the text prevents weird loops or multiple attempts from the observer.
            element.innerHTML = "<i class=\"lc-pending\">Decoding Longcomment " + id + "...</i>";
            getLongComment(id, function (data) {
                if (data.success) {
                    //Clear the text
                    element.innerHTML = "";
                    var lines = data.comment.split('\n').map(function (v) {
                        return v.trimEnd();
                    });
                    //Try to restore the lines from the comment
                    for (var i = 0; i < lines.length - 1; i++) {
                        //Do not add multiple empty lines
                        if (i === 0 || lines[i].length > 0 || lines[i - 1].length > 0) {
                            element.insertAdjacentText('beforeend', lines[i]);
                            element.appendChild(document.createElement("br"));
                        }
                    }
                    element.insertAdjacentText('beforeend', lines[lines.length - 1]);
                    //element.textContent = data.comment;
                    element.appendChild(document.createElement("br"));
                    element.appendChild(document.createElement("hr"));
                    var hint = element.appendChild(document.createElement("i"));
                    hint.classList.add("via-lc");
                    hint.innerHTML = "Via <a href=\"https://longcomment.com/\" target=\"_blank\">Longcomment</a>. Id: " + id;
                } else {
                    //Reset text
                    element.innerHTML = id;
                    element.insertAdjacentHTML("afterbegin", "<span style=\"color:#F00;\">Invalid or unavailable Longcomment:</span><br />");
                    console.error("Longcomment API error message:", data.msg);
                }

            });
        }
    };

    //Extracts the LC id from a text
    var getLongCommentId = function (text) {
        if (typeof(text) === typeof("")) {
            var m = text.split(' ')[0].match(/^LC:([\w\-]{10}[AEIMQUYcgkosw048])$/);
            if (m) {
                return m[1];
            }
        }
        return null;
    };

    //Checks if the given value is an LC id
    var isLongComment = function (text) {
        return typeof(getLongCommentId(text) === typeof(""));
    };

    //Checks for new Longcomments
    //This works identical for old and new design
    var checkLC = function () {
        //Supports old and new design
        $$(".comment .usertext .linkified,.GalleryComment .Linkify").forEach(function (v) {
            if (isLongComment(v.textContent)) {
                replaceLongComment(v);
            } else {
                console.log("Not a Longcomment:", v.textContent);
            }
        });
    };

    //Enables or disables the Long Comment button depending on text length
    //This works identical for old and new design
    var onTextareaChange = function (tb) {
        var btn;
        if (isNewDesign) {
            btn = findNode(tb, "form").querySelector("#long-comment");
        } else {
            btn = tb.parentNode.querySelector("#long-comment");
        }
        if (btn) {
            var l = getByteLength(tb.value);
            btn.disabled = l < minlen || l > maxlen;
        } else {
            console.warn("Long comment button was deleted");
        }
    };

    //This watches for text changes in a textbox
    var watchChange = function (e) {
        e.addEventListener("input", function () {
            onTextareaChange(this);
        });
        e.addEventListener("change", function () {
            onTextareaChange(this);
        });
    };

    //Adds the long comment buttons where needed
    var addButtons = function () {
        $$("#submit-comment,.Create-submitBtn").forEach(function (v) {
            if (!v.parentNode.querySelector("#long-comment")) {
                var container = findNode(v, "textarea").parentNode;
                console.log(container);
                container.querySelectorAll("textarea").forEach(watchChange);
                if (isNewDesign) {
                    v.parentNode.appendChild(createLongBtn());
                } else {
                    v.parentNode.insertBefore(createLongBtn(), v);
                }
            }
        });
    };

    //This watches for changes in the DOM
    var mo = new MutationObserver(function () {
        if (isNewDesign === null) {
            var mode = {
                "old": $$(".post-title").length > 0,
                "beta": $$(".Gallery-Title").length > 0
            }
            if (mode.old || mode.beta) {
                isNewDesign = mode.beta;
                console.log("New design:", isNewDesign);
                //Add style only after we decided on the mode
                addStyle();
            }
        }
        if (isNewDesign !== null) {
            //Add longcomment button where needed
            addButtons();
            //Parse longcomment text
            checkLC();
        }
    });
    //Obtain current API limits
    getInfo(function (data) {
        //Do not update the cache if the item has the cachedate property.
        //This property is from a cached copy
        if (data && !data.cachedate) {
            data.cachedate = Date.now();
            localStorage.setItem("lc-properties", JSON.stringify(data));
            minlen = data.minlen;
            maxlen = data.maxlen;
        }
        mo.observe(document.body, {
            subtree: true,
            childList: true
        });
    });
})();

/*
LICENSE:
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
The full license text can be found here: http://creativecommons.org/licenses/by-nc-sa/4.0/
The link has an easy to understand version of the license and the full license text.

DISCLAIMER:
THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.
*/

Copyright © 2018 by Kevin Gut 📧 | More services | Generated for 35.173.35.159