mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
Merge branch 'master' of github.com:bitsocialnet/5chan
# Conflicts: # public/translations/ar/default.json # public/translations/bn/default.json # public/translations/cs/default.json # public/translations/da/default.json # public/translations/de/default.json # public/translations/el/default.json # public/translations/en/default.json # public/translations/es/default.json # public/translations/fa/default.json # public/translations/fi/default.json # public/translations/fil/default.json # public/translations/fr/default.json # public/translations/he/default.json # public/translations/hi/default.json # public/translations/hu/default.json # public/translations/id/default.json # public/translations/it/default.json # public/translations/ja/default.json # public/translations/ko/default.json # public/translations/mr/default.json # public/translations/nl/default.json # public/translations/no/default.json # public/translations/pl/default.json # public/translations/pt/default.json # public/translations/ro/default.json # public/translations/ru/default.json # public/translations/sq/default.json # public/translations/sv/default.json # public/translations/te/default.json # public/translations/th/default.json # public/translations/tr/default.json # public/translations/uk/default.json # public/translations/ur/default.json # public/translations/vi/default.json # public/translations/zh/default.json
This commit is contained in:
+3
-1
@@ -16,7 +16,7 @@
|
||||
"@capawesome/capacitor-android-edge-to-edge-support": "7.2.2",
|
||||
"@chenglou/pretext": "0.0.5",
|
||||
"@floating-ui/react": "0.26.1",
|
||||
"@pkcprotocol/pkc-js": "0.0.35",
|
||||
"@pkcprotocol/pkc-js": "0.0.37",
|
||||
"@react-spring/web": "10.0.3",
|
||||
"@types/node": "20.19.37",
|
||||
"@types/react": "18.2.25",
|
||||
@@ -183,7 +183,9 @@
|
||||
"portless": "0.11.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@pkcprotocol/pkc-js": "0.0.37",
|
||||
"follow-redirects": "1.16.0",
|
||||
"@libp2p/gossipsub": "15.0.23",
|
||||
"@libp2p/kad-dht": "16.2.6",
|
||||
"brace-expansion@npm:^5.0.2": "5.0.6",
|
||||
"protobufjs": "7.5.8",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.5 KiB |
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "لم يتم التحقق من عنوان هذه اللوحة. قد لا يكون مالك اللوحة مالكًا لهذا العنوان.",
|
||||
"go_to_settings_to_import_mod_account": "انتقل إلى <1>الإعدادات</1> لاستيراد حساب المشرف الخاص بك",
|
||||
"post_form_rules_faq_prompt": "يُرجى قراءة <rules>القواعد</rules> و<faq>الأسئلة الشائعة</faq> قبل النشر.",
|
||||
"pending_mod_approval_reason": "أرسل الإشراف بالذكاء الاصطناعي هذا المنشور إلى قائمة انتظار المشرفين. السبب: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "أرسل الإشراف بالذكاء الاصطناعي هذا المنشور إلى قائمة انتظار المشرفين. السبب: \"{{reason}}\".",
|
||||
"flag": "علم"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "এই বোর্ড ঠিকানাটি যাচাই করা হয়নি। বোর্ডের মালিক এই ঠিকানার মালিক নাও হতে পারেন।",
|
||||
"go_to_settings_to_import_mod_account": "আপনার মড অ্যাকাউন্ট আমদানি করতে <1>সেটিংস</1>-এ যান",
|
||||
"post_form_rules_faq_prompt": "পোস্ট করার আগে <rules>নিয়ম</rules> এবং <faq>প্রশ্নোত্তর</faq> পড়ুন।",
|
||||
"pending_mod_approval_reason": "AI মডারেশন এই পোস্টটি মডারেটর কিউতে পাঠিয়েছে। কারণ: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "AI মডারেশন এই পোস্টটি মডারেটর কিউতে পাঠিয়েছে। কারণ: \"{{reason}}\".",
|
||||
"flag": "পতাকা"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Tato adresa nástěnky není ověřená. Vlastník nástěnky nemusí tuto adresu vlastnit.",
|
||||
"go_to_settings_to_import_mod_account": "Přejděte do <1>nastavení</1> a importujte svůj moderátorský účet",
|
||||
"post_form_rules_faq_prompt": "Před odesláním si přečtěte <rules>Pravidla</rules> a <faq>FAQ</faq>.",
|
||||
"pending_mod_approval_reason": "AI moderace odeslala tento příspěvek do fronty moderátorů. Důvod: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "AI moderace odeslala tento příspěvek do fronty moderátorů. Důvod: \"{{reason}}\".",
|
||||
"flag": "Vlajka"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Denne boardadresse er ikke verificeret. Boardets ejer ejer muligvis ikke denne adresse.",
|
||||
"go_to_settings_to_import_mod_account": "Gå til <1>indstillinger</1> for at importere din mod-konto",
|
||||
"post_form_rules_faq_prompt": "Læs venligst <rules>Reglerne</rules> og <faq>FAQ</faq>, før du poster.",
|
||||
"pending_mod_approval_reason": "AI-moderering sendte dette opslag til moderatorkøen. Grund: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "AI-moderering sendte dette opslag til moderatorkøen. Grund: \"{{reason}}\".",
|
||||
"flag": "Flag"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Diese Board-Adresse ist nicht verifiziert. Der Board-Betreiber besitzt diese Adresse möglicherweise nicht.",
|
||||
"go_to_settings_to_import_mod_account": "Gehe zu <1>Einstellungen</1>, um dein Moderatorkonto zu importieren",
|
||||
"post_form_rules_faq_prompt": "Bitte lesen Sie die <rules>Regeln</rules> und die <faq>FAQ</faq>, bevor Sie posten.",
|
||||
"pending_mod_approval_reason": "Die KI-Moderation hat diesen Beitrag in die Moderationswarteschlange verschoben. Grund: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "Die KI-Moderation hat diesen Beitrag in die Moderationswarteschlange verschoben. Grund: \"{{reason}}\".",
|
||||
"flag": "Flagge"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Αυτή η διεύθυνση board δεν έχει επαληθευτεί. Ο ιδιοκτήτης του board ενδέχεται να μην κατέχει αυτή τη διεύθυνση.",
|
||||
"go_to_settings_to_import_mod_account": "Μεταβείτε στις <1>ρυθμίσεις</1> για να εισαγάγετε τον λογαριασμό διαχειριστή σας",
|
||||
"post_form_rules_faq_prompt": "Παρακαλώ διαβάστε τους <rules>Κανόνες</rules> και τις <faq>Συχνές ερωτήσεις</faq> πριν δημοσιεύσετε.",
|
||||
"pending_mod_approval_reason": "Η εποπτεία AI έστειλε αυτή την ανάρτηση στην ουρά των συντονιστών. Λόγος: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "Η εποπτεία AI έστειλε αυτή την ανάρτηση στην ουρά των συντονιστών. Λόγος: \"{{reason}}\".",
|
||||
"flag": "Σημαία"
|
||||
}
|
||||
|
||||
@@ -411,5 +411,6 @@
|
||||
"modQueue.dismiss": "Dismiss",
|
||||
"board_address_unverified_warning": "This board address is not verified. The board owner might not own this address.",
|
||||
"post_form_rules_faq_prompt": "Please read the <rules>Rules</rules> and <faq>FAQ</faq> before posting.",
|
||||
"pending_mod_approval_reason": "AI moderation sent this post to the mod queue. Reason: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "AI moderation sent this post to the mod queue. Reason: \"{{reason}}\".",
|
||||
"flag": "Flag"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Esta dirección del tablón no está verificada. Es posible que el propietario del tablón no sea dueño de esta dirección.",
|
||||
"go_to_settings_to_import_mod_account": "Ve a <1>configuración</1> para importar tu cuenta de moderador",
|
||||
"post_form_rules_faq_prompt": "Por favor, lee las <rules>Reglas</rules> y las <faq>FAQ</faq> antes de publicar.",
|
||||
"pending_mod_approval_reason": "La moderación con IA envió esta publicación a la cola de moderación. Motivo: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "La moderación con IA envió esta publicación a la cola de moderación. Motivo: \"{{reason}}\".",
|
||||
"flag": "Bandera"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "این نشانی برد تأیید نشده است. ممکن است مالک برد صاحب این نشانی نباشد.",
|
||||
"go_to_settings_to_import_mod_account": "برای وارد کردن حساب مدیر خود به <1>تنظیمات</1> بروید",
|
||||
"post_form_rules_faq_prompt": "لطفاً قبل از ارسال <rules>قوانین</rules> و <faq>پرسشهای متداول</faq> را بخوانید.",
|
||||
"pending_mod_approval_reason": "مدیریت با هوش مصنوعی این پست را به صف مدیران فرستاد. دلیل: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "مدیریت با هوش مصنوعی این پست را به صف مدیران فرستاد. دلیل: \"{{reason}}\".",
|
||||
"flag": "پرچم"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Tätä laudan osoitetta ei ole vahvistettu. Laudan omistaja ei välttämättä omista tätä osoitetta.",
|
||||
"go_to_settings_to_import_mod_account": "Siirry <1>asetuksiin</1> tuodaksesi moderaattoritilisi",
|
||||
"post_form_rules_faq_prompt": "Lue <rules>Säännöt</rules> ja <faq>UKK</faq> ennen julkaisemista.",
|
||||
"pending_mod_approval_reason": "Tekoälymoderointi lähetti tämän viestin moderointijonoon. Syy: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "Tekoälymoderointi lähetti tämän viestin moderointijonoon. Syy: \"{{reason}}\".",
|
||||
"flag": "Lippu"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Hindi beripikado ang address ng board na ito. Maaaring hindi pag-aari ng may-ari ng board ang address na ito.",
|
||||
"go_to_settings_to_import_mod_account": "Pumunta sa <1>mga setting</1> para i-import ang iyong mod account",
|
||||
"post_form_rules_faq_prompt": "Pakibasa ang <rules>Rules</rules> at <faq>FAQ</faq> bago mag-post.",
|
||||
"pending_mod_approval_reason": "Ipinadala ng AI moderation ang post na ito sa mod queue. Dahilan: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "Ipinadala ng AI moderation ang post na ito sa mod queue. Dahilan: \"{{reason}}\".",
|
||||
"flag": "Bandila"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Cette adresse de board n'est pas vérifiée. Le propriétaire du board pourrait ne pas posséder cette adresse.",
|
||||
"go_to_settings_to_import_mod_account": "Allez dans les <1>paramètres</1> pour importer votre compte de modération",
|
||||
"post_form_rules_faq_prompt": "Veuillez lire le <rules>Règlement</rules> et la <faq>FAQ</faq> avant de publier.",
|
||||
"pending_mod_approval_reason": "La modération par IA a envoyé ce post dans la file de modération. Raison: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "La modération par IA a envoyé ce post dans la file de modération. Raison: \"{{reason}}\".",
|
||||
"flag": "Drapeau"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "כתובת הלוח הזו אינה מאומתת. ייתכן שבעל הלוח אינו הבעלים של הכתובת הזו.",
|
||||
"go_to_settings_to_import_mod_account": "עבור אל <1>הגדרות</1> כדי לייבא את חשבון המוד שלך",
|
||||
"post_form_rules_faq_prompt": "אנא קרא את <rules>הכללים</rules> ואת <faq>שאלות נפוצות</faq> לפני פרסום.",
|
||||
"pending_mod_approval_reason": "סינון ה-AI שלח את הפוסט הזה לתור המודרציה. סיבה: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "סינון ה-AI שלח את הפוסט הזה לתור המודרציה. סיבה: \"{{reason}}\".",
|
||||
"flag": "דגל"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "इस बोर्ड पते की पुष्टि नहीं हुई है। हो सकता है बोर्ड का मालिक इस पते का मालिक न हो।",
|
||||
"go_to_settings_to_import_mod_account": "अपना मॉड खाता इम्पोर्ट करने के लिए <1>सेटिंग्स</1> पर जाएं",
|
||||
"post_form_rules_faq_prompt": "पोस्ट करने से पहले कृपया <rules>नियम</rules> और <faq>अक्सर पूछे जाने वाले सवाल</faq> पढ़ें।",
|
||||
"pending_mod_approval_reason": "AI मॉडरेशन ने इस पोस्ट को मॉड कतार में भेजा। कारण: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "AI मॉडरेशन ने इस पोस्ट को मॉड कतार में भेजा। कारण: \"{{reason}}\".",
|
||||
"flag": "ध्वज"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Ez a board-cím nincs ellenőrizve. Lehet, hogy a board tulajdonosa nem birtokolja ezt a címet.",
|
||||
"go_to_settings_to_import_mod_account": "Lépj a <1>beállítások</1> menübe a moderátori fiókod importálásához",
|
||||
"post_form_rules_faq_prompt": "Kérjük, olvasd el a <rules>Szabályokat</rules> és a <faq>GYIK</faq>-et posztolás előtt.",
|
||||
"pending_mod_approval_reason": "Az AI-moderáció ezt a bejegyzést a moderátori várólistára küldte. Ok: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "Az AI-moderáció ezt a bejegyzést a moderátori várólistára küldte. Ok: \"{{reason}}\".",
|
||||
"flag": "Zászló"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Alamat board ini belum diverifikasi. Pemilik board mungkin tidak memiliki alamat ini.",
|
||||
"go_to_settings_to_import_mod_account": "Buka <1>pengaturan</1> untuk mengimpor akun mod Anda",
|
||||
"post_form_rules_faq_prompt": "Silakan baca <rules>Aturan</rules> dan <faq>FAQ</faq> sebelum memposting.",
|
||||
"pending_mod_approval_reason": "Moderasi AI mengirim postingan ini ke antrean moderator. Alasan: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "Moderasi AI mengirim postingan ini ke antrean moderator. Alasan: \"{{reason}}\".",
|
||||
"flag": "Bendera"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Questo indirizzo della board non è verificato. Il proprietario della board potrebbe non possedere questo indirizzo.",
|
||||
"go_to_settings_to_import_mod_account": "Vai alle <1>impostazioni</1> per importare il tuo account mod",
|
||||
"post_form_rules_faq_prompt": "Leggi le <rules>Regole</rules> e le <faq>FAQ</faq> prima di pubblicare.",
|
||||
"pending_mod_approval_reason": "La moderazione AI ha inviato questo post alla coda di moderazione. Motivo: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "La moderazione AI ha inviato questo post alla coda di moderazione. Motivo: \"{{reason}}\".",
|
||||
"flag": "Bandiera"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "このボードアドレスは検証されていません。ボード所有者がこのアドレスを所有していない可能性があります。",
|
||||
"go_to_settings_to_import_mod_account": "モデレーターアカウントをインポートするには<1>設定</1>に移動してください",
|
||||
"post_form_rules_faq_prompt": "投稿する前に<rules>ルール</rules>と<faq>FAQ</faq>をお読みください。",
|
||||
"pending_mod_approval_reason": "AIモデレーションがこの投稿をモデレーターキューに送りました。理由: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "AIモデレーションがこの投稿をモデレーターキューに送りました。理由: \"{{reason}}\".",
|
||||
"flag": "旗"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "이 보드 주소는 확인되지 않았습니다. 보드 소유자가 이 주소를 소유하지 않았을 수 있습니다.",
|
||||
"go_to_settings_to_import_mod_account": "관리자 계정을 가져오려면 <1>설정</1>으로 이동하세요",
|
||||
"post_form_rules_faq_prompt": "게시하기 전에 <rules>규칙</rules>과 <faq>FAQ</faq>을 읽어 주세요.",
|
||||
"pending_mod_approval_reason": "AI 모더레이션이 이 게시물을 모더레이터 대기열로 보냈습니다. 이유: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "AI 모더레이션이 이 게시물을 모더레이터 대기열로 보냈습니다. 이유: \"{{reason}}\".",
|
||||
"flag": "깃발"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "हा बोर्ड पत्ता सत्यापित केलेला नाही. बोर्डचा मालक हा पत्ता मालक नसू शकतो.",
|
||||
"go_to_settings_to_import_mod_account": "तुमचे मॉड खाते आयात करण्यासाठी <1>सेटिंग्ज</1> वर जा",
|
||||
"post_form_rules_faq_prompt": "पोस्ट करण्यापूर्वी <rules>नियम</rules> आणि <faq>वारंवार विचारले जाणारे प्रश्न</faq> वाचा.",
|
||||
"pending_mod_approval_reason": "AI मॉडरेशनने ही पोस्ट मॉड कतारमध्ये पाठवली. कारण: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "AI मॉडरेशनने ही पोस्ट मॉड कतारमध्ये पाठवली. कारण: \"{{reason}}\".",
|
||||
"flag": "ध्वज"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Dit boardadres is niet geverifieerd. De eigenaar van het board is mogelijk niet de eigenaar van dit adres.",
|
||||
"go_to_settings_to_import_mod_account": "Ga naar <1>instellingen</1> om je mod-account te importeren",
|
||||
"post_form_rules_faq_prompt": "Lees de <rules>Regels</rules> en de <faq>FAQ</faq> voordat je plaatst.",
|
||||
"pending_mod_approval_reason": "AI-moderatie heeft dit bericht naar de moderatiewachtrij gestuurd. Reden: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "AI-moderatie heeft dit bericht naar de moderatiewachtrij gestuurd. Reden: \"{{reason}}\".",
|
||||
"flag": "Vlag"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Denne board-adressen er ikke bekreftet. Board-eieren eier kanskje ikke denne adressen.",
|
||||
"go_to_settings_to_import_mod_account": "Gå til <1>innstillinger</1> for å importere modkontoen din",
|
||||
"post_form_rules_faq_prompt": "Vennligst les <rules>Reglene</rules> og <faq>FAQ</faq> før du poster.",
|
||||
"pending_mod_approval_reason": "AI-moderering sendte dette innlegget til moderatorkøen. Årsak: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "AI-moderering sendte dette innlegget til moderatorkøen. Årsak: \"{{reason}}\".",
|
||||
"flag": "Flagg"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Ten adres boarda nie jest zweryfikowany. Właściciel boarda może nie być właścicielem tego adresu.",
|
||||
"go_to_settings_to_import_mod_account": "Przejdź do <1>ustawień</1>, aby zaimportować swoje konto moderatora",
|
||||
"post_form_rules_faq_prompt": "Przed opublikowaniem przeczytaj <rules>Zasady</rules> i <faq>FAQ</faq>.",
|
||||
"pending_mod_approval_reason": "Moderacja AI wysłała ten post do kolejki moderatorów. Powód: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "Moderacja AI wysłała ten post do kolejki moderatorów. Powód: \"{{reason}}\".",
|
||||
"flag": "Flaga"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Este endereço do board não foi verificado. O proprietário do board pode não ser dono deste endereço.",
|
||||
"go_to_settings_to_import_mod_account": "Vá para <1>configurações</1> para importar sua conta de mod",
|
||||
"post_form_rules_faq_prompt": "Leia as <rules>Regras</rules> e as <faq>FAQ</faq> antes de publicar.",
|
||||
"pending_mod_approval_reason": "A moderação por IA enviou esta postagem para a fila de moderação. Razão: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "A moderação por IA enviou esta postagem para a fila de moderação. Razão: \"{{reason}}\".",
|
||||
"flag": "Bandeira"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Această adresă de board nu este verificată. Proprietarul boardului s-ar putea să nu dețină această adresă.",
|
||||
"go_to_settings_to_import_mod_account": "Accesează <1>setările</1> pentru a importa contul tău de moderator",
|
||||
"post_form_rules_faq_prompt": "Citește <rules>Regulile</rules> și <faq>Întrebările frecvente</faq> înainte de a publica.",
|
||||
"pending_mod_approval_reason": "Moderarea AI a trimis această postare în coada moderatorilor. Motiv: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "Moderarea AI a trimis această postare în coada moderatorilor. Motiv: \"{{reason}}\".",
|
||||
"flag": "Steag"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Этот адрес доски не проверен. Владелец доски может не владеть этим адресом.",
|
||||
"go_to_settings_to_import_mod_account": "Перейдите в <1>настройки</1>, чтобы импортировать свой аккаунт модератора",
|
||||
"post_form_rules_faq_prompt": "Пожалуйста, прочитайте <rules>Правила</rules> и <faq>FAQ</faq> перед публикацией.",
|
||||
"pending_mod_approval_reason": "ИИ-модерация отправила этот пост в очередь модераторов. Причина: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "ИИ-модерация отправила этот пост в очередь модераторов. Причина: \"{{reason}}\".",
|
||||
"flag": "Флаг"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Kjo adresë e board-it nuk është verifikuar. Pronari i board-it mund të mos e zotërojë këtë adresë.",
|
||||
"go_to_settings_to_import_mod_account": "Shkoni te <1>parametrat</1> për të importuar llogarinë tuaj të modit",
|
||||
"post_form_rules_faq_prompt": "Ju lutemi lexoni <rules>Rregullat</rules> dhe <faq>Pyetjet e shpeshta</faq> para se të postoni.",
|
||||
"pending_mod_approval_reason": "Moderimi me AI e dërgoi këtë postim në radhën e moderatorëve. Arsye: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "Moderimi me AI e dërgoi këtë postim në radhën e moderatorëve. Arsye: \"{{reason}}\".",
|
||||
"flag": "Flamur"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Den här board-adressen är inte verifierad. Boardets ägare kanske inte äger den här adressen.",
|
||||
"go_to_settings_to_import_mod_account": "Gå till <1>inställningar</1> för att importera ditt modkonto",
|
||||
"post_form_rules_faq_prompt": "Läs <rules>Reglerna</rules> och <faq>FAQ</faq> innan du postar.",
|
||||
"pending_mod_approval_reason": "AI-moderering skickade det här inlägget till moderatorkön. Anledning: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "AI-moderering skickade det här inlägget till moderatorkön. Anledning: \"{{reason}}\".",
|
||||
"flag": "Flagga"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "ఈ బోర్డ్ చిరునామా ధృవీకరించబడలేదు. బోర్డ్ యజమాని ఈ చిరునామాకు యజమాని కాకపోవచ్చు.",
|
||||
"go_to_settings_to_import_mod_account": "మీ మోడ్ ఖాతాను దిగుమతి చేయడానికి <1>అమరికలు</1>కి వెళ్లండి",
|
||||
"post_form_rules_faq_prompt": "పోస్ట్ చేయడానికి ముందు <rules>నియమాలు</rules> మరియు <faq>తరచుగా అడిగే ప్రశ్నలు</faq> చదవండి.",
|
||||
"pending_mod_approval_reason": "AI మోడరేషన్ ఈ పోస్ట్ను మోడరేటర్ క్యూకు పంపింది. కారణం: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "AI మోడరేషన్ ఈ పోస్ట్ను మోడరేటర్ క్యూకు పంపింది. కారణం: \"{{reason}}\".",
|
||||
"flag": "జెండా"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "ที่อยู่บอร์ดนี้ยังไม่ได้รับการยืนยัน เจ้าของบอร์ดอาจไม่ได้เป็นเจ้าของที่อยู่นี้",
|
||||
"go_to_settings_to_import_mod_account": "ไปที่ <1>การตั้งค่า</1> เพื่อนำเข้าบัญชีผู้ดูแลของคุณ",
|
||||
"post_form_rules_faq_prompt": "โปรดอ่าน<rules>กฎ</rules>และ<faq>คำถามที่พบบ่อย</faq>ก่อนโพสต์",
|
||||
"pending_mod_approval_reason": "การกลั่นกรองด้วย AI ส่งโพสต์นี้ไปยังคิวผู้ดูแล เหตุผล: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "การกลั่นกรองด้วย AI ส่งโพสต์นี้ไปยังคิวผู้ดูแล เหตุผล: \"{{reason}}\".",
|
||||
"flag": "ธง"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Bu board adresi doğrulanmamış. Board sahibi bu adresin sahibi olmayabilir.",
|
||||
"go_to_settings_to_import_mod_account": "Mod hesabınızı içe aktarmak için <1>ayarlar</1>a gidin",
|
||||
"post_form_rules_faq_prompt": "Lütfen göndermeden önce <rules>Kuralları</rules> ve <faq>SSS</faq>'yi okuyun.",
|
||||
"pending_mod_approval_reason": "AI moderasyonu bu gönderiyi moderatör kuyruğuna gönderdi. Neden: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "AI moderasyonu bu gönderiyi moderatör kuyruğuna gönderdi. Neden: \"{{reason}}\".",
|
||||
"flag": "Bayrak"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Цю адресу дошки не перевірено. Власник дошки може не володіти цією адресою.",
|
||||
"go_to_settings_to_import_mod_account": "Перейдіть до <1>налаштувань</1>, щоб імпортувати свій обліковий запис модератора",
|
||||
"post_form_rules_faq_prompt": "Будь ласка, прочитайте <rules>Правила</rules> і <faq>Часті запитання</faq> перед публікацією.",
|
||||
"pending_mod_approval_reason": "AI-модерація надіслала цей допис до черги модераторів. Причина: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "AI-модерація надіслала цей допис до черги модераторів. Причина: \"{{reason}}\".",
|
||||
"flag": "Прапор"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "اس بورڈ کے پتے کی تصدیق نہیں ہوئی۔ ممکن ہے بورڈ کا مالک اس پتے کا مالک نہ ہو۔",
|
||||
"go_to_settings_to_import_mod_account": "اپنا موڈ اکاؤنٹ درآمد کرنے کے لیے <1>ترتیبات</1> پر جائیں",
|
||||
"post_form_rules_faq_prompt": "براہ کرم پوسٹ کرنے سے پہلے <rules>اصول</rules> اور <faq>اکثر پوچھے گئے سوالات</faq> پڑھیں۔",
|
||||
"pending_mod_approval_reason": "AI موڈریشن نے اس پوسٹ کو موڈریٹر قطار میں بھیج دیا۔ وجہ: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "AI موڈریشن نے اس پوسٹ کو موڈریٹر قطار میں بھیج دیا۔ وجہ: \"{{reason}}\".",
|
||||
"flag": "پرچم"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "Địa chỉ bảng này chưa được xác minh. Chủ bảng có thể không sở hữu địa chỉ này.",
|
||||
"go_to_settings_to_import_mod_account": "Vào <1>cài đặt</1> để nhập tài khoản mod của bạn",
|
||||
"post_form_rules_faq_prompt": "Vui lòng đọc <rules>Quy tắc</rules> và <faq>Câu hỏi thường gặp</faq> trước khi đăng.",
|
||||
"pending_mod_approval_reason": "Kiểm duyệt AI đã gửi bài viết này vào hàng đợi kiểm duyệt. Lý do: \"{{reason}}\"."
|
||||
"pending_mod_approval_reason": "Kiểm duyệt AI đã gửi bài viết này vào hàng đợi kiểm duyệt. Lý do: \"{{reason}}\".",
|
||||
"flag": "Cờ"
|
||||
}
|
||||
|
||||
@@ -392,5 +392,6 @@
|
||||
"board_address_unverified_warning": "此版面地址尚未验证。版主可能并不拥有此地址。",
|
||||
"go_to_settings_to_import_mod_account": "前往<1>设置</1>以导入您的版主账户",
|
||||
"post_form_rules_faq_prompt": "发帖前请阅读<rules>规则</rules>和<faq>常见问题</faq>。",
|
||||
"pending_mod_approval_reason": "AI 审核已将此帖子发送到版主队列。原因:\"{{reason}}\"。"
|
||||
"pending_mod_approval_reason": "AI 审核已将此帖子发送到版主队列。原因:\"{{reason}}\"。",
|
||||
"flag": "旗帜"
|
||||
}
|
||||
|
||||
@@ -438,6 +438,63 @@ describe('ChallengeModal', () => {
|
||||
expect(testState.removeChallengeMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('advances through multiple iframe challenges before publishing answers', async () => {
|
||||
const publication = createPublication();
|
||||
testState.challenges = [
|
||||
createStoredChallenge(
|
||||
[
|
||||
{
|
||||
challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123',
|
||||
type: 'url/iframe',
|
||||
},
|
||||
{
|
||||
challenge: 'https://flags.5chan.app/iframe/session-flag',
|
||||
type: 'url/iframe',
|
||||
},
|
||||
],
|
||||
publication,
|
||||
),
|
||||
];
|
||||
|
||||
await renderModal();
|
||||
await clickButton('Open');
|
||||
|
||||
await act(async () => {
|
||||
window.dispatchEvent(
|
||||
new MessageEvent('message', {
|
||||
data: {
|
||||
type: 'challengeAnswer',
|
||||
challengeAnswers: [''],
|
||||
sessionId: 'session-123',
|
||||
},
|
||||
origin: 'https://spamblocker.bitsocial.net',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
expect(publication.publishChallengeAnswers).not.toHaveBeenCalled();
|
||||
expect(testState.removeChallengeMock).not.toHaveBeenCalled();
|
||||
expect(container.textContent).toContain('mu wants to open flags.5chan.app.');
|
||||
|
||||
await clickButton('Open');
|
||||
|
||||
await act(async () => {
|
||||
window.dispatchEvent(
|
||||
new MessageEvent('message', {
|
||||
data: {
|
||||
type: 'challengeAnswer',
|
||||
challengeAnswers: [''],
|
||||
sessionId: 'session-flag',
|
||||
},
|
||||
origin: 'https://flags.5chan.app',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
expect(publication.publishChallengeAnswers).toHaveBeenCalledWith(['', '']);
|
||||
expect(testState.removeChallengeMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('ignores iframe completion messages with the wrong session id', async () => {
|
||||
const publication = createPublication();
|
||||
testState.challenges = [
|
||||
|
||||
@@ -373,19 +373,41 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const completeCurrentChallenge = useCallback(
|
||||
(challengeAnswers: string[]) => {
|
||||
if (!publication) return;
|
||||
const resolvedAnswers = challengeAnswers.length ? challengeAnswers : [''];
|
||||
const updatedAnswers = [...answers];
|
||||
resolvedAnswers.forEach((answer, index) => {
|
||||
updatedAnswers[currentChallengeIndex + index] = answer;
|
||||
});
|
||||
|
||||
const nextChallengeIndex = currentChallengeIndex + resolvedAnswers.length;
|
||||
if (challenges?.[nextChallengeIndex]) {
|
||||
setAnswers(updatedAnswers);
|
||||
setCurrentChallengeIndex(nextChallengeIndex);
|
||||
setReadyIframeChallengeKey('');
|
||||
return;
|
||||
}
|
||||
|
||||
publication.publishChallengeAnswers(updatedAnswers);
|
||||
setAnswers([]);
|
||||
closeModal();
|
||||
},
|
||||
[answers, challenges, closeModal, currentChallengeIndex, publication],
|
||||
);
|
||||
|
||||
const onIframeDone = useCallback(() => {
|
||||
if (!publication) return;
|
||||
publication.publishChallengeAnswers(['']);
|
||||
closeModal();
|
||||
}, [closeModal, publication]);
|
||||
completeCurrentChallenge(['']);
|
||||
}, [completeCurrentChallenge, publication]);
|
||||
|
||||
const onIframeAutoComplete = useCallback(
|
||||
(challengeAnswers: string[]) => {
|
||||
if (!publication) return;
|
||||
publication.publishChallengeAnswers(challengeAnswers);
|
||||
closeModal();
|
||||
completeCurrentChallenge(challengeAnswers);
|
||||
},
|
||||
[closeModal, publication],
|
||||
[completeCurrentChallenge, publication],
|
||||
);
|
||||
|
||||
const onIframeReady = useCallback(() => {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { getCommentFlagFlairs, getAuthorFlagViewModels } from '../lib/comment-flags';
|
||||
import styles from '../views/post/post.module.css';
|
||||
|
||||
interface PostAuthorFlagsProps {
|
||||
author: unknown;
|
||||
comment: unknown;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
const getBackgroundPosition = (x: number, y: number) => `${x === 0 ? 0 : -x}px ${y === 0 ? 0 : -y}px`;
|
||||
|
||||
const PostAuthorFlags = ({ author, comment, enabled }: PostAuthorFlagsProps) => {
|
||||
if (!enabled) return null;
|
||||
|
||||
const flags = getAuthorFlagViewModels(getCommentFlagFlairs(comment, author));
|
||||
if (flags.length === 0) return null;
|
||||
|
||||
return (
|
||||
<span className={styles.authorFlags}>
|
||||
{flags.map((flag) => (
|
||||
<span
|
||||
key={flag.key}
|
||||
aria-label={flag.label}
|
||||
className={styles.authorFlag}
|
||||
role='img'
|
||||
style={{
|
||||
backgroundImage: `url("${flag.spritePath}")`,
|
||||
backgroundPosition: getBackgroundPosition(flag.x, flag.y),
|
||||
width: flag.width,
|
||||
height: flag.height,
|
||||
}}
|
||||
title={flag.label}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostAuthorFlags;
|
||||
@@ -31,6 +31,7 @@ import EditMenu from '../edit-menu/edit-menu';
|
||||
import FailedPublishNotice from '../failed-publish-notice';
|
||||
import { canEmbed } from '../embed';
|
||||
import LoadingEllipsis from '../loading-ellipsis';
|
||||
import PostAuthorFlags from '../post-author-flags';
|
||||
import PostMenuDesktop from './post-menu-desktop';
|
||||
import ReplyQuotePreview from '../reply-quote-preview';
|
||||
import Tooltip from '../tooltip';
|
||||
@@ -239,7 +240,9 @@ const PostInfo = ({
|
||||
const isReply = parentCid;
|
||||
const { showOmittedReplies } = useShowOmittedReplies();
|
||||
const directories = useDirectories();
|
||||
const directoryEntry = communityAddress ? findDirectoryByAddress(directories, communityAddress) : undefined;
|
||||
const boardPath = communityAddress ? getBoardPath(communityAddress, directories) : undefined;
|
||||
const showAuthorFlags = directoryEntry?.features?.hasFlags === true && !(deleted || removed || purged);
|
||||
const postMenuProps = selectPostMenuProps(post);
|
||||
|
||||
const params = useParams();
|
||||
@@ -415,6 +418,7 @@ const PostInfo = ({
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
<PostAuthorFlags author={author} comment={post} enabled={showAuthorFlags} />
|
||||
<span className={styles.dateTime}>
|
||||
{isInModQueueView && isOverThreshold ? (
|
||||
<>
|
||||
|
||||
@@ -20,7 +20,7 @@ const testState = vi.hoisted(() => ({
|
||||
directories: [
|
||||
{ address: 'music-posting.eth', features: {}, title: '/mu/ - Music' },
|
||||
{ address: 'mod.eth', features: {}, title: '/mod/ - Moderation' },
|
||||
] as Array<{ address: string; features?: Record<string, unknown>; title?: string }>,
|
||||
] as Array<{ address: string; directoryCode?: string; features?: Record<string, unknown>; title?: string }>,
|
||||
editedComment: undefined as { commentModeration?: { archived?: boolean }; deleted?: boolean; locked?: boolean; postCid?: string; removed?: boolean } | undefined,
|
||||
gifFrameStatus: 'idle' as 'idle' | 'ready',
|
||||
handleUploadMock: vi.fn(),
|
||||
@@ -426,6 +426,7 @@ describe('PostForm', () => {
|
||||
testState.comments = {};
|
||||
testState.directories = [
|
||||
{ address: 'music-posting.eth', features: {}, title: '/mu/ - Music' },
|
||||
{ address: 'politically-incorrect.bso', directoryCode: 'pol', features: { hasFlags: true }, title: '/pol/ - Politically Incorrect' },
|
||||
{ address: 'random-nsfw.bso', features: {}, title: '/b/ - Random' },
|
||||
{ address: 'silly-stuff.bso', features: {}, title: '/s5s/ - Silly Stuff' },
|
||||
{ address: 'traditional-games.bso', features: {}, title: '/tg/ - Traditional Games' },
|
||||
@@ -606,6 +607,59 @@ describe('PostForm', () => {
|
||||
expect(testState.publishedPostOptions?.content).toBeUndefined();
|
||||
});
|
||||
|
||||
it('shows a 4chan-style flag field on flag boards and publishes the default geographic request', async () => {
|
||||
testState.resolvedCommunityAddress = 'politically-incorrect.bso';
|
||||
|
||||
await renderPostForm('/pol');
|
||||
await clickByText(container, 'start_new_thread');
|
||||
|
||||
const table = container.querySelector('table');
|
||||
const flagSelect = table?.querySelector<HTMLSelectElement>('select[aria-label="flag"]');
|
||||
const textarea = table?.querySelector<HTMLTextAreaElement>('textarea');
|
||||
|
||||
expect(flagSelect).toBeTruthy();
|
||||
expect(flagSelect?.value).toBe('country:auto');
|
||||
expect(
|
||||
Array.from(flagSelect?.options || [])
|
||||
.slice(0, 4)
|
||||
.map((option) => option.textContent),
|
||||
).toEqual(['Geographic Location', 'Anarcho-Capitalist', 'Anarchist', 'Black Nationalist']);
|
||||
|
||||
await dispatchInput(textarea as HTMLTextAreaElement, 'flagged post');
|
||||
await clickByText(table as HTMLTableElement, 'post');
|
||||
|
||||
expect(testState.publishPostMock).toHaveBeenCalledWith({
|
||||
content: 'flagged post',
|
||||
challengeRequest: {
|
||||
challengeAnswers: ['bitsocial-flags:5chan:flag:country:auto'],
|
||||
},
|
||||
flairs: [{ type: 'country', code: 'auto', text: 'flag:country:auto' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('publishes selected political flags from the post form', async () => {
|
||||
testState.resolvedCommunityAddress = 'politically-incorrect.bso';
|
||||
|
||||
await renderPostForm('/pol');
|
||||
await clickByText(container, 'start_new_thread');
|
||||
|
||||
const table = container.querySelector('table');
|
||||
const flagSelect = table?.querySelector<HTMLSelectElement>('select[aria-label="flag"]');
|
||||
const textarea = table?.querySelector<HTMLTextAreaElement>('textarea');
|
||||
|
||||
await dispatchChange(flagSelect as HTMLSelectElement, 'pol:AC');
|
||||
await dispatchInput(textarea as HTMLTextAreaElement, 'memeflag post');
|
||||
await clickByText(table as HTMLTableElement, 'post');
|
||||
|
||||
expect(testState.publishPostMock).toHaveBeenCalledWith({
|
||||
content: 'memeflag post',
|
||||
challengeRequest: {
|
||||
challengeAnswers: ['bitsocial-flags:5chan:flag:pol:AC'],
|
||||
},
|
||||
flairs: [{ type: 'pol', code: 'AC', text: 'flag:pol:AC' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('validates unsupported options and stores fortune output in post content', async () => {
|
||||
const randomSpy = vi.spyOn(Math, 'random').mockReturnValue(0.25);
|
||||
testState.resolvedCommunityAddress = 'random-nsfw.bso';
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.postFormTable input[type="text"], .postFormTable textarea {
|
||||
.postFormTable input[type="text"], .postFormTable textarea, .postFormTable select {
|
||||
border: var(--post-form-field-input-border, revert);
|
||||
font-family: var(--post-form-field-font-family, revert);
|
||||
appearance: var(--post-form-field-input-appearance, revert);
|
||||
@@ -80,7 +80,7 @@
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
.postFormTable input[type="text"]:focus, .postFormTable textarea:focus {
|
||||
.postFormTable input[type="text"]:focus, .postFormTable textarea:focus, .postFormTable select:focus {
|
||||
border: var(--post-form-field-input-focus-border, revert);
|
||||
}
|
||||
|
||||
@@ -113,6 +113,10 @@
|
||||
width: 190px !important;
|
||||
}
|
||||
|
||||
.flagSelect {
|
||||
width: 254px;
|
||||
}
|
||||
|
||||
.spoilerButton input[type="checkbox"] {
|
||||
margin: 0 3px;
|
||||
vertical-align: middle;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { getPublishURLFilename, isValidPublishURL, isValidURL } from '../../lib/
|
||||
import { hasModQueueAccessRole } from '../../lib/utils/mod-access';
|
||||
import { getBoardPath } from '../../lib/utils/route-utils';
|
||||
import { isAllView, isCatalogView, isModQueueView, isModView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||
import { getCommentFlagOptionsForDirectory, getCommentFlagPublishOptionsFromSelection, type CommentFlagSelectOption } from '../../lib/comment-flag-selection';
|
||||
import { useAccountCommunityAddresses } from '../../hooks/use-account-community-addresses';
|
||||
import { useDirectories, useDirectoryByAddress } from '../../hooks/use-directories';
|
||||
import { useCommunityField } from '../../hooks/use-stable-community';
|
||||
@@ -126,6 +127,7 @@ interface PostFormFieldsProps {
|
||||
postCid: string;
|
||||
subjectRef: React.Ref<HTMLInputElement>;
|
||||
optionsRef: React.RefObject<HTMLInputElement>;
|
||||
flagRef: React.RefObject<HTMLSelectElement>;
|
||||
textRef: React.RefObject<HTMLTextAreaElement>;
|
||||
urlRef: React.Ref<HTMLInputElement>;
|
||||
url: string;
|
||||
@@ -150,6 +152,7 @@ interface PostFormFieldsProps {
|
||||
communityAddress: string | undefined;
|
||||
rulesPath: string;
|
||||
requirePostLinkIsMedia: boolean;
|
||||
flagOptions: CommentFlagSelectOption[];
|
||||
showBbcodeToolbar: boolean;
|
||||
onBbcodePreviewToggle: () => void;
|
||||
onPublishReply: () => void;
|
||||
@@ -168,6 +171,7 @@ const PostFormFields = ({
|
||||
postCid,
|
||||
subjectRef,
|
||||
optionsRef,
|
||||
flagRef,
|
||||
textRef,
|
||||
urlRef,
|
||||
url,
|
||||
@@ -192,6 +196,7 @@ const PostFormFields = ({
|
||||
communityAddress,
|
||||
rulesPath,
|
||||
requirePostLinkIsMedia,
|
||||
flagOptions,
|
||||
showBbcodeToolbar,
|
||||
onBbcodePreviewToggle,
|
||||
onPublishReply,
|
||||
@@ -294,6 +299,26 @@ const PostFormFields = ({
|
||||
{lengthError && <div className={styles.error}>{lengthError}</div>}
|
||||
</td>
|
||||
</tr>
|
||||
{flagOptions.length > 0 && (
|
||||
<tr>
|
||||
<td>{t('flag')}</td>
|
||||
<td>
|
||||
<select
|
||||
key={flagOptions.map((option) => option.value).join('|')}
|
||||
aria-label={t('flag')}
|
||||
className={styles.flagSelect}
|
||||
ref={flagRef}
|
||||
defaultValue={flagOptions[0]?.value}
|
||||
>
|
||||
{flagOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td>{requirePostLinkIsMedia ? t('link_to_file') : t('link')}</td>
|
||||
<td className={styles.linkField}>
|
||||
@@ -422,6 +447,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
const urlRef = useRef<HTMLInputElement>(null);
|
||||
const subjectRef = useRef<HTMLInputElement>(null);
|
||||
const optionsRef = useRef<HTMLInputElement>(null);
|
||||
const flagRef = useRef<HTMLSelectElement>(null);
|
||||
const fortuneEntryRef = useRef<FortuneEntry | null>(null);
|
||||
const diceRollRef = useRef<DiceRoll | null>(null);
|
||||
const nonokoRedirectPathRef = useRef<string | null>(null);
|
||||
@@ -440,6 +466,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
const postOptionsDirectoryCode = getPostOptionsDirectoryCode(directoryEntry, location.pathname);
|
||||
const requirePostLinkIsMediaFeature = directoryEntry?.features?.requirePostLinkIsMedia;
|
||||
const requirePostLinkIsMedia = requirePostLinkIsMediaFeature === true || (requirePostLinkIsMediaFeature === undefined && (isInAllView || isInSubscriptionsView));
|
||||
const flagOptions = getCommentFlagOptionsForDirectory(directoryEntry);
|
||||
|
||||
const accountCommunityAddresses = useAccountCommunityAddresses();
|
||||
const accountAddress = account?.author?.address;
|
||||
@@ -485,6 +512,9 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
if (optionsRef.current) {
|
||||
optionsRef.current.value = '';
|
||||
}
|
||||
if (flagRef.current) {
|
||||
flagRef.current.value = flagRef.current.options[0]?.value ?? '';
|
||||
}
|
||||
checkContentLength.cancel();
|
||||
checkPostOptions.cancel();
|
||||
fortuneEntryRef.current = null;
|
||||
@@ -545,8 +575,10 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
return;
|
||||
}
|
||||
|
||||
const flagPublishOptions = getCommentFlagPublishOptionsFromSelection(flagRef.current?.value);
|
||||
|
||||
nonokoRedirectPathRef.current = hasNonokoOption(currentOptions) ? getBoardIndexPath() : null;
|
||||
publishPost({ content: publishContent });
|
||||
publishPost({ content: publishContent, ...flagPublishOptions });
|
||||
};
|
||||
|
||||
// redirect to pending page when pending comment is created
|
||||
@@ -655,8 +687,10 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
return;
|
||||
}
|
||||
|
||||
const flagPublishOptions = getCommentFlagPublishOptionsFromSelection(flagRef.current?.value);
|
||||
|
||||
nonokoRedirectPathRef.current = hasNonokoOption(currentOptions) ? getBoardIndexPath() : null;
|
||||
publishReply({ content: publishContent });
|
||||
publishReply({ content: publishContent, ...flagPublishOptions });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -715,6 +749,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
postCid={postCid}
|
||||
subjectRef={subjectRef}
|
||||
optionsRef={optionsRef}
|
||||
flagRef={flagRef}
|
||||
textRef={textRef}
|
||||
urlRef={urlRef}
|
||||
url={url}
|
||||
@@ -739,6 +774,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
communityAddress={communityAddress}
|
||||
rulesPath={rulesPath}
|
||||
requirePostLinkIsMedia={requirePostLinkIsMedia}
|
||||
flagOptions={flagOptions}
|
||||
showBbcodeToolbar={showBbcodeToolbar}
|
||||
onBbcodePreviewToggle={handleBbcodePreviewToggle}
|
||||
onPublishReply={onPublishReply}
|
||||
|
||||
@@ -28,6 +28,7 @@ import CommentContent from '../comment-content';
|
||||
import CommentMedia, { MediaLoadFailureInfo } from '../comment-media';
|
||||
import FailedPublishNotice from '../failed-publish-notice';
|
||||
import LoadingEllipsis from '../loading-ellipsis';
|
||||
import PostAuthorFlags from '../post-author-flags';
|
||||
import PostMenuMobile from './post-menu-mobile';
|
||||
import ReplyQuotePreview from '../reply-quote-preview';
|
||||
import Tooltip from '../tooltip';
|
||||
@@ -87,6 +88,8 @@ const PostInfoAndMedia = ({
|
||||
const archived = isCommentArchived(resolvedPost);
|
||||
const purged = resolvedPost?.commentModeration?.purged;
|
||||
const boardPath = communityAddress ? getBoardPath(communityAddress, directories) : undefined;
|
||||
const directoryEntry = communityAddress ? findDirectoryByAddress(directories, communityAddress) : undefined;
|
||||
const showAuthorFlags = directoryEntry?.features?.hasFlags === true && !(deleted || removed || purged);
|
||||
const displayBoardPath =
|
||||
boardPath && communityAddress
|
||||
? boardPath !== communityAddress
|
||||
@@ -348,6 +351,7 @@ const PostInfoAndMedia = ({
|
||||
){' '}
|
||||
</>
|
||||
)}
|
||||
<PostAuthorFlags author={author} comment={resolvedPost} enabled={showAuthorFlags} />
|
||||
{pinned && (
|
||||
<span className={styles.stickyIconWrapper}>
|
||||
<img src='assets/icons/sticky.gif' alt='' className={styles.stickyIcon} title={t('sticky')} />
|
||||
|
||||
@@ -18,7 +18,7 @@ const testState = vi.hoisted(() => ({
|
||||
features: {},
|
||||
title: '/mu/ - Music',
|
||||
},
|
||||
} as Record<string, { address: string; features?: Record<string, unknown>; title?: string }>,
|
||||
} as Record<string, { address: string; directoryCode?: string; features?: Record<string, unknown>; title?: string }>,
|
||||
handleUploadMock: vi.fn(),
|
||||
isMobile: false,
|
||||
isResolvingExternalQuotes: false,
|
||||
@@ -323,6 +323,12 @@ describe('ReplyModal', () => {
|
||||
features: {},
|
||||
title: '/mu/ - Music',
|
||||
},
|
||||
'politically-incorrect.bso': {
|
||||
address: 'politically-incorrect.bso',
|
||||
directoryCode: 'pol',
|
||||
features: { hasFlags: true },
|
||||
title: '/pol/ - Politically Incorrect',
|
||||
},
|
||||
'random-nsfw.bso': {
|
||||
address: 'random-nsfw.bso',
|
||||
features: {},
|
||||
@@ -425,6 +431,52 @@ describe('ReplyModal', () => {
|
||||
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ displayName: 'Alice' });
|
||||
});
|
||||
|
||||
it('shows a flag selector on flag boards and publishes the default geographic request', async () => {
|
||||
await renderReplyModal('/pol/thread/post-1', 'politically-incorrect.bso');
|
||||
|
||||
const flagSelect = container.querySelector<HTMLSelectElement>('select[aria-label="flag"]');
|
||||
|
||||
expect(flagSelect).toBeTruthy();
|
||||
expect(flagSelect?.value).toBe('country:auto');
|
||||
expect(
|
||||
Array.from(flagSelect?.options || [])
|
||||
.slice(0, 3)
|
||||
.map((option) => option.textContent),
|
||||
).toEqual(['Geographic Location', 'Anarcho-Capitalist', 'Anarchist']);
|
||||
|
||||
await clickButtonByText('post');
|
||||
|
||||
expect(testState.publishReplyMock).toHaveBeenCalledWith({
|
||||
content: '>>42\nselected text',
|
||||
challengeRequest: {
|
||||
challengeAnswers: ['bitsocial-flags:5chan:flag:country:auto'],
|
||||
},
|
||||
flairs: [{ type: 'country', code: 'auto', text: 'flag:country:auto' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('publishes selected political flags from the reply modal', async () => {
|
||||
await renderReplyModal('/pol/thread/post-1', 'politically-incorrect.bso');
|
||||
|
||||
const flagSelect = container.querySelector<HTMLSelectElement>('select[aria-label="flag"]');
|
||||
await dispatchInput(container.querySelector<HTMLTextAreaElement>('textarea') as HTMLTextAreaElement, 'reply body');
|
||||
await act(async () => {
|
||||
if (flagSelect) {
|
||||
flagSelect.value = 'pol:AC';
|
||||
flagSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
});
|
||||
await clickButtonByText('post');
|
||||
|
||||
expect(testState.publishReplyMock).toHaveBeenCalledWith({
|
||||
content: 'reply body',
|
||||
challengeRequest: {
|
||||
challengeAnswers: ['bitsocial-flags:5chan:flag:pol:AC'],
|
||||
},
|
||||
flairs: [{ type: 'pol', code: 'AC', text: 'flag:pol:AC' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the shared loading ellipsis while a reply upload is running', async () => {
|
||||
testState.isUploading = true;
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
background-image: var(--close-button-background-image);
|
||||
}
|
||||
|
||||
.container input[type="text"], .container textarea {
|
||||
.container input[type="text"], .container textarea, .container select {
|
||||
border: var(--reply-modal-field-input-border, revert);
|
||||
font-family: var(--post-form-field-font-family, revert);
|
||||
appearance: var(--post-form-field-input-appearance, revert);
|
||||
@@ -53,7 +53,7 @@
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.container input[type="text"]:focus, .container textarea:focus {
|
||||
.container input[type="text"]:focus, .container textarea:focus, .container select:focus {
|
||||
border: var(--reply-modal-field-input-border-focus, revert);
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.container input[type="text"], .container textarea {
|
||||
.container input[type="text"], .container textarea, .container select {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { setAccount, useAccount } from '@bitsocial/bitsocial-react-hooks';
|
||||
import { getExpiringMediaLinkAlert } from '../../lib/utils/media-link-validation-utils';
|
||||
import { getCommentFlagOptionsForDirectory, getCommentFlagPublishOptionsFromSelection } from '../../lib/comment-flag-selection';
|
||||
import {
|
||||
type DiceRoll,
|
||||
type FortuneEntry,
|
||||
@@ -64,6 +65,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
const postOptionsDirectoryCode = getPostOptionsDirectoryCode(directoryEntry, location.pathname);
|
||||
const requirePostLinkIsMediaFeature = directoryEntry?.features?.requirePostLinkIsMedia;
|
||||
const requirePostLinkIsMedia = requirePostLinkIsMediaFeature === true || (requirePostLinkIsMediaFeature === undefined && (isInAllView || isInSubscriptionsView));
|
||||
const flagOptions = getCommentFlagOptionsForDirectory(directoryEntry);
|
||||
const { isResolvingExternalQuotes, publishReply, publishReplyError, publishReplyStateMessage, resetPublishReplyOptions, replyIndex, setPublishReplyOptions } =
|
||||
usePublishReply({
|
||||
cid: parentCid,
|
||||
@@ -89,6 +91,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
});
|
||||
const urlRef = useRef<HTMLInputElement>(null);
|
||||
const optionsRef = useRef<HTMLInputElement>(null);
|
||||
const flagRef = useRef<HTMLSelectElement>(null);
|
||||
const fortuneEntryRef = useRef<FortuneEntry | null>(null);
|
||||
const diceRollRef = useRef<DiceRoll | null>(null);
|
||||
const nonokoRedirectPathRef = useRef<string | null>(null);
|
||||
@@ -166,9 +169,11 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
return;
|
||||
}
|
||||
|
||||
const flagPublishOptions = getCommentFlagPublishOptionsFromSelection(flagRef.current?.value);
|
||||
|
||||
setError(null);
|
||||
nonokoRedirectPathRef.current = hasNonokoOption(currentOptions) ? `/${postOptionsDirectoryCode || params.boardIdentifier || communityAddress}` : null;
|
||||
publishReply({ content: publishContent });
|
||||
publishReply({ content: publishContent, ...flagPublishOptions });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -515,6 +520,17 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{flagOptions.length > 0 && (
|
||||
<div>
|
||||
<select key={flagOptions.map((option) => option.value).join('|')} aria-label={t('flag')} ref={flagRef} defaultValue={flagOptions[0]?.value}>
|
||||
{flagOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.link}>
|
||||
<input
|
||||
type='text'
|
||||
|
||||
@@ -189,6 +189,7 @@ describe('use-directories', () => {
|
||||
expect(findDirectoryByAddress(communities, 'music-posting.bso')?.address).toBe('music-posting.bso');
|
||||
expect(findDirectoryByAddress(communities, 'music-posting.eth')?.address).toBe('music-posting.bso');
|
||||
expect(findDirectoryByAddress(communities, '12D3KooWQdQ6TkVA1Xe9zzaFP6vXBgsLeMAewpLpLwbsAYKivnQy')?.address).toBe('music-posting.bso');
|
||||
expect(findDirectoryByAddress([], '12D3KooWNFgjQWX2EUEs7pixdjkWSLh21EZ9NeYnV8iMaCyYhLGJ')).toBeUndefined();
|
||||
expect(findDirectoryByAddress(communities, undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
|
||||
@@ -155,4 +155,30 @@ describe('usePublishPost', () => {
|
||||
expect(testState.lastPublishOptions?.content).toBe('Fresh body');
|
||||
expect(testState.publishCommentMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('clears stale flag data from one-shot publish options', async () => {
|
||||
await act(async () => {
|
||||
latestValue.setPublishPostOptions({
|
||||
content: 'Flag body',
|
||||
challengeRequest: {
|
||||
challengeAnswers: ['bitsocial-flags:5chan:flag:country:auto'],
|
||||
},
|
||||
flairs: [{ text: 'flag:country:auto', type: 'country', code: 'auto' }],
|
||||
} as never);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
latestValue.publishPost({
|
||||
content: 'Plain body',
|
||||
challengeRequest: undefined,
|
||||
flairs: undefined,
|
||||
} as never);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(testState.lastPublishOptions?.content).toBe('Plain body');
|
||||
expect(testState.lastPublishOptions?.challengeRequest).toBeUndefined();
|
||||
expect(testState.lastPublishOptions?.flairs).toBeUndefined();
|
||||
expect(testState.publishCommentMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,8 +83,10 @@ describe('usePublishReply', () => {
|
||||
usePostNumberStore.setState({ cidToNumber: {}, numberToCid: { 'music.eth': { 12: 'quoted-cid' } } });
|
||||
usePublishReplyStore.setState({
|
||||
author: {},
|
||||
challengeRequest: {},
|
||||
content: {},
|
||||
displayName: {},
|
||||
flairs: {},
|
||||
link: {},
|
||||
publishCommentOptions: {},
|
||||
spoiler: {},
|
||||
@@ -186,6 +188,33 @@ describe('usePublishReply', () => {
|
||||
expect(testState.publishCommentMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('clears stale flag data from one-shot reply options', async () => {
|
||||
await act(async () => {
|
||||
latestValue.setPublishReplyOptions({
|
||||
content: 'Flag reply',
|
||||
challengeRequest: {
|
||||
challengeAnswers: ['bitsocial-flags:5chan:flag:pol:AC'],
|
||||
},
|
||||
flairs: [{ text: 'flag:pol:AC', type: 'pol', code: 'AC' }],
|
||||
} as never);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await latestValue.publishReply({
|
||||
content: 'Plain reply',
|
||||
challengeRequest: undefined,
|
||||
flairs: undefined,
|
||||
} as never);
|
||||
await Promise.resolve();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(testState.lastPublishOptions?.content).toBe('Plain reply');
|
||||
expect(testState.lastPublishOptions?.challengeRequest).toBeUndefined();
|
||||
expect(testState.lastPublishOptions?.flairs).toBeUndefined();
|
||||
expect(testState.publishCommentMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('blocks publish when a same-board external quote cannot be resolved', async () => {
|
||||
testState.resolveExternalQuoteTargetMock.mockResolvedValue(null);
|
||||
|
||||
|
||||
@@ -136,7 +136,14 @@ export const findDirectoryByAddress = (directories: DirectoryCommunity[], addres
|
||||
}
|
||||
|
||||
const normalizedAddress = normalizeBoardAddress(address);
|
||||
return directories.find((community) => getDirectoryIdentifiers(community).some((identifier) => normalizeBoardAddress(identifier) === normalizedAddress));
|
||||
const normalizedMatch = directories.find((community) =>
|
||||
getDirectoryIdentifiers(community).some((identifier) => normalizeBoardAddress(identifier) === normalizedAddress),
|
||||
);
|
||||
if (normalizedMatch) {
|
||||
return normalizedMatch;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const adaptV1Communities = (value: Record<string, unknown>): DirectoryCommunity[] => {
|
||||
|
||||
@@ -10,12 +10,13 @@ type UsePublishPostOptions = {
|
||||
};
|
||||
|
||||
const usePublishPost = ({ communityAddress }: UsePublishPostOptions) => {
|
||||
const { author, title, content, link, spoiler, publishCommentOptions } = usePublishPostStore(
|
||||
const { author, title, content, link, flairs, spoiler, publishCommentOptions } = usePublishPostStore(
|
||||
useShallow((state) => ({
|
||||
author: state.author,
|
||||
title: state.title || undefined,
|
||||
content: state.content || undefined,
|
||||
link: state.link || undefined,
|
||||
flairs: state.flairs,
|
||||
spoiler: state.spoiler || false,
|
||||
publishCommentOptions: state.publishCommentOptions,
|
||||
})),
|
||||
@@ -39,7 +40,9 @@ const usePublishPost = ({ communityAddress }: UsePublishPostOptions) => {
|
||||
title,
|
||||
content,
|
||||
link,
|
||||
flairs,
|
||||
spoiler,
|
||||
...(publishCommentOptions.challengeRequest ? { challengeRequest: publishCommentOptions.challengeRequest } : {}),
|
||||
};
|
||||
|
||||
const displayName = author?.displayName;
|
||||
@@ -48,7 +51,7 @@ const usePublishPost = ({ communityAddress }: UsePublishPostOptions) => {
|
||||
}
|
||||
|
||||
return baseOptions;
|
||||
}, [author, content, link, spoiler, communityAddress, title]);
|
||||
}, [author, content, flairs, link, spoiler, communityAddress, title, publishCommentOptions.challengeRequest]);
|
||||
|
||||
const setPublishPostOptions = useCallback(
|
||||
(options: Partial<Comment>) => {
|
||||
@@ -90,7 +93,7 @@ const usePublishPost = ({ communityAddress }: UsePublishPostOptions) => {
|
||||
|
||||
useEffect(() => {
|
||||
setPublishPostError(null);
|
||||
}, [author?.displayName, blockedReason, communityAddress, content, link, spoiler, title]);
|
||||
}, [author?.displayName, blockedReason, communityAddress, content, flairs, link, spoiler, title]);
|
||||
|
||||
const startPublishPost = useCallback(() => {
|
||||
if (blockedReason) {
|
||||
|
||||
@@ -23,11 +23,12 @@ const usePublishReply = ({ cid, communityAddress, postCid }: UsePublishReplyOpti
|
||||
const account = useAccount();
|
||||
const directories = useDirectories();
|
||||
|
||||
const { author, content, link, spoiler, publishCommentOptions } = usePublishReplyStore(
|
||||
const { author, content, link, flairs, spoiler, publishCommentOptions } = usePublishReplyStore(
|
||||
useShallow((state) => ({
|
||||
author: state.author[parentCid],
|
||||
content: state.content[parentCid] || undefined,
|
||||
link: state.link[parentCid] || undefined,
|
||||
flairs: state.flairs[parentCid],
|
||||
spoiler: state.spoiler[parentCid] || false,
|
||||
publishCommentOptions: state.publishCommentOptions[parentCid],
|
||||
})),
|
||||
@@ -57,7 +58,9 @@ const usePublishReply = ({ cid, communityAddress, postCid }: UsePublishReplyOpti
|
||||
postCid: postCid ?? parentCid,
|
||||
content,
|
||||
link,
|
||||
flairs,
|
||||
spoiler,
|
||||
...(publishCommentOptions?.challengeRequest ? { challengeRequest: publishCommentOptions.challengeRequest } : {}),
|
||||
};
|
||||
|
||||
const displayName = author?.displayName;
|
||||
@@ -66,7 +69,7 @@ const usePublishReply = ({ cid, communityAddress, postCid }: UsePublishReplyOpti
|
||||
}
|
||||
|
||||
return baseOptions;
|
||||
}, [author, content, link, parentCid, postCid, spoiler, communityAddress]);
|
||||
}, [author, content, flairs, link, parentCid, postCid, spoiler, communityAddress, publishCommentOptions?.challengeRequest]);
|
||||
|
||||
const setPublishReplyOptions = useCallback(
|
||||
(options: Partial<Comment>) => {
|
||||
@@ -142,7 +145,7 @@ const usePublishReply = ({ cid, communityAddress, postCid }: UsePublishReplyOpti
|
||||
setPublishReplyError(null);
|
||||
setPublishReplyStateMessage(null);
|
||||
setIsResolvingExternalQuotes(false);
|
||||
}, [blockedReason, content, communityAddress]);
|
||||
}, [blockedReason, content, communityAddress, flairs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingPublishRequestId === 0 || pendingPublishRequestId === startedPublishRequestIdRef.current) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getBoardFlagDefinition, normalizeBoardFlagKind } from '../board-flags';
|
||||
|
||||
describe('board-flags', () => {
|
||||
it('normalizes board flag kind aliases', () => {
|
||||
expect(normalizeBoardFlagKind('pol')).toBe('pol');
|
||||
expect(normalizeBoardFlagKind('memeflags')).toBe('pol');
|
||||
expect(normalizeBoardFlagKind('mlp')).toBe('pony');
|
||||
expect(normalizeBoardFlagKind('unknown')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('maps political flags to the flags-2 sprite grid', () => {
|
||||
expect(getBoardFlagDefinition('pol', 'AC')).toMatchObject({
|
||||
code: 'AC',
|
||||
label: 'Anarcho-Capitalist',
|
||||
spritePath: 'assets/icons/flags-2.png',
|
||||
width: 16,
|
||||
height: 12,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
expect(getBoardFlagDefinition('pol', 'WP')).toMatchObject({ code: 'WP', x: 64, y: 48 });
|
||||
});
|
||||
|
||||
it('maps pony flags to the flags-pony sprite grid', () => {
|
||||
expect(getBoardFlagDefinition('pony', '4CC')).toMatchObject({
|
||||
code: '4CC',
|
||||
label: '4cc /mlp/',
|
||||
spritePath: 'assets/icons/flags-pony.png',
|
||||
width: 16,
|
||||
height: 16,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
expect(getBoardFlagDefinition('pony', 'AJ')).toMatchObject({ code: 'AJ', label: 'Applejack', x: 80, y: 0 });
|
||||
expect(getBoardFlagDefinition('pony', 'ZS')).toMatchObject({ code: 'ZS', label: 'Zipp Storm', x: 16, y: 144 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getCommentFlagOptionsForDirectory, getCommentFlagPublishOptionsFromSelection, getCommentFlagRequestFromSelection } from '../comment-flag-selection';
|
||||
|
||||
describe('comment-flag-selection', () => {
|
||||
it('does not expose options for boards without flags', () => {
|
||||
expect(getCommentFlagOptionsForDirectory({ features: {}, title: '/mu/ - Music' })).toEqual([]);
|
||||
});
|
||||
|
||||
it('uses geographic location as the default for country flag boards', () => {
|
||||
expect(
|
||||
getCommentFlagOptionsForDirectory({
|
||||
directoryCode: 'int',
|
||||
features: { hasFlags: true },
|
||||
title: '/int/ - International',
|
||||
}),
|
||||
).toEqual([{ label: 'Geographic Location', value: 'country:auto' }]);
|
||||
});
|
||||
|
||||
it('matches the 4chan /pol/ flag order', () => {
|
||||
const options = getCommentFlagOptionsForDirectory({
|
||||
directoryCode: 'pol',
|
||||
features: { hasFlags: true },
|
||||
title: '/pol/ - Politically Incorrect',
|
||||
});
|
||||
|
||||
expect(options.slice(0, 6)).toEqual([
|
||||
{ label: 'Geographic Location', value: 'country:auto' },
|
||||
{ label: 'Anarcho-Capitalist', value: 'pol:AC' },
|
||||
{ label: 'Anarchist', value: 'pol:AN' },
|
||||
{ label: 'Black Nationalist', value: 'pol:BL' },
|
||||
{ label: 'Confederate', value: 'pol:CF' },
|
||||
{ label: 'Communist', value: 'pol:CM' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('matches the 4chan /mlp/ flag default', () => {
|
||||
const options = getCommentFlagOptionsForDirectory({
|
||||
directoryCode: 'mlp',
|
||||
features: { hasFlags: true },
|
||||
title: '/mlp/ - Pony',
|
||||
});
|
||||
|
||||
expect(options.slice(0, 4)).toEqual([
|
||||
{ label: 'None', value: 'none' },
|
||||
{ label: '4cc /mlp/', value: 'pony:4CC' },
|
||||
{ label: 'Adagio Dazzle', value: 'pony:ADA' },
|
||||
{ label: 'Anon', value: 'pony:AN' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('falls back to the board title when directoryCode is blank', () => {
|
||||
const options = getCommentFlagOptionsForDirectory({
|
||||
directoryCode: ' ',
|
||||
features: { hasFlags: true },
|
||||
title: '/mlp/ - Pony',
|
||||
});
|
||||
|
||||
expect(options.slice(0, 2)).toEqual([
|
||||
{ label: 'None', value: 'none' },
|
||||
{ label: '4cc /mlp/', value: 'pony:4CC' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('serializes selected flags as challenge-readable flag requests', () => {
|
||||
expect(getCommentFlagRequestFromSelection('country:auto')).toEqual({
|
||||
type: 'country',
|
||||
code: 'auto',
|
||||
text: 'flag:country:auto',
|
||||
});
|
||||
expect(getCommentFlagRequestFromSelection('pol:AC')).toEqual({
|
||||
type: 'pol',
|
||||
code: 'AC',
|
||||
text: 'flag:pol:AC',
|
||||
});
|
||||
expect(getCommentFlagRequestFromSelection('pony:AJ')).toEqual({
|
||||
type: 'pony',
|
||||
code: 'AJ',
|
||||
text: 'flag:pony:AJ',
|
||||
});
|
||||
expect(getCommentFlagRequestFromSelection('none')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('publishes selected flags as signed comment flairs and challenge answers', () => {
|
||||
expect(getCommentFlagPublishOptionsFromSelection('country:auto')).toEqual({
|
||||
challengeRequest: {
|
||||
challengeAnswers: ['bitsocial-flags:5chan:flag:country:auto'],
|
||||
},
|
||||
flairs: [{ type: 'country', code: 'auto', text: 'flag:country:auto' }],
|
||||
});
|
||||
expect(getCommentFlagPublishOptionsFromSelection('pony:AJ')).toEqual({
|
||||
challengeRequest: {
|
||||
challengeAnswers: ['bitsocial-flags:5chan:flag:pony:AJ'],
|
||||
},
|
||||
flairs: [{ type: 'pony', code: 'AJ', text: 'flag:pony:AJ' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('clears flag publish data when no flag is selected', () => {
|
||||
expect(getCommentFlagPublishOptionsFromSelection('none')).toEqual({
|
||||
challengeRequest: undefined,
|
||||
flairs: undefined,
|
||||
});
|
||||
expect(getCommentFlagPublishOptionsFromSelection(undefined)).toEqual({
|
||||
challengeRequest: undefined,
|
||||
flairs: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getCommentFlagFlairs, getAuthorFlagFlairs, getAuthorFlagViewModels } from '../comment-flags';
|
||||
|
||||
describe('comment-flags', () => {
|
||||
it('parses the pkc-js challenge flair example for country flags', () => {
|
||||
const [flag] = getAuthorFlagViewModels([{ text: 'US-emoji' }], 1000);
|
||||
|
||||
expect(flag).toMatchObject({
|
||||
key: 'country:us',
|
||||
type: 'country',
|
||||
code: 'us',
|
||||
spritePath: 'assets/icons/flags-1.png',
|
||||
width: 16,
|
||||
height: 11,
|
||||
x: 240,
|
||||
y: 154,
|
||||
});
|
||||
});
|
||||
|
||||
it('parses namespaced country, political, and pony flag flairs', () => {
|
||||
const flags = getAuthorFlagViewModels([{ text: 'flag:country:DE' }, { text: 'flag:pol:AC' }, { text: 'flag:pony:AJ' }], 1000);
|
||||
|
||||
expect(flags.map((flag) => flag.key)).toEqual(['country:de', 'pol:AC', 'pony:AJ']);
|
||||
expect(flags[1]).toMatchObject({ label: 'Anarcho-Capitalist', x: 0, y: 0 });
|
||||
expect(flags[2]).toMatchObject({ label: 'Applejack', x: 80, y: 0 });
|
||||
});
|
||||
|
||||
it('normalizes text flag kinds before parsing', () => {
|
||||
const flags = getAuthorFlagViewModels([{ text: 'FLAG:COUNTRY:DE' }, { text: 'POL:AC' }, { text: 'flag:MLP:AJ' }], 1000);
|
||||
|
||||
expect(flags.map((flag) => flag.key)).toEqual(['country:de', 'pol:AC', 'pony:AJ']);
|
||||
});
|
||||
|
||||
it('parses structured flag flair fields', () => {
|
||||
const flags = getAuthorFlagViewModels(
|
||||
[
|
||||
{ text: 'ignored', type: 'Country', country: 'GB' },
|
||||
{ text: 'ignored', kind: 'MLP', code: 'RD' },
|
||||
],
|
||||
1000,
|
||||
);
|
||||
|
||||
expect(flags.map((flag) => flag.key)).toEqual(['country:gb', 'pony:RD']);
|
||||
});
|
||||
|
||||
it('skips expired, duplicate, and unknown flags', () => {
|
||||
const flags = getAuthorFlagViewModels(
|
||||
[{ text: 'flag:country:DE', expiresAt: 999 }, { text: 'flag:country:DE' }, { text: 'DE-emoji' }, { text: 'flag:pony:NOPE' }],
|
||||
1000,
|
||||
);
|
||||
|
||||
expect(flags.map((flag) => flag.key)).toEqual(['country:de']);
|
||||
});
|
||||
|
||||
it('prefers trusted challenge-written author community flairs', () => {
|
||||
const flairs = getAuthorFlagFlairs({
|
||||
flairs: [{ text: 'flag:pol:AC' }],
|
||||
community: {
|
||||
flairs: [{ text: 'US-emoji' }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(getAuthorFlagViewModels(flairs).map((flag) => flag.key)).toEqual(['country:us']);
|
||||
});
|
||||
|
||||
it('reads the signed 5chan flag assertion from the comment', () => {
|
||||
const flairs = getCommentFlagFlairs({
|
||||
'5chan': {
|
||||
country: 'VN',
|
||||
flag: {
|
||||
code: 'VN',
|
||||
text: 'flag:country:vn',
|
||||
type: 'country',
|
||||
},
|
||||
},
|
||||
flairs: [{ code: 'auto', text: 'flag:country:auto', type: 'country' }],
|
||||
author: {
|
||||
community: {
|
||||
flairs: [{ text: 'US-emoji' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getAuthorFlagViewModels(flairs).map((flag) => flag.key)).toEqual(['country:vn']);
|
||||
});
|
||||
|
||||
it('falls back to author or post flairs when there is no signed flag assertion', () => {
|
||||
expect(
|
||||
getAuthorFlagViewModels(
|
||||
getCommentFlagFlairs({
|
||||
author: {
|
||||
subplebbit: {
|
||||
flairs: [{ text: 'flag:country:DE' }],
|
||||
},
|
||||
},
|
||||
flairs: [{ text: 'flag:pony:AJ' }],
|
||||
}),
|
||||
).map((flag) => flag.key),
|
||||
).toEqual(['country:de']);
|
||||
|
||||
expect(
|
||||
getAuthorFlagViewModels(
|
||||
getCommentFlagFlairs({
|
||||
flairs: [{ text: 'flag:pony:AJ' }],
|
||||
}),
|
||||
).map((flag) => flag.key),
|
||||
).toEqual(['pony:AJ']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
export type BoardFlagKind = 'pol' | 'pony';
|
||||
|
||||
export interface BoardFlagDefinition {
|
||||
kind: BoardFlagKind;
|
||||
code: string;
|
||||
label: string;
|
||||
spritePath: string;
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
const POLITICAL_FLAG_WIDTH = 16;
|
||||
const POLITICAL_FLAG_HEIGHT = 12;
|
||||
const POLITICAL_FLAG_COLUMNS = 5;
|
||||
const POLITICAL_FLAG_SPRITE_PATH = 'assets/icons/flags-2.png';
|
||||
|
||||
const PONY_FLAG_WIDTH = 16;
|
||||
const PONY_FLAG_HEIGHT = 16;
|
||||
const PONY_FLAG_COLUMNS = 9;
|
||||
const PONY_FLAG_SPRITE_PATH = 'assets/icons/flags-pony.png';
|
||||
|
||||
const POLITICAL_FLAG_ENTRIES = [
|
||||
['AC', 'Anarcho-Capitalist'],
|
||||
['AN', 'Anarchist'],
|
||||
['BL', 'Black Nationalist'],
|
||||
['CF', 'Confederate'],
|
||||
['CM', 'Communist'],
|
||||
['CT', 'Catalonia'],
|
||||
['DM', 'Democrat'],
|
||||
['EU', 'European'],
|
||||
['FC', 'Fascist'],
|
||||
['GN', 'Gadsden'],
|
||||
['GY', 'Gay'],
|
||||
['JH', 'Jihadi'],
|
||||
['KN', 'Kekistani'],
|
||||
['MF', 'Muslim'],
|
||||
['NB', 'National Bolshevik'],
|
||||
['NT', 'NATO'],
|
||||
['NZ', 'Nazi'],
|
||||
['PC', 'Hippie'],
|
||||
['PR', 'Pirate'],
|
||||
['RE', 'Republican'],
|
||||
['TM', 'Templar'],
|
||||
['MZ', 'Task Force Z'],
|
||||
['TR', 'Tree Hugger'],
|
||||
['UN', 'United Nations'],
|
||||
['WP', 'White Supremacist'],
|
||||
] as const;
|
||||
|
||||
const PONY_FLAG_ENTRIES = [
|
||||
['4CC', '4cc /mlp/'],
|
||||
['ADA', 'Adagio Dazzle'],
|
||||
['AN', 'Anon'],
|
||||
['ANF', 'Anonfilly'],
|
||||
['APB', 'Apple Bloom'],
|
||||
['AJ', 'Applejack'],
|
||||
['AB', 'Aria Blaze'],
|
||||
['AU', 'Autumn Blaze'],
|
||||
['BB', 'Bon Bon'],
|
||||
['BM', 'Big Mac'],
|
||||
['BP', 'Berry Punch'],
|
||||
['BS', 'Babs Seed'],
|
||||
['CL', 'Changeling'],
|
||||
['CO', 'Coco Pommel'],
|
||||
['CG', 'Cozy Glow'],
|
||||
['CHE', 'Cheerilee'],
|
||||
['CB', 'Cherry Berry'],
|
||||
['DAY', 'Daybreaker'],
|
||||
['DD', 'Daring Do'],
|
||||
['DER', 'Derpy Hooves'],
|
||||
['DT', 'Diamond Tiara'],
|
||||
['DIS', 'Discord'],
|
||||
['EQA', 'EqG Applejack'],
|
||||
['EQF', 'EqG Fluttershy'],
|
||||
['EQP', 'EqG Pinkie Pie'],
|
||||
['EQR', 'EqG Rainbow Dash'],
|
||||
['EQT', 'EqG Trixie'],
|
||||
['EQI', 'EqG Twilight Sparkle'],
|
||||
['EQS', 'EqG Sunset Shimmer'],
|
||||
['ERA', 'EqG Rarity'],
|
||||
['FAU', 'Fausticorn'],
|
||||
['FLE', 'Fleur de lis'],
|
||||
['FL', 'Fluttershy'],
|
||||
['GI', 'Gilda'],
|
||||
['HT', 'Hitch Trailblazer'],
|
||||
['IZ', 'Izzy Moonbow'],
|
||||
['LI', 'Limestone'],
|
||||
['LT', 'Lord Tirek'],
|
||||
['LY', 'Lyra Heartstrings'],
|
||||
['MA', 'Marble'],
|
||||
['MAU', 'Maud'],
|
||||
['MIN', 'Minuette'],
|
||||
['NI', 'Nightmare Moon'],
|
||||
['NUR', 'Nurse Redheart'],
|
||||
['OCT', 'Octavia'],
|
||||
['PAR', 'Parasprite'],
|
||||
['PC', 'Princess Cadance'],
|
||||
['PCE', 'Princess Celestia'],
|
||||
['PI', 'Pinkie Pie'],
|
||||
['PLU', 'Princess Luna'],
|
||||
['PM', 'Pinkamena'],
|
||||
['PP', 'Pipp Petals'],
|
||||
['QC', 'Queen Chrysalis'],
|
||||
['RAR', 'Rarity'],
|
||||
['RD', 'Rainbow Dash'],
|
||||
['RLU', 'Roseluck'],
|
||||
['S1L', 'S1 Luna'],
|
||||
['SCO', 'Scootaloo'],
|
||||
['SHI', 'Shining Armor'],
|
||||
['SIL', 'Silver Spoon'],
|
||||
['SON', 'Sonata Dusk'],
|
||||
['SP', 'Spike'],
|
||||
['SPI', 'Spitfire'],
|
||||
['SS', 'Sunny Starscout'],
|
||||
['STA', 'Star Dancer'],
|
||||
['STL', 'Starlight Glimmer'],
|
||||
['SPT', 'Sprout'],
|
||||
['SUN', 'Sunburst'],
|
||||
['SUS', 'Sunset Shimmer'],
|
||||
['SWB', 'Sweetie Belle'],
|
||||
['TFA', 'TFH Arizona'],
|
||||
['TFO', 'TFH Oleander'],
|
||||
['TFP', 'TFH Paprika'],
|
||||
['TFS', 'TFH Shanty'],
|
||||
['TFT', 'TFH Tianhuo'],
|
||||
['TFV', 'TFH Velvet'],
|
||||
['TP', 'TFH Pom'],
|
||||
['TS', 'Tempest Shadow'],
|
||||
['TWI', 'Twilight Sparkle'],
|
||||
['TX', 'Trixie'],
|
||||
['VS', 'Vinyl Scratch'],
|
||||
['ZE', 'Zecora'],
|
||||
['ZS', 'Zipp Storm'],
|
||||
] as const;
|
||||
|
||||
const normalizeCode = (value: string | undefined) => value?.trim().toUpperCase();
|
||||
|
||||
const buildFlagDefinitions = (
|
||||
entries: readonly (readonly [string, string])[],
|
||||
kind: BoardFlagKind,
|
||||
spritePath: string,
|
||||
width: number,
|
||||
height: number,
|
||||
columns: number,
|
||||
): ReadonlyMap<string, BoardFlagDefinition> =>
|
||||
new Map(
|
||||
entries.map(([code, label], index) => [
|
||||
code,
|
||||
{
|
||||
kind,
|
||||
code,
|
||||
label,
|
||||
spritePath,
|
||||
width,
|
||||
height,
|
||||
x: (index % columns) * width,
|
||||
y: Math.floor(index / columns) * height,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const BOARD_FLAGS_BY_KIND: Record<BoardFlagKind, ReadonlyMap<string, BoardFlagDefinition>> = {
|
||||
pol: buildFlagDefinitions(POLITICAL_FLAG_ENTRIES, 'pol', POLITICAL_FLAG_SPRITE_PATH, POLITICAL_FLAG_WIDTH, POLITICAL_FLAG_HEIGHT, POLITICAL_FLAG_COLUMNS),
|
||||
pony: buildFlagDefinitions(PONY_FLAG_ENTRIES, 'pony', PONY_FLAG_SPRITE_PATH, PONY_FLAG_WIDTH, PONY_FLAG_HEIGHT, PONY_FLAG_COLUMNS),
|
||||
};
|
||||
|
||||
export const normalizeBoardFlagKind = (value: string | undefined): BoardFlagKind | undefined => {
|
||||
const kind = value?.trim().toLowerCase();
|
||||
if (kind === 'pol' || kind === 'political' || kind === 'meme' || kind === 'memeflag' || kind === 'memeflags') {
|
||||
return 'pol';
|
||||
}
|
||||
if (kind === 'pony' || kind === 'mlp') {
|
||||
return 'pony';
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getBoardFlagDefinition = (kindValue: string | undefined, codeValue: string | undefined): BoardFlagDefinition | undefined => {
|
||||
const kind = normalizeBoardFlagKind(kindValue);
|
||||
const code = normalizeCode(codeValue);
|
||||
if (!kind || !code) return undefined;
|
||||
return BOARD_FLAGS_BY_KIND[kind].get(code);
|
||||
};
|
||||
|
||||
export const getBoardFlagDefinitions = (kindValue: string | undefined): BoardFlagDefinition[] => {
|
||||
const kind = normalizeBoardFlagKind(kindValue);
|
||||
return kind ? [...BOARD_FLAGS_BY_KIND[kind].values()] : [];
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import { getBoardFlagDefinition, getBoardFlagDefinitions, type BoardFlagKind } from './board-flags';
|
||||
import type { DirectoryCommunity } from './utils/directory-list-utils';
|
||||
|
||||
const GEOGRAPHIC_LOCATION_FLAG_VALUE = 'country:auto';
|
||||
const NO_FLAG_VALUE = 'none';
|
||||
const FLAG_CHALLENGE_ANSWER_PREFIX = 'bitsocial-flags:5chan:';
|
||||
|
||||
export interface CommentFlagSelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface CommentFlagRequest {
|
||||
type: 'country' | BoardFlagKind;
|
||||
code: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface CommentFlagChallengeRequest {
|
||||
challengeAnswers: string[];
|
||||
}
|
||||
|
||||
export interface CommentFlagPublishOptions {
|
||||
challengeRequest: CommentFlagChallengeRequest | undefined;
|
||||
flairs: CommentFlagRequest[] | undefined;
|
||||
}
|
||||
|
||||
const GEOGRAPHIC_LOCATION_OPTION: CommentFlagSelectOption = {
|
||||
value: GEOGRAPHIC_LOCATION_FLAG_VALUE,
|
||||
label: 'Geographic Location',
|
||||
};
|
||||
|
||||
const NO_FLAG_OPTION: CommentFlagSelectOption = {
|
||||
value: NO_FLAG_VALUE,
|
||||
label: 'None',
|
||||
};
|
||||
|
||||
const getDirectoryCode = (directory: Pick<DirectoryCommunity, 'directoryCode' | 'title'> | undefined): string | undefined => {
|
||||
const directoryCode = directory?.directoryCode?.trim().toLowerCase();
|
||||
return directoryCode || directory?.title?.match(/^\/([^/]+)\//)?.[1]?.toLowerCase();
|
||||
};
|
||||
|
||||
const getBoardFlagOptions = (kind: BoardFlagKind): CommentFlagSelectOption[] =>
|
||||
getBoardFlagDefinitions(kind).map((flag) => ({
|
||||
value: `${flag.kind}:${flag.code}`,
|
||||
label: flag.label,
|
||||
}));
|
||||
|
||||
export const getCommentFlagOptionsForDirectory = (directory: Pick<DirectoryCommunity, 'directoryCode' | 'features' | 'title'> | undefined): CommentFlagSelectOption[] => {
|
||||
if (directory?.features?.hasFlags !== true) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const directoryCode = getDirectoryCode(directory);
|
||||
if (directoryCode === 'mlp') {
|
||||
return [NO_FLAG_OPTION, ...getBoardFlagOptions('pony')];
|
||||
}
|
||||
|
||||
if (directoryCode === 'pol') {
|
||||
return [GEOGRAPHIC_LOCATION_OPTION, ...getBoardFlagOptions('pol')];
|
||||
}
|
||||
|
||||
return [GEOGRAPHIC_LOCATION_OPTION];
|
||||
};
|
||||
|
||||
export const getCommentFlagRequestFromSelection = (value: string | undefined): CommentFlagRequest | undefined => {
|
||||
if (!value || value === NO_FLAG_VALUE) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (value === GEOGRAPHIC_LOCATION_FLAG_VALUE) {
|
||||
return {
|
||||
type: 'country',
|
||||
code: 'auto',
|
||||
text: 'flag:country:auto',
|
||||
};
|
||||
}
|
||||
|
||||
const match = value.match(/^(pol|pony):([A-Z0-9]+)$/);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [, kind, code] = match;
|
||||
const flag = getBoardFlagDefinition(kind, code);
|
||||
return flag
|
||||
? {
|
||||
type: flag.kind,
|
||||
code: flag.code,
|
||||
text: `flag:${flag.kind}:${flag.code}`,
|
||||
}
|
||||
: undefined;
|
||||
};
|
||||
|
||||
export const getCommentFlagPublishOptionsFromSelection = (value: string | undefined): CommentFlagPublishOptions => {
|
||||
const flag = getCommentFlagRequestFromSelection(value);
|
||||
return flag
|
||||
? {
|
||||
challengeRequest: {
|
||||
challengeAnswers: [`${FLAG_CHALLENGE_ANSWER_PREFIX}${flag.text}`],
|
||||
},
|
||||
flairs: [flag],
|
||||
}
|
||||
: {
|
||||
challengeRequest: undefined,
|
||||
flairs: undefined,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,221 @@
|
||||
import { COUNTRY_FLAG_HEIGHT, COUNTRY_FLAG_WIDTH, getCountryFlagPosition, getCountryLabel, normalizeCountryCode } from './country-flags';
|
||||
import { getBoardFlagDefinition, normalizeBoardFlagKind, type BoardFlagKind } from './board-flags';
|
||||
|
||||
const COUNTRY_FLAG_SPRITE_PATH = 'assets/icons/flags-1.png';
|
||||
const FLAG_TEXT_PATTERN = /^flag:([a-z-]+):([a-z0-9-]+)$/i;
|
||||
const SHORT_FLAG_TEXT_PATTERN = /^(country|geo|pol|political|meme|memeflag|memeflags|pony|mlp):([a-z0-9-]+)$/i;
|
||||
const COUNTRY_EMOJI_TEXT_PATTERN = /^([a-z]{2}|catalonia|eu|fam|xe|xs|xw|xk|xx)-emoji$/i;
|
||||
|
||||
type FlagSource =
|
||||
| { type: 'country'; code: string }
|
||||
| {
|
||||
type: BoardFlagKind;
|
||||
code: string;
|
||||
};
|
||||
|
||||
interface AuthorFlairLike {
|
||||
text?: unknown;
|
||||
code?: unknown;
|
||||
country?: unknown;
|
||||
type?: unknown;
|
||||
kind?: unknown;
|
||||
expiresAt?: unknown;
|
||||
}
|
||||
|
||||
interface AuthorLike {
|
||||
flairs?: unknown;
|
||||
community?: {
|
||||
flairs?: unknown;
|
||||
};
|
||||
subplebbit?: {
|
||||
flairs?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
interface CommentLike {
|
||||
'5chan'?: unknown;
|
||||
author?: unknown;
|
||||
flairs?: unknown;
|
||||
raw?: {
|
||||
comment?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AuthorFlagViewModel {
|
||||
key: string;
|
||||
type: 'country' | BoardFlagKind;
|
||||
code: string;
|
||||
label: string;
|
||||
spritePath: string;
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
const toString = (value: unknown): string | undefined => (typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined);
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
|
||||
const normalizeFlairsContainer = (flairs: unknown): unknown => {
|
||||
if (Array.isArray(flairs)) return flairs;
|
||||
return isRecord(flairs) && Array.isArray(flairs.author) ? flairs.author : undefined;
|
||||
};
|
||||
|
||||
export const getAuthorFlagFlairs = (author: unknown): unknown => {
|
||||
if (!isRecord(author)) return undefined;
|
||||
|
||||
const authorLike = author as AuthorLike;
|
||||
return normalizeFlairsContainer(authorLike.community?.flairs) ?? normalizeFlairsContainer(authorLike.subplebbit?.flairs) ?? normalizeFlairsContainer(authorLike.flairs);
|
||||
};
|
||||
|
||||
const getFiveChanFlagFlair = (comment: unknown): AuthorFlairLike | undefined => {
|
||||
if (!isRecord(comment)) return undefined;
|
||||
|
||||
const fiveChan = (comment as CommentLike)['5chan'];
|
||||
if (!isRecord(fiveChan) || !isRecord(fiveChan.flag)) return undefined;
|
||||
|
||||
const flag = fiveChan.flag;
|
||||
const text = toString(flag.text);
|
||||
const type = toString(flag.type);
|
||||
const code = toString(flag.code) ?? toString(fiveChan.country);
|
||||
const country = toString(fiveChan.country);
|
||||
|
||||
if (!text && (!type || !code)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
text,
|
||||
type,
|
||||
code,
|
||||
country,
|
||||
};
|
||||
};
|
||||
|
||||
export const getCommentFlagFlairs = (comment: unknown, author?: unknown): unknown => {
|
||||
if (!isRecord(comment)) return getAuthorFlagFlairs(author);
|
||||
|
||||
const commentLike = comment as CommentLike;
|
||||
const fiveChanFlagFlair = getFiveChanFlagFlair(comment) ?? getFiveChanFlagFlair(commentLike.raw?.comment);
|
||||
if (fiveChanFlagFlair) {
|
||||
return [fiveChanFlagFlair];
|
||||
}
|
||||
|
||||
return getAuthorFlagFlairs(author ?? commentLike.author) ?? normalizeFlairsContainer(commentLike.flairs);
|
||||
};
|
||||
|
||||
const isFreshFlair = (flair: AuthorFlairLike, nowSeconds: number) =>
|
||||
typeof flair.expiresAt !== 'number' || !Number.isFinite(flair.expiresAt) || flair.expiresAt > nowSeconds;
|
||||
|
||||
const parseTextFlagSource = (text: string | undefined): FlagSource | undefined => {
|
||||
if (!text) return undefined;
|
||||
|
||||
const flagMatch = text.match(FLAG_TEXT_PATTERN);
|
||||
if (flagMatch) {
|
||||
const [, kindValue, code] = flagMatch;
|
||||
const normalizedKindValue = kindValue.toLowerCase();
|
||||
if (normalizedKindValue === 'country' || normalizedKindValue === 'geo') {
|
||||
return { type: 'country', code };
|
||||
}
|
||||
const kind = normalizeBoardFlagKind(normalizedKindValue);
|
||||
return kind ? { type: kind, code } : undefined;
|
||||
}
|
||||
|
||||
const shortMatch = text.match(SHORT_FLAG_TEXT_PATTERN);
|
||||
if (shortMatch) {
|
||||
const [, kindValue, code] = shortMatch;
|
||||
const normalizedKindValue = kindValue.toLowerCase();
|
||||
if (normalizedKindValue === 'country' || normalizedKindValue === 'geo') {
|
||||
return { type: 'country', code };
|
||||
}
|
||||
const kind = normalizeBoardFlagKind(normalizedKindValue);
|
||||
return kind ? { type: kind, code } : undefined;
|
||||
}
|
||||
|
||||
const countryEmojiMatch = text.match(COUNTRY_EMOJI_TEXT_PATTERN);
|
||||
return countryEmojiMatch ? { type: 'country', code: countryEmojiMatch[1] } : undefined;
|
||||
};
|
||||
|
||||
const parseStructuredFlagSource = (flair: AuthorFlairLike): FlagSource | undefined => {
|
||||
const typeValue = toString(flair.type) ?? toString(flair.kind);
|
||||
if (!typeValue) return undefined;
|
||||
|
||||
const normalizedTypeValue = typeValue.toLowerCase();
|
||||
if (normalizedTypeValue === 'country' || normalizedTypeValue === 'geo') {
|
||||
const code = toString(flair.country) ?? toString(flair.code);
|
||||
return code ? { type: 'country', code } : undefined;
|
||||
}
|
||||
|
||||
const kind = normalizeBoardFlagKind(normalizedTypeValue);
|
||||
const code = toString(flair.code);
|
||||
return kind && code ? { type: kind, code } : undefined;
|
||||
};
|
||||
|
||||
const toCountryFlagViewModel = (codeValue: string): AuthorFlagViewModel | undefined => {
|
||||
const code = normalizeCountryCode(codeValue);
|
||||
const position = getCountryFlagPosition(code);
|
||||
if (!code || !position) return undefined;
|
||||
|
||||
return {
|
||||
key: `country:${code}`,
|
||||
type: 'country',
|
||||
code,
|
||||
label: getCountryLabel(code) ?? code.toUpperCase(),
|
||||
spritePath: COUNTRY_FLAG_SPRITE_PATH,
|
||||
width: COUNTRY_FLAG_WIDTH,
|
||||
height: COUNTRY_FLAG_HEIGHT,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
};
|
||||
};
|
||||
|
||||
const toBoardFlagViewModel = (type: BoardFlagKind, code: string): AuthorFlagViewModel | undefined => {
|
||||
const flag = getBoardFlagDefinition(type, code);
|
||||
return flag
|
||||
? {
|
||||
key: `${flag.kind}:${flag.code}`,
|
||||
type: flag.kind,
|
||||
code: flag.code,
|
||||
label: flag.label,
|
||||
spritePath: flag.spritePath,
|
||||
width: flag.width,
|
||||
height: flag.height,
|
||||
x: flag.x,
|
||||
y: flag.y,
|
||||
}
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const toFlagViewModel = (source: FlagSource | undefined): AuthorFlagViewModel | undefined => {
|
||||
if (!source) return undefined;
|
||||
return source.type === 'country' ? toCountryFlagViewModel(source.code) : toBoardFlagViewModel(source.type, source.code);
|
||||
};
|
||||
|
||||
export const getAuthorFlagViewModels = (flairs: unknown, nowSeconds = Date.now() / 1000): AuthorFlagViewModel[] => {
|
||||
if (!Array.isArray(flairs)) return [];
|
||||
|
||||
const seenKeys = new Set<string>();
|
||||
const flags: AuthorFlagViewModel[] = [];
|
||||
|
||||
for (const flair of flairs) {
|
||||
if (typeof flair !== 'object' || flair === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const flairRecord = flair as AuthorFlairLike;
|
||||
if (!isFreshFlair(flairRecord, nowSeconds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const flag = toFlagViewModel(parseStructuredFlagSource(flairRecord) ?? parseTextFlagSource(toString(flairRecord.text)));
|
||||
if (!flag || seenKeys.has(flag.key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seenKeys.add(flag.key);
|
||||
flags.push(flag);
|
||||
}
|
||||
|
||||
return flags;
|
||||
};
|
||||
@@ -43,6 +43,10 @@ describe('publish stores', () => {
|
||||
spoiler: true,
|
||||
communityAddress: 'music-posting.eth',
|
||||
title: 'Hello',
|
||||
challengeRequest: {
|
||||
challengeAnswers: ['bitsocial-flags:5chan:flag:country:auto'],
|
||||
},
|
||||
flairs: [{ text: 'flag:country:auto', type: 'country', code: 'auto' }],
|
||||
};
|
||||
|
||||
usePublishPostStore.getState().setPublishPostStore(comment);
|
||||
@@ -53,6 +57,10 @@ describe('publish stores', () => {
|
||||
expect(state.publishCommentOptions.author).toEqual({ address: '0x123', role: 'mod', displayName: 'Poster Alias' });
|
||||
expect(state.publishCommentOptions.communityAddress).toBe('music-posting.eth');
|
||||
expect(state.publishCommentOptions.title).toBe('Hello');
|
||||
expect(state.publishCommentOptions.challengeRequest).toEqual({
|
||||
challengeAnswers: ['bitsocial-flags:5chan:flag:country:auto'],
|
||||
});
|
||||
expect(state.publishCommentOptions.flairs).toEqual([{ text: 'flag:country:auto', type: 'country', code: 'auto' }]);
|
||||
|
||||
state.publishCommentOptions.onChallengeVerification?.({} as never, comment);
|
||||
expect(testState.alertChallengeVerificationFailedMock).toHaveBeenCalledWith({}, comment);
|
||||
@@ -88,6 +96,10 @@ describe('publish stores', () => {
|
||||
parentCid: 'parent-1',
|
||||
spoiler: false,
|
||||
communityAddress: 'music-posting.eth',
|
||||
challengeRequest: {
|
||||
challengeAnswers: ['bitsocial-flags:5chan:flag:pol:AC'],
|
||||
},
|
||||
flairs: [{ text: 'flag:pol:AC', type: 'pol', code: 'AC' }],
|
||||
};
|
||||
|
||||
usePublishReplyStore.getState().setPublishReplyStore(comment);
|
||||
@@ -98,6 +110,10 @@ describe('publish stores', () => {
|
||||
expect(state.publishCommentOptions['parent-1']?.communityAddress).toBe('music-posting.eth');
|
||||
expect(state.publishCommentOptions['parent-1']?.parentCid).toBe('parent-1');
|
||||
expect(state.publishCommentOptions['parent-1']?.postCid).toBe('parent-1');
|
||||
expect(state.publishCommentOptions['parent-1']?.challengeRequest).toEqual({
|
||||
challengeAnswers: ['bitsocial-flags:5chan:flag:pol:AC'],
|
||||
});
|
||||
expect(state.publishCommentOptions['parent-1']?.flairs).toEqual([{ text: 'flag:pol:AC', type: 'pol', code: 'AC' }]);
|
||||
|
||||
state.publishCommentOptions['parent-1']?.onChallengeVerification?.({ token: 'challenge' } as never, comment);
|
||||
expect(testState.alertChallengeVerificationFailedMock).toHaveBeenCalledWith({ token: 'challenge' }, comment);
|
||||
|
||||
@@ -11,7 +11,9 @@ type SubmitState = {
|
||||
title: string | undefined;
|
||||
content: string | undefined;
|
||||
link: string | undefined;
|
||||
flairs: Comment['flairs'] | undefined;
|
||||
spoiler: boolean | undefined;
|
||||
challengeRequest: Record<string, unknown> | undefined;
|
||||
publishCommentOptions: PublishCommentOptions;
|
||||
setPublishPostStore: (data: Partial<SubmitState>) => void;
|
||||
resetPublishPostStore: () => void;
|
||||
@@ -24,11 +26,13 @@ const usePublishPostStore = create<SubmitState>((set) => ({
|
||||
title: undefined,
|
||||
content: undefined,
|
||||
link: undefined,
|
||||
flairs: undefined,
|
||||
spoiler: undefined,
|
||||
challengeRequest: undefined,
|
||||
publishCommentOptions: {},
|
||||
setPublishPostStore: (comment: Comment) =>
|
||||
set(() => {
|
||||
const { author, content, spoiler, title } = comment;
|
||||
const { author, challengeRequest, content, flairs, spoiler, title } = comment;
|
||||
const link = comment.link ? normalizePublishURL(comment.link) : undefined;
|
||||
const communityAddress = getCommentCommunityAddress(comment);
|
||||
|
||||
@@ -44,7 +48,9 @@ const usePublishPostStore = create<SubmitState>((set) => ({
|
||||
title,
|
||||
content,
|
||||
link,
|
||||
flairs,
|
||||
spoiler,
|
||||
...(challengeRequest ? { challengeRequest } : {}),
|
||||
onChallengeVerification: (challengeVerification: ChallengeVerification, comment: Comment) => {
|
||||
alertChallengeVerificationFailed(challengeVerification, comment);
|
||||
},
|
||||
@@ -65,7 +71,9 @@ const usePublishPostStore = create<SubmitState>((set) => ({
|
||||
title,
|
||||
content,
|
||||
link,
|
||||
flairs,
|
||||
spoiler,
|
||||
challengeRequest,
|
||||
publishCommentOptions,
|
||||
};
|
||||
}),
|
||||
@@ -77,7 +85,9 @@ const usePublishPostStore = create<SubmitState>((set) => ({
|
||||
title: undefined,
|
||||
content: undefined,
|
||||
link: undefined,
|
||||
flairs: undefined,
|
||||
spoiler: undefined,
|
||||
challengeRequest: undefined,
|
||||
publishCommentOptions: {},
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -9,7 +9,9 @@ type ReplyState = {
|
||||
displayName: { [parentCid: string]: string | undefined };
|
||||
content: { [parentCid: string]: string | undefined };
|
||||
link: { [parentCid: string]: string | undefined };
|
||||
flairs: { [parentCid: string]: Comment['flairs'] | undefined };
|
||||
spoiler: { [parentCid: string]: boolean | undefined };
|
||||
challengeRequest: { [parentCid: string]: Record<string, unknown> | undefined };
|
||||
publishCommentOptions: { [parentCid: string]: PublishCommentOptions | undefined };
|
||||
setPublishReplyStore: (comment: Comment) => void;
|
||||
resetPublishReplyStore: (parentCid: string) => void;
|
||||
@@ -20,12 +22,14 @@ const usePublishReplyStore = create<ReplyState>((set) => ({
|
||||
displayName: {},
|
||||
content: {},
|
||||
link: {},
|
||||
flairs: {},
|
||||
spoiler: {},
|
||||
challengeRequest: {},
|
||||
publishCommentOptions: {},
|
||||
|
||||
setPublishReplyStore: (comment: Comment) =>
|
||||
set((state) => {
|
||||
const { parentCid, author, content, spoiler } = comment;
|
||||
const { parentCid, author, challengeRequest, content, flairs, spoiler } = comment;
|
||||
const link = comment.link ? normalizePublishURL(comment.link) : undefined;
|
||||
const communityAddress = getCommentCommunityAddress(comment);
|
||||
|
||||
@@ -42,7 +46,9 @@ const usePublishReplyStore = create<ReplyState>((set) => ({
|
||||
postCid: comment?.postCid || parentCid,
|
||||
content,
|
||||
link,
|
||||
flairs,
|
||||
spoiler,
|
||||
...(challengeRequest ? { challengeRequest } : {}),
|
||||
onChallengeVerification: (challengeVerification: ChallengeVerification, comment: Comment) => {
|
||||
alertChallengeVerificationFailed(challengeVerification, comment);
|
||||
},
|
||||
@@ -61,7 +67,9 @@ const usePublishReplyStore = create<ReplyState>((set) => ({
|
||||
displayName: { ...state.displayName, [parentCid]: displayName },
|
||||
content: { ...state.content, [parentCid]: content },
|
||||
link: { ...state.link, [parentCid]: link },
|
||||
flairs: { ...state.flairs, [parentCid]: flairs },
|
||||
spoiler: { ...state.spoiler, [parentCid]: spoiler },
|
||||
challengeRequest: { ...state.challengeRequest, [parentCid]: challengeRequest },
|
||||
publishCommentOptions: { ...state.publishCommentOptions, [parentCid]: publishCommentOptions },
|
||||
};
|
||||
}),
|
||||
@@ -72,7 +80,9 @@ const usePublishReplyStore = create<ReplyState>((set) => ({
|
||||
displayName: { ...state.displayName, [parentCid]: undefined },
|
||||
content: { ...state.content, [parentCid]: undefined },
|
||||
link: { ...state.link, [parentCid]: undefined },
|
||||
flairs: { ...state.flairs, [parentCid]: undefined },
|
||||
spoiler: { ...state.spoiler, [parentCid]: undefined },
|
||||
challengeRequest: { ...state.challengeRequest, [parentCid]: undefined },
|
||||
publishCommentOptions: { ...state.publishCommentOptions, [parentCid]: undefined },
|
||||
})),
|
||||
}));
|
||||
|
||||
@@ -129,6 +129,21 @@
|
||||
font-weight: var(--post-name-font-weight);
|
||||
}
|
||||
|
||||
.authorFlags {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
margin-right: 3px;
|
||||
vertical-align: -1px;
|
||||
}
|
||||
|
||||
.authorFlag {
|
||||
display: inline-block;
|
||||
flex: 0 0 auto;
|
||||
background-repeat: no-repeat;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.postDesktop .postNumLink a, .postDesktop .postNumLink span, .replyDesktop .postNumLink a, .replyDesktop .postNumLink span {
|
||||
color: unset;
|
||||
text-decoration: unset;
|
||||
|
||||
@@ -25,7 +25,7 @@ __metadata:
|
||||
"@electron-forge/maker-zip": "npm:7.8.0"
|
||||
"@electron/rebuild": "npm:3.7.2"
|
||||
"@floating-ui/react": "npm:0.26.1"
|
||||
"@pkcprotocol/pkc-js": "npm:0.0.35"
|
||||
"@pkcprotocol/pkc-js": "npm:0.0.37"
|
||||
"@react-spring/web": "npm:10.0.3"
|
||||
"@reforged/maker-appimage": "npm:5.1.1"
|
||||
"@types/lodash": "npm:4.17.24"
|
||||
@@ -3958,15 +3958,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@libp2p/gossipsub@npm:15.0.21":
|
||||
version: 15.0.21
|
||||
resolution: "@libp2p/gossipsub@npm:15.0.21"
|
||||
"@libp2p/gossipsub@npm:15.0.23":
|
||||
version: 15.0.23
|
||||
resolution: "@libp2p/gossipsub@npm:15.0.23"
|
||||
dependencies:
|
||||
"@libp2p/crypto": "npm:^5.1.17"
|
||||
"@libp2p/crypto": "npm:^5.1.18"
|
||||
"@libp2p/interface": "npm:^3.2.2"
|
||||
"@libp2p/interface-internal": "npm:^3.1.3"
|
||||
"@libp2p/peer-id": "npm:^6.0.8"
|
||||
"@libp2p/utils": "npm:^7.1.0"
|
||||
"@libp2p/interface-internal": "npm:^3.1.5"
|
||||
"@libp2p/peer-id": "npm:^6.0.9"
|
||||
"@libp2p/utils": "npm:^7.2.1"
|
||||
"@multiformats/multiaddr": "npm:^13.0.1"
|
||||
denque: "npm:^2.1.0"
|
||||
it-length-prefixed: "npm:^10.0.1"
|
||||
@@ -3976,7 +3976,7 @@ __metadata:
|
||||
protons-runtime: "npm:^6.0.1"
|
||||
uint8arraylist: "npm:^2.4.8"
|
||||
uint8arrays: "npm:^5.0.1"
|
||||
checksum: 10c0/d7001866a16020dd0a9cf49df0a798c97102851d991da21528dda91b190bb5e069151d08e36549d5c9ccdce481fe876adadd448121fcafb267049bc9ba5264d2
|
||||
checksum: 10c0/1bf9caeea0ae27813e6c22ce855d372d96bea2ac8599dca40722b727a40f6dc1625b07a26de82a62e1bfdc6de820fb13ba8c392d8a06aa93f7d6669bf192bbd3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -5829,9 +5829,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@pkcprotocol/pkc-js@npm:0.0.35":
|
||||
version: 0.0.35
|
||||
resolution: "@pkcprotocol/pkc-js@npm:0.0.35"
|
||||
"@pkcprotocol/pkc-js@npm:0.0.37":
|
||||
version: 0.0.37
|
||||
resolution: "@pkcprotocol/pkc-js@npm:0.0.37"
|
||||
dependencies:
|
||||
"@enhances/with-resolvers": "npm:0.0.5"
|
||||
"@helia/block-brokers": "npm:5.2.4"
|
||||
@@ -5880,7 +5880,7 @@ __metadata:
|
||||
typestub-ipfs-only-hash: "npm:4.0.0"
|
||||
uuid: "npm:13.0.0"
|
||||
zod: "npm:4.3.6"
|
||||
checksum: 10c0/b218f19f8f01c422f68069f9f49e08dd3f57cfa082cb1213cc30c624e2ee6e7608480ae34152e23ba5d6b78ba21b0841f29e623632f7b97561cb1314a748d265
|
||||
checksum: 10c0/4a76a0a64685414ce3eee004189b96beb6ddfeee654eb191c104c8a38654b0b15de591966f39de67f5961ba07fe2cf0bedc744c280382b93e52ece2887618c58
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user