Lobsters Latest Comments

A bookmarklet that adds a "Latest" tab to Lobste.rs comment threads, showing all comments in a flat chronological view with newest first.

Features:

Install the Bookmarklet

Drag this button to your bookmarks bar:

Lobsters Latest

Installation Instructions

Chrome / Firefox / Desktop Safari

  1. Make sure your bookmarks bar is visible
    • Chrome: Press Cmd+Shift+B (Mac) or Ctrl+Shift+B (Windows)
    • Firefox: Press Cmd+B (Mac) or Ctrl+B (Windows)
    • Safari: View → Show Favorites Bar
  2. Drag the red "Lobsters Latest" button above to your bookmarks bar
  3. Navigate to any Lobste.rs comment thread
  4. Click the bookmarklet to activate

Mobile Safari (iPhone/iPad)

  1. Copy the bookmarklet code using the button below:
  1. Create a new bookmark:
    • Go to any webpage and tap the Share button
    • Tap "Add Bookmark"
    • Name it "Lobsters Latest" and save
  2. Edit the bookmark:
    • Open your Bookmarks (tap the book icon)
    • Tap "Edit" at the bottom
    • Find and tap "Lobsters Latest"
    • Delete the URL and paste the copied code
    • Tap "Done"
  3. To use: Navigate to a Lobste.rs thread, tap the bookmarks icon, and select "Lobsters Latest"
Note: This bookmarklet only works on Lobste.rs comment thread pages (URLs like lobste.rs/s/...). It won't do anything on the homepage or other pages.

How It Works

The bookmarklet:

  1. Extracts all comments from the page, including their timestamps and parent-child relationships
  2. Adds two tabs below the comment count header
  3. When "Latest" is selected, displays comments in a flat list sorted by newest first
  4. Adds "reply to @username" links for comments that are replies
  5. Clicking the timestamp link (e.g., "14 hours ago") switches back to Default view and scrolls to that comment in the tree, with a brief yellow highlight

Source Code

The readable, unminified source code:

