From 9b3a95dd95b5b85e4ee40ad0826162ca4b87d31d Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Sat, 30 May 2026 16:07:41 +0700 Subject: [PATCH] feat(flash board): add SWF posting support (#1145) --- .../fivechan/android/FileUploaderPlugin.java | 9 +- electron/media-upload-automation.js | 2 +- electron/media-upload-automation.test.js | 5 + package.json | 1 + public/translations/ar/default.json | 4 +- public/translations/bn/default.json | 4 +- public/translations/cs/default.json | 4 +- public/translations/da/default.json | 4 +- public/translations/de/default.json | 4 +- public/translations/el/default.json | 4 +- public/translations/en/default.json | 4 +- public/translations/es/default.json | 4 +- public/translations/fa/default.json | 4 +- public/translations/fi/default.json | 4 +- public/translations/fil/default.json | 4 +- public/translations/fr/default.json | 4 +- public/translations/he/default.json | 4 +- public/translations/hi/default.json | 4 +- public/translations/hu/default.json | 4 +- public/translations/id/default.json | 4 +- public/translations/it/default.json | 4 +- public/translations/ja/default.json | 4 +- public/translations/ko/default.json | 4 +- public/translations/mr/default.json | 4 +- public/translations/nl/default.json | 4 +- public/translations/no/default.json | 4 +- public/translations/pl/default.json | 4 +- public/translations/pt/default.json | 4 +- public/translations/ro/default.json | 4 +- public/translations/ru/default.json | 4 +- public/translations/sq/default.json | 4 +- public/translations/sv/default.json | 4 +- public/translations/te/default.json | 4 +- public/translations/th/default.json | 4 +- public/translations/tr/default.json | 4 +- public/translations/uk/default.json | 4 +- public/translations/ur/default.json | 4 +- public/translations/vi/default.json | 4 +- public/translations/zh/default.json | 4 +- scripts/sync-directories.js | 60 ++++++- .../__tests__/comment-media.test.tsx | 62 +++++++ .../comment-media/comment-media.module.css | 66 +++++++ .../comment-media/comment-media.tsx | 11 +- .../comment-media/ruffle-player.tsx | 127 +++++++++++++ .../flash-board-table.module.css | 166 +++++++++++++++++ .../flash-board-table/flash-board-table.tsx | 168 ++++++++++++++++++ src/components/post-desktop/post-desktop.tsx | 2 + src/components/post-flash-tag.tsx | 23 +++ .../post-form/__tests__/post-form.test.tsx | 77 +++++++- src/components/post-form/post-form.tsx | 57 +++++- src/components/post-mobile/post-mobile.tsx | 2 + src/data/5chan-directory-lists.json | 33 +++- src/hooks/__tests__/use-directories.test.tsx | 6 +- src/hooks/__tests__/use-file-upload.test.ts | 1 + src/hooks/use-file-upload.ts | 2 +- src/lib/__tests__/flash-tags.test.ts | 45 +++++ src/lib/flash-tags.ts | 61 +++++++ .../__tests__/direct-url.test.ts | 5 + src/lib/media-hosting/direct-url.ts | 6 +- .../__tests__/directory-list-utils.test.ts | 34 +++- src/lib/utils/__tests__/media-utils.test.ts | 4 + src/lib/utils/directory-list-utils.ts | 16 ++ src/lib/utils/media-utils.ts | 11 +- src/types/ruffle.d.ts | 23 +++ src/views/board/__tests__/board.test.tsx | 125 ++++++++++++- src/views/board/board.tsx | 22 ++- src/views/post/post.module.css | 4 + vite.config.js | 51 +++++- yarn.lock | 8 + 69 files changed, 1372 insertions(+), 63 deletions(-) create mode 100644 src/components/comment-media/ruffle-player.tsx create mode 100644 src/components/flash-board-table/flash-board-table.module.css create mode 100644 src/components/flash-board-table/flash-board-table.tsx create mode 100644 src/components/post-flash-tag.tsx create mode 100644 src/lib/__tests__/flash-tags.test.ts create mode 100644 src/lib/flash-tags.ts create mode 100644 src/types/ruffle.d.ts diff --git a/android/app/src/main/java/fivechan/android/FileUploaderPlugin.java b/android/app/src/main/java/fivechan/android/FileUploaderPlugin.java index 61d73831..08a0c7d6 100644 --- a/android/app/src/main/java/fivechan/android/FileUploaderPlugin.java +++ b/android/app/src/main/java/fivechan/android/FileUploaderPlugin.java @@ -44,7 +44,14 @@ public class FileUploaderPlugin extends Plugin { } Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("*/*"); - String[] mimeTypes = {"image/jpeg", "image/png", "video/mp4", "video/webm"}; + String[] mimeTypes = { + "image/jpeg", + "image/png", + "video/mp4", + "video/webm", + "application/x-shockwave-flash", + "application/vnd.adobe.flash.movie" + }; intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes); startActivityForResult(call, intent, "pickFileResult"); } diff --git a/electron/media-upload-automation.js b/electron/media-upload-automation.js index 9b4daff4..540746a0 100644 --- a/electron/media-upload-automation.js +++ b/electron/media-upload-automation.js @@ -14,7 +14,7 @@ import { MEDIA_UPLOAD_RECIPES } from './media-upload-recipes.js'; const POLL_INTERVAL_MS = 500; /** File extensions that denote direct media URLs (mirrors src/lib/media-hosting/direct-url.ts) */ -const DIRECT_MEDIA_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.webm', '.mp4', '.mov', '.avi', '.mkv', '.gifv']; +const DIRECT_MEDIA_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.webm', '.mp4', '.mov', '.avi', '.mkv', '.gifv', '.swf']; /** Returns true if the URL appears to point to a direct media file. Exported for tests. */ export function isDirectMediaUrl(url) { diff --git a/electron/media-upload-automation.test.js b/electron/media-upload-automation.test.js index 6ce7aad0..8da7f2ba 100644 --- a/electron/media-upload-automation.test.js +++ b/electron/media-upload-automation.test.js @@ -105,6 +105,11 @@ describe('media-upload-automation', () => { expect(isDirectMediaUrl('https://example.com/video.mp4')).toBe(true); }); + it('returns true for Flash movie extensions', () => { + expect(isDirectMediaUrl('https://example.com/movie.swf')).toBe(true); + expect(isDirectMediaUrl('https://example.com/movie.SWF?download=1')).toBe(true); + }); + it('strips query strings and fragments before checking', () => { expect(isDirectMediaUrl('https://i.imgur.com/abc.png?size=large')).toBe(true); expect(isDirectMediaUrl('https://imgur.com/page.html?img=photo.jpg')).toBe(false); diff --git a/package.json b/package.json index 37bd0b02..061eb433 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@floating-ui/react": "0.26.1", "@pkcprotocol/pkc-js": "0.0.37", "@react-spring/web": "10.0.3", + "@ruffle-rs/ruffle": "0.2.0", "@types/node": "20.19.37", "@types/react": "18.2.25", "@types/react-dom": "18.2.10", diff --git a/public/translations/ar/default.json b/public/translations/ar/default.json index 25c4e071..2475c092 100644 --- a/public/translations/ar/default.json +++ b/public/translations/ar/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "مدة الحظر بالأيام", "challenge_answer": "إجابة التحدي", "ban": "حظر", - "timestamp": "الطابع الزمني" + "timestamp": "الطابع الزمني", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/bn/default.json b/public/translations/bn/default.json index d4d5bc62..a8f9da3b 100644 --- a/public/translations/bn/default.json +++ b/public/translations/bn/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "ব্যান মেয়াদ দিনে", "challenge_answer": "চ্যালেঞ্জ উত্তর", "ban": "নিষেধ করুন", - "timestamp": "টাইমস্ট্যাম্প" + "timestamp": "টাইমস্ট্যাম্প", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/cs/default.json b/public/translations/cs/default.json index 0e43bedd..b62cd25f 100644 --- a/public/translations/cs/default.json +++ b/public/translations/cs/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "doba zákazu v dnech", "challenge_answer": "odpověď na výzvu", "ban": "zakázat", - "timestamp": "časové razítko" + "timestamp": "časové razítko", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/da/default.json b/public/translations/da/default.json index 2cefbb46..17045636 100644 --- a/public/translations/da/default.json +++ b/public/translations/da/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "banvarighed i dage", "challenge_answer": "svar på udfordring", "ban": "forbyd", - "timestamp": "tidsstempel" + "timestamp": "tidsstempel", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/de/default.json b/public/translations/de/default.json index 43de0d67..c9e0ec33 100644 --- a/public/translations/de/default.json +++ b/public/translations/de/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "bannierungsdauer in tagen", "challenge_answer": "herausforderungsantwort", "ban": "sperren", - "timestamp": "Zeitstempel" + "timestamp": "Zeitstempel", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/el/default.json b/public/translations/el/default.json index 256a9f9e..2ae041a2 100644 --- a/public/translations/el/default.json +++ b/public/translations/el/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "διάρκεια απαγόρευσης σε ημέρες", "challenge_answer": "απάντηση πρόκλησης", "ban": "απαγόρευση", - "timestamp": "χρονοσήμανση" + "timestamp": "χρονοσήμανση", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/en/default.json b/public/translations/en/default.json index 86a5bf99..37c97a0c 100644 --- a/public/translations/en/default.json +++ b/public/translations/en/default.json @@ -416,5 +416,7 @@ "ban_duration_days": "ban duration in days", "challenge_answer": "challenge answer", "ban": "ban", - "timestamp": "timestamp" + "timestamp": "timestamp", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/es/default.json b/public/translations/es/default.json index be0682a1..39162f60 100644 --- a/public/translations/es/default.json +++ b/public/translations/es/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "duración del baneo en días", "challenge_answer": "respuesta del desafío", "ban": "prohibir", - "timestamp": "marca de tiempo" + "timestamp": "marca de tiempo", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/fa/default.json b/public/translations/fa/default.json index eb474882..c9ad89dc 100644 --- a/public/translations/fa/default.json +++ b/public/translations/fa/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "مدت بن به روز", "challenge_answer": "پاسخ چالش", "ban": "مسدود کردن", - "timestamp": "مهر زمان" + "timestamp": "مهر زمان", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/fi/default.json b/public/translations/fi/default.json index 56a35535..d91352b5 100644 --- a/public/translations/fi/default.json +++ b/public/translations/fi/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "kiellon kesto päivissä", "challenge_answer": "haasteen vastaus", "ban": "kielto", - "timestamp": "aikaleimaustiedosto" + "timestamp": "aikaleimaustiedosto", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/fil/default.json b/public/translations/fil/default.json index 9410ff0f..8b3d74e5 100644 --- a/public/translations/fil/default.json +++ b/public/translations/fil/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "tagal ng ban sa araw", "challenge_answer": "sagot sa hamon", "ban": "ipagbawal", - "timestamp": "timestamp" + "timestamp": "timestamp", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/fr/default.json b/public/translations/fr/default.json index b86ba1f4..45aca0d6 100644 --- a/public/translations/fr/default.json +++ b/public/translations/fr/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "durée du bannissement en jours", "challenge_answer": "réponse au défi", "ban": "interdire", - "timestamp": "horodatage" + "timestamp": "horodatage", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/he/default.json b/public/translations/he/default.json index 9a6d8325..046059c4 100644 --- a/public/translations/he/default.json +++ b/public/translations/he/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "משך הבן בימים", "challenge_answer": "תשובת אתגר", "ban": "חסימה", - "timestamp": "חותמת זמן" + "timestamp": "חותמת זמן", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/hi/default.json b/public/translations/hi/default.json index 3aca4ada..b8a62a8c 100644 --- a/public/translations/hi/default.json +++ b/public/translations/hi/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "प्रतिबंध की अवधि दिनों में", "challenge_answer": "चुनौती उत्तर", "ban": "प्रतिबंध", - "timestamp": "टाइमस्टैंप" + "timestamp": "टाइमस्टैंप", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/hu/default.json b/public/translations/hu/default.json index 9c2916f2..18e701de 100644 --- a/public/translations/hu/default.json +++ b/public/translations/hu/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "kitiltás időtartama napokban", "challenge_answer": "kihívás válasza", "ban": "tiltás", - "timestamp": "időbélyeg" + "timestamp": "időbélyeg", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/id/default.json b/public/translations/id/default.json index cc58d8a2..68872425 100644 --- a/public/translations/id/default.json +++ b/public/translations/id/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "durasi larangan dalam hari", "challenge_answer": "jawaban tantangan", "ban": "larangan", - "timestamp": "stempel waktu" + "timestamp": "stempel waktu", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/it/default.json b/public/translations/it/default.json index f2360635..bc32a557 100644 --- a/public/translations/it/default.json +++ b/public/translations/it/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "durata del ban in giorni", "challenge_answer": "risposta alla sfida", "ban": "vietare", - "timestamp": "marca temporale" + "timestamp": "marca temporale", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/ja/default.json b/public/translations/ja/default.json index aa167399..4c513368 100644 --- a/public/translations/ja/default.json +++ b/public/translations/ja/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "バン期間(日数)", "challenge_answer": "チャレンジ回答", "ban": "ブロック", - "timestamp": "タイムスタンプ" + "timestamp": "タイムスタンプ", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/ko/default.json b/public/translations/ko/default.json index 59b9b40a..38fa563d 100644 --- a/public/translations/ko/default.json +++ b/public/translations/ko/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "차단 기간(일)", "challenge_answer": "챌린지 답변", "ban": "차단", - "timestamp": "타임스탬프" + "timestamp": "타임스탬프", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/mr/default.json b/public/translations/mr/default.json index 142c6994..b49f090a 100644 --- a/public/translations/mr/default.json +++ b/public/translations/mr/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "बंद मुदत दिवसांमध्ये", "challenge_answer": "आव्हान उत्तर", "ban": "प्रतिबंध", - "timestamp": "टाइमस्टॅम्प" + "timestamp": "टाइमस्टॅम्प", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/nl/default.json b/public/translations/nl/default.json index d3d6d222..7c27d424 100644 --- a/public/translations/nl/default.json +++ b/public/translations/nl/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "banningsduur in dagen", "challenge_answer": "antwoord op uitdaging", "ban": "verbannen", - "timestamp": "tijdstempel" + "timestamp": "tijdstempel", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/no/default.json b/public/translations/no/default.json index d0c16b40..ae92db17 100644 --- a/public/translations/no/default.json +++ b/public/translations/no/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "bannvarighet i dager", "challenge_answer": "svar på utfordring", "ban": "forbud", - "timestamp": "tidsstempel" + "timestamp": "tidsstempel", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/pl/default.json b/public/translations/pl/default.json index 62b3a662..16531a36 100644 --- a/public/translations/pl/default.json +++ b/public/translations/pl/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "czas trwania banu w dniach", "challenge_answer": "odpowiedź na wyzwanie", "ban": "zbanować", - "timestamp": "znacznik czasu" + "timestamp": "znacznik czasu", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/pt/default.json b/public/translations/pt/default.json index 491378f3..8f312551 100644 --- a/public/translations/pt/default.json +++ b/public/translations/pt/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "duração do banimento em dias", "challenge_answer": "resposta do desafio", "ban": "banir", - "timestamp": "marca de tempo" + "timestamp": "marca de tempo", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/ro/default.json b/public/translations/ro/default.json index dec40c4c..7e89022c 100644 --- a/public/translations/ro/default.json +++ b/public/translations/ro/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "durata banului în zile", "challenge_answer": "răspunsul la provocare", "ban": "interzice", - "timestamp": "marcă de timp" + "timestamp": "marcă de timp", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/ru/default.json b/public/translations/ru/default.json index 2707ffef..3d40efd8 100644 --- a/public/translations/ru/default.json +++ b/public/translations/ru/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "продолжительность бана в днях", "challenge_answer": "ответ на задачу", "ban": "блокировка", - "timestamp": "временная метка" + "timestamp": "временная метка", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/sq/default.json b/public/translations/sq/default.json index a50991bb..aeeefa4f 100644 --- a/public/translations/sq/default.json +++ b/public/translations/sq/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "kohëzgjatja e ndalimit në ditë", "challenge_answer": "përgjigje ndaj sfidës", "ban": "ndalim", - "timestamp": "vulë kohe" + "timestamp": "vulë kohe", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/sv/default.json b/public/translations/sv/default.json index c085d8f5..9ccc27ce 100644 --- a/public/translations/sv/default.json +++ b/public/translations/sv/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "bannlängd i dagar", "challenge_answer": "svar på utmaning", "ban": "förbjud", - "timestamp": "tidsstämpel" + "timestamp": "tidsstämpel", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/te/default.json b/public/translations/te/default.json index 889e6df9..2beb4767 100644 --- a/public/translations/te/default.json +++ b/public/translations/te/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "నిషేధ వ్యవధి రోజుల్లో", "challenge_answer": "సవాలు సమాధానం", "ban": "నిషేధం", - "timestamp": "టైమ్‌స్టాంప్" + "timestamp": "టైమ్‌స్టాంప్", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/th/default.json b/public/translations/th/default.json index 49a9aa98..f7107f17 100644 --- a/public/translations/th/default.json +++ b/public/translations/th/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "ระยะเวลาแบนในวัน", "challenge_answer": "คำตอบความท้าทาย", "ban": "แบน", - "timestamp": "แสตมป์เวลา" + "timestamp": "แสตมป์เวลา", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/tr/default.json b/public/translations/tr/default.json index ac0ff0a0..2d84909d 100644 --- a/public/translations/tr/default.json +++ b/public/translations/tr/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "ban süresi gün cinsinden", "challenge_answer": "zorluk yanıtı", "ban": "yasakla", - "timestamp": "zaman damgası" + "timestamp": "zaman damgası", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/uk/default.json b/public/translations/uk/default.json index f7390ced..06e09b04 100644 --- a/public/translations/uk/default.json +++ b/public/translations/uk/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "тривалість бану в днях", "challenge_answer": "відповідь на виклик", "ban": "заблокувати", - "timestamp": "часова мітка" + "timestamp": "часова мітка", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/ur/default.json b/public/translations/ur/default.json index 63b647e5..47eee317 100644 --- a/public/translations/ur/default.json +++ b/public/translations/ur/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "بین کی مدت دنوں میں", "challenge_answer": "چ یلنج کا جواب", "ban": "روک", - "timestamp": "ٹائم سٹیمپ" + "timestamp": "ٹائم سٹیمپ", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/vi/default.json b/public/translations/vi/default.json index 469c950a..b99c911a 100644 --- a/public/translations/vi/default.json +++ b/public/translations/vi/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "thời gian cấm theo ngày", "challenge_answer": "câu trả lời thử thách", "ban": "cấm", - "timestamp": "dấu thời gian" + "timestamp": "dấu thời gian", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/public/translations/zh/default.json b/public/translations/zh/default.json index 9d65fddb..96c68340 100644 --- a/public/translations/zh/default.json +++ b/public/translations/zh/default.json @@ -397,5 +397,7 @@ "ban_duration_days": "封禁时长(天数)", "challenge_answer": "挑战答案", "ban": "封禁", - "timestamp": "时间戳" + "timestamp": "时间戳", + "tag": "tag", + "post_form_flash_upload_prompt": "Recommended SWF host: 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." } diff --git a/scripts/sync-directories.js b/scripts/sync-directories.js index 785e9f5e..b0e9e27e 100644 --- a/scripts/sync-directories.js +++ b/scripts/sync-directories.js @@ -23,8 +23,12 @@ const DEFAULT_METADATA = { const DEFAULTS_FILE_NAME = '5chan-directories-defaults.json'; const DIRECTORY_LIST_FILE_RE = /^5chan-.+-directory\.json$/; +const FLASH_DIRECTORY_RULES = [ + 'The tagging of uploaded files is mandatory. Improperly tagged items may be removed without notice. Abuse of the tagging system may result in temporary ban.', +]; const DIRECTORY_CODE_ORDER = [ 'a', + 'f', 'co', 'ck', 'pol', @@ -58,11 +62,53 @@ const DIRECTORY_CODE_ORDER = [ ]; const DIRECTORY_CODE_ORDER_INDEX = new Map(DIRECTORY_CODE_ORDER.map((code, index) => [code, index])); +const BOOTSTRAP_DIRECTORY_LISTS = [ + { + directoryCode: 'f', + title: '/f/ - Flash', + description: + 'Boards competing to host the /f/ directory on 5chan. The highest-scoring board resolves the directory code; if it goes offline, 5chan rotates to the next-highest. Anyone can open a PR on this file to add their board.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories/5chan-f-directory.json', + features: { + pseudonymityMode: 'per-reply', + safeForWork: false, + hasFlags: false, + postFlairs: true, + requirePostFlairs: true, + requirePostLink: true, + requirePostLinkIsMedia: false, + bumpLimit: 300, + noSpoilers: true, + noSpoilerReplies: true, + postsPerPage: 50, + }, + rules: FLASH_DIRECTORY_RULES, + createdAt: 1780123897, + updatedAt: 1780123897, + boards: [ + { + address: 'flash-posting.bso', + publicKey: '12D3KooWPFckNTD8YHVJrjpa9hRYvuomvM9VsvQQkJqjWRtpLv1F', + owner: 'plebeius.bso', + addedAt: 1780123897, + }, + ], + }, +]; + const isRecord = (value) => typeof value === 'object' && value !== null; const isDirectoryListFile = (fileName) => DIRECTORY_LIST_FILE_RE.test(fileName); const toNumber = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : undefined); const toString = (value) => (typeof value === 'string' && value.length > 0 ? value : undefined); +const normalizeRules = (value) => { + if (!Array.isArray(value)) { + return undefined; + } + + const rules = value.filter((rule) => typeof rule === 'string' && rule.length > 0); + return rules.length > 0 ? rules : undefined; +}; + const normalizeFeatures = (value) => { if (!isRecord(value)) { return undefined; @@ -112,10 +158,12 @@ const normalizeDirectoryDefaultsEntry = (code, raw) => { } const features = normalizeFeatures(raw.features); + const rules = normalizeRules(raw.rules); return { directoryCode: toString(raw.directoryCode) || code, ...(toString(raw.title) ? { title: toString(raw.title) } : {}), ...(features ? { features } : {}), + ...(rules ? { rules } : {}), }; }; @@ -141,12 +189,14 @@ const normalizeDirectoryList = (raw, fallbackCode, defaults) => { const rawCode = toString(raw.directoryCode); const defaultEntry = defaults?.directories?.[rawCode || fallbackCode] || defaults?.directories?.[fallbackCode]; const features = normalizeFeatures(defaultEntry?.features) || normalizeFeatures(raw.features); + const rules = normalizeRules(raw.rules); return { directoryCode: toString(defaultEntry?.directoryCode) || rawCode || fallbackCode, ...(toString(defaultEntry?.title) ? { title: toString(defaultEntry.title) } : toString(raw.title) ? { title: toString(raw.title) } : {}), ...(toString(raw.description) ? { description: toString(raw.description) } : {}), ...(features ? { features } : {}), + ...(rules ? { rules } : {}), ...(toNumber(raw.createdAt) !== undefined ? { createdAt: toNumber(raw.createdAt) } : {}), ...(toNumber(raw.updatedAt) !== undefined ? { updatedAt: toNumber(raw.updatedAt) } : {}), boards, @@ -165,8 +215,14 @@ const sortDirectoryLists = (lists) => return a.directoryCode.localeCompare(b.directoryCode); }); +const addBootstrapDirectoryLists = (directories) => { + const codes = new Set(directories.map((directory) => directory.directoryCode)); + return [...directories, ...BOOTSTRAP_DIRECTORY_LISTS.filter((directory) => !codes.has(directory.directoryCode))]; +}; + const toDirectoryListsData = (directories, fallbackMetadata = DEFAULT_METADATA) => { - const timestamps = [fallbackMetadata.createdAt, fallbackMetadata.updatedAt, ...directories.flatMap((list) => [list.createdAt, list.updatedAt])].filter( + const directoryLists = addBootstrapDirectoryLists(directories); + const timestamps = [fallbackMetadata.createdAt, fallbackMetadata.updatedAt, ...directoryLists.flatMap((list) => [list.createdAt, list.updatedAt])].filter( (value) => typeof value === 'number', ); return { @@ -174,7 +230,7 @@ const toDirectoryListsData = (directories, fallbackMetadata = DEFAULT_METADATA) description: fallbackMetadata.description || DEFAULT_METADATA.description, createdAt: timestamps.length > 0 ? Math.min(...timestamps) : fallbackMetadata.createdAt, updatedAt: timestamps.length > 0 ? Math.max(...timestamps) : fallbackMetadata.updatedAt, - directories: sortDirectoryLists(directories), + directories: sortDirectoryLists(directoryLists), }; }; diff --git a/src/components/comment-media/__tests__/comment-media.test.tsx b/src/components/comment-media/__tests__/comment-media.test.tsx index 101d0fb1..6a5bc906 100644 --- a/src/components/comment-media/__tests__/comment-media.test.tsx +++ b/src/components/comment-media/__tests__/comment-media.test.tsx @@ -16,6 +16,7 @@ const testState = vi.hoisted(() => ({ hostname: 'example.com', isMobile: false, unmuteExpandedVideoSound: false, + ruffleLoadMock: vi.fn(), })); vi.mock('react-i18next', () => ({ @@ -69,6 +70,8 @@ vi.mock('../../embed', () => ({ default: ({ url }: { url: string }) => createElement('div', { 'data-testid': 'embed' }, url), })); +vi.mock('@ruffle-rs/ruffle', () => ({})); + let container: HTMLDivElement; let root: Root; let setShowThumbnailMock: ReturnType; @@ -90,6 +93,16 @@ describe('CommentMedia', () => { testState.hostname = 'example.com'; testState.isMobile = false; testState.unmuteExpandedVideoSound = false; + testState.ruffleLoadMock = vi.fn(); + const player = document.createElement('ruffle-player') as HTMLElement & { + ruffle: () => { load: (source: string | Record) => void }; + }; + player.ruffle = () => ({ load: testState.ruffleLoadMock }); + window.RufflePlayer = { + newest: () => ({ + createPlayer: () => player, + }), + }; setShowThumbnailMock = vi.fn(); container = document.createElement('div'); @@ -100,6 +113,7 @@ describe('CommentMedia', () => { afterEach(() => { act(() => root.unmount()); container.remove(); + delete window.RufflePlayer; }); it('toggles image expansion on mobile and renders the media metadata', async () => { @@ -370,4 +384,52 @@ describe('CommentMedia', () => { expect(video).toBeTruthy(); expect(video?.muted).toBe(true); }); + + it('renders collapsed SWF media as a compact placeholder and expands through Ruffle', async () => { + await renderMedia({ + commentMediaInfo: { + type: 'swf', + url: 'https://cdn.example.com/movie.swf', + }, + setShowThumbnail: setShowThumbnailMock, + showThumbnail: true, + }); + + const placeholder = Array.from(container.querySelectorAll('button')).find((node) => node.textContent === '[SWF]'); + expect(placeholder).toBeTruthy(); + expect(container.querySelector('object')).toBeNull(); + expect(container.querySelector('embed')).toBeNull(); + + await act(async () => { + placeholder?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + }); + + expect(setShowThumbnailMock).toHaveBeenCalledWith(false); + + await renderMedia({ + commentMediaInfo: { + type: 'swf', + url: 'https://cdn.example.com/movie.swf', + }, + setShowThumbnail: setShowThumbnailMock, + showThumbnail: false, + }); + + await act(async () => { + await Promise.resolve(); + }); + + expect(container.querySelector('[data-testid="ruffle-player"]')).toBeTruthy(); + expect(testState.ruffleLoadMock).toHaveBeenCalledWith( + expect.objectContaining({ + allowNetworking: 'internal', + allowScriptAccess: false, + autoplay: 'off', + openUrlMode: 'confirm', + url: 'https://cdn.example.com/movie.swf', + }), + ); + expect(container.querySelector('object')).toBeNull(); + expect(container.querySelector('embed')).toBeNull(); + }); }); diff --git a/src/components/comment-media/comment-media.module.css b/src/components/comment-media/comment-media.module.css index e8b6622b..dd399d7a 100644 --- a/src/components/comment-media/comment-media.module.css +++ b/src/components/comment-media/comment-media.module.css @@ -147,11 +147,77 @@ cursor: pointer; } +.swfPlaceholder { + display: inline-block; + width: 100%; + border: 0; + background: transparent; + color: var(--post-link-text-color); + font-family: inherit; + font-size: 9pt; + line-height: 1.2; + padding: 0; + text-align: center; + text-decoration: var(--post-link-text-decoration); + cursor: pointer; +} + +.swfPlaceholder:hover { + color: var(--post-link-text-color-hover); + text-decoration: var(--post-link-text-decoration-hover); +} + .content iframe { border: none; color-scheme: light; } +.rufflePlayer { + display: inline-flex; + align-items: center; + justify-content: center; + width: min(640px, 100%); + height: 480px; + max-width: 100%; + border: var(--reply-desktop-border); + background: var(--media-thumbnail-background-color); + box-sizing: border-box; + line-height: 1.2; +} + +.rufflePlayerElement { + display: block; + width: 100%; + height: 100%; +} + +.rufflePlayerMount { + display: block; + width: 100%; + height: 100%; +} + +.rufflePlayerMountHidden { + display: none; +} + +.ruffleLoading, +.ruffleFallback { + padding: 4px; + color: var(--post-mobile-file-info-text-color); + font-size: 9pt; +} + +.ruffleFallback a { + color: var(--post-link-text-color); + text-decoration: var(--post-link-text-decoration); +} + +.ruffleFallback a:hover { + color: var(--post-link-text-color-hover); + text-decoration: var(--post-link-text-decoration-hover); +} + .fileInfo { padding-bottom: 5px; text-align: center; diff --git a/src/components/comment-media/comment-media.tsx b/src/components/comment-media/comment-media.tsx index f2ad41b7..0c9db5fc 100644 --- a/src/components/comment-media/comment-media.tsx +++ b/src/components/comment-media/comment-media.tsx @@ -7,6 +7,7 @@ import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame'; import useIsMobile from '../../hooks/use-is-mobile'; import styles from './comment-media.module.css'; import Embed, { canEmbed } from '../embed'; +import RufflePlayer from './ruffle-player'; interface MediaProps { commentMediaInfo?: CommentMediaInfo; @@ -143,6 +144,12 @@ const Thumbnail = ({ ) : null; } else if (type === 'audio') { thumbnailComponent =