Greasy Fork is available in English.

v2exBetterReply

better reply experience for v2ex

// ==UserScript==
// @name        v2exBetterReply
// @author      dbw9580
// @namespace   v2ex.com
// @description better reply experience for v2ex
// @include     /^https?:\/\/(\w+\.)?v2ex\.com\/t\//
// @version     2019-06-12
// @grant       GM_log
// @grant       GM_addStyle
// @run-at      document-end
// @require     https://code.jquery.com/jquery-2.2.4.min.js
// @supportURL  https://github.com/dbw9580/v2exBetterReply/
// ==/UserScript==

"use strict";
//===========================
// Configuration Section
//
// Set this to true to enable display of comments by blocked users.
// This now only takes effect on comments referenced on the same page,
// due to API restrictions, in multi-page threads, a comment referenced
// on a different page that should be blocked, will still be displayed.
// This may be fixed in future releases.
var SHOW_BLOCKED_REF = false;

// Set this to your preferred max width of the reference preview floating block.
var REF_PREVIEW_WIDTH = "600px";

// Reference marker: the default is "#", change to whatever you like.
var REF_MARKER = "#";

// End of Configuration Section
//===========================

GM_addStyle(".v2exBR-reply-no-target{background-color: #AAAAAA; color: black !important; cursor: pointer; font-weight:bold;}");
GM_addStyle(".v2exBR-cited-comment-view{background-color: white; position: absolute; display: none; max-width: "+REF_PREVIEW_WIDTH+";}");
GM_addStyle(".v2exBR-reply-citation{color: #778087; cursor: pointer;} .v2exBR-reply-citation:hover{color: #4d5256; text-decoration: underline;}");
GM_addStyle(".v2exBR-cited-comment-view .fr{display: none;}");

/* insert preview block */
$(document.body).append($("<div class=\"v2exBR-cited-comment-view cell\" id=\"v2exBR_citation_div\"></div>"));

var API = {};
API.URL = {};
API.URL.topicReply = "https://www.v2ex.com/api/replies/show.json?topic_id=";
API.getTopicReplies = function (topicId) {
    var url = API.URL.topicReply + topicId.toString();
    var result;
    $.ajax({
        type: "GET",
        url: url,
        dataType: "json",
        success: function (data) { result = data },
        error: function () { return },
        async: false,
        // important! to prevent browser caching results returned by api which is vollatile
        cache: false
    });

    return result;
};
API.getTopicReplyIdsInPostedOrder = function (repliesList) {
    var thisReply, i;
    var replyOrderIdMap = [];

    //assume that replies returned by API are already in the order of them being posted
    //simply walk through the array.
    for (i = 0; i < repliesList.length; i++){
        replyOrderIdMap.push(repliesList[i].id);
    }

    return replyOrderIdMap;
};

function markReplyTrueOrder(replyOrderIdMap, repliesDivList) {
    var lastReplyIndex = parseInt($(repliesDivList).find(".no").eq(0).text()) - 1;
    var thisReplyId;
    $(repliesDivList).each(function (index) {
        thisReplyId = this.id.match(/^r_(\d+)/)[1];
        while (thisReplyId != replyOrderIdMap[lastReplyIndex].toString()) {
            if(lastReplyIndex < replyOrderIdMap.length){
                lastReplyIndex++;
            }
            else{
                return true;
            }
        }
        $(this).attr("v2exBR-true-order", 1 + lastReplyIndex++);
    });
}

function adjustFloorNo(repliesDivList) {
    $(repliesDivList).each(function(){
       var thisReplyTrueOrder = $(this).attr("v2exBR-true-order");
        $(this).find(".no").text(thisReplyTrueOrder);
    });
}

function inflatePreviewBlock(reply, previewDiv) {
    var cc = $(commentCells).eq(0).clone();
    $(cc).find("img.avatar").attr("src", reply.member.avatar_normal.replace(/mini/, "normal")); // avatar urls returned by api contain only the mini version
    $(cc).find("strong>a.dark").attr("href", "/member/" + reply.member.username).text(reply.member.username);
    $(cc).find("strong+span.fade.small").remove(); 
    $(cc).find(".ago").text(getRelativeTime(reply.last_modified));
    $(cc).find(".reply_content").html(reply.content_rendered);
    $(previewDiv).html($(cc).html());
    return $(previewDiv);
}

function getRelativeTime(absTime) {
    var now = parseInt(Date.now() / 1000);
    var then = parseInt(absTime);
    var days = Math.floor((now - then) / (3600 * 24));
    var hours = Math.floor((now - then) / 3600)  - days * 24;
    var mins = Math.floor((now - then) / 60) - days * 24 * 60 - hours * 60;

    if (days > 0) {
        return days + " 天前";
    }
    else if (hours > 0) {
        return hours + " 小时 " + mins + " 分钟前";
    }
    else if (mins > 0) {
        return  mins + " 分钟前";
    }
    else {
        return "几秒前";
    }
}

var numCurrentPage = Math.ceil(parseInt($(".no").eq(0).text()) / 100);
var threadUrl = window.location.href.match(/^.+\/t\/\d+/)[0];
var commentCells = $("div.cell, div.inner").filter(function(){
    return this.id.startsWith("r");
});
var topicId = window.location.href.match(/^.+\/t\/(\d+)/)[1];
var repliesList = API.getTopicReplies(topicId);
var replyOrderIdMap = API.getTopicReplyIdsInPostedOrder(repliesList);

