this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: Flickr thumbnails now a link

+182 -8
+34 -1
internal/service/content.go
··· 114 114 } 115 115 } 116 116 117 + // Flickr 118 + isFlickr := false 119 + if strings.Contains(item.URL, "flickr.com") { 120 + // Attempt to extract photo ID from URL 121 + // Example: http://farm3.staticflickr.com/2362/2362225867_0a3b0b7e05.jpg 122 + // ID is usually the first part of the filename: 2362225867 123 + re := regexp.MustCompile(`\/([0-9]+)_[0-9a-z]+`) 124 + matches := re.FindStringSubmatch(item.URL) 125 + if len(matches) > 1 { 126 + photoID := matches[1] 127 + photoPage := fmt.Sprintf("https://www.flickr.com/photo.gne?id=%s", photoID) 128 + // Use standard image tag but linked to photo page 129 + embed := fmt.Sprintf(`<a href="%s"><img src="%s" alt="%s" /></a>`, photoPage, item.URL, item.Title) 130 + d.Content = template.HTML(embed) 131 + isFlickr = true 132 + } 133 + } 134 + 117 135 // YouTube logic removed: Handled client-side by OGPreview for "click to play" behavior 118 136 // and to correctly handle unavailable videos (404s). 119 137 120 - if !isYoutube && !isTwitter && !isImgur { 138 + if !isYoutube && !isTwitter && !isImgur && !isFlickr { 121 139 baseURL := s.Config.BaseURL 122 140 content := fmt.Sprintf(`<a href="http://%s/irclink/?%d">%s</a>`, baseURL, item.ID, linkFiller) 123 141 d.Content = template.HTML(content) ··· 136 154 BaseURL: s.Config.BaseURL, 137 155 } 138 156 s.formatDate(&d) 157 + 158 + // Flickr Logic for Images 159 + if strings.Contains(item.URL, "flickr.com") { 160 + // Attempt to extract photo ID from URL 161 + re := regexp.MustCompile(`\/([0-9]+)_[0-9a-z]+`) 162 + matches := re.FindStringSubmatch(item.URL) 163 + if len(matches) > 1 { 164 + photoID := matches[1] 165 + photoPage := fmt.Sprintf("https://www.flickr.com/photo.gne?id=%s", photoID) 166 + // Linked Thumbnail 167 + d.Content = template.HTML(fmt.Sprintf(`<a href="%s"><img src="%s" alt="image" /></a>`, photoPage, item.URL)) 168 + return d 169 + } 170 + } 171 + 139 172 d.Content = template.HTML(fmt.Sprintf(`<img src="%s" alt="image" />`, item.URL)) 140 173 return d 141 174 }
+130
internal/service/content_test.go
··· 1 + package service 2 + 3 + import ( 4 + "strings" 5 + "testing" 6 + "time" 7 + 8 + "tumble/internal/config" 9 + "tumble/internal/data" 10 + ) 11 + 12 + func TestProcessIRCLink_Flickr(t *testing.T) { 13 + cfg := &config.Config{BaseURL: "tumble.test"} 14 + svc := NewContentService(cfg) 15 + 16 + tests := []struct { 17 + name string 18 + item data.IRCLink 19 + wantURL string 20 + wantType string // "flickr" or "default" (internal link) 21 + }{ 22 + { 23 + name: "Flickr Static URL", 24 + item: data.IRCLink{ 25 + ID: 1, 26 + Title: "A Photo", 27 + URL: "http://farm3.staticflickr.com/2362/2362225867_0a3b0b7e05.jpg", 28 + ContentType: "image/jpeg", 29 + User: "photog", 30 + Timestamp: time.Now(), 31 + }, 32 + wantURL: "https://www.flickr.com/photo.gne?id=2362225867", 33 + wantType: "flickr", 34 + }, 35 + { 36 + name: "Normal Image", 37 + item: data.IRCLink{ 38 + ID: 2, 39 + Title: "Just an Image", 40 + URL: "http://example.com/image.jpg", 41 + ContentType: "image/jpeg", 42 + User: "user", 43 + Timestamp: time.Now(), 44 + }, 45 + wantURL: "http://tumble.test/irclink/?2", 46 + wantType: "default", 47 + }, 48 + } 49 + 50 + for _, tt := range tests { 51 + t.Run(tt.name, func(t *testing.T) { 52 + got := svc.ProcessIRCLink(tt.item) 53 + html := string(got.Content) 54 + 55 + if tt.wantType == "flickr" { 56 + if !strings.Contains(html, tt.wantURL) { 57 + t.Errorf("ProcessIRCLink() html = %v, want to contain %v", html, tt.wantURL) 58 + } 59 + // Verify it's an anchor tag 60 + if !strings.HasPrefix(html, "<a href=") { 61 + t.Errorf("ProcessIRCLink() html should start with anchor tag, got %v", html) 62 + } 63 + } else { 64 + if !strings.Contains(html, tt.wantURL) { 65 + t.Errorf("ProcessIRCLink() html = %v, want to contain %v", html, tt.wantURL) 66 + } 67 + } 68 + }) 69 + } 70 + } 71 + 72 + func TestProcessImage_Flickr(t *testing.T) { 73 + cfg := &config.Config{BaseURL: "tumble.test"} 74 + svc := NewContentService(cfg) 75 + 76 + tests := []struct { 77 + name string 78 + item data.Image 79 + wantURL string 80 + wantType string // "flickr" or "default" 81 + }{ 82 + { 83 + name: "Flickr Static URL", 84 + item: data.Image{ 85 + ID: 1, 86 + Title: "A Photo", 87 + URL: "http://farm3.staticflickr.com/2362/2362225867_0a3b0b7e05.jpg", 88 + Timestamp: time.Now(), 89 + }, 90 + wantURL: "https://www.flickr.com/photo.gne?id=2362225867", 91 + wantType: "flickr", 92 + }, 93 + { 94 + name: "Normal Image", 95 + item: data.Image{ 96 + ID: 2, 97 + Title: "Just an Image", 98 + URL: "http://example.com/image.jpg", 99 + Timestamp: time.Now(), 100 + }, 101 + wantURL: "http://example.com/image.jpg", 102 + wantType: "default", 103 + }, 104 + } 105 + 106 + for _, tt := range tests { 107 + t.Run(tt.name, func(t *testing.T) { 108 + got := svc.ProcessImage(tt.item) 109 + html := string(got.Content) 110 + 111 + if tt.wantType == "flickr" { 112 + if !strings.Contains(html, tt.wantURL) { 113 + t.Errorf("ProcessImage() html = %v, want to contain %v", html, tt.wantURL) 114 + } 115 + // Verify it's an anchor tag 116 + if !strings.HasPrefix(html, "<a href=") { 117 + t.Errorf("ProcessImage() html should start with anchor tag, got %v", html) 118 + } 119 + } else { 120 + if !strings.Contains(html, tt.wantURL) { 121 + t.Errorf("ProcessImage() html = %v, want to contain %v", html, tt.wantURL) 122 + } 123 + // Verify default is just img tag 124 + if !strings.HasPrefix(html, "<img src=") { 125 + t.Errorf("ProcessImage() html should start with img tag, got %v", html) 126 + } 127 + } 128 + }) 129 + } 130 + }
+18 -7
internal/templates/views/index.html
··· 43 43 function loadOGPreview(item) { 44 44 var url = item.getAttribute("data-url"); 45 45 var contentType = item.getAttribute("data-content-type"); 46 + var ircId = item.getAttribute("data-irc-link-id"); 46 47 var previewDiv = item.querySelector(".og-preview"); 47 48 48 49 // Only load preview for non-image content (text/html) ··· 66 67 if (xhr.status === 200) { 67 68 try { 68 69 var data = JSON.parse(xhr.responseText); 69 - handlePreviewData(data); 70 + handlePreviewData(data, ircId); 70 71 } catch (e) { 71 72 // Silently fail or remove 72 73 previewDiv.remove(); ··· 78 79 }; 79 80 xhr.send(); 80 81 81 - function handlePreviewData(data) { 82 + function handlePreviewData(data, ircId) { 82 83 // Handle Errors 83 84 if (data.error || (data.status && data.status >= 400)) { 84 85 var status = data.status || 404; ··· 106 107 if (oembedData.title) data.title = oembedData.title; 107 108 if (oembedData.provider_name) data.provider_name = oembedData.provider_name; 108 109 data.type = "video"; 109 - renderPreview(data); 110 + renderPreview(data, ircId); 110 111 }) 111 112 .catch(function(e) { 112 113 console.log("Fallback failed", e); 113 - renderPreview(data); 114 + renderPreview(data, ircId); 114 115 }); 115 116 return; 116 117 } 117 118 118 - renderPreview(data); 119 + renderPreview(data, ircId); 119 120 } 120 121 121 - function renderPreview(data) { 122 + function renderPreview(data, ircId) { 122 123 // Use Open Graph image, Twitter image, or nothing 123 124 var imageUrl = data.image || data.twitter_image || null; 124 125 var title = data.title || data.twitter_title || null; ··· 161 162 embedHtmlAttr = ' data-embed-html="' + encodeURIComponent(data.embed_html) + '"'; 162 163 } 163 164 165 + var innerContent = imageContent; 166 + // If we have an IRCLink ID, the image is not a video/rich embed (so no click-to-play), 167 + // and we want it clickable (e.g. Flickr), wrap in anchor. 168 + // We restrict this to Flickr specifically per user request, or generally? 169 + // User "The thumbnail image still isn't clickable... for links from Flickr". 170 + // Let's do it if ircId exists and not video. 171 + if (ircId && !data.embed_html && (data.type === 'photo' || provider === 'Flickr')) { 172 + innerContent = '<a href="/irclink/?id=' + ircId + '" target="_blank">' + imageContent + '</a>'; 173 + } 174 + 164 175 previewHTML += 165 176 '<div class="og-image ' + 166 177 (data.type === "video" || data.type === "rich" ? "is-video" : "") + 167 178 '" ' + onClickAttr + embedHtmlAttr + '>' + 168 - imageContent + 179 + innerContent + 169 180 "</div>"; 170 181 } 171 182