From 834bd504eb5b0fa6995319adbd6662a067b3bcdb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Feb 2026 08:50:05 +0000 Subject: [PATCH 1/3] Add Copy Thread feature to lobsters bookmarklet, extract JS to separate file - Add "Copy Thread" button that exports the comment thread as numbered plain text (e.g., [1.2.3] author: text) to the clipboard, matching the format used by hacker-news-thread-export - Extract bookmarklet source code to lobsters-bookmarklet.js for easier editing - the HTML page now fetches and minifies it dynamically - Source code display in the details section is now loaded from the JS file https://claude.ai/code/session_01918zZQKJt6dazijwFTnMhN --- lobsters-bookmarklet.html | 222 +++++----------------------------- lobsters-bookmarklet.js | 243 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 273 insertions(+), 192 deletions(-) create mode 100644 lobsters-bookmarklet.js diff --git a/lobsters-bookmarklet.html b/lobsters-bookmarklet.html index af51a0c..5a7feef 100644 --- a/lobsters-bookmarklet.html +++ b/lobsters-bookmarklet.html @@ -131,6 +131,7 @@

Lobsters Latest Comments

  • Latest tab: Flat view sorted by time (newest first, oldest at bottom)
  • Reply links: Each reply shows "reply to @username" linking to the parent comment
  • Time link navigation: Clicking a comment's timestamp in Latest view switches back to Default and scrolls to that comment in the tree
  • +
  • Copy Thread: Export the entire comment thread as numbered plain text to the clipboard
  • @@ -138,7 +139,7 @@

    Install the Bookmarklet

    Drag this button to your bookmarks bar:

    - Lobsters Latest + Lobsters Latest

    Installation Instructions

    @@ -198,6 +199,7 @@

    How It Works

  • When "Latest" is selected, displays comments in a flat list sorted by newest first
  • Adds "reply to @username" links for comments that are replies
  • 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
  • +
  • The "Copy Thread" button exports the entire comment tree as numbered plain text (e.g., [1.2.3] author: text) and copies it to the clipboard
  • Source Code

    @@ -206,193 +208,7 @@

    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);
    -            });
    -          }
    -        });
    -      }
    -    });
    -  });
    -})();
    +
    Loading...