Merge branch 'master' of github.com:bitsocialnet/5chan

This commit is contained in:
Tommaso Casaburi
2026-06-01 13:01:42 +07:00
47 changed files with 297 additions and 99 deletions
+1 -1
View File
@@ -71,7 +71,7 @@
"build:fdroid": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=false VITE_APP_DISTRIBUTION=fdroid vite build",
"build: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'",
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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 ভিডিও"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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 वीडियो"
}
+2 -1
View File
@@ -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ó"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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動画"
}
+2 -1
View File
@@ -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 동영상"
}
+2 -1
View File
@@ -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 व्हिडिओ"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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 వీడియో"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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 ویڈیو"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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', () => ({
@@ -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;
+13 -1
View File
@@ -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);
+31
View File
@@ -1,3 +1,5 @@
const STANDARD_YOUTUBE_HOSTS = new Set<string>(['youtube.com', 'www.youtube.com', 'm.youtube.com', 'music.youtube.com', 'youtu.be', 'www.youtu.be']);
export const youtubeHosts = new Set<string>([
'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));
+3 -19
View File
@@ -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 -1
View File
@@ -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': {
+2 -2
View File
@@ -123,8 +123,8 @@ const ContentLinkEmbed = ({ children, href, linkMediaInfo }: ContentLinkEmbedPro
disableToggle={true}
isFloatingEmbed={true}
isReply={false}
setShowThumbnail={setShowMedia}
showThumbnail={false}
setShowThumbnail={() => {}}
showThumbnail={true}
/>
</div>
)}
+12 -7
View File
@@ -5,11 +5,11 @@ import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
import { Comment, useEditedComment, useReplies, useAccount } from '@bitsocial/bitsocial-react-hooks';
import 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>
+35 -4
View File
@@ -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', () => {
+26 -12
View File
@@ -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) {