fix(embed): restore youtube thumbnails and file-row labels (#1148)

* fix(comment-content): render reason text as comment content

* fix(post-form): use native browser styling for flash tag select

Exclude the flash tag dropdown from themed post-form select styling so it
renders with the browser's default select appearance, matching the flag selector.

* fix(p2p-stats): show peer flags for DNS6 relay hostnames

Extract embedded IPv6 addresses from dns6 multiaddrs so geo lookup and country flags work for relay peers that publish IPv6 via DNS hostnames.

* fix(flags): hide geolocation-only selectors on /int/ and /sp/

Country-only boards auto-publish geographic location flags without
showing a flag dropdown, matching existing /bant/ behavior.

* fix(embed): restore youtube thumbnails and file-row labels

Restore thumbnail-first previews for YouTube embeds in post media and markdown hover.
Desktop posts show File with the thumbnail image URL and a youtube video type label.

* fix(embed): address youtube thumbnail review feedback

Translate the youtube video label, handle mobile/music YouTube hosts as standard YouTube URLs, keep affected mocks current, and cap default Vitest workers to reduce local CPU spikes.
This commit is contained in:
Tommaso Casaburi
2026-06-01 12:57:17 +07:00
committed by GitHub
parent d48cf10ee8
commit 829b672053
57 changed files with 482 additions and 127 deletions
+1 -1
View File
@@ -71,7 +71,7 @@
"build:fdroid": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=false VITE_APP_DISTRIBUTION=fdroid vite build", "build:fdroid": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=false VITE_APP_DISTRIBUTION=fdroid vite build",
"build:preload": "vite build --config electron/vite.preload.config.js", "build:preload": "vite build --config electron/vite.preload.config.js",
"build-vercel": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" PUBLIC_URL=./ GENERATE_SOURCEMAP=true VITE_COMMIT_REF=$COMMIT_REF CI='' vite build", "build-vercel": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" PUBLIC_URL=./ GENERATE_SOURCEMAP=true VITE_COMMIT_REF=$COMMIT_REF CI='' vite build",
"test": "vitest", "test": "vitest --maxWorkers=4",
"test:leaks": "vitest run --detectAsyncLeaks", "test:leaks": "vitest run --detectAsyncLeaks",
"preview": "vite preview", "preview": "vite preview",
"analyze-bundle": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=true vite build && npx source-map-explorer 'dist/assets/*.js'", "analyze-bundle": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=true vite build && npx source-map-explorer 'dist/assets/*.js'",
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "حظر", "ban": "حظر",
"timestamp": "الطابع الزمني", "timestamp": "الطابع الزمني",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "فيديو YouTube"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "নিষেধ করুন", "ban": "নিষেধ করুন",
"timestamp": "টাইমস্ট্যাম্প", "timestamp": "টাইমস্ট্যাম্প",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "YouTube ভিডিও"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "zakázat", "ban": "zakázat",
"timestamp": "časové razítko", "timestamp": "časové razítko",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "video YouTube"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "forbyd", "ban": "forbyd",
"timestamp": "tidsstempel", "timestamp": "tidsstempel",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "YouTube-video"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "sperren", "ban": "sperren",
"timestamp": "Zeitstempel", "timestamp": "Zeitstempel",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "YouTube-Video"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "απαγόρευση", "ban": "απαγόρευση",
"timestamp": "χρονοσήμανση", "timestamp": "χρονοσήμανση",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "βίντεο YouTube"
} }
+2 -1
View File
@@ -418,5 +418,6 @@
"ban": "ban", "ban": "ban",
"timestamp": "timestamp", "timestamp": "timestamp",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "YouTube video"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "prohibir", "ban": "prohibir",
"timestamp": "marca de tiempo", "timestamp": "marca de tiempo",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "video de YouTube"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "مسدود کردن", "ban": "مسدود کردن",
"timestamp": "مهر زمان", "timestamp": "مهر زمان",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "ویدیوی YouTube"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "kielto", "ban": "kielto",
"timestamp": "aikaleimaustiedosto", "timestamp": "aikaleimaustiedosto",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "YouTube-video"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "ipagbawal", "ban": "ipagbawal",
"timestamp": "timestamp", "timestamp": "timestamp",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "video sa YouTube"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "interdire", "ban": "interdire",
"timestamp": "horodatage", "timestamp": "horodatage",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "vidéo YouTube"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "חסימה", "ban": "חסימה",
"timestamp": "חותמת זמן", "timestamp": "חותמת זמן",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "סרטון YouTube"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "प्रतिबंध", "ban": "प्रतिबंध",
"timestamp": "टाइमस्टैंप", "timestamp": "टाइमस्टैंप",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "YouTube वीडियो"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "tiltás", "ban": "tiltás",
"timestamp": "időbélyeg", "timestamp": "időbélyeg",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "YouTube-videó"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "larangan", "ban": "larangan",
"timestamp": "stempel waktu", "timestamp": "stempel waktu",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "video YouTube"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "vietare", "ban": "vietare",
"timestamp": "marca temporale", "timestamp": "marca temporale",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "video YouTube"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "ブロック", "ban": "ブロック",
"timestamp": "タイムスタンプ", "timestamp": "タイムスタンプ",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "YouTube動画"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "차단", "ban": "차단",
"timestamp": "타임스탬프", "timestamp": "타임스탬프",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "YouTube 동영상"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "प्रतिबंध", "ban": "प्रतिबंध",
"timestamp": "टाइमस्टॅम्प", "timestamp": "टाइमस्टॅम्प",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "YouTube व्हिडिओ"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "verbannen", "ban": "verbannen",
"timestamp": "tijdstempel", "timestamp": "tijdstempel",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "YouTube-video"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "forbud", "ban": "forbud",
"timestamp": "tidsstempel", "timestamp": "tidsstempel",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "YouTube-video"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "zbanować", "ban": "zbanować",
"timestamp": "znacznik czasu", "timestamp": "znacznik czasu",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "wideo YouTube"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "banir", "ban": "banir",
"timestamp": "marca de tempo", "timestamp": "marca de tempo",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "vídeo do YouTube"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "interzice", "ban": "interzice",
"timestamp": "marcă de timp", "timestamp": "marcă de timp",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "video YouTube"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "блокировка", "ban": "блокировка",
"timestamp": "временная метка", "timestamp": "временная метка",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "видео YouTube"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "ndalim", "ban": "ndalim",
"timestamp": "vulë kohe", "timestamp": "vulë kohe",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "video YouTube"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "förbjud", "ban": "förbjud",
"timestamp": "tidsstämpel", "timestamp": "tidsstämpel",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "YouTube-video"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "నిషేధం", "ban": "నిషేధం",
"timestamp": "టైమ్‌స్టాంప్", "timestamp": "టైమ్‌స్టాంప్",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "YouTube వీడియో"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "แบน", "ban": "แบน",
"timestamp": "แสตมป์เวลา", "timestamp": "แสตมป์เวลา",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "วิดีโอ YouTube"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "yasakla", "ban": "yasakla",
"timestamp": "zaman damgası", "timestamp": "zaman damgası",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "YouTube videosu"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "заблокувати", "ban": "заблокувати",
"timestamp": "часова мітка", "timestamp": "часова мітка",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "відео YouTube"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "روک", "ban": "روک",
"timestamp": "ٹائم سٹیمپ", "timestamp": "ٹائم سٹیمپ",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "YouTube ویڈیو"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "cấm", "ban": "cấm",
"timestamp": "dấu thời gian", "timestamp": "dấu thời gian",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "video YouTube"
} }
+2 -1
View File
@@ -399,5 +399,6 @@
"ban": "封禁", "ban": "封禁",
"timestamp": "时间戳", "timestamp": "时间戳",
"tag": "tag", "tag": "tag",
"post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled." "post_form_flash_upload_prompt": "Recommended SWF host: <catbox>Catbox</catbox>. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.",
"youtube_video": "YouTube 视频"
} }
@@ -151,6 +151,8 @@ vi.mock('../../lib/utils/media-utils', () => ({
getDisplayMediaInfoType: (type?: string) => type ?? 'unknown', getDisplayMediaInfoType: (type?: string) => type ?? 'unknown',
getHasThumbnail: () => true, getHasThumbnail: () => true,
getMediaDimensions: () => '100x100', getMediaDimensions: () => '100x100',
getPostMediaTypeLabel: (_commentMediaInfo: unknown, resolvedType?: string) => resolvedType ?? '',
getYouTubeEmbedPostMediaFileLink: () => undefined,
})); }));
vi.mock('../../lib/utils/post-utils', () => ({ vi.mock('../../lib/utils/post-utils', () => ({
@@ -171,6 +173,7 @@ vi.mock('../../lib/utils/pending-approval-moderation', () => ({
vi.mock('../../lib/utils/url-utils', () => ({ vi.mock('../../lib/utils/url-utils', () => ({
isValidURL: () => true, isValidURL: () => true,
parseHttpUrl: (value: string) => new URL(value),
})); }));
vi.mock('../../lib/utils/view-utils', () => ({ vi.mock('../../lib/utils/view-utils', () => ({
@@ -408,11 +408,11 @@ describe('CommentContent', () => {
cid: 'post-2', cid: 'post-2',
content: 'ignored', content: 'ignored',
postCid: 'post-2', postCid: 'post-2',
reason: 'spam', reason: 'duplicate of >>96',
removed: true, removed: true,
}); });
expect(container.textContent).toContain('this_post_was_removed'); expect(container.textContent).toContain('this_post_was_removed');
expect(container.textContent).toContain('Reason: "spam"'); expect(queryMarkdownText()).toEqual(['Reason: duplicate of >>96']);
await renderContent({ await renderContent({
cid: 'post-3', cid: 'post-3',
@@ -422,7 +422,7 @@ describe('CommentContent', () => {
reason: 'self-delete', reason: 'self-delete',
}); });
expect(container.textContent).toContain('user_deleted_this_post'); expect(container.textContent).toContain('user_deleted_this_post');
expect(container.textContent).toContain('Reason: "self-delete"'); expect(queryMarkdownText()).toEqual(['Reason: self-delete']);
}); });
it('renders pending approval, ban details, and loading or failed states', async () => { it('renders pending approval, ban details, and loading or failed states', async () => {
@@ -442,6 +442,7 @@ describe('CommentContent', () => {
expect(container.textContent).toContain('pending_mod_approval'); expect(container.textContent).toContain('pending_mod_approval');
expect(container.textContent).toContain('pending-reason:rules violation'); expect(container.textContent).toContain('pending-reason:rules violation');
expect(queryMarkdownText()).toEqual(['queued body', 'pending-reason:rules violation']);
const tooltip = container.querySelector('[data-testid="tooltip"]'); const tooltip = container.querySelector('[data-testid="tooltip"]');
expect(tooltip?.getAttribute('title')).toContain('ban:short:music-posting.eth:2024-01-01 12:00:00'); expect(tooltip?.getAttribute('title')).toContain('ban:short:music-posting.eth:2024-01-01 12:00:00');
@@ -156,7 +156,14 @@ const CommentContent = ({
const failedError = getFailedCommentError(resolvedPost); const failedError = getFailedCommentError(resolvedPost);
const failedErrorMessage = formatErrorMessageForDisplay(failedError); const failedErrorMessage = formatErrorMessageForDisplay(failedError);
const shouldShowUnpublishedStateDetails = !cid && (!hasFailedState || Boolean(failedError)); const shouldShowUnpublishedStateDetails = !cid && (!hasFailedState || Boolean(failedError));
const reasonMessage = reason ? `${capitalize(t('reason'))}: ${reason}` : undefined;
const pendingApprovalReason = pendingApproval ? reason?.trim() : undefined; const pendingApprovalReason = pendingApproval ? reason?.trim() : undefined;
const pendingApprovalReasonMessage = pendingApprovalReason
? t('pending_mod_approval_reason', {
reason: pendingApprovalReason,
interpolation: { escapeValue: false },
})
: undefined;
const loadingString = ( const loadingString = (
<div className={styles.stateString}> <div className={styles.stateString}>
@@ -193,21 +200,21 @@ const CommentContent = ({
{purged ? ( {purged ? (
<span className={styles.grayEditMessage}>{capitalize(t('this_post_was_purged'))}</span> <span className={styles.grayEditMessage}>{capitalize(t('this_post_was_purged'))}</span>
) : removed ? ( ) : removed ? (
reason ? ( reasonMessage ? (
<> <>
<span className={styles.redEditMessage}>({t('this_post_was_removed')})</span> <span className={styles.redEditMessage}>({t('this_post_was_removed')})</span>
<br /> <br />
<br /> <br />
<span className={styles.grayEditMessage}>{`${capitalize(t('reason'))}: "${reason}"`}</span> {renderContent(reasonMessage)}
</> </>
) : ( ) : (
<span className={styles.grayEditMessage}>{capitalize(t('this_post_was_removed'))}.</span> <span className={styles.grayEditMessage}>{capitalize(t('this_post_was_removed'))}.</span>
) )
) : deleted ? ( ) : deleted ? (
reason ? ( reasonMessage ? (
<> <>
<span className={styles.grayEditMessage}>{t('user_deleted_this_post')}</span>{' '} <span className={styles.grayEditMessage}>{t('user_deleted_this_post')}</span>{' '}
<span className={styles.grayEditMessage}>{`${capitalize(t('reason'))}: "${reason}"`}</span> {renderContent(reasonMessage)}
</> </>
) : ( ) : (
<span className={styles.grayEditMessage}>{t('user_deleted_this_post')}</span> <span className={styles.grayEditMessage}>{t('user_deleted_this_post')}</span>
@@ -220,18 +227,13 @@ const CommentContent = ({
<br /> <br />
<br /> <br />
<span className={styles.pendingApproval}>({t('pending_mod_approval')})</span> <span className={styles.pendingApproval}>({t('pending_mod_approval')})</span>
{pendingApprovalReason && ( {pendingApprovalReasonMessage ? (
<> <>
<br /> <br />
<br /> <br />
<span className={styles.grayEditMessage}> {renderContent(pendingApprovalReasonMessage)}
{t('pending_mod_approval_reason', {
reason: pendingApprovalReason,
interpolation: { escapeValue: false },
})}
</span>
</> </>
)} ) : null}
</> </>
)} )}
{((!isInPostView && content?.length > 1000 && !showFullComment) || (isInPostView && content?.length > 2000 && !showFullComment)) && !isPrivilegedAuthor && ( {((!isInPostView && content?.length > 1000 && !showFullComment) || (isInPostView && content?.length > 2000 && !showFullComment)) && !isPrivilegedAuthor && (
@@ -295,6 +295,21 @@ describe('CommentMedia', () => {
expect(setShowThumbnailMock).toHaveBeenCalledWith(false); expect(setShowThumbnailMock).toHaveBeenCalledWith(false);
}); });
it('renders a youtube thumbnail before the embed is opened', async () => {
await renderMedia({
commentMediaInfo: {
patternThumbnailUrl: 'https://img.youtube.com/vi/abc123/0.jpg',
type: 'iframe',
url: 'https://www.youtube.com/watch?v=abc123',
},
setShowThumbnail: setShowThumbnailMock,
showThumbnail: true,
});
expect(container.querySelector('img[src="https://img.youtube.com/vi/abc123/0.jpg"]')).toBeTruthy();
expect(container.querySelector('[data-testid="embed"]')).toBeNull();
});
it('renders the expanded embed view with a close button on mobile', async () => { it('renders the expanded embed view with a close button on mobile', async () => {
testState.isMobile = true; testState.isMobile = true;
+13 -1
View File
@@ -3,7 +3,7 @@ import { createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client'; import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import Embed from '../embed'; import Embed from '../embed';
import { canEmbed } from '../embed-utils'; import { canEmbed, getYouTubeVideoId } from '../embed-utils';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>; const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
@@ -56,6 +56,18 @@ describe('Embed', () => {
expect(container.innerHTML).toBe(''); expect(container.innerHTML).toBe('');
}); });
it('extracts youtube video ids for standard, short, and invidious urls', () => {
expect(getYouTubeVideoId(new URL('https://www.youtube.com/watch?v=abc123'))).toBe('abc123');
expect(getYouTubeVideoId(new URL('https://m.youtube.com/watch?v=mobile123'))).toBe('mobile123');
expect(getYouTubeVideoId(new URL('https://music.youtube.com/watch?v=music123'))).toBe('music123');
expect(getYouTubeVideoId(new URL('https://youtu.be/short123'))).toBe('short123');
expect(getYouTubeVideoId(new URL('https://www.youtube.com/shorts/short456'))).toBe('short456');
expect(getYouTubeVideoId(new URL('https://yewtu.be/invidious789'))).toBe('invidious789');
expect(getYouTubeVideoId(new URL('https://m.youtube.com/@channel'))).toBeNull();
expect(getYouTubeVideoId(new URL('https://music.youtube.com/channel/UC123'))).toBeNull();
expect(getYouTubeVideoId(new URL('https://www.youtube.com/playlist?list=PL123'))).toBeNull();
});
it('reports embeddable hosts through canEmbed and rejects unsupported reddit pages', () => { it('reports embeddable hosts through canEmbed and rejects unsupported reddit pages', () => {
expect(canEmbed(new URL('https://www.youtube.com/watch?v=abc123'))).toBe(true); expect(canEmbed(new URL('https://www.youtube.com/watch?v=abc123'))).toBe(true);
expect(canEmbed(new URL('https://yt.example/watch?v=abc123'))).toBe(true); expect(canEmbed(new URL('https://yt.example/watch?v=abc123'))).toBe(true);
+31
View File
@@ -1,3 +1,5 @@
const STANDARD_YOUTUBE_HOSTS = new Set<string>(['youtube.com', 'www.youtube.com', 'm.youtube.com', 'music.youtube.com', 'youtu.be', 'www.youtu.be']);
export const youtubeHosts = new Set<string>([ export const youtubeHosts = new Set<string>([
'youtube.com', 'youtube.com',
'www.youtube.com', 'www.youtube.com',
@@ -46,6 +48,35 @@ const canEmbedHosts = new Set<string>([
...spotifyHosts, ...spotifyHosts,
]); ]);
export const getYouTubeVideoId = (parsedUrl: URL): string | null => {
if (parsedUrl.searchParams.has('list') && !parsedUrl.searchParams.get('v') && !parsedUrl.host.includes('youtu.be') && !parsedUrl.pathname.includes('/shorts/')) {
return null;
}
const videoIdFromQuery = parsedUrl.searchParams.get('v');
if (videoIdFromQuery) {
return videoIdFromQuery;
}
if (parsedUrl.host.includes('youtu.be')) {
return parsedUrl.pathname.slice(1).split('/')[0] || null;
}
if (parsedUrl.pathname.includes('/shorts/')) {
return parsedUrl.pathname.split('/shorts/')[1]?.split('/')[0] || null;
}
const isInvidious = !STANDARD_YOUTUBE_HOSTS.has(parsedUrl.host) && (youtubeHosts.has(parsedUrl.host) || parsedUrl.host.startsWith('yt.'));
if (isInvidious) {
if (parsedUrl.pathname.startsWith('/watch')) {
return parsedUrl.searchParams.get('v');
}
return parsedUrl.pathname.split('/').filter(Boolean).pop() || null;
}
return null;
};
export const canEmbed = (parsedUrl: URL): boolean => { export const canEmbed = (parsedUrl: URL): boolean => {
if (xHosts.has(parsedUrl.host)) { if (xHosts.has(parsedUrl.host)) {
return Boolean(getXTweetId(parsedUrl)); return Boolean(getXTweetId(parsedUrl));
+3 -19
View File
@@ -10,6 +10,7 @@ import {
streamableHosts, streamableHosts,
tiktokHosts, tiktokHosts,
twitchHosts, twitchHosts,
getYouTubeVideoId,
xHosts, xHosts,
youtubeHosts, youtubeHosts,
} from './embed-utils'; } from './embed-utils';
@@ -66,28 +67,11 @@ const remoteEmbedSandbox = `${srcDocSandbox} allow-forms allow-presentation allo
const YoutubeEmbed = ({ parsedUrl }: EmbedComponentProps) => { const YoutubeEmbed = ({ parsedUrl }: EmbedComponentProps) => {
let embedSrc = ''; let embedSrc = '';
const isInvidious = parsedUrl.host !== 'youtube.com' && parsedUrl.host !== 'www.youtube.com' && parsedUrl.host !== 'youtu.be' && parsedUrl.host !== 'www.youtu.be'; if (parsedUrl.searchParams.has('list') && !getYouTubeVideoId(parsedUrl)) {
if (parsedUrl.searchParams.has('list')) {
const playlistId = parsedUrl.searchParams.get('list'); const playlistId = parsedUrl.searchParams.get('list');
embedSrc = `https://www.youtube.com/embed/videoseries?list=${playlistId}`; embedSrc = `https://www.youtube.com/embed/videoseries?list=${playlistId}`;
} else { } else {
let videoId = parsedUrl.searchParams.get('v'); const videoId = getYouTubeVideoId(parsedUrl);
if (!videoId) {
if (parsedUrl.host.includes('youtu.be')) {
videoId = parsedUrl.pathname.substring(1);
} else if (parsedUrl.pathname.includes('/shorts/')) {
videoId = parsedUrl.pathname.split('/shorts/')[1];
} else if (isInvidious) {
if (parsedUrl.pathname.startsWith('/watch')) {
videoId = parsedUrl.searchParams.get('v');
} else {
videoId = parsedUrl.pathname.split('/').pop() || null;
}
}
}
if (videoId) { if (videoId) {
embedSrc = `https://www.youtube.com/embed/${videoId}`; embedSrc = `https://www.youtube.com/embed/${videoId}`;
} }
+1 -1
View File
@@ -1,2 +1,2 @@
export { default } from './embed'; export { default } from './embed';
export { canEmbed } from './embed-utils'; export { canEmbed, getYouTubeVideoId } from './embed-utils';
@@ -24,7 +24,9 @@ const testState = vi.hoisted(() => ({
cidToNumber: {} as Record<string, number>, cidToNumber: {} as Record<string, number>,
internalPathByHref: {} as Record<string, string | null>, internalPathByHref: {} as Record<string, string | null>,
isMobile: false, isMobile: false,
mediaInfoByHref: {} as Record<string, { thumbnail?: string; type: string; url: string }>, mediaInfoByHref: {} as Record<string, { patternThumbnailUrl?: string; thumbnail?: string; type: string; url: string }>,
floatingMediaProps: null as { showThumbnail?: boolean } | null,
floatingOnOpenChange: null as ((open: boolean) => void) | null,
numberToCid: {} as Record<string, Record<number, string>>, numberToCid: {} as Record<string, Record<number, string>>,
unavailableCids: new Set<string>(), unavailableCids: new Set<string>(),
})); }));
@@ -42,20 +44,32 @@ vi.mock('@floating-ui/react', () => ({
shift: () => ({}), shift: () => ({}),
size: () => ({}), size: () => ({}),
useDismiss: () => ({}), useDismiss: () => ({}),
useFloating: () => ({ useFloating: ({ onOpenChange, open }: { onOpenChange?: (open: boolean) => void; open?: boolean }) => {
context: {}, testState.floatingOnOpenChange = onOpenChange ?? null;
floatingStyles: {}, return {
refs: { context: {},
setFloating: () => undefined, floatingStyles: {},
setReference: () => undefined, open,
}, onOpenChange,
update: () => undefined, refs: {
}), setFloating: () => undefined,
setReference: () => undefined,
},
update: () => undefined,
};
},
useFocus: () => ({}), useFocus: () => ({}),
useHover: () => ({}), useHover: () => ({
useInteractions: () => ({ getReferenceProps: (props?: Record<string, unknown>) => ({
...props,
onMouseEnter: () => testState.floatingOnOpenChange?.(true),
onMouseLeave: () => testState.floatingOnOpenChange?.(false),
}),
}),
useInteractions: (interactions: Array<{ getReferenceProps?: (props?: Record<string, unknown>) => Record<string, unknown> }>) => ({
getFloatingProps: (props?: Record<string, unknown>) => props || {}, getFloatingProps: (props?: Record<string, unknown>) => props || {},
getReferenceProps: (props?: Record<string, unknown>) => props || {}, getReferenceProps: (props?: Record<string, unknown>) =>
interactions.reduce((mergedProps, interaction) => interaction.getReferenceProps?.(mergedProps) || mergedProps, props || {}),
}), }),
})); }));
@@ -75,8 +89,13 @@ vi.mock('../../../hooks/use-is-mobile', () => ({
})); }));
vi.mock('../../../lib/utils/media-utils', () => ({ vi.mock('../../../lib/utils/media-utils', () => ({
getHasThumbnail: (linkMediaInfo?: { thumbnail?: string; type?: string }, href?: string) => getHasThumbnail: (linkMediaInfo?: { patternThumbnailUrl?: string; thumbnail?: string; type?: string }, href?: string) =>
Boolean(linkMediaInfo?.thumbnail || linkMediaInfo?.type === 'image' || (href && testState.mediaInfoByHref[href]?.thumbnail)), Boolean(
linkMediaInfo?.thumbnail ||
linkMediaInfo?.patternThumbnailUrl ||
linkMediaInfo?.type === 'image' ||
(href && (testState.mediaInfoByHref[href]?.thumbnail || testState.mediaInfoByHref[href]?.patternThumbnailUrl)),
),
getLinkMediaInfo: (href: string) => testState.mediaInfoByHref[href], getLinkMediaInfo: (href: string) => testState.mediaInfoByHref[href],
})); }));
@@ -139,8 +158,28 @@ vi.mock('../../../stores/use-post-number-store', () => ({
})); }));
vi.mock('../../comment-media', () => ({ vi.mock('../../comment-media', () => ({
default: ({ commentMediaInfo }: { commentMediaInfo?: { type?: string; url?: string } }) => default: ({
createElement('div', { 'data-testid': 'comment-media' }, `${commentMediaInfo?.type}:${commentMediaInfo?.url}`), commentMediaInfo,
isFloatingEmbed,
showThumbnail,
}: {
commentMediaInfo?: { type?: string; url?: string };
isFloatingEmbed?: boolean;
showThumbnail?: boolean;
}) => {
if (isFloatingEmbed) {
testState.floatingMediaProps = { showThumbnail };
}
return createElement(
'div',
{
'data-floating': String(Boolean(isFloatingEmbed)),
'data-show-thumbnail': String(showThumbnail),
'data-testid': 'comment-media',
},
`${commentMediaInfo?.type}:${commentMediaInfo?.url}`,
);
},
})); }));
vi.mock('../../embed', () => ({ vi.mock('../../embed', () => ({
@@ -198,6 +237,8 @@ describe('Markdown', () => {
testState.internalPathByHref = {}; testState.internalPathByHref = {};
testState.isMobile = false; testState.isMobile = false;
testState.mediaInfoByHref = {}; testState.mediaInfoByHref = {};
testState.floatingMediaProps = null;
testState.floatingOnOpenChange = null;
testState.numberToCid = {}; testState.numberToCid = {};
testState.unavailableCids = new Set<string>(); testState.unavailableCids = new Set<string>();
@@ -460,6 +501,33 @@ describe('Markdown', () => {
expect(lazyLinks.map((node) => node.textContent)).toEqual(['>>42', '>>>/fit/77']); expect(lazyLinks.map((node) => node.textContent)).toEqual(['>>42', '>>>/fit/77']);
}); });
it('shows youtube thumbnails in the floating hover preview instead of loading the iframe', async () => {
testState.embeddableHosts = new Set(['www.youtube.com']);
testState.mediaInfoByHref = {
'https://www.youtube.com/watch?v=abc123': {
patternThumbnailUrl: 'https://img.youtube.com/vi/abc123/0.jpg',
type: 'iframe',
url: 'https://www.youtube.com/watch?v=abc123',
},
};
await renderMarkdown({
content: 'https://www.youtube.com/watch?v=abc123',
});
const embedButton = Array.from(container.querySelectorAll('button')).find((node) => node.textContent === 'embed');
expect(embedButton).toBeTruthy();
await act(async () => {
testState.floatingOnOpenChange?.(true);
});
const floatingMedia = container.querySelector('[data-floating="true"]');
expect(floatingMedia).toBeTruthy();
expect(floatingMedia?.getAttribute('data-show-thumbnail')).toBe('true');
expect(testState.floatingMediaProps?.showThumbnail).toBe(true);
});
it('toggles inline media embeds for embeddable links outside catalog view', async () => { it('toggles inline media embeds for embeddable links outside catalog view', async () => {
testState.mediaInfoByHref = { testState.mediaInfoByHref = {
'https://cdn.example/image.png': { 'https://cdn.example/image.png': {
+2 -2
View File
@@ -123,8 +123,8 @@ const ContentLinkEmbed = ({ children, href, linkMediaInfo }: ContentLinkEmbedPro
disableToggle={true} disableToggle={true}
isFloatingEmbed={true} isFloatingEmbed={true}
isReply={false} isReply={false}
setShowThumbnail={setShowMedia} setShowThumbnail={() => {}}
showThumbnail={false} showThumbnail={true}
/> />
</div> </div>
)} )}
+12 -7
View File
@@ -5,11 +5,11 @@ import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
import { Comment, useEditedComment, useReplies, useAccount } from '@bitsocial/bitsocial-react-hooks'; import { Comment, useEditedComment, useReplies, useAccount } from '@bitsocial/bitsocial-react-hooks';
import getShortAddress from '../../lib/get-short-address'; import getShortAddress from '../../lib/get-short-address';
import styles from '../../views/post/post.module.css'; import styles from '../../views/post/post.module.css';
import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils'; import { CommentMediaInfo, getHasThumbnail, getMediaDimensions, getPostMediaTypeLabel, getYouTubeEmbedPostMediaFileLink } from '../../lib/utils/media-utils';
import { hashStringToColor, getTextColorForBackground } from '../../lib/utils/post-utils'; import { hashStringToColor, getTextColorForBackground } from '../../lib/utils/post-utils';
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils'; import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
import { approvePendingCommentModeration, isPendingApprovalRejected, rejectPendingCommentModeration } from '../../lib/utils/pending-approval-moderation'; import { approvePendingCommentModeration, isPendingApprovalRejected, rejectPendingCommentModeration } from '../../lib/utils/pending-approval-moderation';
import { isValidURL } from '../../lib/utils/url-utils'; import { isValidURL, parseHttpUrl } from '../../lib/utils/url-utils';
import { isAllView, isModQueueView, isModView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils'; import { isAllView, isModQueueView, isModView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
import { formatUserIDForDisplay, truncateWithEllipsisInMiddle } from '../../lib/utils/string-utils'; import { formatUserIDForDisplay, truncateWithEllipsisInMiddle } from '../../lib/utils/string-utils';
import useModQueueStore from '../../stores/use-mod-queue-store'; import useModQueueStore from '../../stores/use-mod-queue-store';
@@ -642,12 +642,16 @@ const PostMedia = ({
type = 'static gif'; type = 'static gif';
} }
const embedUrl = url && new URL(url); const youtubeFileLink = getYouTubeEmbedPostMediaFileLink(commentMediaInfo);
const fileLinkUrl = youtubeFileLink ?? url;
const embedUrl = url ? parseHttpUrl(url) : null;
const [showThumbnail, setShowThumbnail] = useState(true); const [showThumbnail, setShowThumbnail] = useState(true);
const mediaDimensions = getMediaDimensions(commentMediaInfo); const mediaDimensions = getMediaDimensions(commentMediaInfo);
const directoryEntry = findDirectoryByAddress(directories, communityAddress); const directoryEntry = findDirectoryByAddress(directories, communityAddress);
const requirePostLinkIsMedia = directoryEntry?.features?.requirePostLinkIsMedia === true; const requirePostLinkIsMedia = directoryEntry?.features?.requirePostLinkIsMedia === true;
const fileLabel = youtubeFileLink || requirePostLinkIsMedia ? t('file') : t('link');
const mediaTypeLabel = type ? lowerCase(getPostMediaTypeLabel(commentMediaInfo, type, t)) : '';
const boardPath = communityAddress ? getBoardPath(communityAddress, directories) : undefined; const boardPath = communityAddress ? getBoardPath(communityAddress, directories) : undefined;
const displayBoardPath = const displayBoardPath =
boardPath && communityAddress && boardPath !== communityAddress boardPath && communityAddress && boardPath !== communityAddress
@@ -666,10 +670,11 @@ const PostMedia = ({
{t('board')}: <Link to={`/${boardPath}`}>{displayBoardPath}</Link>{' '} {t('board')}: <Link to={`/${boardPath}`}>{displayBoardPath}</Link>{' '}
</> </>
)} )}
{requirePostLinkIsMedia ? t('file') : t('link')}:{' '} {fileLabel}:{' '}
<a href={url} target='_blank' rel='noopener noreferrer'> <a href={fileLinkUrl} target='_blank' rel='noopener noreferrer'>
{(() => { {(() => {
if (spoiler) return capitalize(t('spoiler')); if (spoiler) return capitalize(t('spoiler'));
if (youtubeFileLink) return truncateWithEllipsisInMiddle(youtubeFileLink);
if (requirePostLinkIsMedia && url) { if (requirePostLinkIsMedia && url) {
try { try {
const pathParts = new URL(url).pathname.split('/'); const pathParts = new URL(url).pathname.split('/');
@@ -677,10 +682,10 @@ const PostMedia = ({
if (filename && /\.\w+$/.test(filename)) return truncateWithEllipsisInMiddle(filename); if (filename && /\.\w+$/.test(filename)) return truncateWithEllipsisInMiddle(filename);
} catch {} } catch {}
} }
return truncateWithEllipsisInMiddle(url ?? ''); return truncateWithEllipsisInMiddle(fileLinkUrl ?? '');
})()} })()}
</a>{' '} </a>{' '}
({type && lowerCase(getDisplayMediaInfoType(type, t))} ({mediaTypeLabel}
{mediaDimensions && `, ${mediaDimensions}`}) {mediaDimensions && `, ${mediaDimensions}`})
{!showThumbnail && (type === 'iframe' || type === 'video' || type === 'audio') && ( {!showThumbnail && (type === 'iframe' || type === 'video' || type === 'audio') && (
<span> <span>
@@ -444,6 +444,7 @@ describe('PostForm', () => {
testState.directories = [ testState.directories = [
{ address: 'music-posting.eth', features: {}, title: '/mu/ - Music' }, { address: 'music-posting.eth', features: {}, title: '/mu/ - Music' },
{ address: 'politically-incorrect.bso', directoryCode: 'pol', features: { hasFlags: true }, title: '/pol/ - Politically Incorrect' }, { address: 'politically-incorrect.bso', directoryCode: 'pol', features: { hasFlags: true }, title: '/pol/ - Politically Incorrect' },
{ address: 'sports-posting.bso', directoryCode: 'sp', features: { hasFlags: true }, title: '/sp/ - Sports' },
{ address: 'random-nsfw.bso', features: {}, title: '/b/ - Random' }, { address: 'random-nsfw.bso', features: {}, title: '/b/ - Random' },
{ {
address: 'flash-posting.bso', address: 'flash-posting.bso',
@@ -699,6 +700,30 @@ describe('PostForm', () => {
}); });
}); });
it('publishes geographic location on /sp/ without showing a flag field', async () => {
testState.resolvedCommunityAddress = 'sports-posting.bso';
await renderPostForm('/sp');
await clickByText(container, 'start_new_thread');
const table = container.querySelector('table');
const flagSelect = table?.querySelector<HTMLSelectElement>('select[aria-label="flag"]');
const textarea = table?.querySelector<HTMLTextAreaElement>('textarea');
expect(flagSelect).toBeNull();
await dispatchInput(textarea as HTMLTextAreaElement, 'sports post');
await clickByText(table as HTMLTableElement, 'post');
expect(testState.publishPostMock).toHaveBeenCalledWith({
content: 'sports post',
challengeRequest: {
challengeAnswers: ['bitsocial-flags:5chan:flag:country:auto'],
},
flairs: [{ type: 'country', code: 'auto', text: 'flag:country:auto' }],
});
});
it('publishes selected political flags from the post form', async () => { it('publishes selected political flags from the post form', async () => {
testState.resolvedCommunityAddress = 'politically-incorrect.bso'; testState.resolvedCommunityAddress = 'politically-incorrect.bso';
+1 -1
View File
@@ -391,7 +391,7 @@ const PostFormFields = ({
<tr> <tr>
<td>{t('tag')}</td> <td>{t('tag')}</td>
<td> <td>
<select name='flashTag' aria-label={t('tag')} ref={flashTagRef} defaultValue=''> <select name='flashTag' aria-label={t('tag')} className={styles.flagSelector} ref={flashTagRef} defaultValue=''>
<option value=''>{t('choose_one')}</option> <option value=''>{t('choose_one')}</option>
{flashTagOptions.map((option) => ( {flashTagOptions.map((option) => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>
@@ -340,6 +340,18 @@ describe('ReplyModal', () => {
features: { hasFlags: true }, features: { hasFlags: true },
title: '/bant/ - International/Random', title: '/bant/ - International/Random',
}, },
'international-sfw.bso': {
address: 'international-sfw.bso',
directoryCode: 'int',
features: { hasFlags: true },
title: '/int/ - International',
},
'sports-posting.bso': {
address: 'sports-posting.bso',
directoryCode: 'sp',
features: { hasFlags: true },
title: '/sp/ - Sports',
},
'random-nsfw.bso': { 'random-nsfw.bso': {
address: 'random-nsfw.bso', address: 'random-nsfw.bso',
features: {}, features: {},
@@ -494,8 +506,12 @@ describe('ReplyModal', () => {
}); });
}); });
it('publishes geographic location on /bant/ without showing a flag selector', async () => { it.each([
await renderReplyModal('/bant/thread/post-1', 'international-nsfw.bso'); { boardPath: '/bant/thread/post-1', communityAddress: 'international-nsfw.bso' },
{ boardPath: '/int/thread/post-1', communityAddress: 'international-sfw.bso' },
{ boardPath: '/sp/thread/post-1', communityAddress: 'sports-posting.bso' },
])('publishes geographic location on country-only boards without showing a flag selector', async ({ boardPath, communityAddress }) => {
await renderReplyModal(boardPath, communityAddress);
expect(container.querySelector<HTMLSelectElement>('select[aria-label="flag"]')).toBeNull(); expect(container.querySelector<HTMLSelectElement>('select[aria-label="flag"]')).toBeNull();
@@ -256,6 +256,73 @@ describe('P2PStatsSettings', () => {
expect(fetchMock).toHaveBeenCalledWith('https://free.freeipapi.com/api/json/91.234.199.189', expect.objectContaining({ signal: expect.any(AbortSignal) })); expect(fetchMock).toHaveBeenCalledWith('https://free.freeipapi.com/api/json/91.234.199.189', expect.objectContaining({ signal: expect.any(AbortSignal) }));
}); });
it('shows peer country flags for DNS6 relay hostnames with embedded IPv6 addresses', async () => {
const fetchMock = vi.fn(async (url: string | URL | Request) => {
const requestUrl = String(url);
if (requestUrl === 'https://free.freeipapi.com/api/json/2a11%3A6100%3A0%3A5e9f%3A%3A0') {
return {
ok: true,
json: async () => ({
cityName: 'Reykjavik',
countryCode: 'IS',
ipAddress: '2a11:6100:0:5e9f::0',
latitude: 64.1466,
longitude: -21.9426,
}),
};
}
return {
ok: true,
json: async () => ({ country: 'US', ip: '147.75.84.175' }),
};
});
vi.stubGlobal('fetch', fetchMock);
testState.account = {
...testState.account,
pkcOptions: {
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
},
pkc: {
clients: {
libp2pJsClients: {
libp2pjs: {
key: 'libp2pjs',
_helia: {
libp2p: {
getConnections: () => [
{
remoteAddr: {
toString: () => '/dns6/2a11-6100-0-5e9f--0.k51qzi5uqu5djg5pdoi9a98.example/tcp/443/ws/p2p/relay-peer/p2p-circuit/p2p/12D3KooWIPv6Peer',
},
remotePeer: { toString: () => '12D3KooWIPv6Peer' },
},
],
getMultiaddrs: () => ['/ip4/147.75.84.175/tcp/4001/ws'],
getPeers: () => ['12D3KooWIPv6Peer'],
peerId: { toString: () => 'self-peer' },
},
},
},
},
},
},
};
await renderSettings(false);
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
const connectedPeers = container.querySelector('[data-testid="connected-peers"]');
expect(connectedPeers?.textContent).toContain('WebSocket through relay');
expect(connectedPeers?.querySelector('img[aria-label="Iceland"]')).not.toBeNull();
expect(fetchMock).toHaveBeenCalledWith(
'https://free.freeipapi.com/api/json/2a11%3A6100%3A0%3A5e9f%3A%3A0',
expect.objectContaining({ signal: expect.any(AbortSignal) }),
);
});
it('shows the own IP flag and a red precise map marker when leeching', async () => { it('shows the own IP flag and a red precise map marker when leeching', async () => {
const fetchMock = vi.fn(async (url: string | URL | Request) => { const fetchMock = vi.fn(async (url: string | URL | Request) => {
const requestUrl = String(url); const requestUrl = String(url);
@@ -17,30 +17,34 @@ describe('comment-flag-selection', () => {
expect(hasCommentFlagsForDirectory({ features: { hasFlags: true }, title: '/pol/ - Politically Incorrect' })).toBe(true); expect(hasCommentFlagsForDirectory({ features: { hasFlags: true }, title: '/pol/ - Politically Incorrect' })).toBe(true);
}); });
it('uses geographic location as the default for country flag boards', () => { it('uses geographic location as the default for other flag boards', () => {
expect( expect(
getCommentFlagOptionsForDirectory({ getCommentFlagOptionsForDirectory({
directoryCode: 'int', directoryCode: 'fit',
features: { hasFlags: true }, features: { hasFlags: true },
title: '/int/ - International', title: '/fit/ - Fitness',
}), }),
).toEqual([{ label: 'Geographic Location', value: 'country:auto' }]); ).toEqual([{ label: 'Geographic Location', value: 'country:auto' }]);
}); });
it('does not expose a flag selector on /bant/ but still publishes geographic location', () => { it.each([
{ directoryCode: 'bant', title: '/bant/ - International/Random' },
{ directoryCode: 'int', title: '/int/ - International' },
{ directoryCode: 'sp', title: '/sp/ - Sports' },
])('does not expose a flag selector on /$directoryCode/ but still publishes geographic location', ({ directoryCode, title }) => {
expect( expect(
getCommentFlagOptionsForDirectory({ getCommentFlagOptionsForDirectory({
directoryCode: 'bant', directoryCode,
features: { hasFlags: true }, features: { hasFlags: true },
title: '/bant/ - International/Random', title,
}), }),
).toEqual([]); ).toEqual([]);
expect( expect(
getCommentFlagPublishOptionsForDirectory({ getCommentFlagPublishOptionsForDirectory({
directoryCode: 'bant', directoryCode,
features: { hasFlags: true }, features: { hasFlags: true },
title: '/bant/ - International/Random', title,
}), }),
).toEqual({ ).toEqual({
challengeRequest: { challengeRequest: {
+32
View File
@@ -37,6 +37,13 @@ describe('extractIpv6FromAddress', () => {
expect(extractIpv6FromAddress('/ip6/2001:4860:4860::8888/tcp/4001/ws')).toBe('2001:4860:4860::8888'); expect(extractIpv6FromAddress('/ip6/2001:4860:4860::8888/tcp/4001/ws')).toBe('2001:4860:4860::8888');
expect(extractIpFromAddress('/ip6/2001:4860:4860::8888/tcp/4001/ws')).toBe('2001:4860:4860::8888'); expect(extractIpFromAddress('/ip6/2001:4860:4860::8888/tcp/4001/ws')).toBe('2001:4860:4860::8888');
}); });
it('extracts an IPv6 embedded with dashes in a DNS hostname', () => {
const address = '/dns6/2a11-6100-0-5e9f--0.k51qzi5uqu5djg5pdoi9a98.example/tcp/443/wss/p2p/12D3KooWExample';
expect(extractIpv6FromAddress(address)).toBe('2a11:6100:0:5e9f::0');
expect(extractIpFromAddress(address)).toBe('2a11:6100:0:5e9f::0');
});
}); });
describe('isPrivateOrReservedIpv4', () => { describe('isPrivateOrReservedIpv4', () => {
@@ -261,6 +268,31 @@ describe('fetchPeerMapLocation', () => {
vi.unstubAllGlobals(); vi.unstubAllGlobals();
}); });
it('resolves a peer location from a DNS6 hostname with an embedded IPv6', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
cityName: 'Reykjavik',
countryCode: 'IS',
ipAddress: '2a11:6100:0:5e9f::0',
latitude: 64.1466,
longitude: -21.9426,
}),
});
vi.stubGlobal('fetch', fetchMock);
await expect(fetchPeerMapLocation('/dns6/2a11-6100-0-5e9f--0.k51qzi5uqu5djg5pdoi9a98.example/tcp/443/wss')).resolves.toMatchObject({
countryCode: 'is',
label: 'Reykjavik, IS',
lat: 64.1466,
lon: -21.9426,
source: 'geoip',
});
expect(fetchMock.mock.calls[0][0]).toBe('https://free.freeipapi.com/api/json/2a11%3A6100%3A0%3A5e9f%3A%3A0');
vi.unstubAllGlobals();
});
}); });
describe('getApproximateCountryCode', () => { describe('getApproximateCountryCode', () => {
+1 -1
View File
@@ -38,7 +38,7 @@ const NO_FLAG_OPTION: CommentFlagSelectOption = {
}; };
/** Boards that always publish geographic location without showing a flag selector. */ /** Boards that always publish geographic location without showing a flag selector. */
const AUTO_GEOGRAPHIC_FLAG_DIRECTORY_CODES = new Set(['bant']); const AUTO_GEOGRAPHIC_FLAG_DIRECTORY_CODES = new Set(['bant', 'int', 'sp']);
const getDirectoryCode = (directory: Pick<DirectoryCommunity, 'directoryCode' | 'title'> | undefined): string | undefined => { const getDirectoryCode = (directory: Pick<DirectoryCommunity, 'directoryCode' | 'title'> | undefined): string | undefined => {
const directoryCode = directory?.directoryCode?.trim().toLowerCase(); const directoryCode = directory?.directoryCode?.trim().toLowerCase();
+11 -1
View File
@@ -284,7 +284,17 @@ export const extractIpv4FromAddress = (address: string): string | null => {
export const extractIpv6FromAddress = (address: string): string | null => { export const extractIpv6FromAddress = (address: string): string | null => {
const direct = /\/ip6\/([^/]+)/.exec(address); const direct = /\/ip6\/([^/]+)/.exec(address);
return direct ? direct[1] : null; if (direct) return direct[1];
const dns = /\/dns6\/([^/]+)/i.exec(address);
const firstLabel = dns?.[1]?.split('.')[0];
if (!firstLabel || !firstLabel.includes('-') || !/^[0-9a-f-]+$/i.test(firstLabel)) return null;
const candidate = firstLabel.replaceAll('-', ':').toLowerCase();
try {
new URL(`http://[${candidate}]/`);
return candidate;
} catch {
return null;
}
}; };
const parseOctets = (ip: string): number[] | null => { const parseOctets = (ip: string): number[] | null => {
+35 -4
View File
@@ -20,9 +20,13 @@ vi.mock('@bitsocial/bitsocial-react-hooks/dist/lib/localforage-lru/index.js', ()
}, },
})); }));
vi.mock('../../../components/embed', () => ({ vi.mock('../../../components/embed/embed-utils', async (importOriginal) => {
canEmbed: (url: URL) => testState.canEmbedHosts.has(url.hostname), const actual = await importOriginal<typeof import('../../../components/embed/embed-utils')>();
})); return {
...actual,
canEmbed: (url: URL) => testState.canEmbedHosts.has(url.hostname),
};
});
vi.mock('@capacitor/core', () => ({ vi.mock('@capacitor/core', () => ({
Capacitor: { Capacitor: {
@@ -33,7 +37,16 @@ vi.mock('@capacitor/core', () => ({
}, },
})); }));
import { fetchWebpageThumbnailIfNeeded, getCommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getLinkMediaInfo, getMediaDimensions } from '../media-utils'; import {
fetchWebpageThumbnailIfNeeded,
getCommentMediaInfo,
getDisplayMediaInfoType,
getHasThumbnail,
getLinkMediaInfo,
getMediaDimensions,
getPostMediaTypeLabel,
getYouTubeEmbedPostMediaFileLink,
} from '../media-utils';
const clearMemoizedCache = (fn: unknown) => { const clearMemoizedCache = (fn: unknown) => {
const memoized = fn as { clear?: () => void }; const memoized = fn as { clear?: () => void };
@@ -102,6 +115,18 @@ describe('media-utils', () => {
expect(getDisplayMediaInfoType('unknown', t)).toBe('translated:webpage'); expect(getDisplayMediaInfoType('unknown', t)).toBe('translated:webpage');
}); });
it('uses the youtube thumbnail url for post media file links and labels', () => {
const mediaInfo = {
patternThumbnailUrl: 'https://img.youtube.com/vi/abc123/0.jpg',
type: 'iframe',
url: 'https://www.youtube.com/watch?v=abc123',
};
expect(getYouTubeEmbedPostMediaFileLink(mediaInfo)).toBe('https://img.youtube.com/vi/abc123/0.jpg');
expect(getPostMediaTypeLabel(mediaInfo, 'iframe', (key) => key)).toBe('youtube_video');
expect(getYouTubeEmbedPostMediaFileLink({ type: 'iframe', url: 'https://streamable.com/clip123' })).toBeUndefined();
});
it('recognizes which media types expose thumbnails', () => { it('recognizes which media types expose thumbnails', () => {
expect(getHasThumbnail(undefined, 'https://example.com/file.png')).toBe(false); expect(getHasThumbnail(undefined, 'https://example.com/file.png')).toBe(false);
expect(getHasThumbnail({ type: 'image', url: 'https://example.com/file.png' }, 'https://example.com/file.png')).toBe(true); expect(getHasThumbnail({ type: 'image', url: 'https://example.com/file.png' }, 'https://example.com/file.png')).toBe(true);
@@ -156,6 +181,12 @@ describe('media-utils', () => {
type: 'iframe', type: 'iframe',
url: 'https://yt.example/watch?v=yt123', url: 'https://yt.example/watch?v=yt123',
}); });
testState.canEmbedHosts = new Set(['yewtu.be']);
expect(getLinkMediaInfo('https://yewtu.be/invidious123')).toEqual({
patternThumbnailUrl: 'https://img.youtube.com/vi/invidious123/0.jpg',
type: 'iframe',
url: 'https://yewtu.be/invidious123',
});
}); });
it('builds comment media info and strips thumbnails for blacklisted domains', () => { it('builds comment media info and strips thumbnails for blacklisted domains', () => {
+26 -12
View File
@@ -1,5 +1,5 @@
import localForageLru from '@bitsocial/bitsocial-react-hooks/dist/lib/localforage-lru/index.js'; import localForageLru from '@bitsocial/bitsocial-react-hooks/dist/lib/localforage-lru/index.js';
import { canEmbed } from '../../components/embed'; import { canEmbed, getYouTubeVideoId } from '../../components/embed/embed-utils';
import memoize from 'memoizee'; import memoize from 'memoizee';
import { isPrivateNetworkHostname, isValidURL, parseHttpUrl } from './url-utils'; import { isPrivateNetworkHostname, isValidURL, parseHttpUrl } from './url-utils';
import { Capacitor, CapacitorHttp } from '@capacitor/core'; import { Capacitor, CapacitorHttp } from '@capacitor/core';
@@ -40,6 +40,31 @@ export const getDisplayMediaInfoType = (type: string, t: Translate) => {
} }
}; };
export const getYouTubeEmbedPostMediaFileLink = (commentMediaInfo: CommentMediaInfo | undefined): string | undefined => {
if (!commentMediaInfo || commentMediaInfo.type !== 'iframe' || !commentMediaInfo.url) {
return undefined;
}
const parsedUrl = parseHttpUrl(commentMediaInfo.url);
if (!parsedUrl || !getYouTubeVideoId(parsedUrl)) {
return undefined;
}
return commentMediaInfo.patternThumbnailUrl || getPatternThumbnailUrl(parsedUrl);
};
export const getPostMediaTypeLabel = (commentMediaInfo: CommentMediaInfo | undefined, resolvedType: string | undefined, t: Translate): string => {
if (getYouTubeEmbedPostMediaFileLink(commentMediaInfo)) {
return t('youtube_video');
}
if (!resolvedType) {
return '';
}
return getDisplayMediaInfoType(resolvedType, t);
};
export const getHasThumbnail = memoize( export const getHasThumbnail = memoize(
(commentMediaInfo: CommentMediaInfo | undefined, link: string | undefined): boolean => { (commentMediaInfo: CommentMediaInfo | undefined, link: string | undefined): boolean => {
if (!link || !commentMediaInfo) return false; if (!link || !commentMediaInfo) return false;
@@ -55,17 +80,6 @@ export const getHasThumbnail = memoize(
{ max: 1000 }, { max: 1000 },
); );
const getYouTubeVideoId = (url: URL): string | null => {
if (url.host.includes('youtu.be')) {
return url.pathname.slice(1);
} else if (url.pathname.includes('/shorts/')) {
return url.pathname.split('/shorts/')[1].split('/')[0];
} else if (url.searchParams.has('v')) {
return url.searchParams.get('v');
}
return null;
};
const getPatternThumbnailUrl = (url: URL): string | undefined => { const getPatternThumbnailUrl = (url: URL): string | undefined => {
const videoId = getYouTubeVideoId(url); const videoId = getYouTubeVideoId(url);
if (videoId) { if (videoId) {