Merge pull request #348 from plebbit/development

Development
This commit is contained in:
Tom (plebeius.eth)
2024-04-29 12:36:00 +02:00
committed by GitHub
52 changed files with 746 additions and 113 deletions
+1 -4
View File
@@ -28,7 +28,7 @@
"prettier": "3.0.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-draggable": "^4.4.6",
"react-draggable": "4.4.6",
"react-i18next": "13.2.2",
"react-markdown": "8.0.6",
"react-router-dom": "6.16.0",
@@ -136,9 +136,6 @@
"category": "Network"
}
},
"resolutions": {
"styled-components": "^5"
},
"lint-staged": {
"{src,electron}/**/*.{js,ts,tsx}": [
"prettier --write"
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "الاسم",
"comment": "تعليق",
"subject": "الموضوع",
"anonymous": "مجهول"
"anonymous": "مجهول",
"image": "صورة",
"video": "فيديو",
"audio": "صوت",
"webpage": "صفحة ويب",
"iframe": "إطار",
"rules": "القواعد",
"welcome_to_board": "مرحبًا بك في {{board}}",
"comment_too_long": "التعليق طويل جدًا. <1>اضغط هنا</1> لعرض النص الكامل.",
"replies_omitted": "{{repliesCount}} ردود محذوفة. <1>اضغط هنا</1> لعرضها.",
"replies_and_links_omitted": "{{repliesCount}} ردود و{{linksCount}} روابط محذوفة. <1>اضغط هنا</1> لعرضها.",
"thread_closed_alert": "الموضوع مغلق"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "নাম",
"comment": "মন্তব্য",
"subject": "বিষয়",
"anonymous": "অজানা"
"anonymous": "অজানা",
"image": "ছবি",
"video": "ভিডিও",
"audio": "অডিও",
"webpage": "ওয়েবপেজ",
"iframe": "আইফ্রেম",
"rules": "নিয়ম",
"welcome_to_board": "স্বাগতম {{board}}",
"comment_too_long": "মন্তব্য খুব দীর্ঘ। <1>পূর্ণ পাঠ্য দেখতে এখানে ক্লিক করুন</1>।",
"replies_omitted": "{{repliesCount}}টি উত্তর বাদ দেওয়া হয়েছে। <1>দেখতে এখানে ক্লিক করুন</1>।",
"replies_and_links_omitted": "{{repliesCount}}টি উত্তর এবং {{linksCount}}টি লিঙ্ক বাদ দেওয়া হয়েছে। <1>দেখতে এখানে ক্লিক করুন</1>।",
"thread_closed_alert": "থ্রেডটি বন্ধ করা হয়েছে"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "název",
"comment": "komentář",
"subject": "předmět",
"anonymous": "anonymní"
"anonymous": "anonymní",
"image": "obrázek",
"video": "video",
"audio": "audio",
"webpage": "webová stránka",
"iframe": "iframe",
"rules": "pravidla",
"welcome_to_board": "Vítejte na {{board}}",
"comment_too_long": "Komentář je příliš dlouhý. <1>Klikněte zde</1> pro zobrazení celého textu.",
"replies_omitted": "{{repliesCount}} odpovědí vynecháno. <1>Klikněte zde</1> pro zobrazení.",
"replies_and_links_omitted": "{{repliesCount}} odpovědí a {{linksCount}} odkazů vynecháno. <1>Klikněte zde</1> pro zobrazení.",
"thread_closed_alert": "Vlákno je uzavřeno"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "navn",
"comment": "kommentar",
"subject": "emne",
"anonymous": "anonym"
"anonymous": "anonym",
"image": "billede",
"video": "video",
"audio": "lyd",
"webpage": "websted",
"iframe": "iframe",
"rules": "regler",
"welcome_to_board": "Velkommen til {{board}}",
"comment_too_long": "Kommentaren er for lang. <1>Klik her</1> for at se hele teksten.",
"replies_omitted": "{{repliesCount}} svar udeladt. <1>Klik her</1> for at se dem.",
"replies_and_links_omitted": "{{repliesCount}} svar og {{linksCount}} links udeladt. <1>Klik her</1> for at se dem.",
"thread_closed_alert": "Tråden er lukket"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "Name",
"comment": "Kommentar",
"subject": "Betreff",
"anonymous": "anonym"
"anonymous": "anonym",
"image": "Bild",
"video": "Video",
"audio": "Audio",
"webpage": "Webseite",
"iframe": "iframe",
"rules": "Regeln",
"welcome_to_board": "Willkommen bei {{board}}",
"comment_too_long": "Kommentar zu lang. <1>Klicken Sie hier</1>, um den vollständigen Text anzuzeigen.",
"replies_omitted": "{{repliesCount}} Antworten ausgelassen. <1>Klicken Sie hier</1>, um sie anzuzeigen.",
"replies_and_links_omitted": "{{repliesCount}} Antworten und {{linksCount}} Links ausgelassen. <1>Klicken Sie hier</1>, um sie anzuzeigen.",
"thread_closed_alert": "Der Thread ist geschlossen"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "όνομα",
"comment": "σχόλιο",
"subject": "θέμα",
"anonymous": "ανώνυμος"
"anonymous": "ανώνυμος",
"image": "εικόνα",
"video": "βίντεο",
"audio": "ήχος",
"webpage": "ιστοσελίδα",
"iframe": "πλαίσιο",
"rules": "κανόνες",
"welcome_to_board": "Καλώς ήρθατε στο {{board}}",
"comment_too_long": "Το σχόλιο είναι πολύ μακροσκελές. <1>Κάντε κλικ εδώ</1> για να δείτε το πλήρες κείμενο.",
"replies_omitted": "{{repliesCount}} απαντήσεις παραλείφθηκαν. <1>Κάντε κλικ εδώ</1> για να τις δείτε.",
"replies_and_links_omitted": "{{repliesCount}} απαντήσεις και {{linksCount}} σύνδεσμοι παραλείφθηκαν. <1>Κάντε κλικ εδώ</1> για να τις δείτε.",
"thread_closed_alert": "Το νήμα έχει κλείσει"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "name",
"comment": "comment",
"subject": "subject",
"anonymous": "anonymous"
"anonymous": "anonymous",
"image": "image",
"video": "video",
"audio": "audio",
"webpage": "webpage",
"iframe": "iframe",
"rules": "rules",
"welcome_to_board": "Welcome to {{board}}",
"comment_too_long": "Comment too long. <1>Click here</1> to view the full text.",
"replies_omitted": "{{repliesCount}} replies omitted. <1>Click here</1> to view.",
"replies_and_links_omitted": "{{repliesCount}} replies and {{linksCount}} links omitted. <1>Click here</1> to view.",
"thread_closed_alert": "This thread is closed"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "nombre",
"comment": "comentario",
"subject": "asunto",
"anonymous": "anónimo"
"anonymous": "anónimo",
"image": "imagen",
"video": "video",
"audio": "audio",
"webpage": "página web",
"iframe": "iframe",
"rules": "reglas",
"welcome_to_board": "Bienvenido a {{board}}",
"comment_too_long": "Comentario demasiado largo. <1>Haz clic aquí</1> para ver el texto completo.",
"replies_omitted": "{{repliesCount}} respuestas omitidas. <1>Haz clic aquí</1> para verlas.",
"replies_and_links_omitted": "{{repliesCount}} respuestas y {{linksCount}} enlaces omitidos. <1>Haz clic aquí</1> para verlos.",
"thread_closed_alert": "El hilo está cerrado"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "نام",
"comment": "نظر",
"subject": "موضوع",
"anonymous": "ناشناس"
"anonymous": "ناشناس",
"image": "تصویر",
"video": "ویدیو",
"audio": "صدا",
"webpage": "صفحه وب",
"iframe": "فریم",
"rules": "قوانین",
"welcome_to_board": "خوش آمدید به {{board}}",
"comment_too_long": "نظر بسیار طولانی است. <1>اینجا کلیک کنید</1> تا متن کامل را ببینید.",
"replies_omitted": "{{repliesCount}} پاسخ حذف شده است. <1>اینجا کلیک کنید</1> تا آنها را مشاهده کنید.",
"replies_and_links_omitted": "{{repliesCount}} پاسخ و {{linksCount}} لینک حذف شده است. <1>اینجا کلیک کنید</1> تا آنها را مشاهده کنید.",
"thread_closed_alert": "موضوع بسته شده است"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "nimi",
"comment": "kommentti",
"subject": "aihe",
"anonymous": "anonyymi"
"anonymous": "anonyymi",
"image": "kuva",
"video": "video",
"audio": "audio",
"webpage": "verkkosivu",
"iframe": "iframe",
"rules": "säännöt",
"welcome_to_board": "Tervetuloa {{board}}",
"comment_too_long": "Kommentti on liian pitkä. <1>Napsauta tästä</1> nähdäksesi koko tekstin.",
"replies_omitted": "{{repliesCount}} vastausta jätetty pois. <1>Napsauta tästä</1> nähdäksesi ne.",
"replies_and_links_omitted": "{{repliesCount}} vastausta ja {{linksCount}} linkkiä jätetty pois. <1>Napsauta tästä</1> nähdäksesi ne.",
"thread_closed_alert": "Ketju on suljettu"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "pangalan",
"comment": "komento",
"subject": "paksa",
"anonymous": "anonimo"
"anonymous": "anonimo",
"image": "larawan",
"video": "video",
"audio": "audio",
"webpage": "pahinahon",
"iframe": "iframe",
"rules": "mga patakaran",
"welcome_to_board": "Maligayang pagdating sa {{board}}",
"comment_too_long": "Masyadong mahaba ang komento. <1>I-click dito</1> upang makita ang buong teksto.",
"replies_omitted": "{{repliesCount}} mga tugon ang hindi ipinakita. <1>I-click dito</1> upang makita.",
"replies_and_links_omitted": "{{repliesCount}} mga tugon at {{linksCount}} mga link ang hindi ipinakita. <1>I-click dito</1> upang makita.",
"thread_closed_alert": "Sarado na ang thread"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "nom",
"comment": "commentaire",
"subject": "sujet",
"anonymous": "anonyme"
"anonymous": "anonyme",
"image": "image",
"video": "vidéo",
"audio": "audio",
"webpage": "page web",
"iframe": "iframe",
"rules": "règles",
"welcome_to_board": "Bienvenue sur {{board}}",
"comment_too_long": "Commentaire trop long. <1>Cliquez ici</1> pour voir le texte complet.",
"replies_omitted": "{{repliesCount}} réponses omises. <1>Cliquez ici</1> pour les afficher.",
"replies_and_links_omitted": "{{repliesCount}} réponses et {{linksCount}} liens omis. <1>Cliquez ici</1> pour les afficher.",
"thread_closed_alert": "Le fil est fermé"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "שם",
"comment": "תגובה",
"subject": "נושא",
"anonymous": "אנונימי"
"anonymous": "אנונימי",
"image": "תמונה",
"video": "וידאו",
"audio": "שמע",
"webpage": "דף אינטרנט",
"iframe": "מסגרת",
"rules": "חוקים",
"welcome_to_board": "ברוך הבא ל{{board}}",
"comment_too_long": "התגובה ארוכה מדי. <1>לחץ כאן</1> כדי לראות את הטקסט המלא.",
"replies_omitted": "{{repliesCount}} תגובות נשמטו. <1>לחץ כאן</1> לצפייה.",
"replies_and_links_omitted": "{{repliesCount}} תגובות ו{{linksCount}} קישורים נשמטו. <1>לחץ כאן</1> לצפייה.",
"thread_closed_alert": "האשכול סגור"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "नाम",
"comment": "टिप्पणी",
"subject": "विषय",
"anonymous": "अनाम"
"anonymous": "अनाम",
"image": "चित्र",
"video": "वीडियो",
"audio": "ऑडियो",
"webpage": "वेबपेज",
"iframe": "आइफ्रेम",
"rules": "नियम",
"welcome_to_board": "{{board}} में आपका स्वागत है",
"comment_too_long": "टिप्पणी बहुत लंबी है। <1>पूरा टेक्स्ट देखने के लिए यहां क्लिक करें</1>।",
"replies_omitted": "{{repliesCount}} उत्तर छोड़े गए हैं। <1>देखने के लिए यहां क्लिक करें</1>।",
"replies_and_links_omitted": "{{repliesCount}} उत्तर और {{linksCount}} लिंक छोड़े गए हैं। <1>देखने के लिए यहां क्लिक करें</1>।",
"thread_closed_alert": "यह थ्रेड बंद है"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "név",
"comment": "megjegyzés",
"subject": "tárgy",
"anonymous": "névtelen"
"anonymous": "névtelen",
"image": "kép",
"video": "videó",
"audio": "hang",
"webpage": "weboldal",
"iframe": "iframe",
"rules": "szabályok",
"welcome_to_board": "Üdvözöljük a(z) {{board}} oldalon",
"comment_too_long": "A megjegyzés túl hosszú. <1>Kattintson ide</1> a teljes szöveg megtekintéséhez.",
"replies_omitted": "{{repliesCount}} válasz kihagyva. <1>Kattintson ide</1> a megtekintéshez.",
"replies_and_links_omitted": "{{repliesCount}} válasz és {{linksCount}} link kihagyva. <1>Kattintson ide</1> a megtekintéshez.",
"thread_closed_alert": "A téma lezárva"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "nama",
"comment": "komentar",
"subject": "subjek",
"anonymous": "anonim"
"anonymous": "anonim",
"image": "gambar",
"video": "video",
"audio": "audio",
"webpage": "halaman web",
"iframe": "iframe",
"rules": "aturan",
"welcome_to_board": "Selamat datang di {{board}}",
"comment_too_long": "Komentar terlalu panjang. <1>Klik di sini</1> untuk melihat teks lengkap.",
"replies_omitted": "{{repliesCount}} balasan dihilangkan. <1>Klik di sini</1> untuk melihat.",
"replies_and_links_omitted": "{{repliesCount}} balasan dan {{linksCount}} tautan dihilangkan. <1>Klik di sini</1> untuk melihat.",
"thread_closed_alert": "Thread ini ditutup"
}
+13 -2
View File
@@ -12,7 +12,7 @@
"whitepaper": "Libro bianco",
"token": "Token",
"download_app": "Scarica app",
"start_new_thread": "Inizia un Nuovo Thread",
"start_new_thread": "Invia un Nuovo Thread",
"show_stats": "Mostra Statistiche",
"hide": "Nascondi",
"board_stats_hour": "Nell'ultima ora, <1>{{userCount}}</1> utenti hanno inviato <1>{{postCount}}</1> post",
@@ -48,5 +48,16 @@
"name": "nome",
"comment": "commento",
"subject": "oggetto",
"anonymous": "anonimo"
"anonymous": "anonimo",
"image": "immagine",
"video": "video",
"audio": "audio",
"webpage": "pagina web",
"iframe": "iframe",
"rules": "regole",
"welcome_to_board": "Benvenuti su {{board}}",
"comment_too_long": "Commento troppo lungo. <1>Clicca qui</1> per vedere il testo completo.",
"replies_omitted": "{{repliesCount}} risposte omesse. <1>Clicca qui</1> per visualizzare.",
"replies_and_links_omitted": "{{repliesCount}} risposte e {{linksCount}} link omessi. <1>Clicca qui</1> per visualizzare.",
"thread_closed_alert": "Questo thread è chiuso"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "名前",
"comment": "コメント",
"subject": "件名",
"anonymous": "匿名"
"anonymous": "匿名",
"image": "画像",
"video": "ビデオ",
"audio": "音声",
"webpage": "ウェブページ",
"iframe": "アイフレーム",
"rules": "ルール",
"welcome_to_board": "{{board}}へようこそ",
"comment_too_long": "コメントが長すぎます。 <1>ここをクリック</1>して全文を表示。",
"replies_omitted": "{{repliesCount}}件の返信が省略されました。 <1>ここをクリック</1>して表示。",
"replies_and_links_omitted": "{{repliesCount}}件の返信と{{linksCount}}件のリンクが省略されました。 <1>ここをクリック</1>して表示。",
"thread_closed_alert": "このスレッドは閉じられています"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "이름",
"comment": "댓글",
"subject": "제목",
"anonymous": "익명"
"anonymous": "익명",
"image": "이미지",
"video": "비디오",
"audio": "오디오",
"webpage": "웹 페이지",
"iframe": "iframe",
"rules": "규칙",
"welcome_to_board": "{{board}}에 오신 것을 환영합니다",
"comment_too_long": "댓글이 너무 깁니다. <1>여기를 클릭하십시오</1> 전체 텍스트를 보려면.",
"replies_omitted": "{{repliesCount}}개의 답글이 생략되었습니다. <1>여기를 클릭하십시오</1> 보려면.",
"replies_and_links_omitted": "{{repliesCount}}개의 답글과 {{linksCount}}개의 링크가 생략되었습니다. <1>여기를 클릭하십시오</1> 보려면.",
"thread_closed_alert": "이 스레드는 닫혔습니다"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "नाव",
"comment": "टिप्पणी",
"subject": "विषय",
"anonymous": "अनाम"
"anonymous": "अनाम",
"image": "चित्र",
"video": "व्हिडिओ",
"audio": "ऑडिओ",
"webpage": "वेबपेज",
"iframe": "आयफ्रेम",
"rules": "नियम",
"welcome_to_board": "{{board}} वर आपले स्वागत आहे",
"comment_too_long": "टिप्पणी खूप लांब आहे. <1>पूर्ण मजकूर पाहण्यासाठी येथे क्लिक करा</1>.",
"replies_omitted": "{{repliesCount}} प्रतिसाद वगळले गेले आहेत. <1>पाहण्यासाठी येथे क्लिक करा</1>.",
"replies_and_links_omitted": "{{repliesCount}} प्रतिसाद आणि {{linksCount}} दुवे वगळले गेले आहेत. <1>पाहण्यासाठी येथे क्लिक करा</1>.",
"thread_closed_alert": "हा थ्रेड बंद आहे"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "naam",
"comment": "opmerking",
"subject": "onderwerp",
"anonymous": "anoniem"
"anonymous": "anoniem",
"image": "afbeelding",
"video": "video",
"audio": "audio",
"webpage": "webpagina",
"iframe": "iframe",
"rules": "regels",
"welcome_to_board": "Welkom bij {{board}}",
"comment_too_long": "Reactie te lang. <1>Klik hier</1> om de volledige tekst te bekijken.",
"replies_omitted": "{{repliesCount}} reacties weggelaten. <1>Klik hier</1> om ze te bekijken.",
"replies_and_links_omitted": "{{repliesCount}} reacties en {{linksCount}} links weggelaten. <1>Klik hier</1> om ze te bekijken.",
"thread_closed_alert": "De thread is gesloten"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "navn",
"comment": "kommentar",
"subject": "emne",
"anonymous": "anonym"
"anonymous": "anonym",
"image": "bilde",
"video": "video",
"audio": "lyd",
"webpage": "nettside",
"iframe": "iframe",
"rules": "regler",
"welcome_to_board": "Velkommen til {{board}}",
"comment_too_long": "Kommentaren er for lang. <1>Klikk her</1> for å se hele teksten.",
"replies_omitted": "{{repliesCount}} svar utelatt. <1>Klikk her</1> for å se dem.",
"replies_and_links_omitted": "{{repliesCount}} svar og {{linksCount}} lenker utelatt. <1>Klikk her</1> for å se dem.",
"thread_closed_alert": "Tråden er stengt"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "nazwa",
"comment": "komentarz",
"subject": "temat",
"anonymous": "anonimowy"
"anonymous": "anonimowy",
"image": "obraz",
"video": "wideo",
"audio": "audio",
"webpage": "strona internetowa",
"iframe": "iframe",
"rules": "zasady",
"welcome_to_board": "Witaj na {{board}}",
"comment_too_long": "Komentarz jest za długi. <1>Kliknij tutaj</1>, aby zobaczyć pełny tekst.",
"replies_omitted": "Pominięto {{repliesCount}} odpowiedzi. <1>Kliknij tutaj</1>, aby je zobaczyć.",
"replies_and_links_omitted": "Pominięto {{repliesCount}} odpowiedzi i {{linksCount}} linków. <1>Kliknij tutaj</1>, aby je zobaczyć.",
"thread_closed_alert": "Wątek jest zamknięty"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "nome",
"comment": "comentário",
"subject": "assunto",
"anonymous": "anônimo"
"anonymous": "anônimo",
"image": "imagem",
"video": "vídeo",
"audio": "áudio",
"webpage": "página da web",
"iframe": "iframe",
"rules": "regras",
"welcome_to_board": "Bem-vindo ao {{board}}",
"comment_too_long": "Comentário muito longo. <1>Clique aqui</1> para ver o texto completo.",
"replies_omitted": "{{repliesCount}} respostas omitidas. <1>Clique aqui</1> para visualizar.",
"replies_and_links_omitted": "{{repliesCount}} respostas e {{linksCount}} links omitidos. <1>Clique aqui</1> para visualizar.",
"thread_closed_alert": "O tópico está fechado"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "nume",
"comment": "comentariu",
"subject": "subiect",
"anonymous": "anonim"
"anonymous": "anonim",
"image": "imagine",
"video": "video",
"audio": "audio",
"webpage": "pagină web",
"iframe": "iframe",
"rules": "reguli",
"welcome_to_board": "Bine ați venit la {{board}}",
"comment_too_long": "Comentariul este prea lung. <1>Click aici</1> pentru a vedea textul complet.",
"replies_omitted": "{{repliesCount}} răspunsuri omise. <1>Click aici</1> pentru a le vizualiza.",
"replies_and_links_omitted": "{{repliesCount}} răspunsuri și {{linksCount}} linkuri omise. <1>Click aici</1> pentru a le vizualiza.",
"thread_closed_alert": "Acest fir de discuție este închis"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "имя",
"comment": "комментарий",
"subject": "тема",
"anonymous": "анонимный"
"anonymous": "анонимный",
"image": "изображение",
"video": "видео",
"audio": "аудио",
"webpage": "веб-страница",
"iframe": "iframe",
"rules": "правила",
"welcome_to_board": "Добро пожаловать на {{board}}",
"comment_too_long": "Комментарий слишком длинный. <1>Нажмите здесь</1>, чтобы прочитать полный текст.",
"replies_omitted": "{{repliesCount}} ответов пропущено. <1>Нажмите здесь</1>, чтобы посмотреть.",
"replies_and_links_omitted": "{{repliesCount}} ответов и {{linksCount}} ссылок пропущено. <1>Нажмите здесь</1>, чтобы посмотреть.",
"thread_closed_alert": "Тема закрыта"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "emri",
"comment": "koment",
"subject": "subjekt",
"anonymous": "anonim"
"anonymous": "anonim",
"image": "imazh",
"video": "video",
"audio": "audio",
"webpage": "faqja web",
"iframe": "iframe",
"rules": "rregullat",
"welcome_to_board": "Mirë se vini te {{board}}",
"comment_too_long": "Komenti është shumë i gjatë. <1>Kliko këtu</1> për të parë tekstin e plotë.",
"replies_omitted": "{{repliesCount}} përgjigje janë lënë jashtë. <1>Kliko këtu</1> për t'i shikuar.",
"replies_and_links_omitted": "{{repliesCount}} përgjigje dhe {{linksCount}} lidhje janë lënë jashtë. <1>Kliko këtu</1> për t'i shikuar.",
"thread_closed_alert": "Temë është mbyllur"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "namn",
"comment": "kommentar",
"subject": "ämne",
"anonymous": "anonym"
"anonymous": "anonym",
"image": "bild",
"video": "video",
"audio": "ljud",
"webpage": "webbsida",
"iframe": "iframe",
"rules": "regler",
"welcome_to_board": "Välkommen till {{board}}",
"comment_too_long": "Kommentaren är för lång. <1>Klicka här</1> för att se hela texten.",
"replies_omitted": "{{repliesCount}} svar utelämnade. <1>Klicka här</1> för att visa dem.",
"replies_and_links_omitted": "{{repliesCount}} svar och {{linksCount}} länkar utelämnade. <1>Klicka här</1> för att visa dem.",
"thread_closed_alert": "Tråden är stängd"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "పేరు",
"comment": "వ్యాఖ్య",
"subject": "విషయం",
"anonymous": "అనామధుడు"
"anonymous": "అనామధుడు",
"image": "చిత్రం",
"video": "వీడియో",
"audio": "ఆడియో",
"webpage": "వెబ్ పేజీ",
"iframe": "iframe",
"rules": "నియమాలు",
"welcome_to_board": "{{board}} కు స్వాగతం",
"comment_too_long": "వ్యాఖ్య చాలా పొడవుగా ఉంది. <1>పూర్తి టెక్స్ట్ చూడటానికి ఇక్కడ క్లిక్ చేయండి</1>.",
"replies_omitted": "{{repliesCount}} స్పందనలు వదిలివేయబడ్డాయి. <1>చూడటానికి ఇక్కడ క్లిక్ చేయండి</1>.",
"replies_and_links_omitted": "{{repliesCount}} స్పందనలు మరియు {{linksCount}} లింకులు వదిలివేయబడ్డాయి. <1>చూడటానికి ఇక్కడ క్లిక్ చేయండి</1>.",
"thread_closed_alert": "ఈ థ్రెడ్ మూసివేయబడింది"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "ชื่อ",
"comment": "ความคิดเห็น",
"subject": "หัวข้อ",
"anonymous": "นิรนาม"
"anonymous": "นิรนาม",
"image": "ภาพ",
"video": "วิดีโอ",
"audio": "เสียง",
"webpage": "หน้าเว็บ",
"iframe": "iframe",
"rules": "กฎ",
"welcome_to_board": "ยินดีต้อนรับสู่ {{board}}",
"comment_too_long": "ความคิดเห็นยาวเกินไป <1>คลิกที่นี่</1> เพื่อดูข้อความเต็ม.",
"replies_omitted": "ข้าม {{repliesCount}} คำตอบ <1>คลิกที่นี่</1> เพื่อดู.",
"replies_and_links_omitted": "ข้าม {{repliesCount}} คำตอบและ {{linksCount}} ลิงก์ <1>คลิกที่นี่</1> เพื่อดู.",
"thread_closed_alert": "กระทู้นี้ถูกปิด"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "isim",
"comment": "yorum",
"subject": "konu",
"anonymous": "anonim"
"anonymous": "anonim",
"image": "görüntü",
"video": "video",
"audio": "ses",
"webpage": "web sayfası",
"iframe": "iframe",
"rules": "kurallar",
"welcome_to_board": "{{board}}'a hoş geldiniz",
"comment_too_long": "Yorum çok uzun. <1>Buraya tıklayın</1> tam metni görüntülemek için.",
"replies_omitted": "{{repliesCount}} yanıt göz ardı edildi. <1>Buraya tıklayın</1> görüntülemek için.",
"replies_and_links_omitted": "{{repliesCount}} yanıt ve {{linksCount}} bağlantı göz ardı edildi. <1>Buraya tıklayın</1> görüntülemek için.",
"thread_closed_alert": "Bu konu kapatılmıştır"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "ім'я",
"comment": "коментар",
"subject": "тема",
"anonymous": "анонім"
"anonymous": "анонім",
"image": "зображення",
"video": "відео",
"audio": "аудіо",
"webpage": "веб-сторінка",
"iframe": "iframe",
"rules": "правила",
"welcome_to_board": "Ласкаво просимо до {{board}}",
"comment_too_long": "Коментар занадто довгий. <1>Натисніть тут</1>, щоб переглянути повний текст.",
"replies_omitted": "{{repliesCount}} відповідей пропущено. <1>Натисніть тут</1>, щоб переглянути.",
"replies_and_links_omitted": "{{repliesCount}} відповідей та {{linksCount}} посилань пропущено. <1>Натисніть тут</1>, щоб переглянути.",
"thread_closed_alert": "Тему закрито"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "نام",
"comment": "تبصرہ",
"subject": "موضوع",
"anonymous": "ناشناس"
"anonymous": "ناشناس",
"image": "تصویر",
"video": "ویڈیو",
"audio": "آڈیو",
"webpage": "ویب پیج",
"iframe": "آئی فریم",
"rules": "اصول",
"welcome_to_board": "{{board}} میں خوش آمدید",
"comment_too_long": "تبصرہ بہت طویل ہے۔ <1>مکمل متن دیکھنے کے لیے یہاں کلک کریں</1>۔",
"replies_omitted": "{{repliesCount}} جوابات چھوڑ دیے گئے ہیں۔ <1>مکمل دیکھنے کے لیے یہاں کلک کریں</1>۔",
"replies_and_links_omitted": "{{repliesCount}} جوابات اور {{linksCount}} لنکس چھوڑ دیے گئے ہیں۔ <1>مکمل دیکھنے کے لیے یہاں کلک کریں</1>۔",
"thread_closed_alert": "یہ دھاگہ بند ہے"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "tên",
"comment": "bình luận",
"subject": "chủ đề",
"anonymous": "ẩn danh"
"anonymous": "ẩn danh",
"image": "hình ảnh",
"video": "video",
"audio": "âm thanh",
"webpage": "trang web",
"iframe": "iframe",
"rules": "quy tắc",
"welcome_to_board": "Chào mừng bạn đến với {{board}}",
"comment_too_long": "Bình luận quá dài. <1>Nhấn vào đây</1> để xem toàn bộ văn bản.",
"replies_omitted": "{{repliesCount}} câu trả lời bị bỏ qua. <1>Nhấn vào đây</1> để xem.",
"replies_and_links_omitted": "{{repliesCount}} câu trả lời và {{linksCount}} liên kết bị bỏ qua. <1>Nhấn vào đây</1> để xem.",
"thread_closed_alert": "Chủ đề này đã đóng"
}
+12 -1
View File
@@ -48,5 +48,16 @@
"name": "名字",
"comment": "评论",
"subject": "主题",
"anonymous": "匿名"
"anonymous": "匿名",
"image": "图片",
"video": "视频",
"audio": "音频",
"webpage": "网页",
"iframe": "框架",
"rules": "规则",
"welcome_to_board": "欢迎来到{{board}}",
"comment_too_long": "评论太长。 <1>点击这里</1>查看完整文本。",
"replies_omitted": "省略了{{repliesCount}}条回复。 <1>点击这里</1>查看。",
"replies_and_links_omitted": "省略了{{repliesCount}}条回复和{{linksCount}}个链接。 <1>点击这里</1>查看。",
"thread_closed_alert": "该主题已关闭"
}
@@ -58,6 +58,7 @@ export const MobileBoardButtons = () => {
const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any });
const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress;
const isInPostPage = isPostPageView(location.pathname, params);
const isInCatalogView = isCatalogView(location.pathname, params);
const isInPendingPostPage = isPendingPostView(location.pathname, params);
return (
@@ -75,7 +76,7 @@ export const MobileBoardButtons = () => {
) : (
<>
<OptionsButton />
<CatalogButton address={subplebbitAddress} />
<CatalogButton address={subplebbitAddress} isInCatalogView={isInCatalogView} />
<SubscribeButton address={subplebbitAddress} />
</>
)}
@@ -97,7 +98,7 @@ export const DesktopBoardButtons = () => {
<hr />
{isInPostPage || isInPendingPostPage ? (
<>
[<ReturnButton address={subplebbitAddress} />] [<CatalogButton address={subplebbitAddress} isInCatalogView={isInCatalogView} />] [<BottomButton />] [
[<ReturnButton address={subplebbitAddress} />] [<CatalogButton address={subplebbitAddress} />] [<BottomButton />] [
<OptionsButton />]
<span className={styles.subscribeButton}>
[<SubscribeButton address={subplebbitAddress} />]
@@ -74,7 +74,6 @@
}
.challengeFooter button {
margin: 5px 5px 0 0;
cursor: pointer;
filter: var(--filter80);
text-transform: capitalize;
@@ -1,10 +1,11 @@
import { useState } from 'react';
import { useRef, useState } from 'react';
import Draggable from 'react-draggable';
import { useTranslation } from 'react-i18next';
import { Challenge as ChallengeType } from '@plebbit/plebbit-react-hooks';
import { getPublicationType } from '../../lib/utils/challenge-utils';
import useChallenges from '../../hooks/use-challenges';
import styles from './challenge-modal.module.css';
import _ from 'lodash';
interface ChallengeProps {
challenge: ChallengeType;
@@ -50,16 +51,19 @@ const Challenge = ({ challenge, closeModal }: ChallengeProps) => {
}
};
// react-draggable requires a ref to the modal node
const nodeRef = useRef(null);
return (
<Draggable handle='.challengeHandle'>
<div className={styles.container}>
<Draggable handle='.challengeHandle' nodeRef={nodeRef}>
<div className={styles.container} ref={nodeRef}>
<div className={`challengeHandle ${styles.title}`}>
Challenge for {publicationType}
<button className={styles.closeIcon} onClick={closeModal} title='close' />
</div>
<div className={styles.publication}>
<div className={styles.name}>
<input type='text' value={displayName || 'Anonymous'} disabled />
<input type='text' value={displayName || _.capitalize(t('anonymous'))} disabled />
</div>
{title && (
<div className={styles.subject}>
@@ -1,11 +1,11 @@
import React from 'react';
import styles from './comment-media.module.css';
import { CommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
import Embed, { canEmbed } from '../embed';
import { useTranslation } from 'react-i18next';
import useWindowWidth from '../../hooks/use-window-width';
import styles from './comment-media.module.css';
import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail } from '../../lib/utils/media-utils';
import { getHostname } from '../../lib/utils/url-utils';
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
import useWindowWidth from '../../hooks/use-window-width';
import Embed, { canEmbed } from '../embed';
interface MediaProps {
commentMediaInfo?: CommentMediaInfo;
@@ -129,7 +129,7 @@ const Media = ({ commentMediaInfo, isReply, setShowThumbnail }: MediaProps) => {
<a href={url} target='_blank' rel='noopener noreferrer'>
{url && url.length > 30 ? url.slice(0, 30) + '...' : url}
</a>{' '}
({type})
({getDisplayMediaInfoType(type, t)})
</div>
)}
{isMobile && (type === 'iframe' || type === 'video' || type === 'audio') && (
@@ -144,8 +144,10 @@ const Media = ({ commentMediaInfo, isReply, setShowThumbnail }: MediaProps) => {
};
const CommentMedia = ({ commentMediaInfo, isOutOfFeed, isReply, linkHeight, linkWidth, showThumbnail, setShowThumbnail }: MediaProps) => {
const { t } = useTranslation();
const isMobile = useWindowWidth() < 640;
const { type, url } = commentMediaInfo || {};
return (
<span className={styles.content}>
<span className={`${showThumbnail ? styles.show : styles.hide} ${styles.thumbnail}`}>
@@ -160,7 +162,7 @@ const CommentMedia = ({ commentMediaInfo, isOutOfFeed, isReply, linkHeight, link
setShowThumbnail={setShowThumbnail}
/>
)}
{isMobile && type && <div className={styles.fileInfo}>{type}</div>}
{isMobile && type && <div className={styles.fileInfo}>{getDisplayMediaInfoType(type, t)}</div>}
</span>
{!showThumbnail && <Media commentMediaInfo={commentMediaInfo} isReply={isReply} setShowThumbnail={setShowThumbnail} />}
</span>
@@ -1,24 +1,22 @@
.ellipsis {
display: inline-block;
}
.ellipsis:after {
content: '...';
position: absolute;
-webkit-animation: ellipsis steps(4, end) 1200ms infinite;
animation: ellipsis steps(4, end) 1500ms infinite;
width: 0px;
overflow: hidden;
display: inline-block;
position: absolute;
vertical-align: bottom;
-webkit-animation: ellipsis steps(4,end) 1500ms infinite;
animation: ellipsis steps(4,end) 1500ms infinite;
content: "\2026"; /* ascii code for the ellipsis character */
width: 0px;
}
@keyframes ellipsis {
to {
width: 1.3em;
width: 1.25em;
}
}
@-webkit-keyframes ellipsis {
to {
width: 1.3em;
width: 1.25em;
}
}
+80 -42
View File
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { Link, useLocation, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Role, useSubplebbit } from '@plebbit/plebbit-react-hooks';
import { Trans, useTranslation } from 'react-i18next';
import { Role, useAccount, useSubplebbit } from '@plebbit/plebbit-react-hooks';
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
import { getFormattedDate } from '../../lib/utils/time-utils';
@@ -11,10 +11,12 @@ import useReplies from '../../hooks/use-replies';
import useStateString from '../../hooks/use-state-string';
import useWindowWidth from '../../hooks/use-window-width';
import styles from './post.module.css';
import LoadingEllipsis from '../loading-ellipsis';
import Markdown from '../markdown';
import CommentMedia from '../comment-media';
import { canEmbed } from '../embed';
import LoadingEllipsis from '../loading-ellipsis';
import Markdown from '../markdown';
import _ from 'lodash';
import { getDisplayMediaInfoType } from '../../lib/utils/media-utils';
interface PostProps {
index?: number;
@@ -22,9 +24,10 @@ interface PostProps {
reply?: any;
roles?: Role[];
showAllReplies?: boolean;
openReplyModal?: (cid: string) => void;
}
const PostDesktop = ({ post, roles, showAllReplies }: PostProps) => {
const PostDesktop = ({ openReplyModal, post, roles, showAllReplies }: PostProps) => {
const { t } = useTranslation();
const { author, cid, content, link, linkHeight, linkWidth, locked, pinned, postCid, replyCount, shortCid, state, subplebbitAddress, timestamp, title } = post || {};
const { address, displayName, shortAddress } = author || {};
@@ -46,13 +49,17 @@ const PostDesktop = ({ post, roles, showAllReplies }: PostProps) => {
const [showThumbnail, setShowThumbnail] = useState(true);
const replies = useReplies(post);
const visibleLinkCount = useCountLinksInReplies(post, 5);
const totalLinkCount = useCountLinksInReplies(post);
const visiblelinksCount = useCountLinksInReplies(post, 5);
const totallinksCount = useCountLinksInReplies(post);
const repliesCount = pinned ? replyCount : replyCount - 5;
const linkCount = pinned ? totalLinkCount : totalLinkCount - visibleLinkCount;
const linksCount = pinned ? totallinksCount : totallinksCount - visiblelinksCount;
const [menuBtnRotated, setMenuBtnRotated] = useState(false);
// pending reply by account is not yet published
const account = useAccount();
const accountShortAddress = account?.author?.shortAddress;
return (
<div className={styles.postDesktop}>
<div className={styles.hrWrapper}>
@@ -70,7 +77,7 @@ const PostDesktop = ({ post, roles, showAllReplies }: PostProps) => {
<a href={url} target='_blank' rel='noopener noreferrer'>
{url.length > 30 ? url.slice(0, 30) + '...' : url}
</a>{' '}
({type})
({type && getDisplayMediaInfoType(type, t)})
{!showThumbnail && (type === 'iframe' || type === 'video' || type === 'audio') && (
<span>
{' '}
@@ -112,10 +119,10 @@ const PostDesktop = ({ post, roles, showAllReplies }: PostProps) => {
{title && <span className={styles.subject}>{displayTitle} </span>}
<span className={styles.nameBlock}>
<span className={`${styles.name} ${(isDescription || isRules || authorRole) && styles.capcodeMod}`}>
{displayName || 'Anonymous'}
{displayName || _.capitalize(t('anonymous'))}
{authorRole && ` ## Board ${authorRole}`}{' '}
</span>
{!(isDescription || isRules) && <span className={styles.userAddress}>(u/{shortAddress}) </span>}
{!(isDescription || isRules) && <span className={styles.userAddress}>(u/{shortAddress || accountShortAddress}) </span>}
</span>
<span className={styles.dateTime}>
{getFormattedDate(timestamp)}
@@ -124,13 +131,13 @@ const PostDesktop = ({ post, roles, showAllReplies }: PostProps) => {
<span className={styles.postNum}>
{!(isDescription || isRules) && (
<span className={styles.postNumLink}>
<Link to={`/p/${subplebbitAddress}/${cid}`} className={styles.linkToPost} title={t('link_to_post')} onClick={(e) => !cid && e.preventDefault()}>
<Link to={`/p/${subplebbitAddress}/c/${cid}`} className={styles.linkToPost} title={t('link_to_post')} onClick={(e) => !cid && e.preventDefault()}>
c/
</Link>
{!cid ? (
<span className={styles.pendingCid}>{state === 'failed' ? 'Failed' : 'Pending'}</span>
) : (
<span className={styles.replyToPost} title={t('reply_to_post')}>
<span className={styles.replyToPost} title={t('reply_to_post')} onClick={() => openReplyModal && openReplyModal(cid)}>
{shortCid}
</span>
)}
@@ -168,7 +175,7 @@ const PostDesktop = ({ post, roles, showAllReplies }: PostProps) => {
{content.length > 1000 && !isInPostPage && (
<span className={styles.abbr}>
<br />
Comment too long. <Link to={`/p/${subplebbitAddress}/c/${cid}`}>Click here</Link> to view the full text.
<Trans i18nKey={'comment_too_long'} shouldUnescape={true} components={{ 1: <Link to={`/p/${subplebbitAddress}/c/${cid}`} /> }} />
</span>
)}
</blockquote>
@@ -178,24 +185,30 @@ const PostDesktop = ({ post, roles, showAllReplies }: PostProps) => {
<span className={styles.expandButtonWrapper}>
<span className={styles.expandButton} />
</span>
<span>
{repliesCount} replies {linkCount > 0 && `and ${linkCount} links`} omitted.{' '}
</span>
<Link to={`/p/${subplebbitAddress}/c/${cid}`}>Click here</Link> to view.
{linksCount > 0 ? (
<Trans
i18nKey={'replies_and_links_omitted'}
shouldUnescape={true}
components={{ 1: <Link to={`/p/${subplebbitAddress}/c/${cid}`} /> }}
values={{ repliesCount, linksCount }}
/>
) : (
<Trans i18nKey={'replies_omitted'} shouldUnescape={true} components={{ 1: <Link to={`/p/${subplebbitAddress}/c/${cid}`} /> }} values={{ repliesCount }} />
)}
</span>
)}
{!(pinned && !isInPostPage) &&
replies &&
(showAllReplies ? replies : replies.slice(-5)).map((reply, index) => (
<div key={reply.cid} className={styles.replyContainer}>
<ReplyDesktop index={index} reply={reply} roles={roles} />
<div key={index} className={styles.replyContainer}>
<ReplyDesktop index={index} reply={reply} roles={roles} openReplyModal={openReplyModal} />
</div>
))}
</div>
);
};
const ReplyDesktop = ({ reply, roles }: PostProps) => {
const ReplyDesktop = ({ reply, roles, openReplyModal }: PostProps) => {
const { t } = useTranslation();
const { author, cid, content, link, linkHeight, linkWidth, parentCid, pinned, postCid, shortCid, state, subplebbitAddress, timestamp } = reply || {};
const { address, displayName, shortAddress } = author || {};
@@ -216,6 +229,10 @@ const ReplyDesktop = ({ reply, roles }: PostProps) => {
<div className={`${styles.stateString} ${styles.ellipsis}`}>{stateString !== 'Failed' ? <LoadingEllipsis string={stateString} /> : stateString}</div>
);
// pending reply by account is not yet published
const account = useAccount();
const accountShortAddress = account?.author?.shortAddress;
return (
<div className={styles.replyDesktop}>
<div className={styles.sideArrows}>{'>>'}</div>
@@ -226,21 +243,21 @@ const ReplyDesktop = ({ reply, roles }: PostProps) => {
</span>
<span className={styles.nameBlock}>
<span className={`${styles.name} ${authorRole && styles.capcodeMod}`}>
{displayName || 'Anonymous'}
{displayName || _.capitalize(t('anonymous'))}
{authorRole && ` ## Board ${authorRole}`}{' '}
</span>
<span className={styles.userAddress}>(u/{shortAddress}) </span>
<span className={styles.userAddress}>(u/{shortAddress || accountShortAddress}) </span>
</span>
<span className={styles.dateTime}>{getFormattedDate(timestamp)} </span>
<span className={styles.postNum}>
<span className={styles.postNumLink}>
<Link to={`/p/${subplebbitAddress}/${cid}`} className={styles.linkToPost} title={t('link_to_post')} onClick={(e) => !cid && e.preventDefault()}>
<Link to={`/p/${subplebbitAddress}/c/${cid}`} className={styles.linkToPost} title={t('link_to_post')} onClick={(e) => !cid && e.preventDefault()}>
c/
</Link>
{!cid ? (
<span className={styles.pendingCid}>{state === 'failed' ? 'Failed' : 'Pending'}</span>
) : (
<span className={styles.replyToPost} title={t('reply_to_post')}>
<span className={styles.replyToPost} title={t('reply_to_post')} onClick={() => openReplyModal && openReplyModal(cid)}>
{shortCid}
</span>
)}
@@ -268,7 +285,8 @@ const ReplyDesktop = ({ reply, roles }: PostProps) => {
{t('link')}:{' '}
<a href={link} target='_blank' rel='noopener noreferrer'>
{link.length > 30 ? link?.slice(0, 30) + '...' : link}
</a>
</a>{' '}
({type && getDisplayMediaInfoType(type, t)})
{!showThumbnail && (type === 'iframe' || type === 'video' || type === 'audio') && (
<span>
{' '}
@@ -313,7 +331,7 @@ const ReplyDesktop = ({ reply, roles }: PostProps) => {
</>
)}
<Markdown content={content} />
{!cid && (
{!cid && state === 'pending' && (
<>
<br />
{loadingString}
@@ -326,7 +344,7 @@ const ReplyDesktop = ({ reply, roles }: PostProps) => {
);
};
const PostMobile = ({ post, roles, showAllReplies }: PostProps) => {
const PostMobile = ({ openReplyModal, post, roles, showAllReplies }: PostProps) => {
const { t } = useTranslation();
const { author, cid, content, link, linkHeight, linkWidth, locked, pinned, replyCount, shortCid, state, subplebbitAddress, timestamp, title } = post || {};
const { address, displayName, shortAddress } = author || {};
@@ -338,7 +356,7 @@ const PostMobile = ({ post, roles, showAllReplies }: PostProps) => {
const location = useLocation();
const isInPostPage = isPostPageView(location.pathname, params);
const linkCount = useCountLinksInReplies(post);
const linksCount = useCountLinksInReplies(post);
const displayTitle = title && title.length > 30 ? title?.slice(0, 30) + '(...)' : title;
const displayContent = content && !isInPostPage && content.length > 1000 ? content?.slice(0, 1000) : content;
@@ -348,6 +366,10 @@ const PostMobile = ({ post, roles, showAllReplies }: PostProps) => {
const replies = useReplies(post);
// pending reply by account is not yet published
const account = useAccount();
const accountShortAddress = account?.author?.shortAddress;
return (
<div className={styles.postMobile}>
<div className={styles.hrWrapper}>
@@ -362,10 +384,10 @@ const PostMobile = ({ post, roles, showAllReplies }: PostProps) => {
</span>
<span className={styles.nameBlock}>
<span className={`${styles.name} ${(isDescription || isRules || authorRole) && styles.capcodeMod}`}>
{displayName || 'Anonymous'}
{displayName || _.capitalize(t('anonymous'))}
{authorRole && ` ## Board ${authorRole}`}{' '}
</span>
{!(isDescription || isRules) && <span className={styles.address}>(u/{shortAddress || address?.slice(0, 12) + '...'})</span>}
{!(isDescription || isRules) && <span className={styles.address}>(u/{shortAddress || accountShortAddress})</span>}
{pinned && (
<span className={styles.stickyIconWrapper}>
<img src='assets/icons/sticky.gif' alt='' className={styles.stickyIcon} title={t('sticky')} />
@@ -393,7 +415,7 @@ const PostMobile = ({ post, roles, showAllReplies }: PostProps) => {
{!cid ? (
<span className={styles.pendingCid}>{state === 'failed' ? 'Failed' : 'Pending'}</span>
) : (
<span className={styles.replyToPost} title={t('reply_to_post')}>
<span className={styles.replyToPost} title={t('reply_to_post')} onClick={() => openReplyModal && openReplyModal(cid)}>
{shortCid}
</span>
)}
@@ -418,7 +440,7 @@ const PostMobile = ({ post, roles, showAllReplies }: PostProps) => {
{content.length > 1000 && !isInPostPage && (
<span className={styles.abbr}>
<br />
Comment too long. <Link to={`/p/${subplebbitAddress}/c/${cid}`}>Click here</Link> to view the full text.
<Trans i18nKey={'comment_too_long'} shouldUnescape={true} components={{ 1: <Link to={`/p/${subplebbitAddress}/c/${cid}`} /> }} />
</span>
)}
</blockquote>
@@ -428,7 +450,7 @@ const PostMobile = ({ post, roles, showAllReplies }: PostProps) => {
<div className={styles.postLink}>
<span className={styles.info}>
{replyCount > 0 && `${replyCount} Replies`}
{linkCount > 0 && ` / ${linkCount} Links`}
{linksCount > 0 && ` / ${linksCount} Links`}
</span>
<Link to={`/p/${subplebbitAddress}/${isDescription ? 'description' : isRules ? 'rules' : `c/${cid}`}`} className='button'>
{t('view_thread')}
@@ -440,7 +462,7 @@ const PostMobile = ({ post, roles, showAllReplies }: PostProps) => {
replies &&
(showAllReplies ? replies : replies.slice(-5)).map((reply, index) => (
<div key={reply.cid} className={styles.replyContainer}>
<ReplyMobile index={index} reply={reply} roles={roles} />
<ReplyMobile index={index} reply={reply} roles={roles} openReplyModal={openReplyModal} />
</div>
))}
</div>
@@ -448,7 +470,7 @@ const PostMobile = ({ post, roles, showAllReplies }: PostProps) => {
);
};
const ReplyMobile = ({ reply, roles }: PostProps) => {
const ReplyMobile = ({ reply, roles, openReplyModal }: PostProps) => {
const { t } = useTranslation();
const { author, content, cid, link, linkHeight, linkWidth, parentCid, pinned, postCid, shortCid, state, subplebbitAddress, timestamp } = reply || {};
const { address, displayName, shortAddress } = author || {};
@@ -460,6 +482,15 @@ const ReplyMobile = ({ reply, roles }: PostProps) => {
const isReplyingToReply = postCid !== parentCid;
// pending reply by account is not yet published
const account = useAccount();
const accountShortAddress = account?.author?.shortAddress;
const stateString = useStateString(reply);
const loadingString = stateString && (
<div className={`${styles.stateString} ${styles.ellipsis}`}>{stateString !== 'Failed' ? <LoadingEllipsis string={stateString} /> : stateString}</div>
);
return (
<div className={styles.replyMobile}>
<div className={styles.reply}>
@@ -468,10 +499,10 @@ const ReplyMobile = ({ reply, roles }: PostProps) => {
<span className={styles.postMenuBtn}>...</span>
<span className={styles.nameBlock}>
<span className={`${styles.name} ${authorRole && styles.capcodeMod}`}>
{displayName || 'Anonymous'}
{displayName || _.capitalize(t('anonymous'))}
{authorRole && ` ## Board ${authorRole}`}{' '}
</span>
<span className={styles.address}>(u/{shortAddress})</span>
<span className={styles.address}>(u/{shortAddress || accountShortAddress})</span>
{pinned && (
<span className={styles.stickyIconWrapper}>
<img src='assets/icons/sticky.gif' alt='' className={styles.stickyIcon} title={t('sticky')} />
@@ -488,7 +519,7 @@ const ReplyMobile = ({ reply, roles }: PostProps) => {
{!cid ? (
<span className={styles.pendingCid}>{state === 'failed' ? 'Failed' : 'Pending'}</span>
) : (
<span className={styles.replyToPost} title={t('reply_to_post')}>
<span className={styles.replyToPost} title={t('reply_to_post')} onClick={() => openReplyModal && openReplyModal(cid)}>
{shortCid}
</span>
)}
@@ -515,6 +546,12 @@ const ReplyMobile = ({ reply, roles }: PostProps) => {
</>
)}
<Markdown content={content} />
{!cid && state === 'pending' && (
<>
<br />
{loadingString}
</>
)}
</blockquote>
)}
</div>
@@ -523,16 +560,17 @@ const ReplyMobile = ({ reply, roles }: PostProps) => {
);
};
const Post = ({ post, showAllReplies = false }: PostProps) => {
const Post = ({ post, showAllReplies = false, openReplyModal }: PostProps) => {
const subplebbit = useSubplebbit({ subplebbitAddress: post?.subplebbitAddress });
const isMobile = useWindowWidth() < 640;
return (
<div className={styles.thread}>
<div className={styles.postContainer}>
{isMobile ? (
<PostMobile post={post} roles={subplebbit?.roles} showAllReplies={showAllReplies} />
<PostMobile post={post} roles={subplebbit?.roles} showAllReplies={showAllReplies} openReplyModal={openReplyModal} />
) : (
<PostDesktop post={post} roles={subplebbit?.roles} showAllReplies={showAllReplies} />
<PostDesktop post={post} roles={subplebbit?.roles} showAllReplies={showAllReplies} openReplyModal={openReplyModal} />
)}
</div>
</div>
+1
View File
@@ -0,0 +1 @@
export { default } from './reply-modal';
@@ -0,0 +1,60 @@
.container {
position: fixed;
top: calc(50% - 150px);
left: calc(50% - 150px);
display: block;
padding: 2px;
font-size: 10pt;
border-left: none;
border-top: none;
z-index: 1000;
background-color: var(--challenge-modal-background-color);
border: var(--challenge-modal-border);
}
.title {
font-size: var(--challenge-modal-title-font-size);
text-align: center;
margin-bottom: 1px;
padding: 0;
height: 18px;
line-height: 18px;
cursor: move;
font-weight: var(--challenge-modal-title-font-weight);
background-color: var(--challenge-modal-title-background-color);
color: var(--challenge-modal-title-text-color);
border: var(--challenge-modal-title-border);
}
.closeIcon {
all: unset;
float: right;
cursor: pointer;
margin-bottom: -4px;
width: 18px;
height: 18px;
border: none;
image-rendering: pixelated;
background-image: var(--close-button-background-image);
}
.container input, .container textarea {
border: 1px solid #aaa;
font-family: Arial, Helvetica, sans-serif;
font-size: 10pt;
outline: medium none;
width: 298px;
filter: var(--filter80);
padding: 2px;
margin-bottom: 1px;
}
.content textarea {
vertical-align: top;
}
.footer button {
text-transform: capitalize;
float: right;
cursor: pointer;
}
@@ -0,0 +1,80 @@
import { useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import Draggable from 'react-draggable';
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
import { useAccount } from '@plebbit/plebbit-react-hooks';
import { isValidURL } from '../../lib/utils/url-utils';
import useReply from '../../hooks/use-reply';
import styles from './reply-modal.module.css';
import _ from 'lodash';
interface ReplyModalProps {
closeModal: () => void;
parentCid: string;
}
const ReplyModal = ({ closeModal, parentCid }: ReplyModalProps) => {
const { t } = useTranslation();
const { subplebbitAddress } = useParams() as { subplebbitAddress: string };
const { setContent, resetContent, replyIndex, publishReply } = useReply({ cid: parentCid, subplebbitAddress });
const account = useAccount();
const { displayName } = account?.author || {};
const textRef = useRef<HTMLTextAreaElement>(null);
const urlRef = useRef<HTMLInputElement>(null);
const onPublishReply = () => {
const currentContent = textRef.current?.value || '';
const currentUrl = urlRef.current?.value || '';
if (!currentContent.trim() && !currentUrl) {
alert(`Cannot post empty comment`);
return;
}
if (currentUrl && !isValidURL(currentUrl)) {
alert('The provided link is not a valid URL.');
return;
}
publishReply();
};
useEffect(() => {
if (typeof replyIndex === 'number') {
closeModal();
resetContent();
}
}, [replyIndex, resetContent, closeModal]);
// react-draggable requires a ref to the modal node
const nodeRef = useRef(null);
return (
<Draggable handle='.replyModalHandle' nodeRef={nodeRef}>
<div className={styles.container} ref={nodeRef}>
<div className={`replyModalHandle ${styles.title}`}>
Reply to c/{parentCid && Plebbit.getShortCid(parentCid)}
<button className={styles.closeIcon} onClick={closeModal} title='close' />
</div>
<div className={styles.replyForm}>
<div className={styles.name}>
<input type='text' defaultValue={displayName} placeholder={displayName ? undefined : _.capitalize(t('anonymous'))} />
</div>
<div className={styles.link}>
<input type='text' ref={urlRef} placeholder={_.capitalize(t('link'))} onChange={(e) => setContent.link(e.target.value)} />
</div>
<div className={styles.content}>
<textarea cols={48} rows={4} wrap='soft' ref={textRef} placeholder={_.capitalize(t('comment'))} onChange={(e) => setContent.content(e.target.value)} />
</div>
<div className={styles.footer}>
<button onClick={onPublishReply}>{t('reply')}</button>
</div>
</div>
</div>
</Draggable>
);
};
export default ReplyModal;
@@ -1,4 +1,5 @@
import Post from '../post';
import { useTranslation } from 'react-i18next';
interface DescriptionPostProps {
subplebbitAddress: string | undefined;
@@ -9,6 +10,7 @@ interface DescriptionPostProps {
}
const SubplebbitDescription = ({ subplebbitAddress, createdAt, description, avatarUrl, title }: DescriptionPostProps) => {
const { t } = useTranslation();
const post = {
isDescription: true,
subplebbitAddress,
@@ -16,7 +18,7 @@ const SubplebbitDescription = ({ subplebbitAddress, createdAt, description, avat
author: { displayName: '## Board Mods' },
content: description,
link: avatarUrl,
title: 'Welcome to ' + title,
title: t('welcome_to_board', { board: title }),
pinned: true,
locked: true,
};
@@ -1,4 +1,6 @@
import Post from '../post';
import { useTranslation } from 'react-i18next';
import _ from 'lodash';
interface RulesPostProps {
subplebbitAddress: string | undefined;
@@ -7,6 +9,7 @@ interface RulesPostProps {
}
const SubplebbitRules = ({ subplebbitAddress, createdAt, rules }: RulesPostProps) => {
const { t } = useTranslation();
const content = rules.map((rule, index) => `${index + 1}. ${rule}`).join('\n');
const post = {
isRules: true,
@@ -14,7 +17,7 @@ const SubplebbitRules = ({ subplebbitAddress, createdAt, rules }: RulesPostProps
timestamp: createdAt,
author: { displayName: '## Board Mods' },
content,
title: 'Rules',
title: _.capitalize(t('rules')),
pinned: true,
locked: true,
};
+29
View File
@@ -0,0 +1,29 @@
import { useState, useCallback } from 'react';
const useReplyModal = () => {
const [showReplyModal, setShowReplyModal] = useState(false);
const [activeCid, setActiveCid] = useState<string | null>(null);
const openReplyModal = useCallback(
(cid: string) => {
if (activeCid && activeCid !== cid) {
closeModal(); // close modal if a different CID is clicked
setActiveCid(cid);
setShowReplyModal(true);
} else if (!activeCid) {
setActiveCid(cid);
setShowReplyModal(true);
}
},
[activeCid],
);
const closeModal = useCallback(() => {
setActiveCid(null);
setShowReplyModal(false);
}, []);
return { showReplyModal, activeCid, openReplyModal, closeModal };
};
export default useReplyModal;
+15
View File
@@ -11,6 +11,21 @@ export interface CommentMediaInfo {
patternThumbnailUrl?: string;
}
export const getDisplayMediaInfoType = (type: string, t: any) => {
switch (type) {
case 'image':
return t('image');
case 'iframe':
return t('iframe');
case 'video':
return t('video');
case 'audio':
return t('audio');
default:
return t('webpage');
}
};
export const getHasThumbnail = (commentMediaInfo: CommentMediaInfo | undefined, link: string | undefined): boolean => {
const iframeThumbnail = commentMediaInfo?.patternThumbnailUrl || commentMediaInfo?.thumbnail;
return link &&
+11 -1
View File
@@ -5,8 +5,10 @@ import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
import { useTranslation } from 'react-i18next';
import styles from './board.module.css';
import useFeedStateString from '../../hooks/use-feed-state-string';
import useReplyModal from '../../hooks/use-reply-modal';
import LoadingEllipsis from '../../components/loading-ellipsis';
import Post from '../../components/post';
import ReplyModal from '../../components/reply-modal';
import SubplebbitDescription from '../../components/subplebbit-description';
import SubplebbitRules from '../../components/subplebbit-rules';
@@ -22,6 +24,8 @@ const Board = () => {
const subplebbit = useSubplebbit({ subplebbitAddress });
const { createdAt, description, rules, shortAddress, state, suggested, title } = subplebbit || {};
const { showReplyModal, activeCid, openReplyModal, closeModal } = useReplyModal();
const loadingStateString = useFeedStateString(subplebbitAddresses) || t('loading');
const loadingString = <div className={styles.stateString}>{state === 'failed' ? state : <LoadingEllipsis string={loadingStateString} />}</div>;
@@ -58,6 +62,7 @@ const Board = () => {
return (
<div className={styles.content}>
{showReplyModal && activeCid && <ReplyModal closeModal={closeModal} parentCid={activeCid} />}
{feed.length > 0 && (
<>
{rules && rules.length > 0 && <SubplebbitRules subplebbitAddress={subplebbitAddress} createdAt={createdAt} rules={rules} />}
@@ -70,7 +75,12 @@ const Board = () => {
increaseViewportBy={{ bottom: 1200, top: 1200 }}
totalCount={feed?.length || 0}
data={feed}
itemContent={(index, post) => <Post index={index} post={post} />}
itemContent={(index, post) => {
const { deleted, locked, removed } = post || {};
const isThreadClosed = deleted || locked || removed;
return <Post index={index} post={post} openReplyModal={isThreadClosed ? () => alert(t('thread_closed_alert')) : openReplyModal} />;
}}
useWindowScroll={true}
components={{ Footer }}
endReached={loadMore}
+1 -1
View File
@@ -210,7 +210,7 @@ const Footer = () => {
)}
{commitRef && (
<a href={`https://github.com/plebbit/plebchan/commit/${commitRef}`} target='_blank' rel='noopener noreferrer'>
{commitRef.slice(0, 7)}
#{commitRef.slice(0, 7)}
</a>
)}
</div>
+11 -2
View File
@@ -1,13 +1,17 @@
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
import { useLocation, useParams } from 'react-router-dom';
import { isDescriptionView, isRulesView } from '../../lib/utils/view-utils';
import styles from './post-page.module.css';
import { isDescriptionView, isRulesView } from '../../lib/utils/view-utils';
import useReplyModal from '../../hooks/use-reply-modal';
import Post from '../../components/post';
import ReplyModal from '../../components/reply-modal';
import SubplebbitDescription from '../../components/subplebbit-description';
import SubplebbitRules from '../../components/subplebbit-rules';
const PostPage = () => {
const { t } = useTranslation();
const params = useParams();
const { commentCid, subplebbitAddress } = params;
@@ -18,7 +22,11 @@ const PostPage = () => {
const isInDescriptionView = isDescriptionView(location.pathname, params);
const isInRulesView = isRulesView(location.pathname, params);
const { showReplyModal, activeCid, openReplyModal, closeModal } = useReplyModal();
const post = useComment({ commentCid });
const { deleted, locked, removed } = post || {};
const isThreadClosed = deleted || locked || removed;
useEffect(() => {
window.scrollTo(0, 0);
@@ -26,12 +34,13 @@ const PostPage = () => {
return (
<div className={styles.content}>
{showReplyModal && activeCid && <ReplyModal closeModal={closeModal} parentCid={activeCid} />}
{isInDescriptionView ? (
<SubplebbitDescription avatarUrl={suggested?.avatarUrl} createdAt={createdAt} description={description} subplebbitAddress={subplebbitAddress} title={title} />
) : isInRulesView ? (
<SubplebbitRules createdAt={createdAt} rules={rules} subplebbitAddress={subplebbitAddress} />
) : (
<Post post={post} showAllReplies={true} />
<Post post={post} showAllReplies={true} openReplyModal={isThreadClosed ? () => alert(t('thread_closed_alert')) : openReplyModal} />
)}
</div>
);