Click to expand source code
(function() {
  // Don't run twice
  if (document.querySelector('#comment-view-tabs')) return;

  const commentsLabel = document.querySelector('.comments_label');
  const commentsContainer = document.querySelector('ol.comments');

  if (!commentsLabel || !commentsContainer) {
    alert('This bookmarklet only works on Lobste.rs comment pages');
    return;
  }

  // Store original HTML
  const originalCommentsHTML = commentsContainer.innerHTML;

  // Helper to find author name
  function getAuthor(element) {
    const links = element.querySelectorAll('a[href^="/~"]');
    for (const link of links) {
      const text = link.textContent?.trim();
      if (text) return text;
    }
    return null;
  }

  // Extract all comments with their data
  function extractComments() {
    const comments = [];
    document.querySelectorAll('.comments_subtree').forEach(subtree => {
      const comment = subtree.querySelector(':scope > .comment[id^="c_"]');
      if (!comment) return;

      const timeEl = comment.querySelector('time');
      const parentSubtree = subtree.parentElement?.closest('.comments_subtree');
      const parentComment = parentSubtree?.querySelector(':scope > .comment[id^="c_"]');

      comments.push({
        id: comment.id,
        element: comment.cloneNode(true),
        author: getAuthor(comment),
        timestamp: parseInt(timeEl?.getAttribute('data-at-unix') || '0'),
        parentId: parentComment?.id || null,
        parentAuthor: parentComment ? getAuthor(parentComment) : null
      });
    });
    return comments;
  }

  // Create tabs
  const tabsContainer = document.createElement('div');
  tabsContainer.id = 'comment-view-tabs';
  tabsContainer.innerHTML = `
    <style>
      #comment-view-tabs { margin: 10px 0 }
      #comment-view-tabs .tab-buttons { display: flex; gap: 0 }
      #comment-view-tabs .tab-btn {
        padding: 8px 16px;
        border: 1px solid #ac0000;
        background: white;
        cursor: pointer;
        font-size: 14px;
        color: #ac0000;
      }
      #comment-view-tabs .tab-btn:first-child { border-radius: 4px 0 0 4px }
      #comment-view-tabs .tab-btn:last-child {
        border-radius: 0 4px 4px 0;
        border-left: none
      }
      #comment-view-tabs .tab-btn.active { background: #ac0000; color: white }
      #comment-view-tabs .tab-btn:hover:not(.active) { background: #f0f0f0 }
      .flat-comment {
        margin: 0 0 15px 0 !important;
        padding: 10px !important;
        border-left: 3px solid #ddd !important;
      }
      .reply-to-link { font-size: 12px; color: #666; margin-left: 10px }
      .reply-to-link a { color: #ac0000; text-decoration: none }
      .reply-to-link a:hover { text-decoration: underline }
    </style>
    <div class="tab-buttons">
      <button class="tab-btn active" data-view="default">Default</button>
      <button class="tab-btn" data-view="latest">Latest</button>
    </div>
  `;

  // Insert tabs
  const byline = commentsLabel.closest('.byline');
  byline.parentNode.insertBefore(tabsContainer, byline.nextSibling);

  // Switch to default view and optionally scroll to a comment
  function switchToDefault(scrollToId) {
    document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
    document.querySelector('.tab-btn[data-view="default"]').classList.add('active');
    commentsContainer.innerHTML = originalCommentsHTML;

    if (scrollToId) {
      setTimeout(() => {
        const el = document.getElementById(scrollToId);
        if (el) {
          el.scrollIntoView({ behavior: 'smooth', block: 'center' });
          el.style.transition = 'background 0.3s';
          el.style.background = '#ffffd0';
          setTimeout(() => el.style.background = '', 2000);
        }
      }, 100);
    }
  }

  // Build flat view
  function buildFlatView() {
    const comments = extractComments();
    comments.sort((a, b) => b.timestamp - a.timestamp);

    const flatContainer = document.createElement('div');

    comments.forEach(c => {
      const wrapper = document.createElement('div');
      wrapper.className = 'flat-comment-wrapper';

      const commentEl = c.element;
      commentEl.classList.add('flat-comment');
      commentEl.style.marginLeft = '0';

      // Add reply-to link
      if (c.parentId && c.parentAuthor) {
        const byline = commentEl.querySelector('.byline');
        if (byline) {
          const replySpan = document.createElement('span');
          replySpan.className = 'reply-to-link';
          replySpan.innerHTML = ` ↩ reply to <a href="#${c.parentId}">@${c.parentAuthor}</a>`;
          byline.appendChild(replySpan);
        }
      }

      // Add click handler for time link
      const timeLink = commentEl.querySelector('a[href^="/c/"]');
      if (timeLink) {
        const commentId = c.id;
        timeLink.addEventListener('click', function(e) {
          e.preventDefault();
          switchToDefault(commentId);
        });
      }

      wrapper.appendChild(commentEl);
      flatContainer.appendChild(wrapper);
    });

    return flatContainer;
  }

  let flatViewCache = null;

  // Tab switching
  const tabButtons = tabsContainer.querySelectorAll('.tab-btn');
  tabButtons.forEach(btn => {
    btn.addEventListener('click', () => {
      tabButtons.forEach(b => b.classList.remove('active'));
      btn.classList.add('active');

      const view = btn.dataset.view;

      if (view === 'default') {
        commentsContainer.innerHTML = originalCommentsHTML;
      } else if (view === 'latest') {
        if (!flatViewCache) {
          flatViewCache = buildFlatView();
        }
        commentsContainer.innerHTML = '';
        commentsContainer.appendChild(flatViewCache.cloneNode(true));

        // Re-attach click handlers after cloning
        commentsContainer.querySelectorAll('a[href^="/c/"]').forEach(link => {
          const wrapper = link.closest('.flat-comment-wrapper');
          const commentEl = wrapper?.querySelector('.comment');
          const commentId = commentEl?.id;
          if (commentId) {
            link.addEventListener('click', function(e) {
              e.preventDefault();
              switchToDefault(commentId);
            });
          }
        });
      }
    });
  });
})();