v2exBetterReply

better reply experience for v2ex

  1. // ==UserScript==
  2. // @name v2exBetterReply
  3. // @author dbw9580
  4. // @namespace v2ex.com
  5. // @description better reply experience for v2ex
  6. // @include /^https?:\/\/(\w+\.)?v2ex\.com\/t\//
  7. // @version 2019-11-07
  8. // @grant GM_log
  9. // @grant GM_addStyle
  10. // @run-at document-end
  11. // @require https://code.jquery.com/jquery-2.2.4.min.js
  12. // @supportURL https://github.com/dbw9580/v2exBetterReply/
  13. // ==/UserScript==
  14.  
  15. "use strict";
  16. //===========================
  17. // Configuration Section
  18. //
  19. // Set this to true to enable display of comments by blocked users.
  20. // This now only takes effect on comments referenced on the same page,
  21. // due to API restrictions, in multi-page threads, a comment referenced
  22. // on a different page that should be blocked, will still be displayed.
  23. // This may be fixed in future releases.
  24. var SHOW_BLOCKED_REF = false;
  25.  
  26. // Set this to your preferred max width of the reference preview floating block.
  27. var REF_PREVIEW_WIDTH = "600px";
  28.  
  29. // Reference marker: the default is "#", change to whatever you like.
  30. var REF_MARKER = "#";
  31.  
  32. // End of Configuration Section
  33. //===========================
  34.  
  35. GM_addStyle(".v2exBR-reply-no-target{background-color: #AAAAAA; color: black !important; cursor: pointer; font-weight:bold;}");
  36. 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+";}");
  37. GM_addStyle(".v2exBR-reply-citation{color: #778087; cursor: pointer;} .v2exBR-reply-citation:hover{color: #4d5256; text-decoration: underline;}");
  38. GM_addStyle(".v2exBR-cited-comment-view .fr{display: none;}");
  39.  
  40. /* insert preview block */
  41. $(document.body).append($("<div class=\"v2exBR-cited-comment-view cell box\" id=\"v2exBR_citation_div\"></div>"));
  42.  
  43. var API = {};
  44. API.URL = {};
  45. API.URL.topicReply = "https://www.v2ex.com/api/replies/show.json?topic_id=";
  46. API.getTopicReplies = function (topicId) {
  47. var url = API.URL.topicReply + topicId.toString();
  48. var result;
  49. $.ajax({
  50. type: "GET",
  51. url: url,
  52. dataType: "json",
  53. success: function (data) { result = data },
  54. error: function () { return },
  55. async: false,
  56. // important! to prevent browser caching results returned by api which is vollatile
  57. cache: false
  58. });
  59.  
  60. return result;
  61. };
  62. API.getTopicReplyIdsInPostedOrder = function (repliesList) {
  63. var thisReply, i;
  64. var replyOrderIdMap = [];
  65.  
  66. //assume that replies returned by API are already in the order of them being posted
  67. //simply walk through the array.
  68. for (i = 0; i < repliesList.length; i++){
  69. replyOrderIdMap.push(repliesList[i].id);
  70. }
  71.  
  72. return replyOrderIdMap;
  73. };
  74.  
  75. function markReplyTrueOrder(replyOrderIdMap, repliesDivList) {
  76. var lastReplyIndex = parseInt($(repliesDivList).find(".no").eq(0).text()) - 1;
  77. var thisReplyId;
  78. $(repliesDivList).each(function (index) {
  79. thisReplyId = this.id.match(/^r_(\d+)/)[1];
  80. while (thisReplyId != replyOrderIdMap[lastReplyIndex].toString()) {
  81. if(lastReplyIndex < replyOrderIdMap.length){
  82. lastReplyIndex++;
  83. }
  84. else{
  85. return true;
  86. }
  87. }
  88. $(this).attr("v2exBR-true-order", 1 + lastReplyIndex++);
  89. });
  90. }
  91.  
  92. function adjustFloorNo(repliesDivList) {
  93. $(repliesDivList).each(function(){
  94. var thisReplyTrueOrder = $(this).attr("v2exBR-true-order");
  95. $(this).find(".no").text(thisReplyTrueOrder);
  96. });
  97. }
  98.  
  99. function inflatePreviewBlock(reply, previewDiv) {
  100. var cc = $(commentCells).eq(0).clone();
  101. $(cc).find("img.avatar").attr("src", reply.member.avatar_normal.replace(/mini/, "normal")); // avatar urls returned by api contain only the mini version
  102. $(cc).find("strong>a.dark").attr("href", "/member/" + reply.member.username).text(reply.member.username);
  103. $(cc).find("span.fade.small").filter(function(i, e){return e.innerText.startsWith("♥")}).remove();
  104. $(cc).find(".ago").text(getRelativeTime(reply.last_modified));
  105. $(cc).find(".reply_content").html(reply.content_rendered);
  106. $(previewDiv).html($(cc).html());
  107. return $(previewDiv);
  108. }
  109.  
  110. function getRelativeTime(absTime) {
  111. var now = parseInt(Date.now() / 1000);
  112. var then = parseInt(absTime);
  113. var days = Math.floor((now - then) / (3600 * 24));
  114. var hours = Math.floor((now - then) / 3600) - days * 24;
  115. var mins = Math.floor((now - then) / 60) - days * 24 * 60 - hours * 60;
  116.  
  117. if (days > 0) {
  118. return days + " 天前";
  119. }
  120. else if (hours > 0) {
  121. return hours + " 小时 " + mins + " 分钟前";
  122. }
  123. else if (mins > 0) {
  124. return mins + " 分钟前";
  125. }
  126. else {
  127. return "几秒前";
  128. }
  129. }
  130.  
  131. var numCurrentPage = Math.ceil(parseInt($(".no").eq(0).text()) / 100);
  132. var threadUrl = window.location.href.match(/^.+\/t\/\d+/)[0];
  133. var commentCells = $("div.cell, div.inner").filter(function(){
  134. return this.id.startsWith("r");
  135. });
  136. var topicId = window.location.href.match(/^.+\/t\/(\d+)/)[1];
  137. var repliesList = API.getTopicReplies(topicId);
  138. var replyOrderIdMap = API.getTopicReplyIdsInPostedOrder(repliesList);
  139.  
  140. var startId = parseInt(commentCells.eq(0).get(0).id.substring(2));
  141. var endId = parseInt(commentCells.eq(-1).get(0).id.substring(2));
  142. var startNo = replyOrderIdMap.indexOf(startId);
  143. var endNo = replyOrderIdMap.indexOf(endId);
  144. var hiddenReplyIds = [];
  145. for (var i = startNo + 1; i < endNo; i++){
  146. var thisReplyId = replyOrderIdMap[i];
  147. if ($("#r_" + thisReplyId).length == 0) {
  148. hiddenReplyIds.push(thisReplyId);
  149. }
  150. }
  151.  
  152. /* parse reference */
  153. commentCells.find("div.reply_content")
  154. .each(function(index){
  155. var content = $(this).html();
  156. var replacementSpan = "<span class=\"v2exBR-reply-citation\" v2exBR-commentCellId=\"null\" v2exBR-citedPage=\"0\">";
  157. content = content.replace(/(?:#|&gt;&gt;)\d+/g, replacementSpan + "$&" + "</span>");
  158. $(this).html(content);
  159. });
  160.  
  161. markReplyTrueOrder(replyOrderIdMap, commentCells);
  162. bindCitationElements(replyOrderIdMap);
  163. adjustFloorNo(commentCells);
  164.  
  165. /* register floor number functions */
  166. $(".no").hover(function(){
  167. $(this).addClass("v2exBR-reply-no-target");
  168. }, function(){
  169. $(this).removeClass("v2exBR-reply-no-target");
  170. }).click(function(e){
  171. var username = $(this).parent().next().next().children("a").text();
  172. var commentNo = $(this).text();
  173. makeCitedReply(username, commentNo);
  174. //to prevent the vanilla feature provided by v2ex.js to scroll up to the reply
  175. e.stopImmediatePropagation();
  176. });
  177.  
  178.  
  179. $(".v2exBR-reply-citation").hover(function(){
  180. var self = this;
  181. var commentCellId = $(self).attr("v2exBR-commentCellId");
  182. var numCitedPage = parseInt($(self).attr("v2exBR-citedPage"));
  183. var replyNo = parseInt($(self).attr("v2exBR-order"));
  184.  
  185. if (commentCellId === "null") return;
  186. if (commentCellId === "blocked") {
  187. $("#v2exBR_citation_div").html("引用的回复被隐藏或来自已屏蔽的用户。")
  188. .css({
  189. top:$(self).offset().top,
  190. left:$(self).offset().left + $(self).width()
  191. })
  192. .fadeIn(100);
  193.  
  194. return;
  195. }
  196.  
  197. var divPosTopOffset = window.getComputedStyle(self).getPropertyValue("font-size").match(/(\d+)px/)[1];
  198.  
  199. inflatePreviewBlock(repliesList[replyNo - 1], $("#v2exBR_citation_div"))
  200. .css({
  201. top:$(self).offset().top,
  202. left:$(self).offset().left + $(self).width()
  203. })
  204. .fadeIn(100);
  205. }, function(){
  206. $("#v2exBR_citation_div").fadeOut(100);
  207. });
  208.  
  209.  
  210. $(".v2exBR-reply-citation").click(function(){
  211. var commentCellId = $(this).attr("v2exBR-commentCellId");
  212. var numCitedPage = parseInt($(this).attr("v2exBR-citedPage"));
  213. if (commentCellId === "null" || commentCellId === "blocked") return;
  214.  
  215. if(numCitedPage == numCurrentPage){
  216. $("html, body").animate({
  217. scrollTop: $("#r_" + commentCellId).offset().top
  218. }, 500);
  219. }
  220. else{
  221. window.location.href = threadUrl + "?p=" + numCitedPage + "&v2exBR_commentCellId=" + commentCellId;
  222. }
  223.  
  224. });
  225.  
  226. (function(){
  227. var commentCellId = window.location.href.match(/v2exBR_commentCellId=(\d+)/);
  228. if (commentCellId != null){
  229. commentCellId = commentCellId[1];
  230. $("html, body").animate({
  231. scrollTop: $("#r_" + commentCellId).offset().top
  232. }, 500);
  233. }
  234. })();
  235.  
  236. function bindCitationElements(replyOrderIdMap){
  237. $("span.v2exBR-reply-citation").each(function(){
  238. var replyNo = parseInt($(this).text().match(/(?:>>|#)(\d+)/)[1]);
  239. var citedCommentCellId = "";
  240. var numCitedPage = Math.ceil(replyNo / 100);
  241.  
  242. citedCommentCellId = replyOrderIdMap[replyNo - 1];
  243. if (hiddenReplyIds.indexOf(citedCommentCellId) < 0) {
  244. registerCitation(this, citedCommentCellId, numCitedPage, replyNo);
  245. }
  246. else if (SHOW_BLOCKED_REF) {
  247. registerCitation(this, citedCommentCellId, numCitedPage, replyNo);
  248. }
  249. else {
  250. registerCitation(this, "blocked", numCitedPage, replyNo);
  251. }
  252.  
  253. });
  254. }
  255.  
  256.  
  257. function getCommentCellIdFromReplyNo(documentRoot, replyNo){
  258. var thisReplyNo = documentRoot.find(".no").filter(function () {
  259. return parseInt($(this).text()) == replyNo;
  260. });
  261. if (thisReplyNo.length > 0) {
  262. return thisReplyNo.parents("div.cell").get(0).id;
  263. }
  264. else {
  265. return "null";
  266. }
  267. }
  268.  
  269. function registerCitation(elem, id, numPage, order){
  270. $(elem).attr("v2exBR-commentCellId", id);
  271. $(elem).attr("v2exBR-citedPage", numPage);
  272. $(elem).attr("v2exBR-order", order);
  273. }
  274.  
  275. function makeCitedReply(username, commentNo){
  276. var replyContent = $("#reply_content");
  277. var oldContent = replyContent.val();
  278.  
  279. var userTag = "@" + username + " ";
  280. var commentTag = REF_MARKER + commentNo + " \n";
  281.  
  282. var newContent = commentTag + userTag;
  283. if(oldContent.length > 0){
  284. if (oldContent != commentTag + userTag) {
  285. newContent = oldContent + "\n" + commentTag + userTag;
  286. }
  287. } else {
  288. newContent = commentTag + userTag;
  289. }
  290.  
  291. replyContent.focus();
  292. replyContent.val(newContent);
  293. moveEnd($("#reply_content"));
  294. }
  295.  
  296. //copied from v2ex.js in case this script gets executed before v2ex.js
  297. //is loaded
  298. var moveEnd = function (obj) {
  299. obj.focus();
  300. obj = obj.get(0);
  301. var len = obj.value.length;
  302. if (document.selection) {
  303. var sel = obj.createTextRange();
  304. sel.moveStart('character', len);
  305. sel.collapse();
  306. sel.select();
  307. } else if (typeof obj.selectionStart == 'number' && typeof obj.selectionEnd == 'number') {
  308. obj.selectionStart = obj.selectionEnd = len;
  309. }
  310. }
  311.  
  312.