1function renderFeed(container, settings) { 2 const { username, postCount, includePins, includeLink, theme } = settings; 3 4 if (!username) { 5 container.innerHTML = "<p>Please enter a valid username above to see the feed.</p>"; 6 return; 7 } 8 9 // Apply the selected theme 10 container.classList.remove("theme-light", "theme-dim", "theme-dark"); 11 container.classList.add(`theme-${theme}`); 12 13 container.innerHTML = "<p>Loading feed...</p>"; 14 15 axios 16 .get("https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed", { 17 params: { 18 actor: username, 19 limit: postCount, 20 filter: "posts_no_replies", 21 includePins: includePins === 1, 22 }, 23 }) 24 .then((response) => { 25 const { feed: posts } = response.data; 26 27 container.innerHTML = ""; 28 29 if (!posts || posts.length === 0) { 30 container.innerHTML = "<p>No posts found for this author.</p>"; 31 return; 32 } 33 34 posts.forEach((item) => { 35 const post = item.post; 36 37 // Skip pinned posts if includePins is false. 38 if (!includePins && post.record?.pinned) { 39 return; 40 } 41 42 const authorName = post.author.displayName || post.author.handle; 43 const authorAvatar = post.author.avatar || "https://bsky.social/default-avatar.png"; 44 const content = post.record?.text || "[No text content]"; 45 const createdAt = new Date(post.indexedAt).toLocaleString(); 46 47 const postUriSegments = post.uri.split("/"); 48 const postId = postUriSegments.pop(); 49 const handle = post.author.handle; 50 const postUrl = `https://bsky.app/profile/${handle}/post/${postId}`; 51 52 const replyCount = post.replyCount || 0; 53 const repostCount = post.repostCount || 0; 54 const likeCount = post.likeCount || 0; 55 56 // Check if the post is a repost (retweet) 57 let repostIndicator = ""; 58 if (item.reason && item.reason.$type === "app.bsky.feed.defs#reasonRepost") { 59 const originalAuthor = item.reason.by.displayName || item.reason.by.handle; 60 repostIndicator = ` 61 <div class="bluesky-repost-indicator"> 62 <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-repeat" width="18" height="18" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> 63 <path stroke="none" d="M0 0h24V24H0z" fill="none"></path> 64 <path d="M8 9l-4 -4l4 -4"></path> 65 <path d="M16 15l4 4l-4 4"></path> 66 <path d="M4 5h11a4 4 0 0 1 4 4v4"></path> 67 <path d="M20 19h-11a4 4 0 0 1 -4 -4v-4"></path> 68 </svg> 69 <span>Reposted from <strong>${originalAuthor}</strong></span> 70 </div> 71 `; 72 } 73 74 // Extract media images (Force animated GIFs to load correctly) 75 let mediaHtml = ""; 76 if (post.embed?.images) { 77 mediaHtml = `<div class="bluesky-images">`; 78 post.embed.images.forEach((image) => { 79 // Check if the image is a GIF 80 const isGif = image.fullsize.toLowerCase().includes(".gif"); 81 82 // Remove query parameters from GIF URLs 83 let fullsizeUrl = image.fullsize; 84 if (isGif) { 85 fullsizeUrl = fullsizeUrl.split("?")[0]; // Remove ?hh=333&ww=498 86 } 87 88 mediaHtml += ` 89 <img src="${isGif ? fullsizeUrl : image.thumb}" 90 alt="Bluesky Post Image" 91 class="bluesky-image"> 92 `; 93 }); 94 mediaHtml += `</div>`; 95 } 96 97 // Extract external link previews (cards) with large image 98 let linkPreviewHtml = ""; 99 if (post.embed?.external) { 100 const { uri, title, description, thumb } = post.embed.external; 101 102 // Check if the `uri` contains a `.gif`, and remove query parameters 103 const isGif = uri.toLowerCase().includes(".gif"); 104 let gifUrl = uri.split("?")[0];
104 // Remove ?hh=333&ww=498 if present 105 106 linkPreviewHtml = ` 107 <div class="bluesky-link-preview"> 108 <a href="${uri}" target="_blank" rel="noopener noreferrer" class="bluesky-link-card"> 109 ${ 110 isGif 111 ? `<img src="${gifUrl}" alt="Animated GIF" class="bluesky-gif-image">` 112 : `<img src="${thumb}" alt="Link preview image" class="bluesky-link-image-large"> 113 <div class="bluesky-link-info"> 114 <strong>${title}</strong> 115 <p>${description}</p> 116 </div>` 117 } 118 </a> 119 </div> 120 `; 121 } 122 123 // Format post content to preserve line breaks, clickable full URLs, and hashtags 124 const formattedContent = content 125 .replace(/\n/g, "<br>") // Preserve newlines as <br> 126 .replace(/((https?:\/\/[^\s]+))/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>') // Ensure full URLs remain clickable 127 .replace(/(^|\s)(#[a-zA-Z0-9_]+)/g, '$1<span class="bluesky-hashtag">$2</span>'); // Style hashtags 128 129 const postElement = document.createElement("div"); 130 postElement.className = "bg-white dark:bg-gray-900 dim:bg-gray-800 p-4 rounded-md shadow-md mb-4"; 131 132 postElement.innerHTML = ` 133 ${repostIndicator} <!-- Show repost indicator above the post --> 134 <a href="${postUrl}" target="_blank" rel="noopener noreferrer" class="bluesky-post-link"> 135 <div class="bluesky-post-header"> 136 <img src="${authorAvatar}" alt="${authorName}" class="bluesky-avatar"> 137 <span class="bluesky-author">${authorName}</span> 138 </div> 139 <div class="text-gray-700 dark:text-gray-300 dim:text-gray-400 mt-2">${formattedContent}</div> 140 ${mediaHtml} 141 ${linkPreviewHtml} 142 <div class="text-gray-500 text-sm mt-2">${createdAt}</div> 143 ${ 144 includeLink && postUrl 145 ? `</a>` 146 : "" 147 } 148 <div class="reaction-icons mt-4"> 149 <div class="reaction-item"> 150 <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-message-circle" width="20" height="20" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> 151 <path stroke="none" d="M0 0h24V24H0z" fill="none"></path> 152 <path d="M3 21v-4a9 9 0 1 1 4 4h-4"></path> 153 </svg> 154 <span>${replyCount}</span> 155 </div> 156 <div class="reaction-item"> 157 <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-heart" width="20" height="20" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> 158 <path stroke="none" d="M0 0h24V24H0z" fill="none"></path> 159 <path d="M12 20l-7 -7a4 4 0 0 1 0 -5.6a4 4 0 0 1 5.6 0l1.4 1.4l1.4 -1.4a4 4 0 0 1 5.6 0a4 4 0 0 1 0 5.6z"></path> 160 </svg> 161 <span>${likeCount}</span> 162 </div> 163 </a> 164 </div> 165 `; 166
167 container.appendChild(postElement); 168 }); 169 }) 170 .catch((error) => { 171 console.error("Error fetching feed:", error); 172 container.innerHTML = 173 "<p>Failed to fetch feed. Please check the console for details.</p>"; 174 }); 175} 176 177// Export function if needed in modern JS. 178if (typeof module !== "undefined") { 179 module.exports = { renderFeed }; 180}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.