mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
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:
+1
-1
@@ -71,7 +71,7 @@
|
||||
"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-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",
|
||||
"preview": "vite preview",
|
||||
"analyze-bundle": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=true vite build && npx source-map-explorer 'dist/assets/*.js'",
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "حظر",
|
||||
"timestamp": "الطابع الزمني",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "নিষেধ করুন",
|
||||
"timestamp": "টাইমস্ট্যাম্প",
|
||||
"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 ভিডিও"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "zakázat",
|
||||
"timestamp": "časové razítko",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "forbyd",
|
||||
"timestamp": "tidsstempel",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "sperren",
|
||||
"timestamp": "Zeitstempel",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "απαγόρευση",
|
||||
"timestamp": "χρονοσήμανση",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -418,5 +418,6 @@
|
||||
"ban": "ban",
|
||||
"timestamp": "timestamp",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "prohibir",
|
||||
"timestamp": "marca de tiempo",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "مسدود کردن",
|
||||
"timestamp": "مهر زمان",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "kielto",
|
||||
"timestamp": "aikaleimaustiedosto",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "ipagbawal",
|
||||
"timestamp": "timestamp",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "interdire",
|
||||
"timestamp": "horodatage",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "חסימה",
|
||||
"timestamp": "חותמת זמן",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "प्रतिबंध",
|
||||
"timestamp": "टाइमस्टैंप",
|
||||
"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 वीडियो"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "tiltás",
|
||||
"timestamp": "időbélyeg",
|
||||
"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ó"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "larangan",
|
||||
"timestamp": "stempel waktu",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "vietare",
|
||||
"timestamp": "marca temporale",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "ブロック",
|
||||
"timestamp": "タイムスタンプ",
|
||||
"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動画"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "차단",
|
||||
"timestamp": "타임스탬프",
|
||||
"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 동영상"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "प्रतिबंध",
|
||||
"timestamp": "टाइमस्टॅम्प",
|
||||
"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 व्हिडिओ"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "verbannen",
|
||||
"timestamp": "tijdstempel",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "forbud",
|
||||
"timestamp": "tidsstempel",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "zbanować",
|
||||
"timestamp": "znacznik czasu",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "banir",
|
||||
"timestamp": "marca de tempo",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "interzice",
|
||||
"timestamp": "marcă de timp",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "блокировка",
|
||||
"timestamp": "временная метка",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "ndalim",
|
||||
"timestamp": "vulë kohe",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "förbjud",
|
||||
"timestamp": "tidsstämpel",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "నిషేధం",
|
||||
"timestamp": "టైమ్స్టాంప్",
|
||||
"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 వీడియో"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "แบน",
|
||||
"timestamp": "แสตมป์เวลา",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "yasakla",
|
||||
"timestamp": "zaman damgası",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "заблокувати",
|
||||
"timestamp": "часова мітка",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "روک",
|
||||
"timestamp": "ٹائم سٹیمپ",
|
||||
"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 ویڈیو"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "cấm",
|
||||
"timestamp": "dấu thời gian",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -399,5 +399,6 @@
|
||||
"ban": "封禁",
|
||||
"timestamp": "时间戳",
|
||||
"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',
|
||||
getHasThumbnail: () => true,
|
||||
getMediaDimensions: () => '100x100',
|
||||
getPostMediaTypeLabel: (_commentMediaInfo: unknown, resolvedType?: string) => resolvedType ?? '',
|
||||
getYouTubeEmbedPostMediaFileLink: () => undefined,
|
||||
}));
|
||||
|
||||
vi.mock('../../lib/utils/post-utils', () => ({
|
||||
@@ -171,6 +173,7 @@ vi.mock('../../lib/utils/pending-approval-moderation', () => ({
|
||||
|
||||
vi.mock('../../lib/utils/url-utils', () => ({
|
||||
isValidURL: () => true,
|
||||
parseHttpUrl: (value: string) => new URL(value),
|
||||
}));
|
||||
|
||||
vi.mock('../../lib/utils/view-utils', () => ({
|
||||
|
||||
@@ -408,11 +408,11 @@ describe('CommentContent', () => {
|
||||
cid: 'post-2',
|
||||
content: 'ignored',
|
||||
postCid: 'post-2',
|
||||
reason: 'spam',
|
||||
reason: 'duplicate of >>96',
|
||||
removed: true,
|
||||
});
|
||||
expect(container.textContent).toContain('this_post_was_removed');
|
||||
expect(container.textContent).toContain('Reason: "spam"');
|
||||
expect(queryMarkdownText()).toEqual(['Reason: duplicate of >>96']);
|
||||
|
||||
await renderContent({
|
||||
cid: 'post-3',
|
||||
@@ -422,7 +422,7 @@ describe('CommentContent', () => {
|
||||
reason: 'self-delete',
|
||||
});
|
||||
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 () => {
|
||||
@@ -442,6 +442,7 @@ describe('CommentContent', () => {
|
||||
|
||||
expect(container.textContent).toContain('pending_mod_approval');
|
||||
expect(container.textContent).toContain('pending-reason:rules violation');
|
||||
expect(queryMarkdownText()).toEqual(['queued body', 'pending-reason:rules violation']);
|
||||
const tooltip = container.querySelector('[data-testid="tooltip"]');
|
||||
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 failedErrorMessage = formatErrorMessageForDisplay(failedError);
|
||||
const shouldShowUnpublishedStateDetails = !cid && (!hasFailedState || Boolean(failedError));
|
||||
const reasonMessage = reason ? `${capitalize(t('reason'))}: ${reason}` : undefined;
|
||||
const pendingApprovalReason = pendingApproval ? reason?.trim() : undefined;
|
||||
const pendingApprovalReasonMessage = pendingApprovalReason
|
||||
? t('pending_mod_approval_reason', {
|
||||
reason: pendingApprovalReason,
|
||||
interpolation: { escapeValue: false },
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const loadingString = (
|
||||
<div className={styles.stateString}>
|
||||
@@ -193,21 +200,21 @@ const CommentContent = ({
|
||||
{purged ? (
|
||||
<span className={styles.grayEditMessage}>{capitalize(t('this_post_was_purged'))}</span>
|
||||
) : removed ? (
|
||||
reason ? (
|
||||
reasonMessage ? (
|
||||
<>
|
||||
<span className={styles.redEditMessage}>({t('this_post_was_removed')})</span>
|
||||
<br />
|
||||
<br />
|
||||
<span className={styles.grayEditMessage}>{`${capitalize(t('reason'))}: "${reason}"`}</span>
|
||||
{renderContent(reasonMessage)}
|
||||
</>
|
||||
) : (
|
||||
<span className={styles.grayEditMessage}>{capitalize(t('this_post_was_removed'))}.</span>
|
||||
)
|
||||
) : deleted ? (
|
||||
reason ? (
|
||||
reasonMessage ? (
|
||||
<>
|
||||
<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>
|
||||
@@ -220,18 +227,13 @@ const CommentContent = ({
|
||||
<br />
|
||||
<br />
|
||||
<span className={styles.pendingApproval}>({t('pending_mod_approval')})</span>
|
||||
{pendingApprovalReason && (
|
||||
{pendingApprovalReasonMessage ? (
|
||||
<>
|
||||
<br />
|
||||
<br />
|
||||
<span className={styles.grayEditMessage}>
|
||||
{t('pending_mod_approval_reason', {
|
||||
reason: pendingApprovalReason,
|
||||
interpolation: { escapeValue: false },
|
||||
})}
|
||||
</span>
|
||||
{renderContent(pendingApprovalReasonMessage)}
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
{((!isInPostView && content?.length > 1000 && !showFullComment) || (isInPostView && content?.length > 2000 && !showFullComment)) && !isPrivilegedAuthor && (
|
||||
|
||||
@@ -295,6 +295,21 @@ describe('CommentMedia', () => {
|
||||
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 () => {
|
||||
testState.isMobile = true;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createElement } from 'react';
|
||||
import { createRoot, type Root } from 'react-dom/client';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
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;
|
||||
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('');
|
||||
});
|
||||
|
||||
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', () => {
|
||||
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);
|
||||
|
||||
@@ -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>([
|
||||
'youtube.com',
|
||||
'www.youtube.com',
|
||||
@@ -46,6 +48,35 @@ const canEmbedHosts = new Set<string>([
|
||||
...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 => {
|
||||
if (xHosts.has(parsedUrl.host)) {
|
||||
return Boolean(getXTweetId(parsedUrl));
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
streamableHosts,
|
||||
tiktokHosts,
|
||||
twitchHosts,
|
||||
getYouTubeVideoId,
|
||||
xHosts,
|
||||
youtubeHosts,
|
||||
} from './embed-utils';
|
||||
@@ -66,28 +67,11 @@ const remoteEmbedSandbox = `${srcDocSandbox} allow-forms allow-presentation allo
|
||||
const YoutubeEmbed = ({ parsedUrl }: EmbedComponentProps) => {
|
||||
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')) {
|
||||
if (parsedUrl.searchParams.has('list') && !getYouTubeVideoId(parsedUrl)) {
|
||||
const playlistId = parsedUrl.searchParams.get('list');
|
||||
embedSrc = `https://www.youtube.com/embed/videoseries?list=${playlistId}`;
|
||||
} else {
|
||||
let videoId = parsedUrl.searchParams.get('v');
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const videoId = getYouTubeVideoId(parsedUrl);
|
||||
if (videoId) {
|
||||
embedSrc = `https://www.youtube.com/embed/${videoId}`;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
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>,
|
||||
internalPathByHref: {} as Record<string, string | null>,
|
||||
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>>,
|
||||
unavailableCids: new Set<string>(),
|
||||
}));
|
||||
@@ -42,20 +44,32 @@ vi.mock('@floating-ui/react', () => ({
|
||||
shift: () => ({}),
|
||||
size: () => ({}),
|
||||
useDismiss: () => ({}),
|
||||
useFloating: () => ({
|
||||
context: {},
|
||||
floatingStyles: {},
|
||||
refs: {
|
||||
setFloating: () => undefined,
|
||||
setReference: () => undefined,
|
||||
},
|
||||
update: () => undefined,
|
||||
}),
|
||||
useFloating: ({ onOpenChange, open }: { onOpenChange?: (open: boolean) => void; open?: boolean }) => {
|
||||
testState.floatingOnOpenChange = onOpenChange ?? null;
|
||||
return {
|
||||
context: {},
|
||||
floatingStyles: {},
|
||||
open,
|
||||
onOpenChange,
|
||||
refs: {
|
||||
setFloating: () => undefined,
|
||||
setReference: () => undefined,
|
||||
},
|
||||
update: () => undefined,
|
||||
};
|
||||
},
|
||||
useFocus: () => ({}),
|
||||
useHover: () => ({}),
|
||||
useInteractions: () => ({
|
||||
useHover: () => ({
|
||||
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 || {},
|
||||
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', () => ({
|
||||
getHasThumbnail: (linkMediaInfo?: { thumbnail?: string; type?: string }, href?: string) =>
|
||||
Boolean(linkMediaInfo?.thumbnail || linkMediaInfo?.type === 'image' || (href && testState.mediaInfoByHref[href]?.thumbnail)),
|
||||
getHasThumbnail: (linkMediaInfo?: { patternThumbnailUrl?: string; thumbnail?: string; type?: string }, href?: string) =>
|
||||
Boolean(
|
||||
linkMediaInfo?.thumbnail ||
|
||||
linkMediaInfo?.patternThumbnailUrl ||
|
||||
linkMediaInfo?.type === 'image' ||
|
||||
(href && (testState.mediaInfoByHref[href]?.thumbnail || testState.mediaInfoByHref[href]?.patternThumbnailUrl)),
|
||||
),
|
||||
getLinkMediaInfo: (href: string) => testState.mediaInfoByHref[href],
|
||||
}));
|
||||
|
||||
@@ -139,8 +158,28 @@ vi.mock('../../../stores/use-post-number-store', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../../comment-media', () => ({
|
||||
default: ({ commentMediaInfo }: { commentMediaInfo?: { type?: string; url?: string } }) =>
|
||||
createElement('div', { 'data-testid': 'comment-media' }, `${commentMediaInfo?.type}:${commentMediaInfo?.url}`),
|
||||
default: ({
|
||||
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', () => ({
|
||||
@@ -198,6 +237,8 @@ describe('Markdown', () => {
|
||||
testState.internalPathByHref = {};
|
||||
testState.isMobile = false;
|
||||
testState.mediaInfoByHref = {};
|
||||
testState.floatingMediaProps = null;
|
||||
testState.floatingOnOpenChange = null;
|
||||
testState.numberToCid = {};
|
||||
testState.unavailableCids = new Set<string>();
|
||||
|
||||
@@ -460,6 +501,33 @@ describe('Markdown', () => {
|
||||
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 () => {
|
||||
testState.mediaInfoByHref = {
|
||||
'https://cdn.example/image.png': {
|
||||
|
||||
@@ -123,8 +123,8 @@ const ContentLinkEmbed = ({ children, href, linkMediaInfo }: ContentLinkEmbedPro
|
||||
disableToggle={true}
|
||||
isFloatingEmbed={true}
|
||||
isReply={false}
|
||||
setShowThumbnail={setShowMedia}
|
||||
showThumbnail={false}
|
||||
setShowThumbnail={() => {}}
|
||||
showThumbnail={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -5,11 +5,11 @@ import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
||||
import { Comment, useEditedComment, useReplies, useAccount } from '@bitsocial/bitsocial-react-hooks';
|
||||
import getShortAddress from '../../lib/get-short-address';
|
||||
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 { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
||||
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 { formatUserIDForDisplay, truncateWithEllipsisInMiddle } from '../../lib/utils/string-utils';
|
||||
import useModQueueStore from '../../stores/use-mod-queue-store';
|
||||
@@ -642,12 +642,16 @@ const PostMedia = ({
|
||||
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 mediaDimensions = getMediaDimensions(commentMediaInfo);
|
||||
const directoryEntry = findDirectoryByAddress(directories, communityAddress);
|
||||
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 displayBoardPath =
|
||||
boardPath && communityAddress && boardPath !== communityAddress
|
||||
@@ -666,10 +670,11 @@ const PostMedia = ({
|
||||
{t('board')}: <Link to={`/${boardPath}`}>{displayBoardPath}</Link>{' '}
|
||||
</>
|
||||
)}
|
||||
{requirePostLinkIsMedia ? t('file') : t('link')}:{' '}
|
||||
<a href={url} target='_blank' rel='noopener noreferrer'>
|
||||
{fileLabel}:{' '}
|
||||
<a href={fileLinkUrl} target='_blank' rel='noopener noreferrer'>
|
||||
{(() => {
|
||||
if (spoiler) return capitalize(t('spoiler'));
|
||||
if (youtubeFileLink) return truncateWithEllipsisInMiddle(youtubeFileLink);
|
||||
if (requirePostLinkIsMedia && url) {
|
||||
try {
|
||||
const pathParts = new URL(url).pathname.split('/');
|
||||
@@ -677,10 +682,10 @@ const PostMedia = ({
|
||||
if (filename && /\.\w+$/.test(filename)) return truncateWithEllipsisInMiddle(filename);
|
||||
} catch {}
|
||||
}
|
||||
return truncateWithEllipsisInMiddle(url ?? '');
|
||||
return truncateWithEllipsisInMiddle(fileLinkUrl ?? '');
|
||||
})()}
|
||||
</a>{' '}
|
||||
({type && lowerCase(getDisplayMediaInfoType(type, t))}
|
||||
({mediaTypeLabel}
|
||||
{mediaDimensions && `, ${mediaDimensions}`})
|
||||
{!showThumbnail && (type === 'iframe' || type === 'video' || type === 'audio') && (
|
||||
<span>
|
||||
|
||||
@@ -444,6 +444,7 @@ describe('PostForm', () => {
|
||||
testState.directories = [
|
||||
{ address: 'music-posting.eth', features: {}, title: '/mu/ - Music' },
|
||||
{ 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: '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 () => {
|
||||
testState.resolvedCommunityAddress = 'politically-incorrect.bso';
|
||||
|
||||
|
||||
@@ -391,7 +391,7 @@ const PostFormFields = ({
|
||||
<tr>
|
||||
<td>{t('tag')}</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>
|
||||
{flashTagOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
|
||||
@@ -340,6 +340,18 @@ describe('ReplyModal', () => {
|
||||
features: { hasFlags: true },
|
||||
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': {
|
||||
address: 'random-nsfw.bso',
|
||||
features: {},
|
||||
@@ -494,8 +506,12 @@ describe('ReplyModal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('publishes geographic location on /bant/ without showing a flag selector', async () => {
|
||||
await renderReplyModal('/bant/thread/post-1', 'international-nsfw.bso');
|
||||
it.each([
|
||||
{ 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();
|
||||
|
||||
|
||||
+67
@@ -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) }));
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const fetchMock = vi.fn(async (url: string | URL | Request) => {
|
||||
const requestUrl = String(url);
|
||||
|
||||
@@ -17,30 +17,34 @@ describe('comment-flag-selection', () => {
|
||||
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(
|
||||
getCommentFlagOptionsForDirectory({
|
||||
directoryCode: 'int',
|
||||
directoryCode: 'fit',
|
||||
features: { hasFlags: true },
|
||||
title: '/int/ - International',
|
||||
title: '/fit/ - Fitness',
|
||||
}),
|
||||
).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(
|
||||
getCommentFlagOptionsForDirectory({
|
||||
directoryCode: 'bant',
|
||||
directoryCode,
|
||||
features: { hasFlags: true },
|
||||
title: '/bant/ - International/Random',
|
||||
title,
|
||||
}),
|
||||
).toEqual([]);
|
||||
|
||||
expect(
|
||||
getCommentFlagPublishOptionsForDirectory({
|
||||
directoryCode: 'bant',
|
||||
directoryCode,
|
||||
features: { hasFlags: true },
|
||||
title: '/bant/ - International/Random',
|
||||
title,
|
||||
}),
|
||||
).toEqual({
|
||||
challengeRequest: {
|
||||
|
||||
@@ -37,6 +37,13 @@ describe('extractIpv6FromAddress', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
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', () => {
|
||||
@@ -261,6 +268,31 @@ describe('fetchPeerMapLocation', () => {
|
||||
|
||||
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', () => {
|
||||
|
||||
@@ -38,7 +38,7 @@ const NO_FLAG_OPTION: CommentFlagSelectOption = {
|
||||
};
|
||||
|
||||
/** 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 directoryCode = directory?.directoryCode?.trim().toLowerCase();
|
||||
|
||||
+11
-1
@@ -284,7 +284,17 @@ export const extractIpv4FromAddress = (address: string): string | null => {
|
||||
|
||||
export const extractIpv6FromAddress = (address: string): string | null => {
|
||||
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 => {
|
||||
|
||||
@@ -20,9 +20,13 @@ vi.mock('@bitsocial/bitsocial-react-hooks/dist/lib/localforage-lru/index.js', ()
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../../components/embed', () => ({
|
||||
canEmbed: (url: URL) => testState.canEmbedHosts.has(url.hostname),
|
||||
}));
|
||||
vi.mock('../../../components/embed/embed-utils', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../../components/embed/embed-utils')>();
|
||||
return {
|
||||
...actual,
|
||||
canEmbed: (url: URL) => testState.canEmbedHosts.has(url.hostname),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@capacitor/core', () => ({
|
||||
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 memoized = fn as { clear?: () => void };
|
||||
@@ -102,6 +115,18 @@ describe('media-utils', () => {
|
||||
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', () => {
|
||||
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);
|
||||
@@ -156,6 +181,12 @@ describe('media-utils', () => {
|
||||
type: 'iframe',
|
||||
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', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { isPrivateNetworkHostname, isValidURL, parseHttpUrl } from './url-utils';
|
||||
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(
|
||||
(commentMediaInfo: CommentMediaInfo | undefined, link: string | undefined): boolean => {
|
||||
if (!link || !commentMediaInfo) return false;
|
||||
@@ -55,17 +80,6 @@ export const getHasThumbnail = memoize(
|
||||
{ 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 videoId = getYouTubeVideoId(url);
|
||||
if (videoId) {
|
||||
|
||||
Reference in New Issue
Block a user