Greasy Fork 还支持 简体中文。

HN Comments Owl

Highlight new Hacker News comments, block users and other UX tweaks

目前為 2020-08-12 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name HN Comments Owl
  3. // @description Highlight new Hacker News comments, block users and other UX tweaks
  4. // @namespace https://github.com/insin/hn-comments-owl/
  5. // @match https://news.ycombinator.com/*
  6. // @version 39
  7. // ==/UserScript==
  8. const HIGHLIGHT_COLOR = '#ffffde'
  9. const TOGGLE_HIDE = '[–]'
  10. const TOGGLE_SHOW = '[+]'
  11.  
  12. let config = {
  13. addUpvotedToHeader: true,
  14. autoHighlightNew: true,
  15. hideReplyLinks: false,
  16. }
  17.  
  18. config.enableDebugLogging = false
  19.  
  20. //#region Storage
  21. class Visit {
  22. constructor({commentCount, maxCommentId, time}) {
  23. /** @type {number} */
  24. this.commentCount = commentCount
  25. /** @type {number} */
  26. this.maxCommentId = maxCommentId
  27. /** @type {Date} */
  28. this.time = time
  29. }
  30.  
  31. toJSON() {
  32. return {
  33. c: this.commentCount,
  34. m: this.maxCommentId,
  35. t: this.time.getTime(),
  36. }
  37. }
  38. }
  39.  
  40. Visit.fromJSON = function(obj) {
  41. return new Visit({
  42. commentCount: obj.c,
  43. maxCommentId: obj.m,
  44. time: new Date(obj.t),
  45. })
  46. }
  47.  
  48. function getLastVisit(itemId) {
  49. let json = localStorage.getItem(itemId)
  50. if (json == null) {
  51. return null
  52. }
  53. return Visit.fromJSON(JSON.parse(json))
  54. }
  55.  
  56. function storeVisit(itemId, visit) {
  57. log('storing visit', visit)
  58. localStorage.setItem(itemId, JSON.stringify(visit))
  59. }
  60.  
  61. function getIgnoredUsers() {
  62. return new Set(JSON.parse(localStorage.ignoredUsers || '[]'))
  63. }
  64.  
  65. function setIgnoredUsers(ignoredUsers) {
  66. localStorage.ignoredUsers = JSON.stringify(Array.from(ignoredUsers))
  67. }
  68. //#endregion
  69.  
  70. //#region Utility functions
  71. function addStyle(css = '') {
  72. let $style = document.createElement('style')
  73. if (css) {
  74. $style.textContent = css
  75. }
  76. document.querySelector('head').appendChild($style)
  77. return $style
  78. }
  79.  
  80. function checkbox(attributes, label) {
  81. return h('label', null,
  82. h('input', {
  83. style: {verticalAlign: 'middle'},
  84. type: 'checkbox',
  85. ...attributes,
  86. }),
  87. ' ',
  88. label,
  89. )
  90. }
  91.  
  92. function h(tagName, attributes, ...children) {
  93. let $el = document.createElement(tagName)
  94.  
  95. if (attributes) {
  96. for (let [prop, value] of Object.entries(attributes)) {
  97. if (prop.indexOf('on') === 0) {
  98. $el.addEventListener(prop.slice(2).toLowerCase(), value)
  99. }
  100. else if (prop.toLowerCase() == 'style') {
  101. for (let [styleProp, styleValue] of Object.entries(value)) {
  102. $el.style[styleProp] = styleValue
  103. }
  104. }
  105. else {
  106. $el[prop] = value
  107. }
  108. }
  109. }
  110.  
  111. for (let child of children) {
  112. if (child == null || child === false) {
  113. continue
  114. }
  115. if (child instanceof Node) {
  116. $el.appendChild(child)
  117. }
  118. else {
  119. $el.insertAdjacentText('beforeend', String(child))
  120. }
  121. }
  122.  
  123. return $el
  124. }
  125.  
  126. function log(...args) {
  127. if (config.enableDebugLogging) {
  128. console.log('🦉', ...args)
  129. }
  130. }
  131.  
  132. function s(count, suffixes = ',s') {
  133. if (!suffixes.includes(',')) {
  134. suffixes = `,${suffixes}`
  135. }
  136. return suffixes.split(',')[count === 1 ? 0 : 1]
  137. }
  138.  
  139. function toggleDisplay($el, hidden) {
  140. $el.classList.toggle('noshow', hidden)
  141. // We need to enforce display setting as the page's own script expands all
  142. // comments on page load.
  143. $el.style.display = hidden ? 'none' : ''
  144. }
  145.  
  146. function toggleVisibility($el, hidden) {
  147. $el.classList.toggle('nosee', hidden)
  148. // We need to enforce visibility setting as the page's own script expands
  149. // all comments on page load.
  150. $el.style.visibility = hidden ? 'hidden' : 'visible'
  151. }
  152. //#endregion
  153.  
  154. //#region Feature: add upvoted link to header
  155. function addUpvotedLinkToHeader() {
  156. if (window.location.pathname == '/upvoted') {
  157. return
  158. }
  159.  
  160. let $userLink = document.querySelector('span.pagetop a[href^="user?id"]')
  161. if (!$userLink) {
  162. return
  163. }
  164.  
  165. let $pageTop = document.querySelector('span.pagetop')
  166. $pageTop.insertAdjacentText('beforeend', ' | ')
  167. $pageTop.appendChild(h('a', {
  168. href: `/upvoted?id=${$userLink.textContent}`,
  169. }, 'upvoted'))
  170. }
  171. //#endregion
  172.  
  173. //#region Feature: new comment highlighting on comment pages
  174. /**
  175. * Each comment on a comment page has the following structure:
  176. *
  177. * ```html
  178. * <tr class="athing"> (wrapper)
  179. * <td>
  180. * <table>
  181. * <tr>
  182. * <td class="ind">
  183. * <img src="s.gif" height="1" width="123"> (indentation)
  184. * </td>
  185. * <td class="votelinks">…</td> (vote up/down controls)
  186. * <td class="default">
  187. * <div> (meta bar: user, age and folding control)
  188. * …
  189. * <div class="comment">
  190. * <span class="comtext"> (text and reply link)
  191. * ```
  192. *
  193. * We want to be able to collapse comment trees which don't contain new comments
  194. * and highlight new comments, so for each wrapper we'll create a `HNComment`
  195. * object to manage this.
  196. *
  197. * Comments are rendered as a flat list of table rows, so we'll use the width of
  198. * the indentation spacer to determine which comments are descendants of a given
  199. * comment.
  200. *
  201. * Since we have to reimplement our own comment folding, we'll hide the built-in
  202. * folding controls and create new ones in a better position (on the left), with
  203. * a larger hitbox (larger font and an en dash [–] instead of a hyphen [-]).
  204. *
  205. * On each comment page view, we store the current comment count, the max
  206. * comment id on the page and the current time as the last visit time.
  207. */
  208. function commentPage() {
  209. log('comment page')
  210.  
  211. /** @type {boolean} */
  212. let autoHighlightNew = config.autoHighlightNew || location.search.includes('?shownew')
  213.  
  214. /** @type {number} */
  215. let commentCount = 0
  216.  
  217. /** @type {HNComment[]} */
  218. let comments = []
  219.  
  220. /** @type {Object.<string, HNComment>} */
  221. let commentsById = {}
  222.  
  223. /** @type {boolean} */
  224. let hasNewComments = false
  225.  
  226. /** @type {string} */
  227. let itemId = /id=(\d+)/.exec(location.search)[1]
  228.  
  229. /** @type {Visit} */
  230. let lastVisit
  231.  
  232. /** @type {number} */
  233. let maxCommentId = -1
  234.  
  235. /** @type {number} */
  236. let newCommentCount = 0
  237.  
  238. /** @type {Set<string>} */
  239. let ignoredUsers = getIgnoredUsers()
  240.  
  241. class HNComment {
  242. /**
  243. * @param $wrapper {Element}
  244. * @param index {number}
  245. */
  246. constructor($wrapper, index) {
  247. /** @type {number} */
  248. this.indent = Number($wrapper.querySelector('img[src="s.gif"]').width)
  249.  
  250. /** @type {number} */
  251. this.index = index
  252.  
  253. let $user = $wrapper.querySelector('a.hnuser')
  254. /** @type {string} */
  255. this.user = $user ? $user.innerText : null
  256.  
  257. /** @type {Element} */
  258. this.$comment = $wrapper.querySelector('div.comment')
  259.  
  260. /** @type {Element} */
  261. this.$topBar = $wrapper.querySelector('td.default > div')
  262.  
  263. /** @type {Element} */
  264. this.$vote = $wrapper.querySelector('td[valign="top"] > center')
  265.  
  266. /** @type {Element} */
  267. this.$wrapper = $wrapper
  268.  
  269. /** @private @type {HNComment[]} */
  270. this._childComments = null
  271.  
  272. /**
  273. * The comment's id.
  274. * Will be `-1` for deleted comments.
  275. * @type {number}
  276. */
  277. this.id = -1
  278.  
  279. /**
  280. * Some flagged comments are collapsed by default.
  281. * @type {boolean}
  282. */
  283. this.isCollapsed = $wrapper.classList.contains('coll')
  284.  
  285. /**
  286. * Comments whose text has been removed but are still displayed may have
  287. * their text replaced with [flagged], [dead] or similar - we'll take any
  288. * word in square brackets as indication of this.
  289. * @type {boolean}
  290. */
  291. this.isDeleted = /^\s*\[\w+]\s*$/.test(this.$comment.firstChild.nodeValue)
  292.  
  293. /**
  294. * The displayed age of the comment; `${n} minutes/hours/days ago`, or
  295. * `on ${date}` for older comments.
  296. * Will be blank for deleted comments.
  297. * @type {string}
  298. */
  299. this.when = ''
  300.  
  301. /** @type {Element} */
  302. this.$collapsedChildCount = null
  303.  
  304. /** @type {Element} */
  305. this.$comhead = this.$topBar.querySelector('span.comhead')
  306.  
  307. /** @type {Element} */
  308. this.$toggleControl = h('span', {
  309. onclick: () => this.toggleCollapsed(),
  310. style: {cursor: 'pointer'},
  311. }, this.isCollapsed ? TOGGLE_SHOW : TOGGLE_HIDE)
  312.  
  313. if (!this.isDeleted) {
  314. let $permalink = this.$topBar.querySelector('a[href^=item]')
  315. this.id = Number($permalink.href.split('=').pop())
  316. this.when = $permalink.textContent
  317. }
  318.  
  319. this.initDOM()
  320. }
  321.  
  322. initDOM() {
  323. // We want to use the comment meta bar for the folding control, so put
  324. // it back above the deleted comment placeholder.
  325. if (this.isDeleted) {
  326. this.$topBar.style.marginBottom = '4px'
  327. }
  328. this.$topBar.insertAdjacentText('afterbegin', ' ')
  329. this.$topBar.insertAdjacentElement('afterbegin', this.$toggleControl)
  330. }
  331.  
  332. /**
  333. * @param updateChildren {boolean=}
  334. */
  335. updateDisplay(updateChildren = true) {
  336. // Show/hide this comment, preserving display of the meta bar
  337. toggleDisplay(this.$comment, this.isCollapsed)
  338. if (this.$vote) {
  339. toggleVisibility(this.$vote, this.isCollapsed)
  340. }
  341. this.$toggleControl.textContent = this.isCollapsed ? TOGGLE_SHOW : TOGGLE_HIDE
  342.  
  343. // Show/hide the number of child comments when collapsed
  344. if (this.isCollapsed && this.$collapsedChildCount == null) {
  345. let collapsedCommentCount = [
  346. this.isDeleted ? '(' : ' | (',
  347. this.childComments.length,
  348. ` child${s(this.childComments.length, 'ren')})`,
  349. ].join('')
  350. this.$collapsedChildCount = h('span', null, collapsedCommentCount)
  351. this.$comhead.appendChild(this.$collapsedChildCount)
  352. }
  353. toggleDisplay(this.$collapsedChildCount, !this.isCollapsed)
  354.  
  355. // Completely show/hide any child comments
  356. if (updateChildren) {
  357. this.childComments.forEach((child) => toggleDisplay(child.$wrapper, this.isCollapsed))
  358. }
  359. }
  360.  
  361. /**
  362. * @returns {HNComment[]}
  363. */
  364. get childComments() {
  365. if (this._childComments == null) {
  366. this._childComments = []
  367. for (let i = this.index + 1; i < comments.length; i++) {
  368. if (comments[i].indent <= this.indent) {
  369. break
  370. }
  371. this._childComments.push(comments[i])
  372. }
  373. }
  374. return this._childComments
  375. }
  376.  
  377. /**
  378. * @param commentId {number}
  379. * @returns {boolean}
  380. */
  381. hasChildCommentsNewerThan(commentId) {
  382. return this.childComments.some((comment) => comment.isNewerThan(commentId))
  383. }
  384.  
  385. /**
  386. * @param commentId {number}
  387. * @returns {boolean}
  388. */
  389. isNewerThan(commentId) {
  390. return this.id > commentId
  391. }
  392.  
  393. /**
  394. * @param isCollapsed {boolean=}
  395. */
  396. toggleCollapsed(isCollapsed = !this.isCollapsed) {
  397. this.isCollapsed = isCollapsed
  398. this.updateDisplay()
  399. }
  400.  
  401. /**
  402. * @param highlight {boolean}
  403. */
  404. toggleHighlighted(highlight) {
  405. this.$wrapper.style.backgroundColor = highlight ? HIGHLIGHT_COLOR : 'transparent'
  406. }
  407. }
  408.  
  409. /**
  410. * Adds checkboxes to toggle folding and highlighting when there are new
  411. * comments on a comment page.
  412. * @param $container {Element}
  413. */
  414. function addNewCommentControls($container) {
  415. $container.appendChild(
  416. h('div', null,
  417. h('p', null,
  418. `${newCommentCount} new comment${s(newCommentCount)} since ${lastVisit.time.toLocaleString()}`
  419. ),
  420. h('div', null,
  421. checkbox({
  422. checked: autoHighlightNew,
  423. onclick: (e) => {
  424. highlightNewComments(e.target.checked, lastVisit.maxCommentId)
  425. },
  426. }, 'highlight new comments'),
  427. ' ',
  428. checkbox({
  429. checked: autoHighlightNew,
  430. onclick: (e) => {
  431. collapseThreadsWithoutNewComments(e.target.checked, lastVisit.maxCommentId)
  432. },
  433. }, 'collapse threads without new comments'),
  434. ),
  435. )
  436. )
  437. }
  438.  
  439. /**
  440. * Adds a range control and button to show the last X new comments.
  441. */
  442. function addTimeTravelCommentControls($container) {
  443. let sortedCommentIds = comments.map((comment) => comment.id)
  444. .filter(id => id !== -1)
  445. .sort((a, b) => a - b)
  446.  
  447. let showNewCommentsAfter = Math.max(0, sortedCommentIds.length - 1)
  448. let howMany = sortedCommentIds.length - showNewCommentsAfter
  449.  
  450. function getButtonLabel() {
  451. let fromWhen = commentsById[sortedCommentIds[showNewCommentsAfter]].when
  452. // Older comments display `on ${date}` instead of a relative time
  453. if (fromWhen.startsWith(' on')) {
  454. fromWhen = fromWhen.replace(' on', 'since')
  455. }
  456. else {
  457. fromWhen = `from ${fromWhen}`
  458. }
  459. return `highlight ${howMany} comment${s(howMany)} ${fromWhen}`
  460. }
  461.  
  462. let $range = h('input', {
  463. max: sortedCommentIds.length - 1,
  464. min: 1,
  465. oninput(e) {
  466. showNewCommentsAfter = Number(e.target.value)
  467. howMany = sortedCommentIds.length - showNewCommentsAfter
  468. $button.value = getButtonLabel()
  469. },
  470. style: {margin: 0, verticalAlign: 'middle'},
  471. type: 'range',
  472. value: sortedCommentIds.length - 1,
  473. })
  474.  
  475. let $button = h('input', {
  476. onclick() {
  477. let referenceCommentId = sortedCommentIds[showNewCommentsAfter - 1]
  478. log(`manually highlighting ${howMany} comments since ${referenceCommentId}`)
  479. highlightNewComments(true, referenceCommentId)
  480. collapseThreadsWithoutNewComments(true, referenceCommentId)
  481. $timeTravelControl.remove()
  482. },
  483. type: 'button',
  484. value: getButtonLabel(),
  485. })
  486.  
  487. let $timeTravelControl = h('div', {
  488. style: {marginTop: '1em'},
  489. }, $range, ' ', $button)
  490.  
  491. $container.appendChild($timeTravelControl)
  492. }
  493.  
  494. /**
  495. * Adds the appropriate page controls depending on whether or not there are
  496. * new comments or any comments at all.
  497. */
  498. function addPageControls() {
  499. let $container = document.querySelector('td.subtext')
  500. if (!$container) {
  501. log('no container found for page controls')
  502. return
  503. }
  504.  
  505. if (hasNewComments) {
  506. addNewCommentControls($container)
  507. }
  508. else if (commentCount > 1) {
  509. addTimeTravelCommentControls($container)
  510. }
  511. }
  512.  
  513. /**
  514. * Collapses threads which don't have any comments newer than the given
  515. * comment id.
  516. * @param collapse {boolean}
  517. * @param referenceCommentId {number}
  518. */
  519. function collapseThreadsWithoutNewComments(collapse, referenceCommentId) {
  520. for (let i = 0; i < comments.length; i++) {
  521. let comment = comments[i]
  522. if (!comment.isNewerThan(referenceCommentId) &&
  523. !comment.hasChildCommentsNewerThan(referenceCommentId)) {
  524. comment.toggleCollapsed(collapse)
  525. // Skip over child comments
  526. i += comment.childComments.length
  527. }
  528. }
  529. }
  530.  
  531. function hideBuiltInCommentFoldingControls() {
  532. addStyle('a.togg { display: none; }')
  533. }
  534.  
  535. let toggleHideReplyLinks = (function() {
  536. let $style = addStyle()
  537. return () => {
  538. $style.textContent = config.hideReplyLinks ? `
  539. div.reply { margin-top: 8px; }
  540. div.reply p { display: none; }
  541. ` : ''
  542. }
  543. })()
  544.  
  545. /**
  546. * Highlights comments newer than the given comment id.
  547. * @param highlight {boolean}
  548. * @param referenceCommentId {number}
  549. */
  550. function highlightNewComments(highlight, referenceCommentId) {
  551. comments.forEach((comment) => {
  552. if (comment.isNewerThan(referenceCommentId)) {
  553. comment.toggleHighlighted(highlight)
  554. }
  555. })
  556. }
  557.  
  558. function initComments() {
  559. let commentWrappers = document.querySelectorAll('table.comment-tree tr.athing')
  560. log('number of comment wrappers', commentWrappers.length)
  561. let index = 0
  562. let lastMaxCommentId = lastVisit != null ? lastVisit.maxCommentId : -1
  563. let ignoreIndent = null
  564. for (let $wrapper of commentWrappers) {
  565. let comment = new HNComment($wrapper, index++)
  566.  
  567. // Remove comments in threads under ignored users
  568. if (ignoreIndent != null) {
  569. if (comment.indent > ignoreIndent) {
  570. toggleDisplay($wrapper, true)
  571. continue
  572. }
  573. ignoreIndent = null
  574. }
  575.  
  576. // Remove comment from ignored users
  577. if (ignoredUsers.has(comment.user)) {
  578. toggleDisplay($wrapper, true)
  579. ignoreIndent = comment.indent
  580. continue
  581. }
  582.  
  583. if (comment.id > maxCommentId) {
  584. maxCommentId = comment.id
  585. }
  586.  
  587. if (comment.isNewerThan(lastMaxCommentId)) {
  588. newCommentCount++
  589. }
  590.  
  591. comments.push(comment)
  592.  
  593. if (comment.id != -1) {
  594. commentsById[comment.id] = comment
  595. }
  596. }
  597. hasNewComments = lastVisit != null && newCommentCount > 0
  598. }
  599.  
  600. // TODO Only store visit data when the item header is present (i.e. not a comment permalink)
  601. // TODO Only store visit data for commentable items (a reply box / reply links are visible)
  602. // TODO Clear any existing stored visit if the item is no longer commentable
  603. function storePageViewData() {
  604. storeVisit(itemId, new Visit({
  605. commentCount,
  606. maxCommentId,
  607. time: new Date(),
  608. }))
  609. }
  610.  
  611. lastVisit = getLastVisit(itemId)
  612.  
  613. let $commentsLink = document.querySelector('td.subtext > a[href^=item]')
  614. if ($commentsLink && /^\d+/.test($commentsLink.textContent)) {
  615. commentCount = Number($commentsLink.textContent.split(/\s/).shift())
  616. }
  617.  
  618. hideBuiltInCommentFoldingControls()
  619. toggleHideReplyLinks()
  620. initComments()
  621. comments.filter(comment => comment.isCollapsed).forEach(comment => comment.updateDisplay(false))
  622. if (hasNewComments && autoHighlightNew) {
  623. highlightNewComments(true, lastVisit.maxCommentId)
  624. collapseThreadsWithoutNewComments(true, lastVisit.maxCommentId)
  625. }
  626. addPageControls()
  627. storePageViewData()
  628.  
  629. log('page view data', {
  630. autoHighlightNew,
  631. commentCount,
  632. hasNewComments,
  633. itemId,
  634. lastVisit,
  635. maxCommentId,
  636. newCommentCount,
  637. })
  638.  
  639. chrome.storage.onChanged.addListener((changes) => {
  640. if ('hideReplyLinks' in changes) {
  641. config.hideReplyLinks = changes['hideReplyLinks'].newValue
  642. toggleHideReplyLinks()
  643. }
  644. })
  645. }
  646. //#endregion
  647.  
  648. //#region Feature: new comment indicators on link pages
  649. /**
  650. * Each item on an item list page has the following structure:
  651. *
  652. * ```html
  653. * <tr class="athing">…</td> (rank, upvote control, title/link and domain)
  654. * <tr>
  655. * <td>…</td> (spacer)
  656. * <td class="subtext">…</td> (item meta info)
  657. * </tr>
  658. * <tr class="spacer">…</tr>
  659. * ```
  660. *
  661. * Using the comment count stored when you visit a comment page, we'll display
  662. * the number of new comments in the subtext section and provide a link which
  663. * will automatically highlight new comments and collapse comment trees without
  664. * new comments.
  665. *
  666. * For regular stories, the subtext element contains points, user, age (in
  667. * a link to the comments page), flag/hide controls and finally the number of
  668. * comments (in another link to the comments page). We'll look for the latter
  669. * to detemine the current number of comments and the item id.
  670. *
  671. * For job postings, the subtext element only contains age (in
  672. * a link to the comments page) and a hide control, so we'll try to ignore
  673. * those.
  674. */
  675. function itemListPage() {
  676. log('item list page')
  677.  
  678. let commentLinks = document.querySelectorAll('td.subtext > a[href^="item?id="]:last-child')
  679. log('number of comments/discuss links', commentLinks.length)
  680.  
  681. let noCommentsCount = 0
  682. let noLastVisitCount = 0
  683.  
  684. for (let $commentLink of commentLinks) {
  685. let id = $commentLink.href.split('=').pop()
  686.  
  687. let commentCountMatch = /^(\d+)/.exec($commentLink.textContent)
  688. if (commentCountMatch == null) {
  689. noCommentsCount++
  690. continue
  691. }
  692.  
  693. let lastVisit = getLastVisit(id)
  694. if (lastVisit == null) {
  695. noLastVisitCount++
  696. continue
  697. }
  698.  
  699. let commentCount = Number(commentCountMatch[1])
  700. if (commentCount <= lastVisit.commentCount) {
  701. log(`${id} doesn't have any new comments`, lastVisit)
  702. continue
  703. }
  704.  
  705. $commentLink.insertAdjacentElement('afterend',
  706. h('span', null,
  707. ' (',
  708. h('a', {
  709. href: `/item?shownew&id=${id}`,
  710. style: {fontWeight: 'bold'},
  711. },
  712. commentCount - lastVisit.commentCount,
  713. ' new'
  714. ),
  715. ')',
  716. )
  717. )
  718. }
  719.  
  720. if (noCommentsCount > 0) {
  721. log(`${noCommentsCount} item${s(noCommentsCount, " doesn't,s don't")} have any comments`)
  722. }
  723. if (noLastVisitCount > 0) {
  724. log(`${noLastVisitCount} item${s(noLastVisitCount, " doesn't,s don't")} have a last visit stored`)
  725. }
  726. }
  727. //#endregion
  728.  
  729. //#region Feature: block/unblock users on profile pages
  730. function userProfilePage() {
  731. log('user profile page')
  732.  
  733. let $userLink = document.querySelector('a.hnuser')
  734. if ($userLink == null) {
  735. log('not a valid user')
  736. return
  737. }
  738.  
  739. let userId = $userLink.innerText
  740. let $currentUserLink = document.querySelector('a#me')
  741. let currentUser = $currentUserLink ? $currentUserLink.innerText : ''
  742. let ignoredUsers = getIgnoredUsers()
  743. let $tbody = $userLink.closest('table').querySelector('tbody')
  744.  
  745. if (userId == currentUser) {
  746. let first = 0
  747. ignoredUsers.forEach((ignoredUserId) => {
  748. $tbody.appendChild(
  749. h('tr', null,
  750. h('td', {valign: 'top'}, first++ == 0 ? 'blocked:' : ''),
  751. h('td', null,
  752. h('a', {href: `/user?id=${ignoredUserId}`}, ignoredUserId),
  753. h('a', {
  754. href: '#',
  755. onClick: function(e) {
  756. e.preventDefault()
  757. if (ignoredUsers.has(ignoredUserId)) {
  758. ignoredUsers.delete(ignoredUserId)
  759. this.firstElementChild.innerText = 'block'
  760. }
  761. else {
  762. ignoredUsers.add(ignoredUserId)
  763. this.firstElementChild.innerText = 'unblock'
  764. }
  765. setIgnoredUsers(ignoredUsers)
  766. }
  767. },
  768. ' (', h('u', null, 'unblock'), ')'
  769. )
  770. )
  771. )
  772. )
  773. })
  774. }
  775. else {
  776. $tbody.appendChild(
  777. h('tr', null,
  778. h('td'),
  779. h('td', null,
  780. h('a', {
  781. href: '#',
  782. onClick: function(e) {
  783. e.preventDefault()
  784. if (ignoredUsers.has(userId)) {
  785. ignoredUsers.delete(userId)
  786. this.firstElementChild.innerText = 'block'
  787. }
  788. else {
  789. ignoredUsers.add(userId)
  790. this.firstElementChild.innerText = 'unblock'
  791. }
  792. setIgnoredUsers(ignoredUsers)
  793. }
  794. },
  795. h('u', null, ignoredUsers.has(userId) ? 'unblock' : 'block')
  796. )
  797. )
  798. )
  799. )
  800. }
  801. }
  802. //#endregion
  803.  
  804. //#region Main
  805. function main() {
  806. log('config', config)
  807.  
  808. if (config.addUpvotedToHeader) {
  809. addUpvotedLinkToHeader()
  810. }
  811.  
  812. let path = location.pathname.slice(1)
  813.  
  814. if (/^($|active|ask|best|front|news|newest|noobstories|show|submitted|upvoted)/.test(path)) {
  815. itemListPage()
  816. }
  817. else if (/^item/.test(path)) {
  818. commentPage()
  819. }
  820. else if (/^user/.test(path)) {
  821. userProfilePage()
  822. }
  823. }
  824.  
  825. main()
  826. //#endregion