Can we have an official NodeBB-Stylish/userscript topic now?


  • Notification Spam Recipient

    @anotherusername said in Can we have an official NodeBB-Stylish/userscript topic now?:

    @Jaloopa I don't tend to have that problem. 🤷

    I'm having that problem this morning!



  • @Tsaukpaetra my userscript periodically creates an XMLHttpRequest and fetches a page. It does nothing with the page (rendering the page takes way longer than actually fetching its JSON anyway), but it's apparently enough to remind the server that I'm still here.


  • FoxDev

    Upboats and downboats:

    a[component="post/upvote"] .fa.fa-chevron-up:before {
        content: '\f077\f21a';
    }
    
    a[component="post/downvote"] .fa.fa-chevron-down:before {
        content: '\f078\f21a';
    }
    

    0_1498743066957_cd214796-916b-4e91-8d51-ebdff6c14d81-image.png



  • @raceprouk Shirley you mean \26f5: ⛵



  • @hungrier

    I did some slightly different boating:

    a[component="post/upvote"] .fa:before, a[component="post/downvote"] .fa:before {
        content: '\26f5';
    }
    
    a[component="post/downvote"] .fa {
        transform: rotate(180deg);
    }
    

    0_1498744010969_efd509e7-1418-4238-a5b6-040bb621fcc8-image.png


  • FoxDev

    @hungrier said in Can we have an official NodeBB-Stylish/userscript topic now?:

    @hungrier

    I did some slightly different boating:

    a[component="post/upvote"] .fa:before, a[component="post/downvote"] .fa:before {
        content: '\26f5';
    }
    
    a[component="post/downvote"] .fa {
        transform: rotate(180deg);
    }
    

    0_1498744010969_efd509e7-1418-4238-a5b6-040bb621fcc8-image.png

    plwase submit that as a PR for our custom CSS. that's awesome!



  • @hungrier said in Can we have an official NodeBB-Stylish/userscript topic now?:

    @raceprouk Shirley you mean \26f5: ⛵

    If it were general Unicode, sure, but this was to target Font Awesome's use of the private code area instead as the buttons already have FA as the current font family.


  • FoxDev

    @arantor That, and using raw emoji isn't consistent across platforms like FontAwesome is



  • @raceprouk The real problem is that FontAwesome doesn't have a sailboat icon (and that looks like fa_shit)



  • @hungrier it looks better than Windows 10's attempt at it.

    It looks like a fucking yellorange triangle on a green rectangle, so it is clearly meant to be a SCHOONER, not a sailboat hahaha.

    Mallrats rules.


  • Trolleybus Mechanic

    @anotherusername's updated likes topic notification killer codez:

    unsafeWindow.eval(`(${function () {
      // monkey-patch socket.onevent to process incoming events before they go to the default event handler(s)
      var real_onevent = socket.onevent.bind(socket);
      socket.onevent = function onevent(packet) {
        var eventName = packet.data[0], n = packet.data[1];
        var preventDefault = false;
    
        if (eventName == 'event:new_notification') {
          // automatically mark upvote notifications from the "Likes" thread as read
          if (/^upvote/.test(n.nid) && n.topicTitle == "The Official Likes Topic") {
            console.log('Ignoring: ' + n.nid + ', ' + n.bodyShort);
            socket.emit('notifications.markRead', n.nid, function (e) { if (e) console.log(e); });
            preventDefault = true;    // prevents NodeBB's event handlers from even seeing this event
          }
    
          // automatically downvote any posts that mention trust_level_3 (BONUS! \o/)
          if (n.bodyShort.indexOf('[[notifications:user_mentioned_group_in,') === 0 && /:trust_level_3$/.test(n.nid)) {
            console.log('Downvoting @'+'trust_level_3 mention (post ' + n.pid + (n.user ? ' by @' + n.user.username : '') +
                        ' in "' + n.bodyShort.slice(2, -2).split(', ').slice(3).join(', ') + '")');
            socket.emit('posts.downvote', {pid: n.pid, room_id: 'topic_' + n.tid});
          }
        }
    
        if (!preventDefault) real_onevent(packet);
      };
    
      // monkey-patch socket.emit to process sent/received data before it goes to the default event handler(s)
      var real_emit = socket.emit;
      socket.emit = function emit(eventName) {
        var rest = [].slice.call(arguments, 1);
        var preventDefault = false;
    
        if (eventName == 'notifications.get') {
          // when getting the notification list, filter out upvotes from the Likes topic (and mark them read if they're not)
          var i = rest.length - 1, orig_fn = rest[i];
          if (typeof orig_fn == 'function') {
            rest[i] = function (err, obj) {
              function filter(n) {
                if (/^upvote/.test(n.nid) && n.topicTitle == "The Official Likes Topic") {
                  if (!n.read) socket.emit('notifications.markRead', n.nid, e => e ? console.log(e) : 0);
                  return false;
                }
                return true;
              }
    
              if (err === null) {
                obj.unread = obj.unread.filter(filter);
                obj.read = obj.read.filter(filter);
              }
              orig_fn(err, obj);
            };
          }
        }
    
        if (!preventDefault) real_emit.apply(socket, [eventName].concat(rest));
      };
    
      // finally, run through the whole notification list and mark them unread if they're upvotes in the Likes topic
      (function loadNotifications(start, arr, i) {
        var req = new XMLHttpRequest;
        req.open('GET', '/api/notifications' + (start ? '?' + start : ''));
        req.addEventListener('load', function () {
          var obj = JSON.parse(req.responseText);
          if (obj.pagination.currentPage < obj.pagination.pageCount) {
            loadNotifications(obj.pagination.next.qs, (arr || []).concat(obj.notifications));
          } else {
            arr.forEach(function (n) {
              if (!n.read && n.topicTitle == "The Official Likes Topic" && /^upvote/.test(n.nid)) {
                console.log('Clearing notification:', n.nid, '\n', n.bodyShort);
                socket.emit('notifications.markRead', n.nid, function (e) { if (e) console.log(e); });
              }
            });
          }
        });
      })();
    }})`)();
    

    Edit: Fixed post formatting, thanks to @hungrier. Also make sure to give it @grant unsafeWindow in Tampermonkey or whatever extension you use in your browser of choice.



  • @slackerd said in Can we have an official NodeBB-Stylish/userscript topic now?:

    @anotherusername's updated likes topic notification killer codez:

    unsafeWindow.eval(`(${function () {
      // monkey-patch socket.onevent to process incoming events before they go to the default event handler(s)
      var real_onevent = socket.onevent.bind(socket);
      socket.onevent = function onevent(packet) {
        var eventName = packet.data[0], n = packet.data[1];
        var preventDefault = false;
    
        if (eventName == 'event:new_notification') {
          // automatically mark upvote notifications from the "Likes" thread as read
          if (/^upvote/.test(n.nid) && n.topicTitle == "The Official Likes Topic") {
            console.log('Ignoring: ' + n.nid + ', ' + n.bodyShort);
            socket.emit('notifications.markRead', n.nid, function (e) { if (e) console.log(e); });
            preventDefault = true;    // prevents NodeBB's event handlers from even seeing this event
          }
    
          // automatically downvote any posts that mention trust_level_3 (BONUS! \o/)
          if (n.bodyShort.indexOf('notifications:user_mentioned_group_in,') === 0 && /:trust_level_3$/.test(n.nid)) {
            console.log('Downvoting @'+'trust_level_3 mention (post ' + n.pid + (n.user ? ' by @' + n.user.username : '') +
                        ' in "' + n.bodyShort.slice(2, -2).split(', ').slice(3).join(', ') + '")');
            socket.emit('posts.downvote', {pid: n.pid, room_id: 'topic_' + n.tid});
          }
        }
    
        if (!preventDefault) real_onevent(packet);
      };
    
      // monkey-patch socket.emit to process sent/received data before it goes to the default event handler(s)
      var real_emit = socket.emit;
      socket.emit = function emit(eventName) {
        var rest = [].slice.call(arguments, 1);
        var preventDefault = false;
    
        if (eventName == 'notifications.get') {
          // when getting the notification list, filter out upvotes from the Likes topic (and mark them read if they're not)
          var i = rest.length - 1, orig_fn = rest[i];
          if (typeof orig_fn == 'function') {
            rest[i] = function (err, obj) {
              function filter(n) {
                if (/^upvote/.test(n.nid) && n.topicTitle == "The Official Likes Topic") {
                  if (!n.read) socket.emit('notifications.markRead', n.nid, e => e ? console.log(e) : 0);
                  return false;
                }
                return true;
              }
    
              if (err === null) {
                obj.unread = obj.unread.filter(filter);
                obj.read = obj.read.filter(filter);
              }
              orig_fn(err, obj);
            };
          }
        }
    
        if (!preventDefault) real_emit.apply(socket, [eventName].concat(rest));
      };
    
      // finally, run through the whole notification list and mark them unread if they're upvotes in the Likes topic
      (function loadNotifications(start, arr, i) {
        var req = new XMLHttpRequest;
        req.open('GET', '/api/notifications' + (start ? '?' + start : ''));
        req.addEventListener('load', function () {
          var obj = JSON.parse(req.responseText);
          if (obj.pagination.currentPage < obj.pagination.pageCount) {
            loadNotifications(obj.pagination.next.qs, (arr || []).concat(obj.notifications));
          } else {
            arr.forEach(function (n) {
              if (!n.read && n.topicTitle == "The Official Likes Topic" && /^upvote/.test(n.nid)) {
                console.log('Clearing notification:', n.nid, '\n', n.bodyShort);
                socket.emit('notifications.markRead', n.nid, function (e) { if (e) console.log(e); });
              }
            });
          }
        });
      })();
    }})`)();
    

    FTFY


  • Trolleybus Mechanic

    @hungrier said in Can we have an official NodeBB-Stylish/userscript topic now?:

    @slackerd said in Can we have an official NodeBB-Stylish/userscript topic now?:

    @anotherusername's updated likes topic notification killer codez:

    unsafeWindow.eval(`(${function () {
      // monkey-patch socket.onevent to process incoming events before they go to the default event handler(s)
      var real_onevent = socket.onevent.bind(socket);
      socket.onevent = function onevent(packet) {
        var eventName = packet.data[0], n = packet.data[1];
        var preventDefault = false;
    
        if (eventName == 'event:new_notification') {
          // automatically mark upvote notifications from the "Likes" thread as read
          if (/^upvote/.test(n.nid) && n.topicTitle == "The Official Likes Topic") {
            console.log('Ignoring: ' + n.nid + ', ' + n.bodyShort);
            socket.emit('notifications.markRead', n.nid, function (e) { if (e) console.log(e); });
            preventDefault = true;    // prevents NodeBB's event handlers from even seeing this event
          }
    
          // automatically downvote any posts that mention trust_level_3 (BONUS! \o/)
          if (n.bodyShort.indexOf('notifications:user_mentioned_group_in,') === 0 && /:trust_level_3$/.test(n.nid)) {
            console.log('Downvoting @'+'trust_level_3 mention (post ' + n.pid + (n.user ? ' by @' + n.user.username : '') +
                        ' in "' + n.bodyShort.slice(2, -2).split(', ').slice(3).join(', ') + '")');
            socket.emit('posts.downvote', {pid: n.pid, room_id: 'topic_' + n.tid});
          }
        }
    
        if (!preventDefault) real_onevent(packet);
      };
    
      // monkey-patch socket.emit to process sent/received data before it goes to the default event handler(s)
      var real_emit = socket.emit;
      socket.emit = function emit(eventName) {
        var rest = [].slice.call(arguments, 1);
        var preventDefault = false;
    
        if (eventName == 'notifications.get') {
          // when getting the notification list, filter out upvotes from the Likes topic (and mark them read if they're not)
          var i = rest.length - 1, orig_fn = rest[i];
          if (typeof orig_fn == 'function') {
            rest[i] = function (err, obj) {
              function filter(n) {
                if (/^upvote/.test(n.nid) && n.topicTitle == "The Official Likes Topic") {
                  if (!n.read) socket.emit('notifications.markRead', n.nid, e => e ? console.log(e) : 0);
                  return false;
                }
                return true;
              }
    
              if (err === null) {
                obj.unread = obj.unread.filter(filter);
                obj.read = obj.read.filter(filter);
              }
              orig_fn(err, obj);
            };
          }
        }
    
        if (!preventDefault) real_emit.apply(socket, [eventName].concat(rest));
      };
    
      // finally, run through the whole notification list and mark them unread if they're upvotes in the Likes topic
      (function loadNotifications(start, arr, i) {
        var req = new XMLHttpRequest;
        req.open('GET', '/api/notifications' + (start ? '?' + start : ''));
        req.addEventListener('load', function () {
          var obj = JSON.parse(req.responseText);
          if (obj.pagination.currentPage < obj.pagination.pageCount) {
            loadNotifications(obj.pagination.next.qs, (arr || []).concat(obj.notifications));
          } else {
            arr.forEach(function (n) {
              if (!n.read && n.topicTitle == "The Official Likes Topic" && /^upvote/.test(n.nid)) {
                console.log('Clearing notification:', n.nid, '\n', n.bodyShort);
                socket.emit('notifications.markRead', n.nid, function (e) { if (e) console.log(e); });
              }
            });
          }
        });
      })();
    }})`)();
    

    FTFY

    Thanks. I didn't know that nodebb could do markdown. TIL.


  • FoxDev

    @slackerd said in Can we have an official NodeBB-Stylish/userscript topic now?:

    I didn't know that nodebb could do markdown

    It can't. Then again, nothing can do Markdown: every implementation is screwed up somehow.


  • Trolleybus Mechanic

    @raceprouk said in Can we have an official NodeBB-Stylish/userscript topic now?:

    @slackerd said in Can we have an official NodeBB-Stylish/userscript topic now?:

    I didn't know that nodebb could do markdown

    It can't. Then again, nothing can do Markdown: every implementation is screwed up somehow.

    :wtf: The "```" for code blocks is markdown.


  • FoxDev

    @slackerd said in Can we have an official NodeBB-Stylish/userscript topic now?:

    @raceprouk said in Can we have an official NodeBB-Stylish/userscript topic now?:

    @slackerd said in Can we have an official NodeBB-Stylish/userscript topic now?:

    I didn't know that nodebb could do markdown

    It can't. Then again, nothing can do Markdown: every implementation is screwed up somehow.

    :wtf: The "```" for code blocks is markdown.

    :whoosh:



  • @raceprouk said in Can we have an official NodeBB-Stylish/userscript topic now?:

    @slackerd said in Can we have an official NodeBB-Stylish/userscript topic now?:

    I didn't know that nodebb could do markdown

    It can't. Then again, nothing can do Markdown: every implementation is screwed up somehow.

    At least it's not DiscoMarkHTBBMLDownCode



  • New, improved, now-with-100%-more-transitions!, keep-YouTube-embeds-playing-if-you-navigate-while-they're-playing userscript...

    // extract YouTube embeds and make them float on the right side of the window if their parent post is unloaded
    var s = document.createElement('script');
    s.src = "https://www.youtube.com/iframe_api";
    document.head.appendChild(s);
    unsafeWindow.eval(`${function onYouTubeIframeAPIReady() {
      function vote(dir, pid, tid) {
        var cb = null;
        socket.emit(`posts.${dir}vote`, {pid: pid, room_id: `topic_${tid}`}, (...a) => {
          if (a[0]) console.error(a[0]);
          if (cb) cb(...a);
        });
    
        return {then: fn => { cb = fn; }};
      }
    
      console.log('YouTubeIframeAPIReady');
    
      socket.on('event:voted', (vote) => {
        var elem = document.querySelector(`.floating-controls-below[data-pid="${vote.post.pid}"]`);
        console.log(vote, elem);
        if (elem) {
          var upvoted = elem.querySelector('.fa-chevron-up').closest('a');
          var downvoted = elem.querySelector('.fa-chevron-down').closest('a');
    
          if (vote.upvote) upvoted.classList.add('upvoted');
          else upvoted.classList.remove('upvoted');
    
          if (vote.downvote) downvoted.classList.add('downvoted');
          else downvoted.classList.remove('downvoted');
        }
      });
    
      new MutationObserver(m => {
        m.reduce((a, e) => a.concat(...e.addedNodes), []).filter(n => n.querySelector).forEach(n => {
          [n, ...n.querySelectorAll('iframe')].filter(e => e.matches('.js-lazyYT > iframe')).forEach(function (i) {
            i = i.parentElement;
            var post = i.closest('[component="post"]');
            try {
              function f(e) {
                var state = p.getAttribute('data-state-name'), orphaned = p.classList.contains('orphaned');
                if (state == "playing") {
                  player.pauseVideo();
                  setTimeout(function () {
                    player.playVideo();
                  }, 100);
                }
    
                if (state == "playing" || orphaned) {
                  e.returnValue = "Navigating will close the floating video(s).\nDo you want to continue?";
                  return e.returnValue;
                }
              }
    
              var youtube_id = i.getAttribute('data-youtube-id');
              var parameters = i.getAttribute('data-parameters');
              var height = i.getAttribute('data-height');
              var width = i.getAttribute('data-width');
    
              parameters = JSON.parse(`{${parameters.replace(/&/g, ',').replace(/(\w+)=/g, '"$1":')}}`);
              if (!('iv_load_policy' in parameters)) parameters.iv_load_policy = 3; // annotations off by default
              if (!('rel' in parameters)) parameters.rel = 0; // related videos (after video ends) off by default
    
              var elem_id = youtube_id + '_' + (new Date).getTime();
              var d = document.createElement('div');
              d.id = elem_id + '_placeholder';
              d.style.height = height + 'px';
              d.style.width = width + 'px';
              i.parentElement.replaceChild(d, i);
    
              new MutationObserver(function () {
                if (document.getElementById('content').contains(d)) {
                  var r = d.getBoundingClientRect();
                  p.style.top = r.top + document.documentElement.scrollTop + 'px';
                } else {
                  this.disconnect();
                  var s = player.getPlayerState();
                  if ((s == YT.PlayerState.PLAYING || s == YT.PlayerState.BUFFERING) && !player.isMuted()) {
                    p.classList.add('orphaned');
                    t_a.classList.remove('hidden');
                    t_b.classList.remove('hidden');
                  } else {
                    p.remove();
                    window.removeEventListener('beforeunload', f);
                  }
                }
              }).observe(document.getElementById('content'), {childList: true, subtree: true});
    
              var p = document.createElement('div'), r = d.getBoundingClientRect();
              var t_a = document.createElement('div'), t_b = document.createElement('div');
              p.classList.add('floating');
              p.style.top = r.top + document.documentElement.scrollTop + 'px';
              p.style.left = r.left + document.documentElement.scrollLeft + 'px';
              p.addEventListener('click', e => {
                // clicks on the embed are captured by it, not the iframe
                // only the 'X' button :after pseudo-element should fire this event
                if (e.target.classList.contains('orphaned')) {
                  window.removeEventListener('beforeunload', f);
                  e.target.style.opacity = '0';
                  e.target.addEventListener('transitionend', e => {
                    e.target.parentNode.removeChild(e.target);
                  });
                }
              });
    
              t_a.classList.add('floating-controls-above', 'hidden');
              var postHref = document.createElement('a');
              postHref.href = post.querySelector('.permalink').href;
              postHref.appendChild(document.createTextNode('#' + post.getAttribute('data-pid')));
              t_a.appendChild(postHref);
    
              t_b.classList.add('floating-controls-below', 'hidden');
              t_b.setAttribute('data-tid', ajaxify.data.tid);
              t_b.setAttribute('data-pid', post.getAttribute('data-pid'));
              var postUpvote = document.createElement('a');
              postUpvote.appendChild(document.createElement('i'));
              postUpvote.href = '#';
              var postDownvote = postUpvote.cloneNode(true);
              postUpvote.firstChild.classList.add('fa', 'fa-chevron-up');
              postDownvote.firstChild.classList.add('fa', 'fa-chevron-down');
              t_b.appendChild(postUpvote);
              t_b.appendChild(postDownvote);
    
              if (post.querySelector('.upvoted[component="post/upvote"]')) {
                postUpvote.classList.add('upvoted');
              } else if (post.querySelector('.downvoted[component="post/downvote"]')) {
                postDownvote.classList.add('downvoted');
              }
    
              postUpvote.addEventListener('click', () => {
                var dir = postUpvote.classList.contains('upvoted') ? 'un' : 'up';
                vote(dir, t_b.getAttribute('data-pid'), t_b.getAttribute('data-tid')).then(e => {
                  if (e) console.error(e);
                  else {
                    if (dir == 'up') postUpvote.classList.add('upvoted');
                    else postUpvote.classList.remove('upvoted');
                    postDownvote.classList.remove('downvoted');
                  }
                });
              });
              postDownvote.addEventListener('click', () => {
                var dir = postDownvote.classList.contains('downvoted') ? 'un' : 'down';
                vote(dir, t_b.getAttribute('data-pid'), t_b.getAttribute('data-tid')).then(e => {
                  if (e) console.error(e);
                  else {
                    if (dir == 'down') postDownvote.classList.add('downvoted');
                    else postDownvote.classList.remove('downvoted');
                    postUpvote.classList.remove('upvoted');
                  }
                });
              });
    
              var e = document.createElement('div');
              e.id = elem_id;
              e.style.height = height + 'px';
              e.style.width = width + 'px';
    
              p.appendChild(t_a);
              p.appendChild(e);
              p.appendChild(t_b);
    
              var c = document.getElementById('content');
              c.parentElement.insertBefore(p, c);
    
              var player = new YT.Player(elem_id, {
                height: height,
                width: width,
                videoId: youtube_id,
                playerVars: parameters,
                events: {
                  onReady: function onPlayerReady(e) {
                    try {
                      e.target.playVideo();
                    } catch (e) {
                      console.error(e);
                    }
                  },
                  onStateChange: function onPlayerStateChange(e) {
                    var n = Object.keys(YT.PlayerState).filter(t => YT.PlayerState[t] == e.data)[0];
                    p.setAttribute('data-state-name', n.toLowerCase());
                  }
                }
              });
    
              window.addEventListener('beforeunload', f);
            } catch (e) {
              console.log(e);
            }
          });
        });
      }).observe(document.getElementById('content'), {childList: true, subtree: true});
    }}`);
    
    document.head.appendChild(document.createElement('style')).innerHTML += `
    
    .floating {
        position: absolute;
        line-height: 0;
        z-index: 1;
    }
    
    .floating.orphaned {
        position: fixed;
        transition: bottom 1s, opacity 0.2s;
        box-shadow: 0 0 15px 15px white;
        left: unset !important;
        top: unset !important;
        bottom: 15px;
        right: 15px;
        opacity: 1;
    }
    
    .floating.orphaned[data-state-name="playing"] {
        z-index: 2;
    }
    
    .floating.orphaned:after {
        content: 'X';
        position: absolute;
        background: rgba(255, 255, 255, 0.5);
        box-shadow: 0 0 5px white;
        border-radius: 0.75em;
        text-align: center;
        line-height: 1.5em;
        font-weight: bold;
        cursor: pointer;
        height: 1.5em;
        width: 1.5em;
        right: -4px;
        top: -10px;
        opacity: 0;
        transition: 0.2s;
    }
    
    .floating.orphaned:hover {
        z-index: 3;
    }
    
    .floating.orphaned:hover:after {
        opacity: 1;
        top: -2px;
    }
    
    .floating.orphaned iframe {
        width: 240px !important;
        height: unset !important;
    }
    
    .floating-controls-above.hidden, .floating-controls-below.hidden {
        display: none;
    }
    
    .floating-controls-above, .floating-controls-below {
        position: absolute;
        width: 100%;
        transition: 0.2s;
    }
    
    .floating-controls-above a, .floating-controls-below a {
        color: grey;
        outline: none;
        text-decoration: none;
        font-size: small;
        border-radius: 0.5em;
        background: rgba(255,255,255,.2);
        box-shadow: 0 0 3px 3px rgba(255,255,255,.2);
        transition: 0.2s;
        opacity: 0;
    }
    
    .floating-controls-above {
        top: -4px;
    }
    
    .floating-controls-below {
        margin-top: -3px;
        text-align: right;
    }
    
    .floating.orphaned:hover .floating-controls-above {
        top: 0px;
    }
    
    .floating.orphaned:hover .floating-controls-below {
        margin-top: -7px;
    }
    
    .floating-controls-below a {
        margin-right: 6px;
    }
    
    .floating-controls-below a {
        height: 1em;
        width: 1.1em;
        text-align: center;
        display: inline-block;
    }
    
    .floating-controls-below a i.fa {
        position: relative;
        top: -1.5px;
    }
    
    .floating-controls-below a i.fa.fa-chevron-down {
        top: -1px;
    }
    
    .floating-controls-below a.upvoted,
    .floating-controls-below a.upvoted:hover {
        opacity: 0.8;
        background: #0b0;
        box-shadow: none;
        color: #ccc;
    }
    
    .floating-controls-below a.downvoted,
    .floating-controls-below a.downvoted:hover {
        opacity: 0.8;
        background: #c00;
        box-shadow: none;
        color: #ccc;
    }
    
    .floating.orphaned:hover .floating-controls-above a,
    .floating.orphaned:hover .floating-controls-below a {
        opacity: 1;
    }
    
    .floating-controls-above a:hover,
    .floating-controls-below a:hover {
        color: #444;
        background: rgba(255,255,255,.5);
        box-shadow: 0 0 3px 3px rgba(255,255,255,.5);
    }
    
    .floating.orphaned ~ .orphaned {
        bottom: 55px !important;
    }
    
    .floating.orphaned ~ .orphaned ~ .orphaned {
        bottom: 95px !important;
    }
    
    .floating.orphaned ~ .orphaned ~ .orphaned ~ .orphaned {
        bottom: 135px !important;
    }
    
    .floating.orphaned ~ .orphaned ~ .orphaned ~ .orphaned ~ .orphaned {
        bottom: 175px !important;
    }
    
    /* 5 videos should be enough for anyone, right? */
    
    `;
    

    Quick demonstration:

    tl;dw: the floating video now contains a permalink to the post that it was posted in, and upvote/downvote controls for that post.

    Known issue: the upvote/downvote controls won't update unless you're in the topic where the video was posted, or when you click on them directly. (In other words, if you're not in that topic in that tab, but you have that topic open in a different window/tab, and you upvote/downvote the post from the other window/tab, the controls on the video won't update to reflect that.)


  • ♿ (Parody)

    @anotherusername said in Can we have an official NodeBB-Stylish/userscript topic now?:

    New, improved, now-with-100%-more-transitions!, keep-YouTube-embeds-playing-if-you-navigate-while-they're-playing userscript...

    Hmmm...interesting...



  • Here's one I wrote the other day. It marks topics as unread when you've opened them since the last new post, but haven't read them all the way to the end yet.

    // ==UserScript==
    // @name         mywtf_unread_real
    // @namespace    WTDWTF
    // @version      0.4
    // @description  mark topics unread that are read but not all the way
    // @author       hungrier
    // @match        https://what.thedailywtf.com/*
    // @match        http://what.thedailywtf.com/*
    // @grant        none
    // ==/UserScript==
    
    (function() {
        'use strict';
    
        function markUnreadTopics() {
            //console.log('marking unread');
            var topicsNodeList = document.querySelectorAll('ul.topic-list li');
            var topics = Array.from(topicsNodeList);
            var incompleteTopics = topics.filter((t)=> {
                var titleUrl = t.querySelector('a[itemprop=url]').href;
                var previewUrl = t.querySelector('a.permalink').href;
                return !t.classList.contains('unread') && titleUrl !== previewUrl;
            });
    
            incompleteTopics.map((t)=>{
                t.classList.add('unread');
            });
        }
    
        $(window).on('action:topics.loaded', markUnreadTopics);
    })();
    

  • Notification Spam Recipient



  • As seen in The Official Status Thread:

    // pop up the post inside a tooltip when the mouse is hovered over a notification
    var orig_x, orig_y, orig_e, mouseX, mouseY, mouse_timeout;
    var mouse_offset_x = -10, mouse_offset_y = 15, deadzone = 25;
    var user_cache = {}, tooltip = document.createElement('div');
    tooltip.classList.add('notif-tooltip', 'topic');
    function notif_hover(e) {
      try {
        mouseX = e.pageX;
        mouseY = e.pageY;
    
        function move_tt() {
          var x = Math.min(document.body.offsetWidth - tooltip.offsetWidth - 10, mouseX + mouse_offset_x);
          var y = Math.min(window.scrollY + window.innerHeight - tooltip.offsetHeight - 10, mouseY + mouse_offset_y);
          y = Math.max(window.scrollY + 10, y);
          if (x != parseInt(tooltip.style.left, 10)) tooltip.style.left = x + 'px';
          if (y != parseInt(tooltip.style.top, 10)) tooltip.style.top = y + 'px';
        }
    
        if (e.type == 'mouseout' || e.type == 'scroll') {
          // if this is a mouseout event, check whether the mouse has left the ul element
          if (!e.target.closest || !e.target.closest('ul') || !e.target.closest('ul').contains(e.relatedTarget)) {
            clearTimeout(mouse_timeout);
            tooltip.remove();
            orig_e = undefined;
            orig_x = undefined;
            orig_y = undefined;
          }
    
        } else {
          var moved;
          if (Math.abs(orig_x - e.pageX) > deadzone || Math.abs(orig_y - e.pageY) > deadzone) moved = true;
          if (e.target.closest('li') != orig_e) moved = true;
          else if (moved && tooltip.parentNode) {
            moved = false;
            orig_x = mouseX;
            orig_y = mouseY;
            move_tt();
          }
    
          if (orig_x == undefined || moved) {
            clearTimeout(mouse_timeout);
            tooltip.remove();
    
            orig_e = e.target.closest('li');
            orig_x = e.pageX;
            orig_y = e.pageY;
    
            mouse_timeout = setTimeout(() => {
              var x = mouseX + mouse_offset_x, y = mouseY + mouse_offset_y;
              var elem = orig_e, a = elem.querySelector('a[href^="/post/"]');
    
              tooltip.innerHTML = '<i class="fa fa-fw fa-spinner fa-spin"></i>';
              move_tt();
              document.body.appendChild(tooltip);
    
              if (a) {
                var pid = a.href.split('/').pop();
                socket.emit('posts.getPost', pid, (err, post) => {
                  if (!post.deleted) {
                    var score = (post.upvotes || 0) - (post.downvotes || 0), replies = post.replies || 0, uid = post.uid;
    
                    if (user_cache[uid]) proceed();
                    else {
                      var r1 = new XMLHttpRequest;
                      r1.open('GET', '/api/uid/' + uid);
                      r1.addEventListener('load', () => {
                        if (r1.responseText) {
                          var r2 = new XMLHttpRequest;
                          r2.open('GET', '/api' + JSON.parse(r1.responseText));
                          r2.addEventListener('load', () => {
                            var {uid, username, userslug, picture, url} = JSON.parse(r2.responseText);
                            user_cache[uid] = {uid, username, userslug, picture, url};
                            proceed();
                          });
                          r2.send();
                        }
                      });
                      r1.send();
                    }
    
                    function proceed() {
                      var user = user_cache[uid];
    
                      require(['translator'], function (translator) {
                        replies = replies == 1 ? '[[topic:one_reply_to_this_post]]' : '[[topic:replies_to_this_post,' + replies + ']]';
                        translator.translate(replies).then(replies => {
                          socket.emit('plugins.composer.renderPreview', post.content, (err, html) => {
                            if (html) {
                              tooltip.innerHTML = `
                                <div class="posts">
                                  <div class="clearfix post-header"><small class="pull-left">
                                    <strong><a href="${user.url}">${user.username}</a></strong>
                                    <span class="visible-xs-inline-block visible-sm-inline-block visible-md-inline-block visible-lg-inline-block">
                                      <a class="permalink" href="/post/${pid}"><span class="timeago">${jQuery.timeago.inWords(new Date - post.timestamp)}</span></a>
                                      ${post.toPid ? `<a component="post/parent" class="btn btn-xs btn-default hidden-xs" href="/post/${post.toPid}"><i class="fa fa-reply"></i></a>` : ''}
                                    </span>
                                  </small></div>
                                  <div class="content">${html}</div>
                                  <div class="clearfix post-footer">
                                    <small class="pull-right">
                                      <span class="votes">
                                        <a href="#" component="post/upvote"><i class="fa fa-chevron-up"></i></a>
                                        <span component="post/vote-count">${score}</span>
                                        <a component="post/downvote" href="#"><i class="fa fa-chevron-down"></i></a>
                                      </span>
                                    </small>
                                    ${post.replies ? `<a component="post/reply-count" href="#" class="threaded-replies">
                                      <span class="replies-count" component="post/reply-count/text">${replies}</span>
                                    </a>` : ''}
                                  </div>
                                </div>
                              `;
                              [...tooltip.querySelectorAll('img')].forEach(img => img.addEventListener('load', move_tt));
                              move_tt();
                            }
                          });
                        });
                      });
                    }
                  }
                });
    
              } else {
                if (tooltip) tooltip.remove();
                orig_e = undefined;
                orig_x = undefined;
                orig_y = undefined;
              }
            }, 750);
          }
        }
    
      } catch (err) {
        console.error(err, e);
      }
    }
    window.addEventListener('scroll', notif_hover);
    [...document.querySelectorAll('.notifications-list, .notification-list')].forEach(l => {
      l.addEventListener('mousemove', notif_hover);
      l.addEventListener('mouseout', notif_hover);
    });
    $(window).on('action:ajaxify.contentLoaded', (e, o) => {
      if (o.tpl == 'notifications') {
        [...document.querySelectorAll('.notifications-list')].forEach(l => {
          l.addEventListener('scroll', notif_hover);
          l.addEventListener('mousemove', notif_hover);
          l.addEventListener('mouseout', notif_hover);
        });
      }
    });
    
    document.head.appendChild(document.createElement('style')).innerText = `
    /* Styles for notification tooltip popup */
    .notif-tooltip {
      pointer-events: none;
      display: inline-block;
      position: absolute;
      z-index: 999999;
      border: solid 1px gray;
      background: white;
      max-width: 750px;
      padding: 1em;
    }
    .notif-tooltip .posts {
      min-width: 520px;
    }
    .notif-tooltip .post-header {
      margin-bottom: 8px;
    }
    .notif-tooltip .content, .notif-tooltip .threaded-replies {
      margin: 0;
    }
    .notif-tooltip .votes i {
      border: solid 1px lightgray;
      background: white;
      color: lightgray;
    }
    .notif-tooltip .threaded-replies {
      border: solid 1px lightgray;
      background: white;
    }
    `;
    

    It needs to run in the page's script context, not the userscript sandbox, so you may need to wrap the whole thing in

    unsafeWindow.eval(`(${function () {
    
      //code goes here
    
    }})`)();
    

    to make it work.



  • @anotherusername I made a slight mod: background: inherit; in the style section so it matches my colours.



  • For some reason NodeBB makes it a pain in the ass to start a chat with someone. You have to find your way all the way to their profile and click the hamburger menu to even find the option.

    And yet, "Follow", which I virtually never use, is prominently available on the usercard, where I only ever click it by mistake because I think maybe it's the button to start a chat with the user. It never is, but I never remember this because I never use it. :thonking:

    I can fix this...

    (function () {
      // the addButtons function runs whenever anything in the document changes -- keep it as small as possible
      function addButtons(m) {
        // disconnect to avoid generating another mutation event for DOM changes made inside this function
        observer.disconnect();
    
        // add begin chat option to usercards
        [].slice.call(document.querySelectorAll('.usercard-body')).forEach(function (e) {
          try {
            var uid = e.closest('[component="post"]').getAttribute('data-uid');
    
            if (!e.querySelector('.usercard-beginchat') && e.querySelector('.plus')) {
              var s = document.createElement('span');
              s.classList.add('usercard-beginchat');
    
              var i = document.createElement('i');
              i.setAttribute('component', 'chat/icon');
              i.classList.add('fa', 'fa-comment-o', 'fa-fw');
    
              s.appendChild(i);
              e.appendChild(s);
    
              s.addEventListener('click', function (c) {
                socket.emit('modules.chats.hasPrivateChat', uid, function (e, cid) {
                  if (e) app.alertError(e.message);
                  else if (cid) app.openChat(cid);
                  else app.newChat(uid);
                });
              });
            }
    
          } catch (e) {
            console.error('addButtons .usercard-body forEach:', e);
          }
        });
    
        // reconnect to continue observing for DOM changes made outside this function
        observer.connect();
      }
    
      var observer = new MutationObserver(addButtons);
      observer.connect = function (e) {
        observer.observe(document.body, {childList: true, subtree: true});
      };
      addButtons();
    
      var stylesheet =  document.head.appendChild(document.createElement('style'));
      stylesheet.innerHTML = '.usercard-beginchat{cursor:pointer;float:right;font-size:xx-large;padding-top:6pt;position:relative;text-align:center;top:-100%}html:not([data-disable-tdwtf-css]) .persona-usercard .usercard-name{padding-right:42px}';
    })();
    

    Result: usercards now look like this.

    0_1540874837913_Untitled.png

    Unfortunately, as far as I can tell, there's not an event being triggered to indicate that a usercard has been rendered, and I don't know the code path well enough to try to inject this in when it's generated, so I ended up using a MutationObserver that runs on every DOM change in the page. It typically doesn't do anything, and I don't think it's really a significant performance issue (and it's not the first time I've done this, either -- there's also other stuff inside my addButtons function), but still, if you know of a better way to tell when a usercard is added, let me know.


  • Discourse touched me in a no-no place

    @Lorne-Kates said in Can we have an official NodeBB-Stylish/userscript topic now?:

    And speaking of quotes, fuck you NodeBB for assuming I don't want to see nested quotes. The last thing I need is more information hidden behind annoying UX I don't want.

    /* N-level deep quotes show */
    .topic .posts .content > blockquote > blockquote > *:not(.blockquote)
    {
        display:block !important;
    }
    
    .topic .posts .content blockquote i.fa.pointer.toggle
    {
        display:none !important;
    }
    

    http://i.imgur.com/OR5Kwr0.png

    👍🏻



  • Fix Reddit embeds for dark themes:

    // ==UserScript==
    // @name         Fix WTDWTF Reddit Embeds for Dark Themes
    // @namespace    https://github.com/Choonster
    // @version      0.1
    // @description  Fix Reddit embeds on WTDWTF for dark themes
    // @author       Choonster
    // @match        https://what.thedailywtf.com/*
    // @grant        none
    // ==/UserScript==
    
    (function() {
        'use strict';
    
        embedly('on', 'card.rendered', function(iframe) {
            const innerDocument = iframe.contentDocument;
    
            const style = innerDocument.createElement('style');
            style.type = 'text/css';
            innerDocument.head.append(style);
    
            style.sheet.insertRule(".card.reddit { background-color: white !important; }");
        });
    })();
    

    This is a userscript rather than custom CSS because embeds are in iframes.

    Before:

    0c614941-44b3-4ce0-8539-9115370d815f-image.png

    After:

    3188d049-35a4-4292-96cf-c3344a1d43a3-image.png



  • Does 👃👶 have any support for wikis? I think rather than a thread where things are impossible to find and locked from editing after a while, that would be better for userscripts and CSS


  • ♿ (Parody)

    @hungrier looks like this might work for us:

    This plugin exposes a new category-level privilege called "Knowledge Base". When enabled for a specific category, all top level posts in that category will become editable by other users.



  • @boomzilla So maybe a forum customization category? As long as the posts can be edited indefinitely, that should be pretty good


  • Discourse touched me in a no-no place

    @hungrier said in Can we have an official NodeBB-Stylish/userscript topic now?:

    @boomzilla So maybe a forum customization category? As long as the posts can be edited indefinitely, that should be pretty good

    Maybe a generic Wiki/KB category? Then other formerly editable stuff like the Discopaedia topic could be included, plus anything else that may or may not be useful



  • @boomzilla said in Can we have an official NodeBB-Stylish/userscript topic now?:

    When enabled for a specific category, all top level posts in that category will become editable by other users.

    What could possibly go wrong in a place like this?


  • Discourse touched me in a no-no place

    @Mason_Wheeler said in Can we have an official NodeBB-Stylish/userscript topic now?:

    What could possibly go wrong in a place like this?

    We had Wiki's on Discohorse, I don't recall them going particularly wrong.


  • :belt_onion:

    @loopback0 said in Can we have an official NodeBB-Stylish/userscript topic now?:

    We had Wiki's on Discohorse, I don't recall them going particularly wrong.

    My favorite thread was the Official Wiki Thread


Log in to reply