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 const isGif = image.fullsize.toLowerCase().includes(".gif"); 80 let fullsizeUrl = image.fullsize; 81 if (isGif) { 82 fullsizeUrl = fullsizeUrl.split("?")[0]; 83 } 84 mediaHtml += ` 85 <img src="${isGif ? fullsizeUrl : image.thumb}" 86 alt="Bluesky Post Image" 87 class="bluesky-image"> 88 `; 89 }); 90 mediaHtml += `</div>`; 91 } 92 93 // Extract external link previews (cards) with large image 94 let linkPreviewHtml = ""; 95 if (post.embed?.external) { 96 const { uri, title, description, thumb } = post.embed.external; 97 const isGif = uri.toLowerCase().includes(".gif"); 98 let gifUrl = uri; // Use original uri by default 99 if (isGif) { 100 gifUrl = uri.split("?")[0]; // Remove query parameters if it's a GIF 101 } 102 103 linkPreviewHtml = `
104 <div class="bluesky-link-preview"> 105 <a href="${uri}" target="_blank" rel="noopener noreferrer" class="bluesky-link-card"> 106 ${ 107 isGif && thumb // Check if thumb exists for GIF, otherwise it might be a direct GIF link 108 ? `<img src="${gifUrl}" alt="Animated GIF" class="bluesky-gif-image">` // If it's a GIF and we want to display it directly 109 : (thumb ? `<img src="${thumb}" alt="Link preview image" class="bluesky-link-image-large">` : '') + // Standard image preview 110 `<div class="bluesky-link-info"> 111 <strong>${title}</strong> 112 <p>${description}</p> 113 </div>` 114 } 115 </a> 116 </div> 117 `; 118 } 119 120 // Format post content to preserve line breaks, clickable full URLs, and hashtags 121 const formattedContent = content 122 .replace(/\n/g, "<br>") 123 .replace(/((https?:\/\/[^\s]+))/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>') 124 .replace(/(^|\s)(#[a-zA-Z0-9_]+)/g, '$1<span class="bluesky-hashtag">$2</span>'); 125 126 const postElement = document.createElement("div"); 127 postElement.className = "bg-white dark:bg-gray-900 dim:bg-gray-800 p-4 rounded-md shadow-md mb-4"; 128 129 // Define the main body of the post (header, text, media, date) 130 const postBodyHtml = ` 131 <div class="bluesky-post-header"> 132 <img src="${authorAvatar}" alt="${authorName}" class="bluesky-avatar"> 133 <span class="bluesky-author">${authorName}</span> 134 </div> 135 <div class="text-gray-700 dark:text-gray-300 dim:text-gray-400 mt-2">${formattedContent}</div> 136 ${mediaHtml} 137 ${linkPreviewHtml} 138 <div class="text-gray-500 text-sm mt-2">${createdAt}</div> 139 `; 140 141 // Conditionally wrap the post body with a link or a div 142 let wrappedPostBody; 143 if (includeLink && postUrl) { 144 wrappedPostBody = `<a href="${postUrl}" target="_blank" rel="noopener noreferrer" class="bluesky-post-link">${postBodyHtml}</a>`; 145 } else { 146 // If not linking, wrap in a div, possibly with the same class if it carries styling 147 wrappedPostBody = `<div class="bluesky-post-link-wrapper">${postBodyHtml}</div>`; 148 } 149 150 // Define the reaction icons HTML 151 const reactionsHtml = ` 152 <div class="reaction-icons mt-4"> 153 <div class="reaction-item"> 154 <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"> 155 <path stroke="none" d="M0 0h24V24H0z" fill="none"></path> 156 <path d="M3 21v-4a9 9 0 1 1 4 4h-4"></path> 157 </svg> 158 <span>${replyCount}</span> 159 </div> 160 <div class="reaction-item"> 161 <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"> 162 <path stroke="none" d="M0 0h24V24H0z" fill="none"></path> 163 <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> 164 </svg>
165 <span>${likeCount}</span> 166 </div> 167 </div> 168 `; 169 170 // Assemble the final HTML for the post element 171 postElement.innerHTML = ` 172 ${repostIndicator} 173 ${wrappedPostBody} 174 ${reactionsHtml} 175 `; 176 177 container.appendChild(postElement); 178 }); 179 }) 180 .catch((error) => { 181 console.error("Error fetching feed:", error); 182 container.innerHTML = 183 "<p>Failed to fetch feed. Please check the console for details.</p>"; 184 }); 185} 186 187// Export function if needed in modern JS. 188if (typeof module !== "undefined") { 189 module.exports = { renderFeed }; 190}
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.