PageSourceSearch

https://www.davehone.co.uk/wp-content/plugins/bluesky-feed-for-wor…/assets/js/feed-renderer.js?ver=1.1.0

js davehone.co.uk collected 2026-10-02 10:31:07 UTC 9,290 bytes, 180 lines download raw bytes

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.