fix(post form): respect reply link rules

This commit is contained in:
Tommaso Casaburi
2026-06-29 09:55:55 +07:00
parent 3b3a5a5e6c
commit 69ba4ace7a
42 changed files with 476 additions and 93 deletions
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "المساهمون", "site_legal_meta_contributors_link": "المساهمون",
"post_link_required_alert": "يجب أن تنشر رابطًا.", "post_link_required_alert": "يجب أن تنشر رابطًا.",
"post_media_link_required_alert": "يجب أن تنشر رابط صورة أو فيديو.", "post_media_link_required_alert": "يجب أن تنشر رابط صورة أو فيديو.",
"post_form_code_tags_prompt": "يمكنك تمييز بناء الجملة والحفاظ على المسافات البيضاء باستخدام وسوم [code]." "post_form_code_tags_prompt": "يمكنك تمييز بناء الجملة والحفاظ على المسافات البيضاء باستخدام وسوم [code].",
"reply_link_required_alert": "يجب عليك الرد برابط.",
"reply_media_link_required_alert": "يجب عليك الرد برابط لصورة أو فيديو.",
"reply_links_not_allowed_alert": "لا يُسمح بروابط الردود في هذه اللوحة."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "অবদানকারীরা", "site_legal_meta_contributors_link": "অবদানকারীরা",
"post_link_required_alert": "আপনাকে একটি লিঙ্ক পোস্ট করতে হবে।", "post_link_required_alert": "আপনাকে একটি লিঙ্ক পোস্ট করতে হবে।",
"post_media_link_required_alert": "আপনাকে একটি ছবি বা ভিডিওর লিঙ্ক পোস্ট করতে হবে।", "post_media_link_required_alert": "আপনাকে একটি ছবি বা ভিডিওর লিঙ্ক পোস্ট করতে হবে।",
"post_form_code_tags_prompt": "আপনি [code] ট্যাগ ব্যবহার করে সিনট্যাক্স হাইলাইট করতে এবং সাদা স্থান সংরক্ষণ করতে পারেন।" "post_form_code_tags_prompt": "আপনি [code] ট্যাগ ব্যবহার করে সিনট্যাক্স হাইলাইট করতে এবং সাদা স্থান সংরক্ষণ করতে পারেন।",
"reply_link_required_alert": "আপনাকে একটি লিঙ্ক দিয়ে উত্তর দিতে হবে।",
"reply_media_link_required_alert": "আপনাকে একটি ছবি বা ভিডিও লিঙ্ক দিয়ে উত্তর দিতে হবে।",
"reply_links_not_allowed_alert": "এই বোর্ডে উত্তরের লিঙ্ক অনুমোদিত নয়।"
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Přispěvatelé", "site_legal_meta_contributors_link": "Přispěvatelé",
"post_link_required_alert": "Musíte přidat odkaz.", "post_link_required_alert": "Musíte přidat odkaz.",
"post_media_link_required_alert": "Musíte přidat odkaz na obrázek nebo video.", "post_media_link_required_alert": "Musíte přidat odkaz na obrázek nebo video.",
"post_form_code_tags_prompt": "Syntax můžete zvýraznit a zachovat mezery pomocí značek [code]." "post_form_code_tags_prompt": "Syntax můžete zvýraznit a zachovat mezery pomocí značek [code].",
"reply_link_required_alert": "Musíte odpovědět odkazem.",
"reply_media_link_required_alert": "Musíte odpovědět odkazem na obrázek nebo video.",
"reply_links_not_allowed_alert": "Odkazy v odpovědích nejsou na této nástěnce povoleny."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Bidragydere", "site_legal_meta_contributors_link": "Bidragydere",
"post_link_required_alert": "Du skal poste et link.", "post_link_required_alert": "Du skal poste et link.",
"post_media_link_required_alert": "Du skal poste et billed- eller videolink.", "post_media_link_required_alert": "Du skal poste et billed- eller videolink.",
"post_form_code_tags_prompt": "Du kan fremhæve syntaks og bevare mellemrum ved at bruge [code]-tags." "post_form_code_tags_prompt": "Du kan fremhæve syntaks og bevare mellemrum ved at bruge [code]-tags.",
"reply_link_required_alert": "Du skal svare med et link.",
"reply_media_link_required_alert": "Du skal svare med et billed- eller videolink.",
"reply_links_not_allowed_alert": "Svarlinks er ikke tilladt på dette board."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Mitwirkende", "site_legal_meta_contributors_link": "Mitwirkende",
"post_link_required_alert": "Du musst einen Link posten.", "post_link_required_alert": "Du musst einen Link posten.",
"post_media_link_required_alert": "Du musst einen Bild- oder Videolink posten.", "post_media_link_required_alert": "Du musst einen Bild- oder Videolink posten.",
"post_form_code_tags_prompt": "Sie können Syntax hervorheben und Leerzeichen beibehalten, indem Sie [code]-Tags verwenden." "post_form_code_tags_prompt": "Sie können Syntax hervorheben und Leerzeichen beibehalten, indem Sie [code]-Tags verwenden.",
"reply_link_required_alert": "Du musst mit einem Link antworten.",
"reply_media_link_required_alert": "Du musst mit einem Bild- oder Videolink antworten.",
"reply_links_not_allowed_alert": "Antwortlinks sind auf diesem Board nicht erlaubt."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Συνεισφέροντες", "site_legal_meta_contributors_link": "Συνεισφέροντες",
"post_link_required_alert": "Πρέπει να δημοσιεύσετε έναν σύνδεσμο.", "post_link_required_alert": "Πρέπει να δημοσιεύσετε έναν σύνδεσμο.",
"post_media_link_required_alert": "Πρέπει να δημοσιεύσετε σύνδεσμο εικόνας ή βίντεο.", "post_media_link_required_alert": "Πρέπει να δημοσιεύσετε σύνδεσμο εικόνας ή βίντεο.",
"post_form_code_tags_prompt": "Μπορείτε να επισημάνετε τη σύνταξη και να διατηρήσετε τα κενά χρησιμοποιώντας ετικέτες [code]." "post_form_code_tags_prompt": "Μπορείτε να επισημάνετε τη σύνταξη και να διατηρήσετε τα κενά χρησιμοποιώντας ετικέτες [code].",
"reply_link_required_alert": "Πρέπει να απαντήσετε με έναν σύνδεσμο.",
"reply_media_link_required_alert": "Πρέπει να απαντήσετε με σύνδεσμο εικόνας ή βίντεο.",
"reply_links_not_allowed_alert": "Οι σύνδεσμοι στις απαντήσεις δεν επιτρέπονται σε αυτόν τον πίνακα."
} }
+4 -1
View File
@@ -465,5 +465,8 @@
"site_legal_meta_contributors_link": "Contributors", "site_legal_meta_contributors_link": "Contributors",
"post_link_required_alert": "You must post a link.", "post_link_required_alert": "You must post a link.",
"post_media_link_required_alert": "You must post an image or video link.", "post_media_link_required_alert": "You must post an image or video link.",
"post_form_code_tags_prompt": "You may highlight syntax and preserve whitespace by using [code] tags." "post_form_code_tags_prompt": "You may highlight syntax and preserve whitespace by using [code] tags.",
"reply_link_required_alert": "You must reply with a link.",
"reply_media_link_required_alert": "You must reply with an image or video link.",
"reply_links_not_allowed_alert": "Reply links are not allowed on this board."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Colaboradores", "site_legal_meta_contributors_link": "Colaboradores",
"post_link_required_alert": "Debes publicar un enlace.", "post_link_required_alert": "Debes publicar un enlace.",
"post_media_link_required_alert": "Debes publicar un enlace a una imagen o un video.", "post_media_link_required_alert": "Debes publicar un enlace a una imagen o un video.",
"post_form_code_tags_prompt": "Puede resaltar la sintaxis y conservar los espacios en blanco usando etiquetas [code]." "post_form_code_tags_prompt": "Puede resaltar la sintaxis y conservar los espacios en blanco usando etiquetas [code].",
"reply_link_required_alert": "Debes responder con un enlace.",
"reply_media_link_required_alert": "Debes responder con un enlace a una imagen o un video.",
"reply_links_not_allowed_alert": "Los enlaces en respuestas no están permitidos en este tablón."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "مشارکت‌کنندگان", "site_legal_meta_contributors_link": "مشارکت‌کنندگان",
"post_link_required_alert": "باید یک پیوند ارسال کنید.", "post_link_required_alert": "باید یک پیوند ارسال کنید.",
"post_media_link_required_alert": "باید پیوند یک تصویر یا ویدیو را ارسال کنید.", "post_media_link_required_alert": "باید پیوند یک تصویر یا ویدیو را ارسال کنید.",
"post_form_code_tags_prompt": "می‌توانید با استفاده از برچسب‌های [code] نحو را برجسته کرده و فاصله‌ها را حفظ کنید." "post_form_code_tags_prompt": "می‌توانید با استفاده از برچسب‌های [code] نحو را برجسته کرده و فاصله‌ها را حفظ کنید.",
"reply_link_required_alert": "باید با یک پیوند پاسخ دهید.",
"reply_media_link_required_alert": "باید با یک پیوند تصویر یا ویدیو پاسخ دهید.",
"reply_links_not_allowed_alert": "پیوندها در پاسخ‌ها در این برد مجاز نیستند."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Avustajat", "site_legal_meta_contributors_link": "Avustajat",
"post_link_required_alert": "Sinun on julkaistava linkki.", "post_link_required_alert": "Sinun on julkaistava linkki.",
"post_media_link_required_alert": "Sinun on julkaistava kuva- tai videolinkki.", "post_media_link_required_alert": "Sinun on julkaistava kuva- tai videolinkki.",
"post_form_code_tags_prompt": "Voit korostaa syntaksia ja säilyttää välilyönnit käyttämällä [code]-tageja." "post_form_code_tags_prompt": "Voit korostaa syntaksia ja säilyttää välilyönnit käyttämällä [code]-tageja.",
"reply_link_required_alert": "Sinun on vastattava linkillä.",
"reply_media_link_required_alert": "Sinun on vastattava kuva- tai videolinkillä.",
"reply_links_not_allowed_alert": "Vastauslinkit eivät ole sallittuja tällä laudalla."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Mga Kontribyutor", "site_legal_meta_contributors_link": "Mga Kontribyutor",
"post_link_required_alert": "Kailangan mong mag-post ng link.", "post_link_required_alert": "Kailangan mong mag-post ng link.",
"post_media_link_required_alert": "Kailangan mong mag-post ng link ng larawan o video.", "post_media_link_required_alert": "Kailangan mong mag-post ng link ng larawan o video.",
"post_form_code_tags_prompt": "Maaari mong i-highlight ang syntax at panatilihin ang whitespace gamit ang [code] tags." "post_form_code_tags_prompt": "Maaari mong i-highlight ang syntax at panatilihin ang whitespace gamit ang [code] tags.",
"reply_link_required_alert": "Kailangan mong tumugon gamit ang isang link.",
"reply_media_link_required_alert": "Kailangan mong tumugon gamit ang link ng larawan o video.",
"reply_links_not_allowed_alert": "Hindi pinapayagan ang mga link sa tugon sa board na ito."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Contributeurs", "site_legal_meta_contributors_link": "Contributeurs",
"post_link_required_alert": "Vous devez publier un lien.", "post_link_required_alert": "Vous devez publier un lien.",
"post_media_link_required_alert": "Vous devez publier un lien vers une image ou une vidéo.", "post_media_link_required_alert": "Vous devez publier un lien vers une image ou une vidéo.",
"post_form_code_tags_prompt": "Vous pouvez surligner la syntaxe et préserver les espaces en utilisant des balises [code]." "post_form_code_tags_prompt": "Vous pouvez surligner la syntaxe et préserver les espaces en utilisant des balises [code].",
"reply_link_required_alert": "Vous devez répondre avec un lien.",
"reply_media_link_required_alert": "Vous devez répondre avec un lien d'image ou de vidéo.",
"reply_links_not_allowed_alert": "Les liens dans les réponses ne sont pas autorisés sur ce board."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "תורמים", "site_legal_meta_contributors_link": "תורמים",
"post_link_required_alert": "עליך לפרסם קישור.", "post_link_required_alert": "עליך לפרסם קישור.",
"post_media_link_required_alert": "עליך לפרסם קישור לתמונה או לסרטון.", "post_media_link_required_alert": "עליך לפרסם קישור לתמונה או לסרטון.",
"post_form_code_tags_prompt": "ניתן להדגיש תחביר ולשמור על רווחים לבנים באמצעות תגיות [code]." "post_form_code_tags_prompt": "ניתן להדגיש תחביר ולשמור על רווחים לבנים באמצעות תגיות [code].",
"reply_link_required_alert": "עליך להשיב עם קישור.",
"reply_media_link_required_alert": "עליך להשיב עם קישור לתמונה או לסרטון.",
"reply_links_not_allowed_alert": "קישורים בתגובות אינם מותרים בלוח הזה."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "योगदानकर्ता", "site_legal_meta_contributors_link": "योगदानकर्ता",
"post_link_required_alert": "आपको एक लिंक पोस्ट करना होगा।", "post_link_required_alert": "आपको एक लिंक पोस्ट करना होगा।",
"post_media_link_required_alert": "आपको एक छवि या वीडियो लिंक पोस्ट करना होगा।", "post_media_link_required_alert": "आपको एक छवि या वीडियो लिंक पोस्ट करना होगा।",
"post_form_code_tags_prompt": "आप [code] टैग का उपयोग करके सिंटैक्स को हाइलाइट कर सकते हैं और व्हाइटस्पेस को संरक्षित कर सकते हैं।" "post_form_code_tags_prompt": "आप [code] टैग का उपयोग करके सिंटैक्स को हाइलाइट कर सकते हैं और व्हाइटस्पेस को संरक्षित कर सकते हैं।",
"reply_link_required_alert": "आपको एक लिंक के साथ जवाब देना होगा।",
"reply_media_link_required_alert": "आपको किसी छवि या वीडियो लिंक के साथ जवाब देना होगा।",
"reply_links_not_allowed_alert": "इस बोर्ड पर जवाबों में लिंक की अनुमति नहीं है।"
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Közreműködők", "site_legal_meta_contributors_link": "Közreműködők",
"post_link_required_alert": "Közzé kell tenned egy linket.", "post_link_required_alert": "Közzé kell tenned egy linket.",
"post_media_link_required_alert": "Közzé kell tenned egy kép- vagy videólinket.", "post_media_link_required_alert": "Közzé kell tenned egy kép- vagy videólinket.",
"post_form_code_tags_prompt": "A szintaxis kiemelhető és a szóközök megőrizhetők a [code] címkék használatával." "post_form_code_tags_prompt": "A szintaxis kiemelhető és a szóközök megőrizhetők a [code] címkék használatával.",
"reply_link_required_alert": "Linkkel kell válaszolnod.",
"reply_media_link_required_alert": "Kép- vagy videólinkkel kell válaszolnod.",
"reply_links_not_allowed_alert": "Ezen a táblán nem engedélyezettek a válaszlinkek."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Kontributor", "site_legal_meta_contributors_link": "Kontributor",
"post_link_required_alert": "Anda harus memposting tautan.", "post_link_required_alert": "Anda harus memposting tautan.",
"post_media_link_required_alert": "Anda harus memposting tautan gambar atau video.", "post_media_link_required_alert": "Anda harus memposting tautan gambar atau video.",
"post_form_code_tags_prompt": "Anda dapat menyorot sintaks dan mempertahankan spasi dengan menggunakan tag [code]." "post_form_code_tags_prompt": "Anda dapat menyorot sintaks dan mempertahankan spasi dengan menggunakan tag [code].",
"reply_link_required_alert": "Anda harus membalas dengan tautan.",
"reply_media_link_required_alert": "Anda harus membalas dengan tautan gambar atau video.",
"reply_links_not_allowed_alert": "Tautan balasan tidak diizinkan di papan ini."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Collaboratori", "site_legal_meta_contributors_link": "Collaboratori",
"post_link_required_alert": "Devi pubblicare un link.", "post_link_required_alert": "Devi pubblicare un link.",
"post_media_link_required_alert": "Devi pubblicare un link a un'immagine o a un video.", "post_media_link_required_alert": "Devi pubblicare un link a un'immagine o a un video.",
"post_form_code_tags_prompt": "Puoi evidenziare la sintassi e preservare gli spazi bianchi usando i tag [code]." "post_form_code_tags_prompt": "Puoi evidenziare la sintassi e preservare gli spazi bianchi usando i tag [code].",
"reply_link_required_alert": "Devi rispondere con un link.",
"reply_media_link_required_alert": "Devi rispondere con un link a un'immagine o a un video.",
"reply_links_not_allowed_alert": "I link nelle risposte non sono consentiti su questa board."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "貢献者", "site_legal_meta_contributors_link": "貢献者",
"post_link_required_alert": "リンクを投稿する必要があります。", "post_link_required_alert": "リンクを投稿する必要があります。",
"post_media_link_required_alert": "画像または動画のリンクを投稿する必要があります。", "post_media_link_required_alert": "画像または動画のリンクを投稿する必要があります。",
"post_form_code_tags_prompt": "[code] タグを使用して、構文をハイライトし、空白を保持できます。" "post_form_code_tags_prompt": "[code] タグを使用して、構文をハイライトし、空白を保持できます。",
"reply_link_required_alert": "リンク付きで返信する必要があります。",
"reply_media_link_required_alert": "画像または動画のリンク付きで返信する必要があります。",
"reply_links_not_allowed_alert": "この板では返信リンクは許可されていません。"
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "기여자", "site_legal_meta_contributors_link": "기여자",
"post_link_required_alert": "링크를 게시해야 합니다.", "post_link_required_alert": "링크를 게시해야 합니다.",
"post_media_link_required_alert": "이미지 또는 동영상 링크를 게시해야 합니다.", "post_media_link_required_alert": "이미지 또는 동영상 링크를 게시해야 합니다.",
"post_form_code_tags_prompt": "[code] 태그를 사용하여 구문을 강조 표시하고 공백을 유지할 수 있습니다." "post_form_code_tags_prompt": "[code] 태그를 사용하여 구문을 강조 표시하고 공백을 유지할 수 있습니다.",
"reply_link_required_alert": "링크를 포함해 답글을 달아야 합니다.",
"reply_media_link_required_alert": "이미지 또는 동영상 링크를 포함해 답글을 달아야 합니다.",
"reply_links_not_allowed_alert": "이 게시판에서는 답글 링크가 허용되지 않습니다."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "योगदानकर्ते", "site_legal_meta_contributors_link": "योगदानकर्ते",
"post_link_required_alert": "तुम्हाला एक लिंक पोस्ट करावी लागेल.", "post_link_required_alert": "तुम्हाला एक लिंक पोस्ट करावी लागेल.",
"post_media_link_required_alert": "तुम्हाला प्रतिमा किंवा व्हिडिओची लिंक पोस्ट करावी लागेल.", "post_media_link_required_alert": "तुम्हाला प्रतिमा किंवा व्हिडिओची लिंक पोस्ट करावी लागेल.",
"post_form_code_tags_prompt": "आप [code] टॅग वापरून सिंटॅक्स हायलाइट करू शकता आणि व्हाइटस्पेस जतन करू शकता." "post_form_code_tags_prompt": "आप [code] टॅग वापरून सिंटॅक्स हायलाइट करू शकता आणि व्हाइटस्पेस जतन करू शकता.",
"reply_link_required_alert": "तुम्ही दुव्यासह उत्तर देणे आवश्यक आहे.",
"reply_media_link_required_alert": "तुम्ही प्रतिमा किंवा व्हिडिओ दुव्यासह उत्तर देणे आवश्यक आहे.",
"reply_links_not_allowed_alert": "या बोर्डवर उत्तरांमधील दुवे परवानगी नाहीत."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Bijdragers", "site_legal_meta_contributors_link": "Bijdragers",
"post_link_required_alert": "Je moet een link plaatsen.", "post_link_required_alert": "Je moet een link plaatsen.",
"post_media_link_required_alert": "Je moet een afbeelding- of videolink plaatsen.", "post_media_link_required_alert": "Je moet een afbeelding- of videolink plaatsen.",
"post_form_code_tags_prompt": "U kunt syntaxis markeren en witruimte behouden door [code]-tags te gebruiken." "post_form_code_tags_prompt": "U kunt syntaxis markeren en witruimte behouden door [code]-tags te gebruiken.",
"reply_link_required_alert": "Je moet antwoorden met een link.",
"reply_media_link_required_alert": "Je moet antwoorden met een afbeeldings- of videolink.",
"reply_links_not_allowed_alert": "Antwoordlinks zijn niet toegestaan op dit bord."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Bidragsytere", "site_legal_meta_contributors_link": "Bidragsytere",
"post_link_required_alert": "Du må poste en lenke.", "post_link_required_alert": "Du må poste en lenke.",
"post_media_link_required_alert": "Du må poste en bilde- eller videolenke.", "post_media_link_required_alert": "Du må poste en bilde- eller videolenke.",
"post_form_code_tags_prompt": "Du kan fremheve syntaks og bevare mellomrom ved å bruke [code]-tagger." "post_form_code_tags_prompt": "Du kan fremheve syntaks og bevare mellomrom ved å bruke [code]-tagger.",
"reply_link_required_alert": "Du må svare med en lenke.",
"reply_media_link_required_alert": "Du må svare med en bilde- eller videolenke.",
"reply_links_not_allowed_alert": "Svarlenker er ikke tillatt på dette forumet."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Współtwórcy", "site_legal_meta_contributors_link": "Współtwórcy",
"post_link_required_alert": "Musisz opublikować link.", "post_link_required_alert": "Musisz opublikować link.",
"post_media_link_required_alert": "Musisz opublikować link do obrazu lub filmu.", "post_media_link_required_alert": "Musisz opublikować link do obrazu lub filmu.",
"post_form_code_tags_prompt": "Możesz podświetlać składnię i zachować spacje, używając tagów [code]." "post_form_code_tags_prompt": "Możesz podświetlać składnię i zachować spacje, używając tagów [code].",
"reply_link_required_alert": "Musisz odpowiedzieć linkiem.",
"reply_media_link_required_alert": "Musisz odpowiedzieć linkiem do obrazu lub filmu.",
"reply_links_not_allowed_alert": "Linki w odpowiedziach nie są dozwolone na tej tablicy."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Colaboradores", "site_legal_meta_contributors_link": "Colaboradores",
"post_link_required_alert": "Você deve postar um link.", "post_link_required_alert": "Você deve postar um link.",
"post_media_link_required_alert": "Você deve postar um link de imagem ou vídeo.", "post_media_link_required_alert": "Você deve postar um link de imagem ou vídeo.",
"post_form_code_tags_prompt": "Você pode destacar a sintaxe e preservar espaços em branco usando tags [code]." "post_form_code_tags_prompt": "Você pode destacar a sintaxe e preservar espaços em branco usando tags [code].",
"reply_link_required_alert": "Você deve responder com um link.",
"reply_media_link_required_alert": "Você deve responder com um link de imagem ou vídeo.",
"reply_links_not_allowed_alert": "Links em respostas não são permitidos neste board."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Contributori", "site_legal_meta_contributors_link": "Contributori",
"post_link_required_alert": "Trebuie să postezi un link.", "post_link_required_alert": "Trebuie să postezi un link.",
"post_media_link_required_alert": "Trebuie să postezi un link către o imagine sau un videoclip.", "post_media_link_required_alert": "Trebuie să postezi un link către o imagine sau un videoclip.",
"post_form_code_tags_prompt": "Puteți evidenția sintaxa și păstra spațiile albe folosind tag-uri [code]." "post_form_code_tags_prompt": "Puteți evidenția sintaxa și păstra spațiile albe folosind tag-uri [code].",
"reply_link_required_alert": "Trebuie să răspunzi cu un link.",
"reply_media_link_required_alert": "Trebuie să răspunzi cu un link de imagine sau video.",
"reply_links_not_allowed_alert": "Linkurile în răspunsuri nu sunt permise pe acest board."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Участники", "site_legal_meta_contributors_link": "Участники",
"post_link_required_alert": "Вы должны опубликовать ссылку.", "post_link_required_alert": "Вы должны опубликовать ссылку.",
"post_media_link_required_alert": "Вы должны опубликовать ссылку на изображение или видео.", "post_media_link_required_alert": "Вы должны опубликовать ссылку на изображение или видео.",
"post_form_code_tags_prompt": "Вы можете выделять синтаксис и сохранять пробелы с помощью тегов [code]." "post_form_code_tags_prompt": "Вы можете выделять синтаксис и сохранять пробелы с помощью тегов [code].",
"reply_link_required_alert": "Вы должны ответить ссылкой.",
"reply_media_link_required_alert": "Вы должны ответить ссылкой на изображение или видео.",
"reply_links_not_allowed_alert": "Ссылки в ответах на этой доске запрещены."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Kontribuesit", "site_legal_meta_contributors_link": "Kontribuesit",
"post_link_required_alert": "Duhet të postosh një lidhje.", "post_link_required_alert": "Duhet të postosh një lidhje.",
"post_media_link_required_alert": "Duhet të postosh një lidhje imazhi ose videoje.", "post_media_link_required_alert": "Duhet të postosh një lidhje imazhi ose videoje.",
"post_form_code_tags_prompt": "Mund të theksosh sintaksën dhe të ruash hapësirat duke përdorur etiketat [code]." "post_form_code_tags_prompt": "Mund të theksosh sintaksën dhe të ruash hapësirat duke përdorur etiketat [code].",
"reply_link_required_alert": "Duhet të përgjigjesh me një lidhje.",
"reply_media_link_required_alert": "Duhet të përgjigjesh me një lidhje imazhi ose videoje.",
"reply_links_not_allowed_alert": "Lidhjet në përgjigje nuk lejohen në këtë bord."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Bidragsgivare", "site_legal_meta_contributors_link": "Bidragsgivare",
"post_link_required_alert": "Du måste posta en länk.", "post_link_required_alert": "Du måste posta en länk.",
"post_media_link_required_alert": "Du måste posta en bild- eller videolänk.", "post_media_link_required_alert": "Du måste posta en bild- eller videolänk.",
"post_form_code_tags_prompt": "Du kan markera syntax och bevara mellanslag genom att använda [code]-taggar." "post_form_code_tags_prompt": "Du kan markera syntax och bevara mellanslag genom att använda [code]-taggar.",
"reply_link_required_alert": "Du måste svara med en länk.",
"reply_media_link_required_alert": "Du måste svara med en bild- eller videolänk.",
"reply_links_not_allowed_alert": "Svarslänkar är inte tillåtna på detta bräde."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "సహకారులు", "site_legal_meta_contributors_link": "సహకారులు",
"post_link_required_alert": "మీరు ఒక లింక్ పోస్ట్ చేయాలి.", "post_link_required_alert": "మీరు ఒక లింక్ పోస్ట్ చేయాలి.",
"post_media_link_required_alert": "మీరు చిత్రం లేదా వీడియో లింక్ పోస్ట్ చేయాలి.", "post_media_link_required_alert": "మీరు చిత్రం లేదా వీడియో లింక్ పోస్ట్ చేయాలి.",
"post_form_code_tags_prompt": "మీరు [code] ట్యాగ్‌లను ఉపయోగించి సింటాక్స్‌ను హైలైట్ చేయవచ్చు మరియు ఖాళీ స్థలాలను కాపాడవచ్చు." "post_form_code_tags_prompt": "మీరు [code] ట్యాగ్‌లను ఉపయోగించి సింటాక్స్‌ను హైలైట్ చేయవచ్చు మరియు ఖాళీ స్థలాలను కాపాడవచ్చు.",
"reply_link_required_alert": "మీరు లింక్‌తో ప్రత్యుత్తరం ఇవ్వాలి.",
"reply_media_link_required_alert": "మీరు చిత్రం లేదా వీడియో లింక్‌తో ప్రత్యుత్తరం ఇవ్వాలి.",
"reply_links_not_allowed_alert": "ఈ బోర్డులో ప్రత్యుత్తర లింక్‌లకు అనుమతి లేదు."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "ผู้มีส่วนร่วม", "site_legal_meta_contributors_link": "ผู้มีส่วนร่วม",
"post_link_required_alert": "คุณต้องโพสต์ลิงก์", "post_link_required_alert": "คุณต้องโพสต์ลิงก์",
"post_media_link_required_alert": "คุณต้องโพสต์ลิงก์รูปภาพหรือวิดีโอ", "post_media_link_required_alert": "คุณต้องโพสต์ลิงก์รูปภาพหรือวิดีโอ",
"post_form_code_tags_prompt": "คุณสามารถไฮไลต์ไวยากรณ์และรักษาช่องว่างได้โดยใช้แท็ก [code]" "post_form_code_tags_prompt": "คุณสามารถไฮไลต์ไวยากรณ์และรักษาช่องว่างได้โดยใช้แท็ก [code]",
"reply_link_required_alert": "คุณต้องตอบกลับพร้อมลิงก์",
"reply_media_link_required_alert": "คุณต้องตอบกลับพร้อมลิงก์รูปภาพหรือวิดีโอ",
"reply_links_not_allowed_alert": "ไม่อนุญาตให้ใส่ลิงก์ในคำตอบบนบอร์ดนี้"
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Katkıda Bulunanlar", "site_legal_meta_contributors_link": "Katkıda Bulunanlar",
"post_link_required_alert": "Bir bağlantı paylaşmalısınız.", "post_link_required_alert": "Bir bağlantı paylaşmalısınız.",
"post_media_link_required_alert": "Bir görsel veya video bağlantısı paylaşmalısınız.", "post_media_link_required_alert": "Bir görsel veya video bağlantısı paylaşmalısınız.",
"post_form_code_tags_prompt": "[code] etiketlerini kullanarak sözdizimini vurgulayabilir ve boşlukları koruyabilirsiniz." "post_form_code_tags_prompt": "[code] etiketlerini kullanarak sözdizimini vurgulayabilir ve boşlukları koruyabilirsiniz.",
"reply_link_required_alert": "Bir bağlantıyla yanıt vermelisiniz.",
"reply_media_link_required_alert": "Bir görsel veya video bağlantısıyla yanıt vermelisiniz.",
"reply_links_not_allowed_alert": "Bu panoda yanıt bağlantılarına izin verilmiyor."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Учасники", "site_legal_meta_contributors_link": "Учасники",
"post_link_required_alert": "Потрібно опублікувати посилання.", "post_link_required_alert": "Потрібно опублікувати посилання.",
"post_media_link_required_alert": "Потрібно опублікувати посилання на зображення або відео.", "post_media_link_required_alert": "Потрібно опублікувати посилання на зображення або відео.",
"post_form_code_tags_prompt": "Ви можете підсвічувати синтаксис і зберігати пробіли, використовуючи теги [code]." "post_form_code_tags_prompt": "Ви можете підсвічувати синтаксис і зберігати пробіли, використовуючи теги [code].",
"reply_link_required_alert": "Ви маєте відповісти посиланням.",
"reply_media_link_required_alert": "Ви маєте відповісти посиланням на зображення або відео.",
"reply_links_not_allowed_alert": "Посилання у відповідях на цій дошці заборонені."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "شراکت کنندگان", "site_legal_meta_contributors_link": "شراکت کنندگان",
"post_link_required_alert": "آپ کو ایک لنک پوسٹ کرنا ہوگا۔", "post_link_required_alert": "آپ کو ایک لنک پوسٹ کرنا ہوگا۔",
"post_media_link_required_alert": "آپ کو تصویر یا ویڈیو کا لنک پوسٹ کرنا ہوگا۔", "post_media_link_required_alert": "آپ کو تصویر یا ویڈیو کا لنک پوسٹ کرنا ہوگا۔",
"post_form_code_tags_prompt": "آپ [code] ٹیگز استعمال کر کے نحو کو نمایاں کر سکتے ہیں اور خالی جگہ کو محفوظ رکھ سکتے ہیں۔" "post_form_code_tags_prompt": "آپ [code] ٹیگز استعمال کر کے نحو کو نمایاں کر سکتے ہیں اور خالی جگہ کو محفوظ رکھ سکتے ہیں۔",
"reply_link_required_alert": "آپ کو ایک لنک کے ساتھ جواب دینا ہوگا۔",
"reply_media_link_required_alert": "آپ کو تصویر یا ویڈیو کے لنک کے ساتھ جواب دینا ہوگا۔",
"reply_links_not_allowed_alert": "اس بورڈ پر جوابات میں لنکس کی اجازت نہیں ہے۔"
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "Người đóng góp", "site_legal_meta_contributors_link": "Người đóng góp",
"post_link_required_alert": "Bạn phải đăng một liên kết.", "post_link_required_alert": "Bạn phải đăng một liên kết.",
"post_media_link_required_alert": "Bạn phải đăng liên kết hình ảnh hoặc video.", "post_media_link_required_alert": "Bạn phải đăng liên kết hình ảnh hoặc video.",
"post_form_code_tags_prompt": "Bạn có thể tô sáng cú pháp và giữ nguyên khoảng trắng bằng cách sử dụng thẻ [code]." "post_form_code_tags_prompt": "Bạn có thể tô sáng cú pháp và giữ nguyên khoảng trắng bằng cách sử dụng thẻ [code].",
"reply_link_required_alert": "Bạn phải trả lời bằng một liên kết.",
"reply_media_link_required_alert": "Bạn phải trả lời bằng một liên kết hình ảnh hoặc video.",
"reply_links_not_allowed_alert": "Liên kết trong trả lời không được phép trên bảng này."
} }
+4 -1
View File
@@ -446,5 +446,8 @@
"site_legal_meta_contributors_link": "贡献者", "site_legal_meta_contributors_link": "贡献者",
"post_link_required_alert": "你必须发布一个链接。", "post_link_required_alert": "你必须发布一个链接。",
"post_media_link_required_alert": "你必须发布图片或视频链接。", "post_media_link_required_alert": "你必须发布图片或视频链接。",
"post_form_code_tags_prompt": "您可以使用 [code] 标签来高亮语法并保留空白字符。" "post_form_code_tags_prompt": "您可以使用 [code] 标签来高亮语法并保留空白字符。",
"reply_link_required_alert": "你必须使用链接回复。",
"reply_media_link_required_alert": "你必须使用图片或视频链接回复。",
"reply_links_not_allowed_alert": "此版块不允许回复链接。"
} }
@@ -1783,6 +1783,93 @@ describe('PostForm', () => {
expect(testState.publishReplyMock).toHaveBeenCalledTimes(1); expect(testState.publishReplyMock).toHaveBeenCalledTimes(1);
}); });
it('does not require an inline reply link when only post links are required', async () => {
testState.comments = {
'thread-cid': {
postCid: 'thread-cid',
},
};
testState.resolvedCommunityAddress = 'music-posting.eth';
testState.communities['music-posting.eth'] = {
address: 'music-posting.eth',
features: { requirePostLink: true },
};
await renderPostForm('/mu/thread/thread-cid');
await clickByText(container, 'post_a_reply');
const table = container.querySelector('table') as HTMLTableElement;
const textarea = table.querySelector('textarea') as HTMLTextAreaElement;
await dispatchInput(textarea, 'Reply body');
await clickByText(table, 'post');
expect(container.textContent).not.toContain('post_link_required_alert');
expect(testState.publishReplyMock).toHaveBeenCalledTimes(1);
});
it('requires an inline reply link when reply links are required', async () => {
testState.comments = {
'thread-cid': {
postCid: 'thread-cid',
},
};
testState.resolvedCommunityAddress = 'music-posting.eth';
testState.communities['music-posting.eth'] = {
address: 'music-posting.eth',
features: { requireReplyLink: true },
};
await renderPostForm('/mu/thread/thread-cid');
await clickByText(container, 'post_a_reply');
const table = container.querySelector('table') as HTMLTableElement;
const textarea = table.querySelector('textarea') as HTMLTextAreaElement;
const linkInput = table.querySelector<HTMLInputElement>('input[aria-label="link"]') as HTMLInputElement;
await dispatchInput(textarea, 'Reply body');
await clickByText(table, 'post');
expect(container.textContent).toContain('error: reply_link_required_alert');
expect(testState.publishReplyMock).not.toHaveBeenCalled();
await dispatchInput(linkInput, 'https://example.com/reply');
await clickByText(table, 'post');
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ link: 'https://example.com/reply' });
expect(testState.publishReplyMock).toHaveBeenCalledTimes(1);
expect(testState.publishReplyMock).toHaveBeenCalledWith(expect.objectContaining({ content: 'Reply body' }));
});
it('rejects inline reply links when reply links are disabled', async () => {
testState.comments = {
'thread-cid': {
postCid: 'thread-cid',
},
};
testState.resolvedCommunityAddress = 'music-posting.eth';
testState.communities['music-posting.eth'] = {
address: 'music-posting.eth',
features: { noReplyLinks: true },
};
await renderPostForm('/mu/thread/thread-cid');
await clickByText(container, 'post_a_reply');
const table = container.querySelector('table') as HTMLTableElement;
const textarea = table.querySelector('textarea') as HTMLTextAreaElement;
const linkInput = table.querySelector<HTMLInputElement>('input[aria-label="link"]') as HTMLInputElement;
expect(linkInput.disabled).toBe(true);
linkInput.disabled = false;
await dispatchInput(textarea, 'Reply body');
await dispatchInput(linkInput, 'https://example.com/reply.png');
await clickByText(table, 'post');
expect(container.textContent).toContain('error: reply_links_not_allowed_alert');
expect(testState.publishReplyMock).not.toHaveBeenCalled();
});
it('publishes known twimg query-format reply links with a path extension without editing the field', async () => { it('publishes known twimg query-format reply links with a path extension without editing the field', async () => {
const twimgLink = 'https://pbs.twimg.com/media/HJxnhNKWMAAhqFU?format=jpg&name=medium'; const twimgLink = 'https://pbs.twimg.com/media/HJxnhNKWMAAhqFU?format=jpg&name=medium';
const publishLink = 'https://pbs.twimg.com/media/HJxnhNKWMAAhqFU.jpg'; const publishLink = 'https://pbs.twimg.com/media/HJxnhNKWMAAhqFU.jpg';
+43 -19
View File
@@ -7,7 +7,16 @@ import getShortAddress from '../../lib/get-short-address';
import { communitiesPagesStore as useCommunitiesPagesStore } from '../../lib/bitsocial-internals/stores'; import { communitiesPagesStore as useCommunitiesPagesStore } from '../../lib/bitsocial-internals/stores';
import { getDisplayMediaInfoType, getLinkMediaInfo, getTwimgMediaFilePublishUrl } from '../../lib/utils/media-utils'; import { getDisplayMediaInfoType, getLinkMediaInfo, getTwimgMediaFilePublishUrl } from '../../lib/utils/media-utils';
import { getExpiringMediaLinkAlert, getPublishFileDisplayName, getPublishLinkOptions, isPublishFileMediaType } from '../../lib/utils/media-link-validation-utils'; import { getExpiringMediaLinkAlert, getPublishFileDisplayName, getPublishLinkOptions, isPublishFileMediaType } from '../../lib/utils/media-link-validation-utils';
import { getEffectivePublishLinkFeatures, getPublishLinkValidationError, getRequirePostLink, getRequirePostLinkIsMedia } from '../../lib/utils/publish-link-requirements'; import {
getEffectivePostLinkFeatures,
getEffectiveReplyLinkFeatures,
getNoReplyLinks,
getPublishLinkValidationError,
getRequirePostLink,
getRequirePostLinkIsMedia,
getRequireReplyLink,
getRequireReplyLinkIsMedia,
} from '../../lib/utils/publish-link-requirements';
import { import {
type DiceRoll, type DiceRoll,
type FortuneEntry, type FortuneEntry,
@@ -177,7 +186,7 @@ interface PostFormFieldsProps {
subscriptions: string[]; subscriptions: string[];
communityAddress: string | undefined; communityAddress: string | undefined;
rulesPath: string; rulesPath: string;
requirePostLinkIsMedia: boolean; requireCurrentLinkIsMedia: boolean;
flagOptions: CommentFlagSelectOption[]; flagOptions: CommentFlagSelectOption[];
flashTagOptions: FlashTagOption[]; flashTagOptions: FlashTagOption[];
showFlashTagSelector: boolean; showFlashTagSelector: boolean;
@@ -232,7 +241,7 @@ const PostFormFields = ({
subscriptions, subscriptions,
communityAddress, communityAddress,
rulesPath, rulesPath,
requirePostLinkIsMedia, requireCurrentLinkIsMedia,
flagOptions, flagOptions,
flashTagOptions, flashTagOptions,
showFlashTagSelector, showFlashTagSelector,
@@ -367,15 +376,15 @@ const PostFormFields = ({
</tr> </tr>
)} )}
<tr> <tr>
<td>{requirePostLinkIsMedia ? t('link_to_file') : t('link')}</td> <td>{requireCurrentLinkIsMedia ? t('link_to_file') : t('link')}</td>
<td className={styles.linkField}> <td className={styles.linkField}>
<input <input
type='text' type='text'
aria-label={requirePostLinkIsMedia ? t('link_to_file') : t('link')} aria-label={requireCurrentLinkIsMedia ? t('link_to_file') : t('link')}
autoCorrect='off' autoCorrect='off'
autoComplete='off' autoComplete='off'
spellCheck='false' spellCheck='false'
placeholder={requirePostLinkIsMedia ? FILE_LINK_PLACEHOLDER : undefined} placeholder={requireCurrentLinkIsMedia ? FILE_LINK_PLACEHOLDER : undefined}
ref={urlRef} ref={urlRef}
disabled={disableLinkInput} disabled={disableLinkInput}
onChange={(e) => { onChange={(e) => {
@@ -383,7 +392,7 @@ const PostFormFields = ({
}} }}
onBlur={handleLinkBlur} onBlur={handleLinkBlur}
/> />
<span className={styles.linkType}> {url && <LinkTypePreviewer link={url} requireFile={requirePostLinkIsMedia} />}</span> <span className={styles.linkType}> {url && <LinkTypePreviewer link={url} requireFile={requireCurrentLinkIsMedia} />}</span>
</td> </td>
</tr> </tr>
{showUploadControls && ( {showUploadControls && (
@@ -402,8 +411,12 @@ const PostFormFields = ({
isUploading={isUploading} isUploading={isUploading}
showUploadControls={showUploadControls} showUploadControls={showUploadControls}
/> />
<span title={getPublishFileDisplayName(url, uploadedFileName, requirePostLinkIsMedia) || undefined}> <span title={getPublishFileDisplayName(url, uploadedFileName, requireCurrentLinkIsMedia) || undefined}>
{isUploading ? <LoadingEllipsis string={t('uploading')} /> : getPostFormFileDisplayLabel(url, uploadedFileName, t('no_file_chosen'), requirePostLinkIsMedia)} {isUploading ? (
<LoadingEllipsis string={t('uploading')} />
) : (
getPostFormFileDisplayLabel(url, uploadedFileName, t('no_file_chosen'), requireCurrentLinkIsMedia)
)}
</span> </span>
</td> </td>
</tr> </tr>
@@ -577,9 +590,14 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
const postOptionsDirectoryCode = getPostOptionsDirectoryCode(directoryEntry, location.pathname); const postOptionsDirectoryCode = getPostOptionsDirectoryCode(directoryEntry, location.pathname);
const showOekakiControls = postOptionsDirectoryCode === 'i' || directoryEntry?.directoryCode === 'i'; const showOekakiControls = postOptionsDirectoryCode === 'i' || directoryEntry?.directoryCode === 'i';
const communityFeatures = useCommunityField(effectiveBoardAddress, (community) => community?.features); const communityFeatures = useCommunityField(effectiveBoardAddress, (community) => community?.features);
const publishLinkFeatures = getEffectivePublishLinkFeatures(communityFeatures, directoryEntry?.features); const postLinkFeatures = getEffectivePostLinkFeatures(communityFeatures, directoryEntry?.features);
const requirePostLink = getRequirePostLink(publishLinkFeatures); const replyLinkFeatures = getEffectiveReplyLinkFeatures(communityFeatures, directoryEntry?.features);
const requirePostLinkIsMedia = getRequirePostLinkIsMedia(publishLinkFeatures, isInAllView || isInSubscriptionsView); const requirePostLink = getRequirePostLink(postLinkFeatures);
const requirePostLinkIsMedia = getRequirePostLinkIsMedia(postLinkFeatures, isInAllView || isInSubscriptionsView);
const requireReplyLink = getRequireReplyLink(replyLinkFeatures);
const requireReplyLinkIsMedia = getRequireReplyLinkIsMedia(replyLinkFeatures, isInAllView || isInSubscriptionsView);
const noReplyLinks = getNoReplyLinks(replyLinkFeatures);
const requireCurrentLinkIsMedia = isInPostView ? requireReplyLinkIsMedia : requirePostLinkIsMedia;
const flagOptions = getCommentFlagOptionsForDirectory(directoryEntry); const flagOptions = getCommentFlagOptionsForDirectory(directoryEntry);
const showFlashUploadPrompt = isFlashDirectoryCode(postOptionsDirectoryCode); const showFlashUploadPrompt = isFlashDirectoryCode(postOptionsDirectoryCode);
const showFlashTagSelector = showFlashUploadPrompt && !isInPostView; const showFlashTagSelector = showFlashUploadPrompt && !isInPostView;
@@ -687,6 +705,8 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
link: currentUrl, link: currentUrl,
requireLink: requirePostLink, requireLink: requirePostLink,
requireMedia: requirePostLinkIsMedia, requireMedia: requirePostLinkIsMedia,
requiredLinkAlertKey: 'post_link_required_alert',
requiredMediaLinkAlertKey: 'post_media_link_required_alert',
t, t,
}); });
if (linkValidationError) { if (linkValidationError) {
@@ -816,8 +836,12 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
const linkValidationError = getPublishLinkValidationError({ const linkValidationError = getPublishLinkValidationError({
link: currentUrl, link: currentUrl,
requireLink: requirePostLink, noLinks: noReplyLinks,
requireMedia: requirePostLinkIsMedia, requireLink: requireReplyLink,
requireMedia: requireReplyLinkIsMedia,
requiredLinkAlertKey: 'reply_link_required_alert',
requiredMediaLinkAlertKey: 'reply_media_link_required_alert',
noLinksAlertKey: 'reply_links_not_allowed_alert',
t, t,
}); });
if (linkValidationError) { if (linkValidationError) {
@@ -856,7 +880,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
noticeCountdown: youtubeThumbnailConversionCountdown, noticeCountdown: youtubeThumbnailConversionCountdown,
queueLinkConversion, queueLinkConversion,
} = useYouTubeThumbnailLinkConversion({ } = useYouTubeThumbnailLinkConversion({
enabled: requirePostLinkIsMedia, enabled: requireCurrentLinkIsMedia && !(isInPostView && noReplyLinks),
onContentChange: handleContentValueChange, onContentChange: handleContentValueChange,
onLinkChange: setLinkValue, onLinkChange: setLinkValue,
textRef, textRef,
@@ -954,13 +978,13 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
handleLinkChange={handleLinkChange} handleLinkChange={handleLinkChange}
handleLinkBlur={handleLinkBlur} handleLinkBlur={handleLinkBlur}
handleOptionsChange={handleOptionsChange} handleOptionsChange={handleOptionsChange}
disableLinkInput={isUploading || youtubeThumbnailConversionCountdown !== null} disableLinkInput={isUploading || youtubeThumbnailConversionCountdown !== null || (isInPostView && noReplyLinks)}
setPublishPostOptions={setPublishPostOptions} setPublishPostOptions={setPublishPostOptions}
setPublishReplyOptions={setPublishReplyOptions} setPublishReplyOptions={setPublishReplyOptions}
isUploading={isUploading} isUploading={isUploading}
uploadedFileName={uploadedFileName} uploadedFileName={uploadedFileName}
showUploadControls={showUploadControls} showUploadControls={showUploadControls && !(isInPostView && noReplyLinks)}
showOekakiControls={showOekakiControls} showOekakiControls={showOekakiControls && !(isInPostView && noReplyLinks)}
showSpoilerForPost={showSpoilerForPost} showSpoilerForPost={showSpoilerForPost}
showSpoilerForReply={showSpoilerForReply} showSpoilerForReply={showSpoilerForReply}
isInAllView={isInAllView} isInAllView={isInAllView}
@@ -971,7 +995,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
subscriptions={subscriptions} subscriptions={subscriptions}
communityAddress={communityAddress} communityAddress={communityAddress}
rulesPath={rulesPath} rulesPath={rulesPath}
requirePostLinkIsMedia={requirePostLinkIsMedia} requireCurrentLinkIsMedia={requireCurrentLinkIsMedia}
flagOptions={flagOptions} flagOptions={flagOptions}
flashTagOptions={FLASH_TAG_OPTIONS} flashTagOptions={FLASH_TAG_OPTIONS}
showFlashTagSelector={showFlashTagSelector} showFlashTagSelector={showFlashTagSelector}
@@ -530,7 +530,7 @@ describe('ReplyModal', () => {
testState.directoryByAddress['oekaki-posting.bso'] = { testState.directoryByAddress['oekaki-posting.bso'] = {
address: 'oekaki-posting.bso', address: 'oekaki-posting.bso',
directoryCode: 'i', directoryCode: 'i',
features: { requirePostLink: true, requirePostLinkIsMedia: true }, features: { requireReplyLink: true, requireReplyLinkIsMedia: true },
title: '/i/ - Oekaki', title: '/i/ - Oekaki',
}; };
testState.communities['oekaki-posting.bso'] = { address: 'oekaki-posting.bso' }; testState.communities['oekaki-posting.bso'] = { address: 'oekaki-posting.bso' };
@@ -765,7 +765,7 @@ describe('ReplyModal', () => {
expect(testState.publishReplyMock).toHaveBeenCalledTimes(1); expect(testState.publishReplyMock).toHaveBeenCalledTimes(1);
}); });
it('requires a link when live community features require post links', async () => { it('does not require a reply link when only live community features require post links', async () => {
testState.openEmpty = true; testState.openEmpty = true;
testState.selectedText = 'Reply body'; testState.selectedText = 'Reply body';
testState.communities['music-posting.eth'] = { testState.communities['music-posting.eth'] = {
@@ -777,8 +777,24 @@ describe('ReplyModal', () => {
await clickButtonByText('post'); await clickButtonByText('post');
expect(container.textContent).toContain('error: post_link_required_alert'); expect(container.textContent).not.toContain('post_link_required_alert');
const linkRequiredError = Array.from(container.querySelectorAll('div')).find((element) => element.textContent === 'error: post_link_required_alert'); expect(testState.publishReplyMock).toHaveBeenCalledTimes(1);
});
it('requires a link when live community features require reply links', async () => {
testState.openEmpty = true;
testState.selectedText = 'Reply body';
testState.communities['music-posting.eth'] = {
address: 'music-posting.eth',
features: { requireReplyLink: true },
};
await renderReplyModal('/mu/thread/post-1');
await clickButtonByText('post');
expect(container.textContent).toContain('error: reply_link_required_alert');
const linkRequiredError = Array.from(container.querySelectorAll('div')).find((element) => element.textContent === 'error: reply_link_required_alert');
expect(linkRequiredError?.className).toContain('error'); expect(linkRequiredError?.className).toContain('error');
expect(testState.publishReplyMock).not.toHaveBeenCalled(); expect(testState.publishReplyMock).not.toHaveBeenCalled();
@@ -790,20 +806,20 @@ describe('ReplyModal', () => {
expect(testState.publishReplyMock).toHaveBeenCalledTimes(1); expect(testState.publishReplyMock).toHaveBeenCalledTimes(1);
}); });
it('requires a media link when live community features require post links to be media', async () => { it('requires a media link when live community features require reply links to be media', async () => {
testState.openEmpty = true; testState.openEmpty = true;
testState.selectedText = 'Reply body'; testState.selectedText = 'Reply body';
testState.communities['music-posting.eth'] = { testState.communities['music-posting.eth'] = {
address: 'music-posting.eth', address: 'music-posting.eth',
features: { requirePostLink: true, requirePostLinkIsMedia: true }, features: { requireReplyLink: true, requireReplyLinkIsMedia: true },
}; };
await renderReplyModal('/mu/thread/post-1'); await renderReplyModal('/mu/thread/post-1');
await clickButtonByText('post'); await clickButtonByText('post');
expect(container.textContent).toContain('error: post_media_link_required_alert'); expect(container.textContent).toContain('error: reply_media_link_required_alert');
const mediaRequiredError = Array.from(container.querySelectorAll('div')).find((element) => element.textContent === 'error: post_media_link_required_alert'); const mediaRequiredError = Array.from(container.querySelectorAll('div')).find((element) => element.textContent === 'error: reply_media_link_required_alert');
expect(mediaRequiredError?.className).toContain('error'); expect(mediaRequiredError?.className).toContain('error');
expect(testState.publishReplyMock).not.toHaveBeenCalled(); expect(testState.publishReplyMock).not.toHaveBeenCalled();
@@ -826,14 +842,14 @@ describe('ReplyModal', () => {
testState.selectedText = 'Reply body'; testState.selectedText = 'Reply body';
testState.communities['music-posting.eth'] = { testState.communities['music-posting.eth'] = {
address: 'music-posting.eth', address: 'music-posting.eth',
features: { requirePostLink: false, requirePostLinkIsMedia: true }, features: { requireReplyLink: false, requireReplyLinkIsMedia: true },
}; };
await renderReplyModal('/mu/thread/post-1'); await renderReplyModal('/mu/thread/post-1');
await clickButtonByText('post'); await clickButtonByText('post');
expect(container.textContent).not.toContain('post_media_link_required_alert'); expect(container.textContent).not.toContain('reply_media_link_required_alert');
expect(testState.publishReplyMock).toHaveBeenCalledTimes(1); expect(testState.publishReplyMock).toHaveBeenCalledTimes(1);
testState.publishReplyMock.mockClear(); testState.publishReplyMock.mockClear();
@@ -845,6 +861,27 @@ describe('ReplyModal', () => {
expect(testState.publishReplyMock).not.toHaveBeenCalled(); expect(testState.publishReplyMock).not.toHaveBeenCalled();
}); });
it('rejects reply links when live community features disallow reply links', async () => {
testState.openEmpty = true;
testState.selectedText = 'Reply body';
testState.communities['music-posting.eth'] = {
address: 'music-posting.eth',
features: { noReplyLinks: true },
};
await renderReplyModal('/mu/thread/post-1');
const linkInput = container.querySelectorAll<HTMLInputElement>('input[type="text"]')[2];
expect(linkInput.disabled).toBe(true);
linkInput.disabled = false;
await dispatchInput(linkInput, 'https://example.com/reply.png');
await clickButtonByText('post');
expect(container.textContent).toContain('error: reply_links_not_allowed_alert');
expect(testState.publishReplyMock).not.toHaveBeenCalled();
});
it('publishes twimg query-format reply links with a path extension', async () => { it('publishes twimg query-format reply links with a path extension', async () => {
testState.openEmpty = true; testState.openEmpty = true;
testState.selectedText = 'reply body'; testState.selectedText = 'reply body';
@@ -887,7 +924,7 @@ describe('ReplyModal', () => {
testState.selectedText = 'reply body'; testState.selectedText = 'reply body';
testState.directoryByAddress['music-posting.eth'] = { testState.directoryByAddress['music-posting.eth'] = {
address: 'music-posting.eth', address: 'music-posting.eth',
features: { requirePostLinkIsMedia: true }, features: { requireReplyLinkIsMedia: true },
title: '/mu/ - Music', title: '/mu/ - Music',
}; };
@@ -930,7 +967,7 @@ describe('ReplyModal', () => {
testState.selectedText = 'reply body'; testState.selectedText = 'reply body';
testState.directoryByAddress['music-posting.eth'] = { testState.directoryByAddress['music-posting.eth'] = {
address: 'music-posting.eth', address: 'music-posting.eth',
features: { requirePostLinkIsMedia: true }, features: { requireReplyLinkIsMedia: true },
title: '/mu/ - Music', title: '/mu/ - Music',
}; };
@@ -960,7 +997,7 @@ describe('ReplyModal', () => {
testState.selectedText = 'reply body'; testState.selectedText = 'reply body';
testState.directoryByAddress['music-posting.eth'] = { testState.directoryByAddress['music-posting.eth'] = {
address: 'music-posting.eth', address: 'music-posting.eth',
features: { requirePostLinkIsMedia: true }, features: { requireReplyLinkIsMedia: true },
title: '/mu/ - Music', title: '/mu/ - Music',
}; };
+25 -14
View File
@@ -5,7 +5,13 @@ import type { TFunction } from 'i18next';
import { setAccount, useAccount } from '@bitsocial/bitsocial-react-hooks'; import { setAccount, useAccount } from '@bitsocial/bitsocial-react-hooks';
import { getExpiringMediaLinkAlert, getPublishFileDisplayName, getPublishLinkOptions } from '../../lib/utils/media-link-validation-utils'; import { getExpiringMediaLinkAlert, getPublishFileDisplayName, getPublishLinkOptions } from '../../lib/utils/media-link-validation-utils';
import { getTwimgMediaFilePublishUrl } from '../../lib/utils/media-utils'; import { getTwimgMediaFilePublishUrl } from '../../lib/utils/media-utils';
import { getEffectivePublishLinkFeatures, getPublishLinkValidationError, getRequirePostLink, getRequirePostLinkIsMedia } from '../../lib/utils/publish-link-requirements'; import {
getEffectiveReplyLinkFeatures,
getNoReplyLinks,
getPublishLinkValidationError,
getRequireReplyLink,
getRequireReplyLinkIsMedia,
} from '../../lib/utils/publish-link-requirements';
import { getCommentFlagOptionsForDirectory, getCommentFlagPublishOptionsForDirectory } from '../../lib/comment-flag-selection'; import { getCommentFlagOptionsForDirectory, getCommentFlagPublishOptionsForDirectory } from '../../lib/comment-flag-selection';
import { import {
type DiceRoll, type DiceRoll,
@@ -79,9 +85,10 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
const showOekakiControls = postOptionsDirectoryCode === 'i' || directoryEntry?.directoryCode === 'i'; const showOekakiControls = postOptionsDirectoryCode === 'i' || directoryEntry?.directoryCode === 'i';
const showTexButton = isMathDirectoryCode(postOptionsDirectoryCode) || isMathDirectoryCode(directoryEntry?.directoryCode); const showTexButton = isMathDirectoryCode(postOptionsDirectoryCode) || isMathDirectoryCode(directoryEntry?.directoryCode);
const communityFeatures = useCommunityField(communityAddress, (community) => community?.features); const communityFeatures = useCommunityField(communityAddress, (community) => community?.features);
const publishLinkFeatures = getEffectivePublishLinkFeatures(communityFeatures, directoryEntry?.features); const replyLinkFeatures = getEffectiveReplyLinkFeatures(communityFeatures, directoryEntry?.features);
const requirePostLink = getRequirePostLink(publishLinkFeatures); const requireReplyLink = getRequireReplyLink(replyLinkFeatures);
const requirePostLinkIsMedia = getRequirePostLinkIsMedia(publishLinkFeatures, isInAllView || isInSubscriptionsView); const requireReplyLinkIsMedia = getRequireReplyLinkIsMedia(replyLinkFeatures, isInAllView || isInSubscriptionsView);
const noReplyLinks = getNoReplyLinks(replyLinkFeatures);
const flagOptions = getCommentFlagOptionsForDirectory(directoryEntry); const flagOptions = getCommentFlagOptionsForDirectory(directoryEntry);
const { isResolvingExternalQuotes, publishReply, publishReplyError, publishReplyStateMessage, resetPublishReplyOptions, replyIndex, setPublishReplyOptions } = const { isResolvingExternalQuotes, publishReply, publishReplyError, publishReplyStateMessage, resetPublishReplyOptions, replyIndex, setPublishReplyOptions } =
usePublishReply({ usePublishReply({
@@ -188,8 +195,12 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
const linkValidationError = getPublishLinkValidationError({ const linkValidationError = getPublishLinkValidationError({
link: currentUrl, link: currentUrl,
requireLink: requirePostLink, noLinks: noReplyLinks,
requireMedia: requirePostLinkIsMedia, requireLink: requireReplyLink,
requireMedia: requireReplyLinkIsMedia,
requiredLinkAlertKey: 'reply_link_required_alert',
requiredMediaLinkAlertKey: 'reply_media_link_required_alert',
noLinksAlertKey: 'reply_links_not_allowed_alert',
t, t,
}); });
if (linkValidationError) { if (linkValidationError) {
@@ -444,7 +455,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
noticeCountdown: youtubeThumbnailConversionCountdown, noticeCountdown: youtubeThumbnailConversionCountdown,
queueLinkConversion, queueLinkConversion,
} = useYouTubeThumbnailLinkConversion({ } = useYouTubeThumbnailLinkConversion({
enabled: requirePostLinkIsMedia, enabled: requireReplyLinkIsMedia && !noReplyLinks,
onContentChange: handleConvertedContentChange, onContentChange: handleConvertedContentChange,
onLinkChange: setLinkValue, onLinkChange: setLinkValue,
textRef, textRef,
@@ -538,7 +549,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
}; };
const uploadMode = useMediaHostingStore((state) => state.uploadMode); const uploadMode = useMediaHostingStore((state) => state.uploadMode);
const showUploadControls = getShowUploadControls(uploadMode, isWebRuntime()); const showUploadControls = getShowUploadControls(uploadMode, isWebRuntime());
const displayedFileName = getPublishFileDisplayName(url, uploadedFileName, requirePostLinkIsMedia); const displayedFileName = getPublishFileDisplayName(url, uploadedFileName, requireReplyLinkIsMedia);
const youtubeThumbnailConversionNotice = const youtubeThumbnailConversionNotice =
youtubeThumbnailConversionCountdown !== null ? t('youtube_thumbnail_link_conversion_notice', { count: youtubeThumbnailConversionCountdown }) : null; youtubeThumbnailConversionCountdown !== null ? t('youtube_thumbnail_link_conversion_notice', { count: youtubeThumbnailConversionCountdown }) : null;
@@ -652,22 +663,22 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
<input <input
type='text' type='text'
ref={urlRef} ref={urlRef}
aria-label={requirePostLinkIsMedia ? t('link_to_file') : t('link')} aria-label={requireReplyLinkIsMedia ? t('link_to_file') : t('link')}
placeholder={requirePostLinkIsMedia ? FILE_LINK_PLACEHOLDER : capitalize(t('link'))} placeholder={requireReplyLinkIsMedia ? FILE_LINK_PLACEHOLDER : capitalize(t('link'))}
disabled={isUploading} disabled={isUploading || noReplyLinks}
onChange={(e) => { onChange={(e) => {
handleLinkChange(e.target.value); handleLinkChange(e.target.value);
}} }}
onBlur={handleLinkBlur} onBlur={handleLinkBlur}
/> />
</div> </div>
{showOekakiControls && ( {showOekakiControls && !noReplyLinks && (
<div className={styles.oekakiRow}> <div className={styles.oekakiRow}>
<span className={styles.oekakiLabel}>Draw</span> <span className={styles.oekakiLabel}>Draw</span>
<OekakiDrawingControls className={styles.oekakiControls} disabled={isUploading} uploadFile={uploadFile} onClearUploadedUrl={handleOekakiClearUploadedUrl} /> <OekakiDrawingControls className={styles.oekakiControls} disabled={isUploading} uploadFile={uploadFile} onClearUploadedUrl={handleOekakiClearUploadedUrl} />
</div> </div>
)} )}
{showOekakiControls && isWebRuntime() ? <div className={styles.oekakiWarning}>{OEKAKI_WEB_WARNING_TEXT}</div> : null} {showOekakiControls && !noReplyLinks && isWebRuntime() ? <div className={styles.oekakiWarning}>{OEKAKI_WEB_WARNING_TEXT}</div> : null}
{flagOptions.length > 0 && ( {flagOptions.length > 0 && (
<div> <div>
<select <select
@@ -687,7 +698,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
</div> </div>
)} )}
<div className={styles.footer}> <div className={styles.footer}>
{showUploadControls && ( {showUploadControls && !noReplyLinks && (
<span className={styles.uploadContainer}> <span className={styles.uploadContainer}>
<span className={styles.uploadButton}> <span className={styles.uploadButton}>
<button type='button' onClick={handleUpload} disabled={isUploading}> <button type='button' onClick={handleUpload} disabled={isUploading}>
@@ -1,40 +1,105 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import type { TFunction } from 'i18next'; import type { TFunction } from 'i18next';
import { getEffectivePublishLinkFeatures, getPublishLinkValidationError, getRequirePostLink, getRequirePostLinkIsMedia } from '../publish-link-requirements'; import {
getEffectivePostLinkFeatures,
getEffectiveReplyLinkFeatures,
getNoReplyLinks,
getPublishLinkValidationError,
getRequirePostLink,
getRequirePostLinkIsMedia,
getRequireReplyLink,
getRequireReplyLinkIsMedia,
} from '../publish-link-requirements';
const t = ((key: string) => key) as TFunction; const t = ((key: string) => key) as TFunction;
describe('getEffectivePublishLinkFeatures', () => { describe('getEffectivePostLinkFeatures', () => {
it('uses live community feature values before directory defaults', () => { it('uses live community feature values before directory defaults', () => {
expect(getEffectivePublishLinkFeatures({ requirePostLink: false }, { requirePostLink: true, requirePostLinkIsMedia: true })).toEqual({ expect(getEffectivePostLinkFeatures({ requirePostLink: false }, { requirePostLink: true, requirePostLinkIsMedia: true })).toEqual({
requirePostLink: false, requirePostLink: false,
requirePostLinkIsMedia: true, requirePostLinkIsMedia: true,
}); });
}); });
it('falls back per feature when live community features are partial', () => { it('falls back per feature when live community features are partial', () => {
expect(getEffectivePublishLinkFeatures({ requirePostLinkIsMedia: false }, { requirePostLink: true, requirePostLinkIsMedia: true })).toEqual({ expect(getEffectivePostLinkFeatures({ requirePostLinkIsMedia: false }, { requirePostLink: true, requirePostLinkIsMedia: true })).toEqual({
requirePostLink: true, requirePostLink: true,
requirePostLinkIsMedia: false, requirePostLinkIsMedia: false,
}); });
}); });
}); });
describe('getEffectiveReplyLinkFeatures', () => {
it('uses reply features separately from post features', () => {
expect(
getEffectiveReplyLinkFeatures(
{ requirePostLink: true, requirePostLinkIsMedia: true, requireReplyLink: false, noReplyLinks: true },
{ requireReplyLink: true, requireReplyLinkIsMedia: true, noReplyLinks: false },
),
).toEqual({
requireReplyLink: false,
requireReplyLinkIsMedia: true,
noReplyLinks: true,
});
});
});
describe('getPublishLinkValidationError', () => { describe('getPublishLinkValidationError', () => {
it('does not require an empty link when only media links are enabled', () => { it('does not require an empty link when only media links are enabled', () => {
expect(getPublishLinkValidationError({ link: '', requireLink: false, requireMedia: true, t })).toBeNull(); expect(
getPublishLinkValidationError({
link: '',
requireLink: false,
requireMedia: true,
requiredLinkAlertKey: 'post_link_required_alert',
requiredMediaLinkAlertKey: 'post_media_link_required_alert',
t,
}),
).toBeNull();
}); });
it('requires a media link when both link and media requirements are enabled', () => { it('requires a media link when both link and media requirements are enabled', () => {
expect(getPublishLinkValidationError({ link: '', requireLink: true, requireMedia: true, t })).toBe('error: post_media_link_required_alert'); expect(
getPublishLinkValidationError({
link: '',
requireLink: true,
requireMedia: true,
requiredLinkAlertKey: 'post_link_required_alert',
requiredMediaLinkAlertKey: 'post_media_link_required_alert',
t,
}),
).toBe('error: post_media_link_required_alert');
});
it('rejects reply links when links are disabled', () => {
expect(
getPublishLinkValidationError({
link: 'https://example.com/reply.png',
noLinks: true,
requireLink: false,
requireMedia: true,
requiredLinkAlertKey: 'reply_link_required_alert',
requiredMediaLinkAlertKey: 'reply_media_link_required_alert',
noLinksAlertKey: 'reply_links_not_allowed_alert',
t,
}),
).toBe('error: reply_links_not_allowed_alert');
}); });
}); });
describe('publish link feature helpers', () => { describe('publish link feature helpers', () => {
it('does not treat media-only validation as a required link', () => { it('does not treat media-only validation as a required link', () => {
const features = getEffectivePublishLinkFeatures({ requirePostLink: false, requirePostLinkIsMedia: true }, undefined); const features = getEffectivePostLinkFeatures({ requirePostLink: false, requirePostLinkIsMedia: true }, undefined);
expect(getRequirePostLink(features)).toBe(false); expect(getRequirePostLink(features)).toBe(false);
expect(getRequirePostLinkIsMedia(features, false)).toBe(true); expect(getRequirePostLinkIsMedia(features, false)).toBe(true);
}); });
it('does not treat post link requirements as reply link requirements', () => {
const features = getEffectiveReplyLinkFeatures({ requirePostLink: true, requirePostLinkIsMedia: true }, undefined);
expect(getRequireReplyLink(features)).toBe(false);
expect(getRequireReplyLinkIsMedia(features, false)).toBe(false);
expect(getNoReplyLinks(features)).toBe(false);
});
}); });
+3
View File
@@ -9,6 +9,9 @@ interface DirectoryFeatures {
hasFlags?: boolean; hasFlags?: boolean;
requirePostLink?: boolean; requirePostLink?: boolean;
requirePostLinkIsMedia?: boolean; requirePostLinkIsMedia?: boolean;
requireReplyLink?: boolean;
requireReplyLinkIsMedia?: boolean;
noReplyLinks?: boolean;
[key: string]: unknown; [key: string]: unknown;
} }
+56 -5
View File
@@ -5,12 +5,26 @@ import { isValidPublishURL } from './url-utils';
interface PublishLinkFeatures { interface PublishLinkFeatures {
requirePostLink?: unknown; requirePostLink?: unknown;
requirePostLinkIsMedia?: unknown; requirePostLinkIsMedia?: unknown;
requireReplyLink?: unknown;
requireReplyLinkIsMedia?: unknown;
noReplyLinks?: unknown;
} }
export const getEffectivePublishLinkFeatures = ( interface EffectivePostLinkFeatures {
requirePostLink?: unknown;
requirePostLinkIsMedia?: unknown;
}
interface EffectiveReplyLinkFeatures {
requireReplyLink?: unknown;
requireReplyLinkIsMedia?: unknown;
noReplyLinks?: unknown;
}
export const getEffectivePostLinkFeatures = (
communityFeatures: PublishLinkFeatures | undefined, communityFeatures: PublishLinkFeatures | undefined,
directoryFeatures: PublishLinkFeatures | undefined, directoryFeatures: PublishLinkFeatures | undefined,
): PublishLinkFeatures | undefined => { ): EffectivePostLinkFeatures | undefined => {
const requirePostLink = communityFeatures?.requirePostLink ?? directoryFeatures?.requirePostLink; const requirePostLink = communityFeatures?.requirePostLink ?? directoryFeatures?.requirePostLink;
const requirePostLinkIsMedia = communityFeatures?.requirePostLinkIsMedia ?? directoryFeatures?.requirePostLinkIsMedia; const requirePostLinkIsMedia = communityFeatures?.requirePostLinkIsMedia ?? directoryFeatures?.requirePostLinkIsMedia;
@@ -24,24 +38,61 @@ export const getEffectivePublishLinkFeatures = (
}; };
}; };
export const getRequirePostLink = (features: PublishLinkFeatures | undefined): boolean => Boolean(features?.requirePostLink); export const getEffectiveReplyLinkFeatures = (
communityFeatures: PublishLinkFeatures | undefined,
directoryFeatures: PublishLinkFeatures | undefined,
): EffectiveReplyLinkFeatures | undefined => {
const requireReplyLink = communityFeatures?.requireReplyLink ?? directoryFeatures?.requireReplyLink;
const requireReplyLinkIsMedia = communityFeatures?.requireReplyLinkIsMedia ?? directoryFeatures?.requireReplyLinkIsMedia;
const noReplyLinks = communityFeatures?.noReplyLinks ?? directoryFeatures?.noReplyLinks;
export const getRequirePostLinkIsMedia = (features: PublishLinkFeatures | undefined, useMediaDefault: boolean): boolean => if (requireReplyLink === undefined && requireReplyLinkIsMedia === undefined && noReplyLinks === undefined) {
return undefined;
}
return {
requireReplyLink,
requireReplyLinkIsMedia,
noReplyLinks,
};
};
export const getRequirePostLink = (features: EffectivePostLinkFeatures | undefined): boolean => Boolean(features?.requirePostLink);
export const getRequirePostLinkIsMedia = (features: EffectivePostLinkFeatures | undefined, useMediaDefault: boolean): boolean =>
Boolean(features?.requirePostLinkIsMedia) || (features?.requirePostLinkIsMedia === undefined && useMediaDefault); Boolean(features?.requirePostLinkIsMedia) || (features?.requirePostLinkIsMedia === undefined && useMediaDefault);
export const getRequireReplyLink = (features: EffectiveReplyLinkFeatures | undefined): boolean => Boolean(features?.requireReplyLink);
export const getRequireReplyLinkIsMedia = (features: EffectiveReplyLinkFeatures | undefined, useMediaDefault: boolean): boolean =>
Boolean(features?.requireReplyLinkIsMedia) || (features?.requireReplyLinkIsMedia === undefined && useMediaDefault);
export const getNoReplyLinks = (features: EffectiveReplyLinkFeatures | undefined): boolean => Boolean(features?.noReplyLinks);
export const getPublishLinkValidationError = ({ export const getPublishLinkValidationError = ({
link, link,
noLinks = false,
requireLink, requireLink,
requireMedia, requireMedia,
requiredLinkAlertKey,
requiredMediaLinkAlertKey,
noLinksAlertKey,
t, t,
}: { }: {
link: string; link: string;
noLinks?: boolean;
requireLink: boolean; requireLink: boolean;
requireMedia: boolean; requireMedia: boolean;
requiredLinkAlertKey: string;
requiredMediaLinkAlertKey: string;
noLinksAlertKey?: string;
t: TFunction; t: TFunction;
}): string | null => { }): string | null => {
if (link && noLinks && noLinksAlertKey) {
return `${t('error')}: ${t(noLinksAlertKey)}`;
}
if (!link && requireLink) { if (!link && requireLink) {
return `${t('error')}: ${t(requireMedia ? 'post_media_link_required_alert' : 'post_link_required_alert')}`; return `${t('error')}: ${t(requireMedia ? requiredMediaLinkAlertKey : requiredLinkAlertKey)}`;
} }
if (link && !isValidPublishURL(link)) { if (link && !isValidPublishURL(link)) {
return `${t('error')}: ${t('invalid_url_alert')}`; return `${t('error')}: ${t('invalid_url_alert')}`;