// ==UserScript==
// @name v2exBetterReply
// @author dbw9580
// @namespace v2ex.com
// @description better reply experience for v2ex
// @include /^https?:\/\/(\w+\.)?v2ex\.com\/t\//
// @version 2019-11-07
// @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{box-shadow: 0 0 3px rgba(0,0,0,.1); 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 box\" 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("span.fade.small").filter(function(i, e){return e.innerText.startsWith("♥")}).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(/(?:#|>>)\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;
}
}