var startId = parseInt(commentCells.eq(0).get(0).id.substring(2));
var endId = parseInt(commentCells.eq(-1).get(0).id.substring(2));
var startNo = replyOrderIdMap.indexOf(startId);
var endNo = replyOrderIdMap.indexOf(endId);
var hiddenReplyIds = [];
for (var i = startNo + 1; i < endNo; i++){
    var thisReplyId = replyOrderIdMap[i];
        if ($("#r_" + thisReplyId).length == 0) {
        hiddenReplyIds.push(thisReplyId);
    }
}

/* parse reference */
commentCells.find("div.reply_content")
    .each(function(index){
        var content = $(this).html();
        var replacementSpan = "<span class=\"v2exBR-reply-citation\" v2exBR-commentCellId=\"null\" v2exBR-citedPage=\"0\">";
        content = content.replace(/(?:#|&gt;&gt;)\d+/g, replacementSpan + "$&" + "</span>");
        $(this).html(content);
    });

markReplyTrueOrder(replyOrderIdMap, commentCells);
bindCitationElements(replyOrderIdMap);
adjustFloorNo(commentCells);

/* register floor number functions */
$(".no").hover(function(){
    $(this).addClass("v2exBR-reply-no-target");
}, function(){
    $(this).removeClass("v2exBR-reply-no-target");
}).click(function(e){
    var username = $(this).parent().next().next().children("a").text();
    var commentNo = $(this).text();
    makeCitedReply(username, commentNo);
    //to prevent the vanilla feature provided by v2ex.js to scroll up to the reply
    e.stopImmediatePropagation();
});


$(".v2exBR-reply-citation").hover(function(){
    var self = this;
    var commentCellId = $(self).attr("v2exBR-commentCellId");
    var numCitedPage = parseInt($(self).attr("v2exBR-citedPage"));
    var replyNo = parseInt($(self).attr("v2exBR-order"));

    if (commentCellId === "null") return;
    if (commentCellId === "blocked") {
        $("#v2exBR_citation_div").html("引用的回复被隐藏或来自已屏蔽的用户。")
            .css({
                top:$(self).offset().top,
                left:$(self).offset().left + $(self).width()
            })
            .fadeIn(100);

        return;
    }

    var divPosTopOffset = window.getComputedStyle(self).getPropertyValue("font-size").match(/(\d+)px/)[1];

    inflatePreviewBlock(repliesList[replyNo - 1], $("#v2exBR_citation_div"))
        .css({
            top:$(self).offset().top,
            left:$(self).offset().left + $(self).width()
        })
        .fadeIn(100);
}, function(){
    $("#v2exBR_citation_div").fadeOut(100);
});


$(".v2exBR-reply-citation").click(function(){
    var commentCellId = $(this).attr("v2exBR-commentCellId");
    var numCitedPage = parseInt($(this).attr("v2exBR-citedPage"));
    if (commentCellId === "null" || commentCellId === "blocked") return;

    if(numCitedPage == numCurrentPage){
        $("html, body").animate({
            scrollTop: $("#r_" + commentCellId).offset().top
        }, 500);
    }
    else{
        window.location.href = threadUrl + "?p=" + numCitedPage + "&v2exBR_commentCellId=" + commentCellId;
    }

});

(function(){
    var commentCellId = window.location.href.match(/v2exBR_commentCellId=(\d+)/);
    if (commentCellId != null){
        commentCellId = commentCellId[1];
        $("html, body").animate({
            scrollTop: $("#r_" + commentCellId).offset().top
        }, 500);
    }
})();

function bindCitationElements(replyOrderIdMap){
    $("span.v2exBR-reply-citation").each(function(){
        var replyNo = parseInt($(this).text().match(/(?:>>|#)(\d+)/)[1]);
        var citedCommentCellId = "";
        var numCitedPage = Math.ceil(replyNo / 100);

        citedCommentCellId = replyOrderIdMap[replyNo - 1];
        if (hiddenReplyIds.indexOf(citedCommentCellId) < 0) {
            registerCitation(this, citedCommentCellId, numCitedPage, replyNo);
        }
        else if (SHOW_BLOCKED_REF) {
            registerCitation(this, citedCommentCellId, numCitedPage, replyNo);
        }
        else {
            registerCitation(this, "blocked", numCitedPage, replyNo);
        }

    });
}


function getCommentCellIdFromReplyNo(documentRoot, replyNo){
    var thisReplyNo = documentRoot.find(".no").filter(function () {
        return parseInt($(this).text()) == replyNo;
    });
    if (thisReplyNo.length > 0) {
        return thisReplyNo.parents("div.cell").get(0).id;
    }
    else {
        return "null";
    }
}

function registerCitation(elem, id, numPage, order){
    $(elem).attr("v2exBR-commentCellId", id);
    $(elem).attr("v2exBR-citedPage", numPage);
    $(elem).attr("v2exBR-order", order);
}

function makeCitedReply(username, commentNo){
    var replyContent = $("#reply_content");
    var oldContent = replyContent.val();

    var userTag = "@" + username + " ";
    var commentTag = REF_MARKER + commentNo + " \n";

    var newContent = commentTag + userTag;
    if(oldContent.length > 0){
        if (oldContent != commentTag + userTag) {
            newContent = oldContent + "\n" + commentTag + userTag;
        }
    } else {
        newContent = commentTag + userTag;
    }

    replyContent.focus();
    replyContent.val(newContent);
    moveEnd($("#reply_content"));
}

//copied from v2ex.js in case this script gets executed before v2ex.js
//is loaded
var moveEnd = function (obj) {
    obj.focus();
    obj = obj.get(0);
    var len = obj.value.length;
    if (document.selection) {
        var sel = obj.createTextRange();
        sel.moveStart('character', len);
        sel.collapse();
        sel.select();
    } else if (typeof obj.selectionStart == 'number' && typeof obj.selectionEnd == 'number') {
        obj.selectionStart = obj.selectionEnd = len;
    }
}