Juick tweaks

Feature testing

目前为 2016-10-29 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Juick tweaks
  3. // @namespace ForJuickCom
  4. // @description Feature testing
  5. // @match *://juick.com/*
  6. // @author Killy
  7. // @version 2.4.0
  8. // @date 2016.09.02 - 2016.10.29
  9. // @run-at document-end
  10. // @grant GM_xmlhttpRequest
  11. // @grant GM_addStyle
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @grant GM_deleteValue
  15. // @grant GM_listValues
  16. // @grant GM_info
  17. // @connect api.juick.com
  18. // @connect twitter.com
  19. // @connect bandcamp.com
  20. // @connect flickr.com
  21. // @connect flic.kr
  22. // @connect deviantart.com
  23. // @connect gist.github.com
  24. // @connect codepen.io
  25. // ==/UserScript==
  26.  
  27.  
  28. // pages and elements =====================================================================================
  29.  
  30. var content = document.getElementById("content");
  31. var isPost = (content !== null) && content.hasAttribute("data-mid");
  32. var isFeed = (document.querySelectorAll("#content article[data-mid]").length > 0);
  33. var isPostEditorSharp = (document.getElementById('newmessage') === null) ? false : true;
  34. var isTagsPage = window.location.pathname.endsWith('/tags');
  35. var isSingleTagPage = (window.location.pathname.indexOf('/tag/') != -1);
  36. var isSettingsPage = window.location.pathname.endsWith('/settings');
  37. var isUserColumn = (document.querySelector("aside#column > div#ctitle:not(.tag)") === null) ? false : true;
  38. var isUsersTable = (document.querySelector("table.users") === null) ? false : true;
  39.  
  40.  
  41. // userscript features =====================================================================================
  42.  
  43. addStyle(); // минимальный набор стилей, необходимый для работы скрипта
  44.  
  45. if(isPost) { // на странице поста
  46. updateTagsOnAPostPage();
  47. addTagEditingLinkUnderPost();
  48. addCommentRemovalLinks();
  49. embedLinksToPost();
  50. }
  51.  
  52. if(isFeed) { // в ленте или любом списке постов
  53. updateTagsInFeed();
  54. embedLinksToArticles();
  55. }
  56.  
  57. if(isUserColumn) { // если колонка пользователя присутствует слева
  58. addYearLinks();
  59. colorizeTagsInUserColumn();
  60. addSettingsLink();
  61. updateAvatar();
  62. }
  63.  
  64. if(isPostEditorSharp) { // на форме создания поста (/#post)
  65. addEasyTagsUnderPostEditorSharp();
  66. }
  67.  
  68. if(isTagsPage) { // на странице тегов пользователя
  69. sortTagsPage();
  70. }
  71.  
  72. if(isSingleTagPage) { // на странице тега (/tag/...)
  73. addTagPageToolbar();
  74. }
  75.  
  76. if(isUsersTable) { // на странице подписок или подписчиков
  77. addUsersSortingButton();
  78. }
  79.  
  80. if(isSettingsPage) { // на странице настроек
  81. addTweaksSettingsButton();
  82. }
  83.  
  84.  
  85. // function definitions =====================================================================================
  86.  
  87. function updateTagsOnAPostPage() {
  88. if(!GM_getValue('enable_user_tag_links', true)) { return; }
  89. var tagsDiv = document.querySelector("div.msg-tags");
  90. if(tagsDiv === null) { return; }
  91. var userId = document.querySelector("div.msg-avatar > a > img").alt;
  92. [].forEach.call(tagsDiv.childNodes, function(item, i, arr) {
  93. var link = item.href;
  94. item.href = link.replace("tag/", userId + "/?tag=");
  95. });
  96. }
  97.  
  98. function updateTagsInFeed() {
  99. if(!GM_getValue('enable_user_tag_links', true)) { return; }
  100. [].forEach.call(document.querySelectorAll("#content > article"), function(article, i, arr) {
  101. if(!article.hasAttribute('data-mid')) { return; }
  102. var userId = article.querySelector("div.msg-avatar > a > img").alt;
  103. var tagsDiv = article.getElementsByClassName("msg-tags")[0];
  104. if(tagsDiv === null) { return; }
  105. [].forEach.call(tagsDiv.childNodes, function(item, j, arrj) {
  106. var link = item.href;
  107. item.href = link.replace("tag/", userId + "/?tag=");
  108. });
  109. });
  110. }
  111.  
  112. function addTagEditingLinkUnderPost() {
  113. if(!GM_getValue('enable_tags_editing_link', true)) { return; }
  114. var mtoolbar = document.getElementById("mtoolbar").childNodes[0];
  115. var canEdit = (mtoolbar.textContent.indexOf('Удалить') > -1) ? true : false;
  116. if(!canEdit) { return; }
  117. var linode = document.createElement("li");
  118. var anode = document.createElement("a");
  119. var mid = document.getElementById("content").getAttribute("data-mid");
  120. anode.href = "http://juick.com/post?body=%23" + mid + "+%2ATag";
  121. anode.innerHTML = "<div style='background-position: -16px 0'></div>Теги";
  122. linode.appendChild(anode);
  123. mtoolbar.appendChild(linode);
  124. }
  125.  
  126. function addCommentRemovalLinks() {
  127. if(!GM_getValue('enable_comment_removal_links', true)) { return; }
  128. var myUserIdLink = document.querySelector("nav#actions > ul > li:nth-child(2) > a");
  129. var myUserId = (myUserIdLink === null) ? null : myUserIdLink.textContent.replace('@', '');
  130. var commentsBlock = document.querySelector("ul#replies");
  131. if((commentsBlock !== null) && (myUserId !== null)) {
  132. [].forEach.call(commentsBlock.children, function(linode, i, arr) {
  133. var postUserAvatar = linode.querySelector("div.msg-avatar > a > img");
  134. if(postUserAvatar !== null) {
  135. var postUserId = postUserAvatar.alt;
  136. if(postUserId == myUserId) {
  137. var linksBlock = linode.querySelector("div.msg-links");
  138. var commentLink = linode.querySelector("div.msg-ts > a");
  139. var postId = commentLink.pathname.replace('/','');
  140. var commentId = commentLink.hash.replace('#','');
  141. var anode = document.createElement("a");
  142. anode.href = "http://juick.com/post?body=D+%23" + postId + "%2F" + commentId;
  143. anode.innerHTML = "Удалить";
  144. anode.style.cssFloat = "right";
  145. linksBlock.appendChild(anode);
  146. }
  147. }
  148. });
  149. }
  150. }
  151.  
  152. function addTagPageToolbar() {
  153. if(!GM_getValue('enable_tag_page_toolbar', true)) { return; }
  154. var asideColumn = document.querySelector("aside#column");
  155. var tag = document.location.pathname.split("/").pop(-1);
  156. var html = '<div id="ctitle" class="tag"><a href="/tag/%TAG%">*%TAGSTR%</a></div>' +
  157. '<ul id="ctoolbar">' +
  158. '<li><a href="/post?body=S+%2a%TAG%" title="Подписаться"><div style="background-position: -16px 0"></div></a></li>' +
  159. '<li><a href="/post?body=BL+%2a%TAG%" title="Заблокировать"><div style="background-position: -80px 0"></div></a></li>' +
  160. '</ul>';
  161. html = html.replace(/%TAG%/g, tag).replace(/%TAGSTR%/g, decodeURIComponent(tag));
  162. asideColumn.innerHTML = html + asideColumn.innerHTML;
  163. }
  164.  
  165. function addYearLinks() {
  166. if(!GM_getValue('enable_year_links', true)) { return; }
  167. var userId = document.querySelector("div#ctitle a").textContent;
  168. var asideColumn = document.querySelector("aside#column");
  169. var hr1 = asideColumn.querySelector("p.tags + hr");
  170. var hr2 = document.createElement("hr");
  171. var linksContainer = document.createElement("p");
  172. var years = [
  173. {y: (new Date()).getFullYear(), b: ""},
  174. {y: 2015, b: "?before=2816362"},
  175. {y: 2014, b: "?before=2761245"},
  176. {y: 2013, b: "?before=2629477"},
  177. {y: 2012, b: "?before=2183986"},
  178. {y: 2011, b: "?before=1695443"}
  179. ];
  180. years.forEach(function(item, i, arr) {
  181. var anode = document.createElement("a");
  182. anode.href = "/" + userId + "/" + item.b;
  183. anode.textContent = item.y;
  184. linksContainer.appendChild(anode);
  185. linksContainer.appendChild(document.createTextNode (" "));
  186. });
  187. asideColumn.insertBefore(hr2, hr1);
  188. asideColumn.insertBefore(linksContainer, hr1);
  189. }
  190.  
  191. function addSettingsLink() {
  192. if(!GM_getValue('enable_settings_link', true)) { return; }
  193. var columnUserId = document.querySelector("div#ctitle a").textContent;
  194. var myUserIdLink = document.querySelector("nav#actions > ul > li:nth-child(2) > a");
  195. var myUserId = (myUserIdLink === null) ? null : myUserIdLink.textContent.replace('@', '');
  196. if(columnUserId == myUserId) {
  197. var asideColumn = document.querySelector("aside#column");
  198. var ctitle = asideColumn.querySelector("#ctitle");
  199. var anode = document.createElement("a");
  200. anode.innerHTML = '<div class="icon icon--ei-heart icon--s "><svg class="icon__cnt"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#ei-gear-icon"></use></svg></div>';
  201. anode.href = 'http://juick.com/settings';
  202. ctitle.appendChild(anode);
  203. ctitle.style.display = 'flex';
  204. ctitle.style.justifyContent = 'space-between';
  205. ctitle.style.alignItems = 'baseline';
  206. }
  207. }
  208.  
  209. function updateAvatar() {
  210. if(!GM_getValue('enable_big_avatar', true)) { return; }
  211. var avatarImg = document.querySelector("div#ctitle a img");
  212. avatarImg.src = avatarImg.src.replace('/as/', '/a/');
  213. }
  214.  
  215. function loadTags(userId, doneCallback) {
  216. setTimeout(function(){
  217. GM_xmlhttpRequest({
  218. method: "GET",
  219. url: "http://juick.com/" + userId + "/tags",
  220. onload: function(response) {
  221. var re = /<section id\=\"content\">[\s]*<p>([\s\S]+)<\/p>[\s]*<\/section>/i;
  222. var result = re.exec(response.responseText);
  223. if(result !== null) {
  224. var tagsStr = result[1];
  225. var tagsContainer = document.createElement('p');
  226. tagsContainer.className += " tagsContainer";
  227. tagsContainer.innerHTML = tagsStr;
  228. doneCallback(tagsContainer);
  229. } else {
  230. console.log("no tags found");
  231. }
  232. }
  233. });
  234. }, 50);
  235. }
  236.  
  237. function addEasyTagsUnderPostEditorSharp() {
  238. if(!GM_getValue('enable_tags_on_new_post_form', true)) { return; }
  239. var userId = document.querySelector("nav#actions > ul > li:nth-child(2) > a").textContent.replace('@', '');
  240. loadTags(userId, function(tagsContainer){
  241. var messageform = document.getElementById("newmessage");
  242. var tagsfield = messageform.getElementsByTagName('div')[0].getElementsByClassName("tags")[0];
  243. messageform.getElementsByTagName('div')[0].appendChild(tagsContainer);
  244. sortAndColorizeTagsInContainer(tagsContainer, 60, true);
  245. [].forEach.call(tagsContainer.childNodes, function(item, i, arr) {
  246. var text = item.textContent;
  247. item.onclick = function() { tagsfield.value = (tagsfield.value + " " + text).trim(); };
  248. item.href = "#";
  249. });
  250. });
  251. }
  252.  
  253. function parseRgbColor(colorStr){
  254. colorStr = colorStr.replace(/ /g,'');
  255. colorStr = colorStr.toLowerCase();
  256. var re = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/;
  257. var bits = re.exec(colorStr);
  258. return [
  259. parseInt(bits[1]),
  260. parseInt(bits[2]),
  261. parseInt(bits[3])
  262. ];
  263. }
  264.  
  265. function getContrastColor(baseColor) {
  266. return (baseColor[0] + baseColor[1] + baseColor[2] > 127*3) ? [0,0,0] : [255,255,255];
  267. }
  268.  
  269. function sortAndColorizeTagsInContainer(tagsContainer, numberLimit, isSorting) {
  270. tagsContainer.className += " tagsContainer";
  271. var linkColor = parseRgbColor(getComputedStyle(tagsContainer.getElementsByTagName('A')[0]).color);
  272. var backColor = parseRgbColor(getComputedStyle(document.documentElement).backgroundColor);
  273. //linkColor = getContrastColor(backColor);
  274. var p0 = 0.7; // 70% of color range is used for color coding
  275. var maxC = 0.1;
  276. var sortedTags = [];
  277. [].forEach.call(tagsContainer.children, function(item, i, arr) {
  278. var anode = (item.tagName == 'A') ? item : item.getElementsByTagName('a')[0];
  279. var c = Math.log(parseInt(anode.title));
  280. maxC = (c > maxC) ? c : maxC;
  281. sortedTags.push({ c: c, a: anode, text: anode.textContent.toLowerCase()});
  282. });
  283. if((numberLimit !== null) && (sortedTags.length > numberLimit)) {
  284. sortedTags = sortedTags.slice(0, numberLimit);
  285. }
  286. if(isSorting) {
  287. sortedTags.sort(function (a, b) {
  288. return a.text.localeCompare(b.text);
  289. });
  290. }
  291. while (tagsContainer.firstChild) {
  292. tagsContainer.removeChild(tagsContainer.firstChild);
  293. }
  294. sortedTags.forEach(function(item, i, arr) {
  295. var c = item.c;
  296. var p = (c/maxC-1)*p0+1; // normalize to [p0..1]
  297. var r = Math.round(linkColor[0]*p + backColor[0]*(1-p));
  298. var g = Math.round(linkColor[1]*p + backColor[1]*(1-p));
  299. var b = Math.round(linkColor[2]*p + backColor[2]*(1-p));
  300. //item.a.style.color = "rgb("+r+","+g+","+b+")";
  301. item.a.style.setProperty("color", "rgb("+r+","+g+","+b+")", "important");
  302. tagsContainer.appendChild(item.a);
  303. tagsContainer.appendChild(document.createTextNode (" "));
  304. });
  305. }
  306.  
  307. function sortTagsPage() {
  308. if(!GM_getValue('enable_tags_page_coloring', true)) { return; }
  309. var tagsContainer = document.querySelector("section#content > p");
  310. sortAndColorizeTagsInContainer(tagsContainer, null, true);
  311. }
  312.  
  313. function colorizeTagsInUserColumn() {
  314. if(!GM_getValue('enable_left_column_tags_coloring', true)) { return; }
  315. var tagsContainer = document.querySelector("aside#column > p.tags");
  316. sortAndColorizeTagsInContainer(tagsContainer, null, false);
  317. }
  318.  
  319. function loadUsers(unprocessedUsers, processedUsers, doneCallback) {
  320. if(unprocessedUsers.length === 0) {
  321. doneCallback();
  322. } else {
  323. var user = unprocessedUsers.splice(0,1)[0];
  324. GM_xmlhttpRequest({
  325. method: "GET",
  326. //url: "http://api.juick.com/messages?uname=" + user.id,
  327. url: "http://juick.com/" + user.id + "/",
  328. onload: function(response) {
  329. if(response.status != 200) {
  330. console.log("" + user.id + ": failed with " + response.status + ", " + response.statusText);
  331. } else {
  332. var re = /datetime\=\"([^\"]+) ([^\"]+)\"/;
  333. //var re = /\"timestamp\"\:\"([^\"]+) ([^\"]+)\"/;
  334. var result = re.exec(response.responseText);
  335. if(result !== null) {
  336. var dateStr = "" + result[1] + "T" + result[2];// + "Z";
  337. var date = new Date(dateStr);
  338. user.date = date;
  339. user.a.appendChild(document.createTextNode (" (" + date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate() + ")" ));
  340. } else {
  341. console.log("" + user.id + ": no posts found");
  342. }
  343. }
  344. processedUsers.push(user);
  345. setTimeout(function(){ loadUsers(unprocessedUsers, processedUsers, doneCallback); }, 100);
  346. }
  347. });
  348. }
  349. }
  350.  
  351. function sortUsers() {
  352. var contentBlock = document.getElementById("content");
  353. var button = document.getElementById("usersSortingButton");
  354. button.parentNode.removeChild(button);
  355. var usersTable = document.querySelector("table.users");
  356. var unsortedUsers = [];
  357. var sortedUsers = [];
  358. [].forEach.call(usersTable.firstChild.children, function(tr, i, arr){
  359. [].forEach.call(tr.children, function(td, j, arrj){
  360. var anode = td.firstChild;
  361. var userId = anode.pathname.replace(/\//g, '');
  362. unsortedUsers.push({a: anode, id: userId, date: (new Date(1970, 1, 1))});
  363. });
  364. });
  365. loadUsers(unsortedUsers, sortedUsers, function(){
  366. sortedUsers.sort(function (b, a) {
  367. return ((a.date > b.date) - (a.date < b.date));
  368. });
  369. usersTable.parentNode.removeChild(usersTable);
  370. var ul = document.createElement("ul");
  371. ul.className = 'users';
  372. sortedUsers.forEach(function(user, i, arr){
  373. var li = document.createElement("li");
  374. li.appendChild(user.a);
  375. ul.appendChild(li);
  376. });
  377. contentBlock.appendChild(ul);
  378. });
  379. }
  380.  
  381. function addUsersSortingButton() {
  382. if(!GM_getValue('enable_users_sorting', true)) { return; }
  383. var contentBlock = document.getElementById("content");
  384. var usersTable = document.querySelector("table.users");
  385. var button = document.createElement("button");
  386. button.id = 'usersSortingButton';
  387. button.textContent="Sort by date";
  388. button.onclick = sortUsers;
  389. contentBlock.insertBefore(button, usersTable);
  390. }
  391.  
  392. function getAllMatchesAndCaptureGroups(re, str) {
  393. var results = [], result;
  394. while ((result = re.exec(str)) !== null) {
  395. results.push(Array.from(result));
  396. }
  397. return results;
  398. }
  399.  
  400. var decodeHtmlEntity = function(str) {
  401. return str.replace(/&#(\d+);/g, function(match, dec) {
  402. return String.fromCharCode(dec);
  403. });
  404. };
  405.  
  406. function htmlDecode(str) {
  407. var e = document.createElement('div');
  408. e.innerHTML = str;
  409. return e.childNodes.length === 0 ? '' : e.childNodes[0].nodeValue;
  410. }
  411.  
  412. function htmlEscape(html) {
  413. var textarea = document.createElement('textarea');
  414. textarea.textContent = html;
  415. return textarea.innerHTML;
  416. }
  417.  
  418. function turnIntoCts(node, makeNodeCallback) {
  419. node.onclick = function(e){
  420. e.preventDefault();
  421. var newNode = makeNodeCallback();
  422. if(this.hasAttribute('data-linkid')) {
  423. newNode.setAttribute('data-linkid', this.getAttribute('data-linkid'));
  424. }
  425. this.parentNode.replaceChild(newNode, this);
  426. };
  427. }
  428.  
  429. function makeCts(makeNodeCallback, title) {
  430. var ctsNode = document.createElement('div');
  431. var placeholder = document.createElement('div');
  432. placeholder.className = 'placeholder';
  433. placeholder.innerHTML = title;
  434. ctsNode.className = 'cts';
  435. ctsNode.appendChild(placeholder);
  436. turnIntoCts(ctsNode, makeNodeCallback);
  437. return ctsNode;
  438. }
  439.  
  440. function makeIframe(src, w, h) {
  441. var iframe = document.createElement("iframe");
  442. iframe.width = w;
  443. iframe.height = h;
  444. iframe.frameBorder = 0;
  445. iframe.scrolling = 'no';
  446. iframe.setAttribute('allowFullScreen', '');
  447. iframe.src = src;
  448. return iframe;
  449. }
  450.  
  451. function naiveEllipsis(str, len) {
  452. var ellStr = '...';
  453. var ellLen = ellStr.length;
  454. if(str.length <= len) { return str; }
  455. var half = Math.floor((len - ellLen) / 2);
  456. var left = str.substring(0, half);
  457. var right = str.substring(str.length - (len - half - ellLen));
  458. return '' + left + ellStr + right;
  459. }
  460.  
  461. function urlReplace(match, p1, p2, offset, string) {
  462. var isBrackets = (typeof p2 !== 'undefined');
  463. var domainRe = /^(?:https?:\/\/)?(?:[^@\/\n]+@)?(?:www\.)?([^:\/\n]+)/i;
  464. return (isBrackets)
  465. ? '<a href="' + p2 + '">' + p1 + '</a>'
  466. : '<a href="' + match + '">' + domainRe.exec(match)[1] + '</a>';
  467. }
  468.  
  469. function bqReplace(match, offset, string) {
  470. return '<q>' + match.replace(/^(?:>|&gt;)\s?/gmi, '') + '</q>';
  471. }
  472.  
  473. function messageReplyReplace(match, mid, rid, offset, string) {
  474. var isReply = (typeof rid !== 'undefined');
  475. return '<a href="//juick.com/' + mid + (isReply ? '#' + rid : '') + '">' + match + '</a>';
  476. }
  477.  
  478. function getEmbedableLinkTypes() {
  479. return [
  480. {
  481. name: 'Juick',
  482. id: 'embed_juick',
  483. ctsDefault: false,
  484. re: /^(?:https?:)?\/\/juick\.com\/(?:([\w-]+)\/)?([\d]+\b)(?:#(\d+))?/i,
  485. makeNode: function(aNode, reResult) {
  486. var juickType = this;
  487.  
  488. var isReply = ((typeof reResult[3] !== 'undefined') && (reResult[3] !== '0'));
  489. var mrid = (isReply) ? parseInt(reResult[3], 10) : 0;
  490. var idStr = '#' + reResult[2] + ((isReply) ? '/' + mrid : '');
  491. var linkStr = '//juick.com/' + reResult[2] + ((isReply) ? '#' + mrid : '');
  492.  
  493. var div = document.createElement("div");
  494. div.textContent = 'loading ' + idStr;
  495. div.className = 'juickEmbed embed loading';
  496.  
  497. if(GM_getValue('enable_link_text_update', true) && (aNode.textContent === 'juick.com')) {
  498. //var isUser = (typeof reResult[1] !== 'undefined');
  499. aNode.textContent = idStr; // + ((!isReply && isUser) ? ' (@' + reResult[1] + ')' : '');
  500. }
  501.  
  502. GM_xmlhttpRequest({
  503. method: "GET",
  504. url: 'https://api.juick.com/thread?mid=' + reResult[2],
  505. onload: function(response) {
  506.  
  507. if(response.status != 200) {
  508. div.textContent = 'Failed to load ' + idStr + ' (' + response.status + ' - ' + response.statusText + ')';
  509. div.className = div.className.replace(' loading', ' failed');
  510. turnIntoCts(div, function(){return juickType.makeNode(aNode, reResult);});
  511. return;
  512. }
  513. var threadInfo = JSON.parse(response.responseText);
  514. var msg = (!isReply) ? threadInfo[0] : threadInfo.find(function(x) {return (x.rid == mrid);});
  515. if((typeof msg == 'undefined')) {
  516. div.textContent = '' + idStr + ' doesn\'t exist';
  517. div.className = div.className.replace(' loading', ' failed');
  518. turnIntoCts(div, function(){return juickType.makeNode(aNode, reResult);});
  519. return;
  520. }
  521.  
  522. var withTags = (typeof msg.tags !== 'undefined');
  523. var withPhoto = (typeof msg.photo !== 'undefined');
  524. var isReplyTo = (typeof msg.replyto !== 'undefined');
  525. var hasReplies = (typeof msg.replies !== 'undefined');
  526.  
  527. var msgLink = '<a href="' + linkStr + '">' + idStr + '</a>';
  528. var userLink = '<a href="//juick.com/' + msg.user.uname + '/">@' + msg.user.uname + '</a>';
  529. var avatarStr = '<div class="msg-avatar"><a href="/' + msg.user.uname + '/"><img src="//i.juick.com/a/' + msg.user.uid + '.png" alt="' + msg.user.uname + '"></a></div>';
  530. var tagsStr = (withTags) ? '<div class="msg-tags">' + msg.tags.map(function(x) { return '<a href="http://juick.com/' + msg.user.uname + '/?tag=' + encodeURIComponent(x) + '">' + x + '</a>'; }).join('') + '</div>' : '';
  531. var photoStr = (withPhoto) ? '<div><a href="' + msg.photo.medium + '"><img src="' + msg.photo.small + '"/></a></div>' : '';
  532. var replyStr = (isReply) ? ( '<div>/' + mrid + (isReplyTo) ? ' in reply to /' + msg.replyto : '' ) + '</div>' : '';
  533. var titleDiv = '<div class="title">' + userLink + '</div>';
  534. var dateDiv = '<div class="date"><a href="' + linkStr + '">' + msg.timestamp + '</a></div>';
  535. var replyStr = (hasReplies)
  536. ? (' · ' + msg.replies + ((msg.replies == '1') ? ' reply' : ' replies'))
  537. : (isReplyTo)
  538. ? 'in reply to <a class="whiteRabbit" href="//juick.com/' + msg.mid + '#' + msg.replyto + '">#' + msg.mid + '/' + msg.replyto + '</a>'
  539. : (isReply)
  540. ? 'in reply to <a class="whiteRabbit" href="//juick.com/' + msg.mid + '">#' + msg.mid + '</a>'
  541. : '';
  542. var replyDiv = '<div class="embedReply msg-links">' + msgLink + ((replyStr.length > 0) ? ' ' + replyStr : '') + '</div>';
  543. var urlRe = /(?:\[([^\]]+)\]\[([^\]]+)\]|\b(?:(?:https?|ftp|file):\/\/|www\.|ftp\.)(?:\([-\w+*&@#/%=~|$?!:,.]*\)|[-\w+*&@#/%=~|$?!:,.])*(?:\([-\w+*&@#/%=~|$?!:,.]*\)|[\w+*&@#/%=~|$]))/gi;
  544. var bqRe = /(?:^(?:>|&gt;)\s?[\s\S]+?$\n?)+/gmi;
  545. var description = htmlEscape(msg.body)
  546. .replace(urlRe, urlReplace)
  547. .replace(bqRe, bqReplace)
  548. .replace(/\n/g,'<br/>')
  549. .replace(/\B#(\d+)(?:\/(\d+))?\b/gmi, messageReplyReplace)
  550. .replace(/\B@([\w-]+)\b/gmi, "<a href=\"//juick.com/$1\">@$1</a>");
  551. var descDiv = '<div class="desc">' + description + '</div>';
  552. div.innerHTML =
  553. '<div class="top">' + avatarStr + '<div class="top-right"><div class="top-right-1st">' + titleDiv + dateDiv + '</div><div class="top-right-2nd">' + tagsStr + '</div></div></div>' +
  554. descDiv + photoStr + replyDiv;
  555.  
  556. var allLinks = div.querySelectorAll(".desc a, .embedReply a.whiteRabbit");
  557. var embedContainer = div.parentNode;
  558. embedLinks(Array.prototype.slice.call(allLinks).reverse(), embedContainer, true, div);
  559.  
  560. div.className = div.className.replace(' loading', '');
  561. }
  562. });
  563.  
  564. return div;
  565. },
  566. makeTitle: function(aNode, reResult) {
  567. var isReply = ((typeof reResult[3] !== 'undefined') && (reResult[3] !== '0'));
  568. var mrid = (isReply) ? parseInt(reResult[3], 10) : 0;
  569. var idStr = '#' + reResult[2] + ((isReply) ? '/' + mrid : '');
  570. return idStr;
  571. }
  572. },
  573. {
  574. name: 'Jpeg and png images',
  575. id: 'embed_jpeg_and_png_images',
  576. ctsDefault: false,
  577. re: /\.(jpeg|jpg|png|svg)(:[a-zA-Z]+)?(?:\?[\w&;\?=]*)?$/i,
  578. makeNode: function(aNode, reResult) {
  579. var aNode2 = document.createElement("a");
  580. var imgNode = document.createElement("img");
  581. imgNode.src = aNode.href;
  582. aNode2.href = aNode.href;
  583. aNode2.appendChild(imgNode);
  584. return aNode2;
  585. }
  586. },
  587. {
  588. name: 'Gif images',
  589. id: 'embed_gif_images',
  590. ctsDefault: true,
  591. re: /\.gif(:[a-zA-Z]+)?(?:\?[\w&;\?=]*)?$/i,
  592. makeNode: function(aNode, reResult) {
  593. var aNode2 = document.createElement("a");
  594. var imgNode = document.createElement("img");
  595. imgNode.src = aNode.href;
  596. aNode2.href = aNode.href;
  597. aNode2.appendChild(imgNode);
  598. return aNode2;
  599. }
  600. },
  601. {
  602. name: 'Webm and mp4 videos',
  603. id: 'embed_webm_and_mp4_videos',
  604. ctsDefault: false,
  605. re: /\.(webm|mp4)(?:\?[\w&;\?=]*)?$/i,
  606. makeNode: function(aNode, reResult) {
  607. var video = document.createElement("video");
  608. video.src = aNode.href;
  609. video.setAttribute('controls', '');
  610. return video;
  611. }
  612. },
  613. {
  614. name: 'YouTube videos',
  615. id: 'embed_youtube_videos',
  616. ctsDefault: false,
  617. re: /^(?:https?:)?\/\/(?:www\.)?youtu(?:be\.com\/watch\?(?:[\w&=;]+&(?:amp;)?)?v=|\.be\/|be\.com\/v\/)([\w\-\_]*)(?:&(?:amp;)?[\w\?=]*)?/i,
  618. makeNode: function(aNode, reResult) {
  619. return makeIframe('//www.youtube-nocookie.com/embed/' + reResult[1] + '?rel=0', 640, 360);
  620. }
  621. },
  622. {
  623. name: 'YouTube playlists',
  624. id: 'embed_youtube_playlists',
  625. ctsDefault: false,
  626. re: /^(?:https?:)?\/\/(?:www\.)?youtube\.com\/playlist\?list=([\w\-\_]*)(&(amp;)?[\w\?=]*)?/i,
  627. makeNode: function(aNode, reResult) {
  628. return makeIframe('//www.youtube-nocookie.com/embed/videoseries?list=' + reResult[1], 640, 360);
  629. }
  630. },
  631. {
  632. name: 'Vimeo videos',
  633. id: 'embed_vimeo_videos',
  634. ctsDefault: false,
  635. //re: /^(?:https?:)?\/\/(?:www\.)?(?:player\.)?vimeo\.com\/(?:(?:video\/|album\/[\d]+\/video\/)?([\d]+)|([\w-]+)\/(?!videos)([\w-]+))/i,
  636. re: /^(?:https?:)?\/\/(?:www\.)?(?:player\.)?vimeo\.com\/(?:video\/|album\/[\d]+\/video\/)?([\d]+)/i,
  637. makeNode: function(aNode, reResult) {
  638. return makeIframe('//player.vimeo.com/video/' + reResult[1], 640, 360);
  639. }
  640. },
  641. {
  642. name: 'Dailymotion videos',
  643. id: 'embed_youtube_videos',
  644. ctsDefault: false,
  645. re: /^(?:https?:)?\/\/(?:www\.)?dailymotion\.com\/video\/([a-zA-Z\d]+)(?:_[\w-%]*)?/i,
  646. makeNode: function(aNode, reResult) {
  647. return makeIframe('//www.dailymotion.com/embed/video/' + reResult[1], 640, 360);
  648. }
  649. },
  650. {
  651. name: 'Coub clips',
  652. id: 'embed_coub_clips',
  653. ctsDefault: false,
  654. re: /^(?:https?:)?\/\/(?:www\.)?coub\.com\/(?:view|embed)\/([a-zA-Z\d]+)/i,
  655. makeNode: function(aNode, reResult) {
  656. var embedUrl = '//coub.com/embed/' + reResult[1] + '?muted=false&autostart=false&originalSize=false&startWithHD=false';
  657. return makeIframe(embedUrl, 640, 360);
  658. }
  659. },
  660. {
  661. name: 'Bandcamp music',
  662. id: 'embed_bandcamp_music',
  663. ctsDefault: false,
  664. re: /^(?:https?:)?\/\/(\w+)\.bandcamp\.com\/(track|album)\/([\w\-]+)/i,
  665. makeNode: function(aNode, reResult) {
  666. var bandcampType = this;
  667. var div = document.createElement("div");
  668. div.textContent = 'loading ' + naiveEllipsis(reResult[0], 65);
  669. div.className = 'bandcamp embed loading';
  670.  
  671. GM_xmlhttpRequest({
  672. method: "GET",
  673. url: reResult[0],
  674. onload: function(response) {
  675. if(response.status != 200) {
  676. div.textContent = 'Failed to load (' + response.status + ' - ' + response.statusText + ')';
  677. div.className = div.className.replace(' loading', ' failed');
  678. turnIntoCts(div, function(){return bandcampType.makeNode(aNode, reResult);});
  679. return;
  680. }
  681. var baseSize = 480;
  682. var videoUrl, videoH;
  683. var metaRe = /<\s*meta\s+(?:property|name)\s*=\s*\"([^\"]+)\"\s+content\s*=\s*\"([^\"]*)\"\s*>/gmi;
  684. var matches = getAllMatchesAndCaptureGroups(metaRe, response.responseText);
  685. [].forEach.call(matches, function(m, i, arr) {
  686. if(m[1] == 'og:video') { videoUrl = m[2]; }
  687. if(m[1] == 'video_height') { videoH = baseSize + parseInt(m[2], 10); }
  688. });
  689. videoUrl = videoUrl.replace('/artwork=small', '');
  690. if(reResult[2] == 'album') {
  691. videoUrl = videoUrl.replace('/tracklist=false', '/tracklist=true');
  692. videoH += 162;
  693. }
  694. var iframe = makeIframe(videoUrl, baseSize, videoH);
  695. div.parentNode.replaceChild(iframe, div);
  696. }
  697. });
  698.  
  699. return div;
  700. }
  701. },
  702. {
  703. name: 'SoundCloud music',
  704. id: 'embed_soundcloud_music',
  705. ctsDefault: false,
  706. re: /(?:https?:)?\/\/(?:www\.)?soundcloud\.com\/(([\w\-\_]*)\/(?:sets\/)?([\w\-\_]*))(?:\/)?/i,
  707. makeNode: function(aNode, reResult) {
  708. var embedUrl = '//w.soundcloud.com/player/?url=//soundcloud.com/' + reResult[1] + '&amp;auto_play=false&amp;hide_related=false&amp;show_comments=true&amp;show_user=true&amp;show_reposts=false&amp;visual=true';
  709. return makeIframe(embedUrl, '100%', 450);
  710. }
  711. },
  712. {
  713. name: 'Instagram',
  714. id: 'embed_instagram',
  715. ctsDefault: false,
  716. re: /(?:https?:)?\/\/(?:www\.)?instagram\.com\/p\/([\w\-\_]*)(?:\/)?(?:\/)?/i,
  717. makeNode: function(aNode, reResult) {
  718. return makeIframe('//www.instagram.com/p/' + reResult[1] + '/embed', 640, 722);
  719. }
  720. },
  721. {
  722. name: 'Flickr images',
  723. id: 'embed_flickr_images',
  724. ctsDefault: false,
  725. re: /^(?:https?:)?\/\/(?:(?:www\.)?flickr\.com\/photos\/([\w@-]+)\/(\d+)|flic.kr\/p\/(\w+))(?:\/)?/i,
  726. makeNode: function(aNode, reResult) {
  727. var flickrType = this;
  728. var div = document.createElement("div");
  729. div.textContent = 'loading ' + naiveEllipsis(reResult[0], 65);
  730. div.className = 'flickr embed loading';
  731.  
  732. GM_xmlhttpRequest({
  733. method: "GET",
  734. url: 'https://www.flickr.com/services/oembed?format=xml&url=' + encodeURIComponent(reResult[0]),
  735. onload: function(response) {
  736. if(response.status != 200) {
  737. div.textContent = 'Failed to load (' + response.status + ')';
  738. div.className = div.className.replace(' loading', ' failed');
  739. turnIntoCts(div, function(){return flickrType.makeNode(aNode, reResult);});
  740. return;
  741. }
  742. var fType, url, thumb, authorName, authorUrl, title, webPage;
  743. var xmlTagRe = /<(\w+)>\s*([^<]+)<\/\w+>/gmi;
  744. var matches = getAllMatchesAndCaptureGroups(xmlTagRe, response.responseText);
  745. [].forEach.call(matches, function(m, i, arr) {
  746. if(m[1] == 'flickr_type') { fType = m[2]; }
  747. if(m[1] == 'url') { url = m[2]; }
  748. if(m[1] == 'thumbnail_url') { thumb = m[2]; }
  749. if(m[1] == 'author_name') { authorName = m[2]; }
  750. if(m[1] == 'author_url') { authorUrl = m[2]; }
  751. if(m[1] == 'title') { title = m[2]; }
  752. if(m[1] == 'web_page') { webPage = m[2]; }
  753. });
  754.  
  755. var imageUrl = (typeof url != 'undefined') ? url : thumb;
  756. var aNode2 = document.createElement("a");
  757. var imgNode = document.createElement("img");
  758. imgNode.src = imageUrl;//.replace('_b.', '_z.');
  759. aNode2.href = aNode.href;
  760. aNode2.appendChild(imgNode);
  761.  
  762. var titleDiv = '<div class="title">' + '<a href="' + webPage + '">' + title + '</a>';
  763. if(fType != 'photo') {
  764. titleDiv += ' (' + fType + ')';
  765. }
  766. titleDiv += ' by <a href="' + authorUrl + '">' + authorName + '</a></div>';
  767. div.innerHTML = '<div class="top">' + titleDiv + '</div>';
  768. div.appendChild(aNode2);
  769.  
  770. div.className = div.className.replace(' loading', '');
  771. }
  772. });
  773.  
  774. return div;
  775. }
  776. },
  777. {
  778. name: 'DeviantArt images',
  779. id: 'embed_deviantart_images',
  780. ctsDefault: false,
  781. re: /^(?:https?:)?\/\/([\w-]+)\.deviantart\.com\/art\/([\w-]+)/i,
  782. makeNode: function(aNode, reResult) {
  783. var daType = this;
  784. var div = document.createElement("div");
  785. div.textContent = 'loading ' + naiveEllipsis(reResult[0], 65);
  786. div.className = 'deviantart embed loading';
  787.  
  788. GM_xmlhttpRequest({
  789. method: "GET",
  790. url: 'https://backend.deviantart.com/oembed?format=xml&url=' + encodeURIComponent(reResult[0]),
  791. onload: function(response) {
  792. if(response.status != 200) {
  793. div.textContent = 'Failed to load (' + response.status + ' - ' + response.statusText + ')';
  794. div.className = div.className.replace(' loading', ' failed');
  795. turnIntoCts(div, function(){return daType.makeNode(aNode, reResult);});
  796. return;
  797. }
  798. var fType, fullsizeUrl, url, thumb, authorName, authorUrl, title, pubdate;
  799. var xmlTagRe = /<(\w+)>\s*([^<]+)<\/\w+>/gmi;
  800. var matches = getAllMatchesAndCaptureGroups(xmlTagRe, response.responseText);
  801. [].forEach.call(matches, function(m, i, arr) {
  802. if(m[1] == 'type') { fType = m[2]; }
  803. if(m[1] == 'fullsize_url') { fullsizeUrl = m[2]; }
  804. if(m[1] == 'url') { url = m[2]; }
  805. if(m[1] == 'thumbnail_url') { thumb = m[2]; }
  806. if(m[1] == 'author_name') { authorName = m[2]; }
  807. if(m[1] == 'author_url') { authorUrl = m[2]; }
  808. if(m[1] == 'title') { title = m[2]; }
  809. if(m[1] == 'pubdate') { pubdate = m[2]; }
  810. });
  811.  
  812. var imageUrl = (typeof fullsizeUrl != 'undefined') ? fullsizeUrl : (typeof url != 'undefined') ? url : thumb;
  813. var aNode2 = document.createElement("a");
  814. var imgNode = document.createElement("img");
  815. imgNode.src = imageUrl;
  816. aNode2.href = aNode.href;
  817. aNode2.appendChild(imgNode);
  818.  
  819. var date = new Date(pubdate);
  820. var dateDiv = '<div class="date">' + date.toLocaleString('ru-RU') + '</div>';
  821. var titleDiv = '<div class="title">' + '<a href="' + reResult[0] + '">' + title + '</a>';
  822. if(fType != 'photo') {
  823. titleDiv += ' (' + fType + ')';
  824. }
  825. titleDiv += ' by <a href="' + authorUrl + '">' + authorName + '</a></div>';
  826. div.innerHTML = '<div class="top">' + titleDiv + dateDiv + '</div>';
  827. div.appendChild(aNode2);
  828.  
  829. div.className = div.className.replace(' loading', '');
  830. }
  831. });
  832.  
  833. return div;
  834. }
  835. },
  836. {
  837. name: 'Imgur gifv videos',
  838. id: 'embed_imgur_gifv_videos',
  839. default: false,
  840. re: /^(?:https?:)?\/\/(?:\w+\.)?imgur\.com\/([a-zA-Z\d]+)\.gifv/i,
  841. makeNode: function(aNode, reResult) {
  842. var video = document.createElement("video");
  843. video.src = '//i.imgur.com/' + reResult[1] + '.mp4';
  844. video.setAttribute('controls', '');
  845. return video;
  846. }
  847. },
  848. {
  849. name: 'Imgur indirect links',
  850. id: 'embed_imgur_indirect_links',
  851. ctsDefault: false,
  852. re: /^(?:https?:)?\/\/(?:\w+\.)?imgur\.com\/(?:(gallery|a)\/)?(?!gallery|jobs|about|blog|apps)([a-zA-Z\d]+)(?:#([a-zA-Z\d]+))?$/i,
  853. makeNode: function(aNode, reResult) {
  854. var isAlbum = (typeof reResult[1] != 'undefined');
  855. var embedUrl;
  856. if(isAlbum) {
  857. var isSpecificImage = (typeof reResult[3] != 'undefined');
  858. if(isSpecificImage) {
  859. embedUrl = '//imgur.com/' + reResult[3] + '/embed?analytics=false&amp;w=540';
  860. } else {
  861. embedUrl = '//imgur.com/a/' + reResult[2] + '/embed?analytics=false&amp;w=540&amp;pub=true';
  862. }
  863. } else {
  864. embedUrl = '//imgur.com/' + reResult[2] + '/embed?analytics=false&amp;w=540';
  865. }
  866. return makeIframe(embedUrl, '100%', 600);
  867. }
  868. },
  869. {
  870. name: 'Gfycat indirect links',
  871. id: 'embed_gfycat_indirect_links',
  872. ctsDefault: true,
  873. re: /^(?:https?:)?\/\/(?:\w+\.)?gfycat\.com\/([a-zA-Z\d]+)$/i,
  874. makeNode: function(aNode, reResult) {
  875. return makeIframe('//gfycat.com/ifr/' + reResult[1], '100%', 480);
  876. }
  877. },
  878. {
  879. name: 'Twitter',
  880. id: 'embed_twitter_status',
  881. ctsDefault: false,
  882. re: /^(?:https?:)?\/\/(?:www\.)?(?:mobile\.)?twitter\.com\/([\w-]+)\/status\/([\d]+)/i,
  883. makeNode: function(aNode, reResult) {
  884. var twitterType = this;
  885. var twitterUrl = reResult[0].replace('mobile.','');
  886. var div = document.createElement("div");
  887. div.textContent = 'loading ' + twitterUrl;
  888. div.className = 'twi embed loading';
  889.  
  890. GM_xmlhttpRequest({
  891. method: "GET",
  892. url: twitterUrl,
  893. onload: function(response) {
  894. if(response.status != 200) {
  895. div.textContent = 'Failed to load (' + response.status + ')';
  896. div.className = div.className.replace(' loading', ' failed');
  897. turnIntoCts(div, function(){return twitterType.makeNode(aNode, reResult);});
  898. return;
  899. }
  900. if(response.finalUrl.endsWith('account/suspended')) {
  901. div.textContent = 'Account @' + reResult[1] + ' is suspended';
  902. return;
  903. }
  904. if(response.finalUrl.indexOf('protected_redirect=true') != -1) {
  905. div.textContent = 'Account @' + reResult[1] + ' is protected';
  906. return;
  907. }
  908. var images = [];
  909. var userGenImg = false;
  910. var isVideo = false;
  911. var videoUrl, videoW, videoH;
  912. var description;
  913. var title;
  914. var id = reResult[1];
  915. var titleDiv, dateDiv ='', descDiv;
  916. var metaRe = /<\s*meta\s+property\s*=\s*\"([^\"]+)\"\s+content\s*=\s*\"([^\"]*)\"\s*>/gmi;
  917. var matches = getAllMatchesAndCaptureGroups(metaRe, response.responseText);
  918. [].forEach.call(matches, function(m, i, arr) {
  919. if(m[1] == 'og:title') { title = m[2]; }
  920. if(m[1] == 'og:description') {
  921. console.log(htmlDecode(m[2]));
  922. description = htmlDecode(m[2])
  923. .replace(/\n/g,'<br/>')
  924. .replace(/\B@(\w{1,15})\b/gmi, "<a href=\"//twitter.com/$1\">@$1</a>")
  925. .replace(/#(\w+)/gmi, "<a href=\"//twitter.com/hashtag/$1\">#$1</a>")
  926. .replace(/(?:https?:)?\/\/t\.co\/([\w]+)/gmi, "<a href=\"$&\">$&</a>");
  927. }
  928. if(m[1] == 'og:image') { images.push(m[2]); }
  929. if(m[1] == 'og:image:user_generated') { userGenImg = true; }
  930. if(m[1] == 'og:video:url') { videoUrl = m[2]; isVideo = true; }
  931. if(m[1] == 'og:video:height') { videoH = '' + m[2] + 'px'; }
  932. if(m[1] == 'og:video:width') { videoW = '' + m[2] + 'px'; }
  933. });
  934. var timestampMsRe = /\bdata-time-ms\s*=\s*\"([^\"]+)\"/gi;
  935. var timestampMsResult = timestampMsRe.exec(response.responseText);
  936. if(timestampMsResult !== null) {
  937. var date = new Date(+timestampMsResult[1]);
  938. dateDiv = '<div class="date">' + date.toLocaleString('ru-RU') + '</div>';
  939. }
  940. titleDiv = '<div class="title">' + title + ' (<a href="//twitter.com/' + id + '">@' + id + '</a>)' + '</div>';
  941. descDiv = '<div class="desc">' + description + '</div>';
  942. div.innerHTML = '<div class="top">' + titleDiv + dateDiv + '</div>' + descDiv;
  943. if(userGenImg) { div.innerHTML += '' + images.map(function(x){ return '<a href="' + x + '"><img src="' + x + '"></a>'; }).join(''); }
  944. if(isVideo) {
  945. var playIcon = '<div class="icon icon--ei-play icon--s " title="Click to play"><svg class="icon__cnt"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#ei-play-icon"></use></svg></div>';
  946. div.appendChild(
  947. makeCts(
  948. function(){ return makeIframe(videoUrl, videoW, videoH); },
  949. '<img src="' + images[0] + '">' + playIcon
  950. )
  951. );
  952. }
  953. div.className = div.className.replace(' loading', '');
  954. }
  955. });
  956.  
  957. return div;
  958. }
  959. },
  960. {
  961. name: 'Gist',
  962. id: 'embed_gist',
  963. ctsDefault: false,
  964. re: /^(?:https?:)?\/\/gist.github.com\/(?:([\w-]+)\/)?([A-Fa-f0-9]+)\b/i,
  965. makeNode: function(aNode, reResult) {
  966. var gistType = this;
  967. var id = reResult[2];
  968.  
  969. var div = document.createElement("div");
  970. div.textContent = 'loading ' + naiveEllipsis(reResult[0], 65);
  971. div.className = 'gistEmbed embed loading';
  972.  
  973. GM_xmlhttpRequest({
  974. method: "GET",
  975. url: 'https://gist.github.com/' + id + '.json',
  976. onload: function(response) {
  977. if(response.status != 200) {
  978. div.textContent = 'Failed to load (' + response.status + ' - ' + response.statusText + ')';
  979. div.className = div.className.replace(' loading', ' failed');
  980. turnIntoCts(div, function(){return gistType.makeNode(aNode, reResult);});
  981. return;
  982. }
  983. var json = JSON.parse(response.responseText);
  984. var titleDiv = '<div class="title">"' + json.description + '" by <a href="https://gist.github.com/' + json.owner + '">' + json.owner + '</a></div>';
  985. var dateDiv = '<div class="date">' + (new Date(json.created_at).toLocaleDateString('ru-RU')) + '</div>';
  986. var stylesheet = '<link rel="stylesheet" href="' + htmlEscape(json.stylesheet) + '"></link>';
  987. div.innerHTML = '<div class="top">' + titleDiv + dateDiv + '</div>' + stylesheet + json.div;
  988.  
  989. div.className = div.className.replace(' loading', ' loaded');
  990. }
  991. });
  992.  
  993. return div;
  994. }
  995. },
  996. {
  997. name: 'JSFiddle',
  998. id: 'embed_jsfiddle',
  999. ctsDefault: false,
  1000. re: /^(?:https?:)?(\/\/(?:jsfiddle|fiddle.jshell)\.net\/(?:(?!embedded\b)[\w]+\/?)+)/i,
  1001. makeNode: function(aNode, reResult) {
  1002. var endsWithSlash = reResult[1].endsWith('/');
  1003. return makeIframe('' + reResult[1] + (endsWithSlash ? '' : '/') + 'embedded/', '100%', 500);
  1004. }
  1005. },
  1006. {
  1007. name: 'Codepen',
  1008. id: 'embed_codepen',
  1009. ctsDefault: false,
  1010. re: /^(?:https?:)?\/\/codepen\.io\/(\w+)\/(?:pen|full)\/(\w+)/i,
  1011. makeNode: function(aNode, reResult) {
  1012. var codepenType = this;
  1013. var div = document.createElement("div");
  1014. div.textContent = 'loading ' + naiveEllipsis(reResult[0], 65);
  1015. div.className = 'codepen embed loading';
  1016.  
  1017. GM_xmlhttpRequest({
  1018. method: "GET",
  1019. url: 'https://codepen.io/api/oembed?format=json&url=' + encodeURIComponent(reResult[0].replace('/full/', '/pen/')),
  1020. onload: function(response) {
  1021. if(response.status != 200) {
  1022. div.textContent = 'Failed to load (' + response.status + ' - ' + response.statusText + ')';
  1023. div.className = div.className.replace(' loading', ' failed');
  1024. turnIntoCts(div, function(){return codepenType.makeNode(aNode, reResult);});
  1025. return;
  1026. }
  1027. var json = JSON.parse(response.responseText);
  1028. var titleDiv = '<div class="title">"' + json.title + '" by <a href="' + json.author_url + '">' + json.author_name + '</a></div>';
  1029. div.innerHTML = '<div class="top">' + titleDiv + '</div>' + json.html;
  1030.  
  1031. div.className = div.className.replace(' loading', '');
  1032. }
  1033. });
  1034.  
  1035. return div;
  1036. }
  1037. }
  1038. ];
  1039. }
  1040.  
  1041. function intersect(a, b) {
  1042. var t;
  1043. if (b.length > a.length) { t = b, b = a, a = t; } // loop over shorter array
  1044. return a.filter(function (e) { return (b.indexOf(e) !== -1); });
  1045. }
  1046.  
  1047. function insertAfter(newNode, referenceNode) {
  1048. referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
  1049. }
  1050.  
  1051. function embedLink(aNode, linkTypes, container, alwaysCts, afterNode) {
  1052. var anyEmbed = false;
  1053. var isAfterNode = (typeof afterNode !== 'undefined');
  1054. var linkId = (aNode.href.replace(/^https?:/i, ''));
  1055. var sameEmbed = container.querySelector('*[data-linkid=\'' + linkId + '\']'); // do not embed the same thing twice
  1056. if(sameEmbed === null) {
  1057. anyEmbed = [].some.call(linkTypes, function(linkType) {
  1058. if(GM_getValue(linkType.id, true)) {
  1059. var reResult = linkType.re.exec(aNode.href);
  1060. var matched = (reResult !== null);
  1061. if(matched) {
  1062. aNode.className += ' embedLink';
  1063. var newNode;
  1064. var isCts = alwaysCts || GM_getValue('cts_' + linkType.id, linkType.ctsDefault);
  1065. if(isCts) {
  1066. var linkTitle = (typeof linkType.makeTitle !== 'undefined') ? linkType.makeTitle(aNode, reResult) : naiveEllipsis(aNode.href, 65);
  1067. newNode = makeCts(function(){ return linkType.makeNode(aNode, reResult); }, 'Click to show: ' + linkTitle);
  1068. } else {
  1069. newNode = linkType.makeNode(aNode, reResult);
  1070. }
  1071. newNode.setAttribute('data-linkid', linkId);
  1072. if(isAfterNode) {
  1073. insertAfter(newNode, afterNode);
  1074. } else {
  1075. container.appendChild(newNode);
  1076. }
  1077. return true;
  1078. }
  1079. }
  1080. });
  1081. }
  1082. return anyEmbed;
  1083. }
  1084.  
  1085. function embedLinks(aNodes, container, alwaysCts, afterNode) {
  1086. var anyEmbed = false;
  1087. var embedableLinkTypes = getEmbedableLinkTypes();
  1088. [].forEach.call(aNodes, function(aNode, i, arr) {
  1089. var isEmbedded = embedLink(aNode, embedableLinkTypes, container, alwaysCts, afterNode);
  1090. anyEmbed = anyEmbed || isEmbedded;
  1091. });
  1092. return anyEmbed;
  1093. }
  1094.  
  1095. function embedLinksToArticles() {
  1096. var cts = GM_getValue('cts_users_and_tags', '').split(/[\s,]+/);
  1097. var ctsUsers = cts.filter(function(x){ return x.startsWith('@'); }).map(function(x){ return x.replace('@','').toLowerCase(); });
  1098. var ctsTags = cts.filter(function(x){ return x.startsWith('*'); }).map(function(x){ return x.replace('*','').toLowerCase(); });
  1099. [].forEach.call(document.querySelectorAll("#content > article"), function(article, i, arr) {
  1100. var userId = article.querySelector("div.msg-avatar > a > img").alt;
  1101. var tagsDiv = article.querySelector(".msg-tags");
  1102. var tags = [];
  1103. if(tagsDiv !== null) {
  1104. [].forEach.call(tagsDiv.childNodes, function(item, j, arrj) {
  1105. tags.push(item.textContent.toLowerCase());
  1106. });
  1107. }
  1108. var isCtsPost = (ctsUsers.indexOf(userId.toLowerCase()) !== -1) || (intersect(tags, ctsTags).length > 0);
  1109. var nav = article.querySelector("nav.l");
  1110. var allLinks = article.querySelectorAll("p:not(.ir) a, pre a");
  1111. var embedContainer = document.createElement("div");
  1112. embedContainer.className = 'embedContainer';
  1113. var anyEmbed = embedLinks(allLinks, embedContainer, isCtsPost);
  1114. if(anyEmbed){
  1115. article.insertBefore(embedContainer, nav);
  1116. }
  1117. });
  1118. }
  1119.  
  1120. function embedLinksToPost() {
  1121. [].forEach.call(document.querySelectorAll("#content .msg-cont"), function(msg, i, arr) {
  1122. var txt = msg.querySelector(".msg-txt");
  1123. var allLinks = txt.querySelectorAll("a");
  1124. var embedContainer = document.createElement("div");
  1125. embedContainer.className = 'embedContainer';
  1126. var anyEmbed = embedLinks(allLinks, embedContainer, false);
  1127. if(anyEmbed){
  1128. msg.insertBefore(embedContainer, txt.nextSibling);
  1129. }
  1130. });
  1131. }
  1132.  
  1133. function getUserscriptSettings() {
  1134. return [
  1135. {
  1136. name: 'Пользовательские теги (/user/?tag=) в постах вместо общих (/tag/)',
  1137. id: 'enable_user_tag_links'
  1138. },
  1139. {
  1140. name: 'Теги на форме редактирования нового поста (/#post)',
  1141. id: 'enable_tags_on_new_post_form'
  1142. },
  1143. {
  1144. name: 'Сортировка и цветовое кодирование тегов на странице /user/tags',
  1145. id: 'enable_tags_page_coloring'
  1146. },
  1147. {
  1148. name: 'Цветовое кодирование тегов в левой колонке',
  1149. id: 'enable_left_column_tags_coloring'
  1150. },
  1151. {
  1152. name: 'Заголовок и управление подпиской на странице тега /tag/...',
  1153. id: 'enable_tag_page_toolbar'
  1154. },
  1155. {
  1156. name: 'Ссылки для удаления комментариев',
  1157. id: 'enable_comment_removal_links'
  1158. },
  1159. {
  1160. name: 'Ссылка для редактирования тегов поста',
  1161. id: 'enable_tags_editing_link'
  1162. },
  1163. {
  1164. name: 'Большая аватарка в левой колонке',
  1165. id: 'enable_big_avatar'
  1166. },
  1167. {
  1168. name: 'Ссылки для перехода к постам пользователя за определённый год',
  1169. id: 'enable_year_links'
  1170. },
  1171. {
  1172. name: 'Ссылка на настройки в левой колонке на своей странице',
  1173. id: 'enable_settings_link'
  1174. },
  1175. {
  1176. name: 'Сортировка подписок/подписчиков по дате последнего сообщения',
  1177. id: 'enable_users_sorting'
  1178. },
  1179. {
  1180. name: 'Min-width для тегов',
  1181. id: 'enable_tags_min_width'
  1182. },
  1183. {
  1184. name: 'Заменять текст ссылок "juick.com" на id постов и комментариев',
  1185. id: 'enable_link_text_update'
  1186. }
  1187. ];
  1188. }
  1189.  
  1190. function makeSettingsCheckbox(caption, id, defaultState) {
  1191. var label = document.createElement("label");
  1192. var cb = document.createElement("input");
  1193. cb.type = 'checkbox';
  1194. cb.checked = GM_getValue(id, defaultState);
  1195. cb.onclick = function(e) { GM_setValue(id, cb.checked); };
  1196. label.appendChild(cb);
  1197. label.appendChild(document.createTextNode(caption));
  1198. return label;
  1199. }
  1200.  
  1201. function makeSettingsTextbox(caption, id, defaultString, placeholder) {
  1202. var label = document.createElement("label");
  1203. var wrapper = document.createElement("div");
  1204. wrapper.className = 'ta-wrapper';
  1205. var textarea = document.createElement("textarea");
  1206. textarea.placeholder = placeholder;
  1207. textarea.value = GM_getValue(id, defaultString);
  1208. textarea.oninput = function(e) { GM_setValue(id, textarea.value); };
  1209. textarea.style = 'width: 100%; height: 100%;';
  1210. wrapper.appendChild(textarea);
  1211. label.appendChild(document.createTextNode('' + caption + ': '));
  1212. label.appendChild(wrapper);
  1213. return label;
  1214. }
  1215.  
  1216. function wrapIntoTag(node, tagName, className) {
  1217. var tag = document.createElement(tagName);
  1218. if(typeof className != 'undefined') {
  1219. tag.className = className;
  1220. }
  1221. tag.appendChild(node);
  1222. return tag;
  1223. }
  1224.  
  1225. function showUserscriptSettings() {
  1226. var contentBlock = document.querySelector("#content > article");
  1227. while (contentBlock.firstChild) {
  1228. contentBlock.removeChild(contentBlock.firstChild);
  1229. }
  1230.  
  1231. var h1 = document.createElement("h1");
  1232. h1.textContent = 'Tweaks';
  1233.  
  1234. var fieldset1 = document.createElement("fieldset");
  1235. var legend1 = document.createElement("legend");
  1236. legend1.textContent = 'UI';
  1237. fieldset1.appendChild(legend1);
  1238.  
  1239. var list1 = document.createElement("ul");
  1240. var allSettings = getUserscriptSettings();
  1241. [].forEach.call(allSettings, function(item, i, arr) {
  1242. var liNode = document.createElement("li");
  1243. var p = document.createElement("p");
  1244. p.appendChild(makeSettingsCheckbox(item.name, item.id, true));
  1245. liNode.appendChild(p);
  1246. list1.appendChild(liNode);
  1247. });
  1248. fieldset1.appendChild(list1);
  1249.  
  1250. var fieldset2 = document.createElement("fieldset");
  1251. var legend2 = document.createElement("legend");
  1252. legend2.textContent = 'Embedding';
  1253. fieldset2.appendChild(legend2);
  1254.  
  1255. var table2 = document.createElement("table");
  1256. table2.style.width = '100%';
  1257. var embedableLinkTypes = getEmbedableLinkTypes();
  1258. [].forEach.call(embedableLinkTypes, function(linkType, i, arr) {
  1259. var row = document.createElement("tr");
  1260. row.appendChild(wrapIntoTag(makeSettingsCheckbox(linkType.name, linkType.id, true), 'td'));
  1261. row.appendChild(wrapIntoTag(makeSettingsCheckbox('Click to show', 'cts_' + linkType.id, linkType.ctsDefault), 'td'));
  1262. table2.appendChild(row);
  1263. });
  1264. fieldset2.appendChild(table2);
  1265.  
  1266. var ctsUsersAndTags = makeSettingsTextbox('Всегда использовать "Click to show" для этих юзеров и тегов в ленте', 'cts_users_and_tags', '', '@users and *tags separated with space or comma');
  1267. ctsUsersAndTags.style = 'display: flex; flex-direction: column; align-items: stretch;';
  1268. fieldset2.appendChild(document.createElement('hr'));
  1269. fieldset2.appendChild(wrapIntoTag(ctsUsersAndTags, 'p'));
  1270.  
  1271.  
  1272. var resetButton = document.createElement("button");
  1273. resetButton.textContent='Reset userscript settings to default';
  1274. resetButton.onclick = function(){
  1275. if(!confirm('Are you sure you want to reset Tweaks settings to default?')) { return; }
  1276. var keys = GM_listValues();
  1277. for (var i=0, key=null; key=keys[i]; i++) {
  1278. GM_deleteValue(key);
  1279. }
  1280. showUserscriptSettings();
  1281. alert('Done!');
  1282. };
  1283.  
  1284.  
  1285. var fieldset3 = document.createElement("fieldset");
  1286. var legend3 = document.createElement("legend");
  1287. legend3.textContent = 'Version info';
  1288. var ver1 = document.createElement("p");
  1289. var ver2 = document.createElement("p");
  1290. ver1.textContent = 'Greasemonkey (or your script runner) version: ' + GM_info.version;
  1291. ver2.textContent = 'Userscript version: ' + GM_info.script.version;
  1292. fieldset3.appendChild(legend3);
  1293. fieldset3.appendChild(ver1);
  1294. fieldset3.appendChild(ver2);
  1295.  
  1296. var support = document.createElement("p");
  1297. support.innerHTML = 'Feedback and feature requests <a href="http://juick.com/killy/?tag=userscript">here</a>.';
  1298.  
  1299. contentBlock.appendChild(h1);
  1300. contentBlock.appendChild(fieldset1);
  1301. contentBlock.appendChild(fieldset2);
  1302. contentBlock.appendChild(resetButton);
  1303. contentBlock.appendChild(fieldset3);
  1304. contentBlock.appendChild(support);
  1305.  
  1306. contentBlock.className = 'tweaksSettings';
  1307. }
  1308.  
  1309. function addTweaksSettingsButton() {
  1310. var tabsList = document.querySelector("#pagetabs > ul");
  1311. var liNode = document.createElement("li");
  1312. var aNode = document.createElement("a");
  1313. aNode.textContent = 'Tweaks';
  1314. aNode.href = '#tweaks';
  1315. aNode.onclick = function(e){ e.preventDefault(); showUserscriptSettings(); };
  1316. liNode.appendChild(aNode);
  1317. tabsList.appendChild(liNode);
  1318. }
  1319.  
  1320. function addStyle() {
  1321. var backColor = parseRgbColor(getComputedStyle(document.documentElement).backgroundColor);
  1322. var textColor = parseRgbColor(getComputedStyle(document.body).color);
  1323. var colorVars = '' +
  1324. '--br: ' + backColor[0] +
  1325. '; --bg: ' +backColor[1] +
  1326. '; --bb: ' +backColor[2] +
  1327. '; --tr: ' +textColor[0] +
  1328. '; --tg: ' +textColor[1] +
  1329. '; --tb: ' +textColor[2] + ";";
  1330.  
  1331. if(GM_getValue('enable_tags_min_width', true)) {
  1332. GM_addStyle(".tagsContainer a { min-width: 25px; display: inline-block; text-align: center; } ");
  1333. }
  1334. GM_addStyle(
  1335. ":root { " + colorVars + " --bg10: rgba(var(--br),var(--bg),var(--bb),1.0); --color10: rgba(var(--tr),var(--tg),var(--tb),1.0); --color07: rgba(var(--tr),var(--tg),var(--tb),0.7); --color03: rgba(var(--tr),var(--tg),var(--tb),0.3); --color02: rgba(var(--tr),var(--tg),var(--tb),0.2); } " +
  1336. ".embedContainer * { box-sizing: border-box; } " +
  1337. ".embedContainer img, .embedContainer video { max-width: 100%; max-height: 80vh; } " +
  1338. ".embedContainer iframe { resize: vertical; } " +
  1339. ".embedContainer { margin-top: 0.7em; } " +
  1340. ".embedContainer > .embed { width: 100%; margin-bottom: 0.3em; border: 1px solid var(--color02); padding: 0.5em; display: flex; flex-direction: column; } " +
  1341. ".embedContainer > .embed.loading, .embedContainer > .embed.failed { text-align: center; color: var(--color07); padding: 0; } " +
  1342. ".embedContainer > .embed.failed { cursor: pointer; } " +
  1343. ".embedContainer .embed .cts { margin: 0; } " +
  1344. ".embed .top { display: flex; flex-shrink: 0; justify-content: space-between; margin-bottom: 0.5em; } " +
  1345. ".embed .date, .embed .date > a, .embed .title { color: var(--color07); } " +
  1346. ".embed .date { font-size: small; } " +
  1347. ".embed .desc { margin-bottom: 0.5em; max-height: 55vh; overflow-y: auto; } " +
  1348. ".twi.embed > .cts > .placeholder { display: inline-block; } " +
  1349. ".juickEmbed > .top > .top-right { display: flex; flex-direction: column; flex: 1; } " +
  1350. ".juickEmbed > .top > .top-right > .top-right-1st { display: flex; flex-direction: row; justify-content: space-between; } " +
  1351. ".gistEmbed .gist-file .gist-data .blob-wrapper, .gistEmbed .gist-file .gist-data article { max-height: 70vh; overflow-y: auto; } " +
  1352. ".gistEmbed.embed.loaded { border-width: 0px; padding: 0; } " +
  1353. ".embedContainer > .embed.twi .cts > .placeholder { border: 0; } " +
  1354. ".embedLink:after { content: ' ↓' } " +
  1355. ".tweaksSettings * { box-sizing: border-box; } " +
  1356. ".tweaksSettings table { border-collapse: collapse; } " +
  1357. ".tweaksSettings tr { border-bottom: 1px solid transparent; } " +
  1358. ".tweaksSettings tr:hover { background: rgba(127,127,127,.1) } " +
  1359. ".tweaksSettings td > * { display: block; width: 100%; height: 100%; } " +
  1360. ".tweaksSettings > button { margin-top: 25px; } " +
  1361. ".embedContainer > .cts { margin-bottom: 0.3em; }" +
  1362. ".embedContainer .cts > .placeholder { border: 1px dotted var(--color03); color: var(--color07); text-align: center; cursor: pointer; word-wrap: break-word; } " +
  1363. ".cts > .placeholder { position: relative; } " +
  1364. ".cts > .placeholder > .icon { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 10; color: var(--bg10); -webkit-filter: drop-shadow( 0 0 10px var(--color10) ); filter: drop-shadow( 0 0 10px var(--color10) ); } " +
  1365. ".embed .cts .icon { display: flex; align-items: center; justify-content: center; } " +
  1366. ".embed .cts .icon > svg { max-width: 100px; max-height: 100px; } " +
  1367. ""
  1368. );
  1369. }