mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
+2
-2
@@ -7,7 +7,7 @@
|
||||
"dependencies": {
|
||||
"@capacitor/app": "1.1.1",
|
||||
"@floating-ui/react": "0.26.1",
|
||||
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#40dfddc6982c85cf07fdfc499cd3787265bc6fe5",
|
||||
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#63833e978ca25f9846544d32b9c67810af0daf19",
|
||||
"@testing-library/jest-dom": "5.14.1",
|
||||
"@testing-library/react": "13.0.0",
|
||||
"@testing-library/user-event": "13.2.1",
|
||||
@@ -43,7 +43,7 @@
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=false react-scripts build",
|
||||
"build-netlify": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=true REACT_APP_COMMIT_REF=$COMMIT_REF react-scripts build",
|
||||
"build-netlify": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=true REACT_APP_COMMIT_REF=$COMMIT_REF CI='' react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"analyze-bundle": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=true react-scripts build && source-map-explorer 'build/static/js/*.js'",
|
||||
"eject": "react-scripts eject",
|
||||
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "فهرس",
|
||||
"all": "الكل",
|
||||
"subscriptions": "الاشتراكات",
|
||||
"board": "منتدى"
|
||||
"board": "منتدى",
|
||||
"link_to_post": "الرابط لهذه الرسالة",
|
||||
"reply_to_post": "الرد على هذه المشاركة",
|
||||
"sticky": "مثبت",
|
||||
"closed": "مغلق",
|
||||
"reply": "رد",
|
||||
"link": "رابط"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "ক্যাটালগ",
|
||||
"all": "সব",
|
||||
"subscriptions": "সাবস্ক্রিপশন",
|
||||
"board": "বোর্ড"
|
||||
"board": "বোর্ড",
|
||||
"link_to_post": "এই পোস্টে লিংক",
|
||||
"reply_to_post": "এই পোস্টে প্রতিক্রিয়া",
|
||||
"sticky": "স্টিকি",
|
||||
"closed": "বন্ধ",
|
||||
"reply": "উত্তর",
|
||||
"link": "লিংক"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Katalog",
|
||||
"all": "Vše",
|
||||
"subscriptions": "Předplatné",
|
||||
"board": "Fórum"
|
||||
"board": "Fórum",
|
||||
"link_to_post": "Odkaz na tento příspěvek",
|
||||
"reply_to_post": "Odpovědět na tento příspěvek",
|
||||
"sticky": "Připnutý",
|
||||
"closed": "Zavřeno",
|
||||
"reply": "Odpovědět",
|
||||
"link": "Odkaz"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Katalog",
|
||||
"all": "Alle",
|
||||
"subscriptions": "Abonnementer",
|
||||
"board": "Forum"
|
||||
"board": "Forum",
|
||||
"link_to_post": "Link til dette indlæg",
|
||||
"reply_to_post": "Svar på dette indlæg",
|
||||
"sticky": "Fikseret",
|
||||
"closed": "Lukket",
|
||||
"reply": "Svar",
|
||||
"link": "Link"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Katalog",
|
||||
"all": "Alle",
|
||||
"subscriptions": "Abonnements",
|
||||
"board": "Forum"
|
||||
"board": "Forum",
|
||||
"link_to_post": "Link zu diesem Beitrag",
|
||||
"reply_to_post": "Antworten auf diesen Beitrag",
|
||||
"sticky": "Angeheftet",
|
||||
"closed": "Geschlossen",
|
||||
"reply": "Antworten",
|
||||
"link": "Link"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Κατάλογος",
|
||||
"all": "Όλα",
|
||||
"subscriptions": "Συνδρομές",
|
||||
"board": "Φόρουμ"
|
||||
"board": "Φόρουμ",
|
||||
"link_to_post": "Σύνδεσμος για αυτήν την ανάρτηση",
|
||||
"reply_to_post": "Απάντηση σε αυτήν την ανάρτηση",
|
||||
"sticky": "Κολλημένος",
|
||||
"closed": "Κλειστό",
|
||||
"reply": "Απάντηση",
|
||||
"link": "Σύνδεσμος"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Catalog",
|
||||
"all": "All",
|
||||
"subscriptions": "Subscriptions",
|
||||
"board": "Board"
|
||||
"board": "Board",
|
||||
"link_to_post": "Link to this post",
|
||||
"reply_to_post": "Reply to this post",
|
||||
"sticky": "Sticky",
|
||||
"closed": "Closed",
|
||||
"reply": "Reply",
|
||||
"link": "Link"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Catálogo",
|
||||
"all": "Todo",
|
||||
"subscriptions": "Suscripciones",
|
||||
"board": "Foro"
|
||||
"board": "Foro",
|
||||
"link_to_post": "Enlace a esta publicación",
|
||||
"reply_to_post": "Responder a esta publicación",
|
||||
"sticky": "Fijado",
|
||||
"closed": "Cerrado",
|
||||
"reply": "Responder",
|
||||
"link": "Enlace"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "کاتالوگ",
|
||||
"all": "همه",
|
||||
"subscriptions": "اشتراکها",
|
||||
"board": "تالار"
|
||||
"board": "تالار",
|
||||
"link_to_post": "پیوند به این پست",
|
||||
"reply_to_post": "پاسخ به این پست",
|
||||
"sticky": "چسبیده",
|
||||
"closed": "بسته",
|
||||
"reply": "پاسخ",
|
||||
"link": "لینک"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Luettelo",
|
||||
"all": "Kaikki",
|
||||
"subscriptions": "Tilaukset",
|
||||
"board": "Foorumi"
|
||||
"board": "Foorumi",
|
||||
"link_to_post": "Linkki tähän viestiin",
|
||||
"reply_to_post": "Vastaa tähän viestiin",
|
||||
"sticky": "Tikki",
|
||||
"closed": "Suljettu",
|
||||
"reply": "Vastaa",
|
||||
"link": "Linkki"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Katalogo",
|
||||
"all": "Lahat",
|
||||
"subscriptions": "Mga Subscription",
|
||||
"board": "Board"
|
||||
"board": "Board",
|
||||
"link_to_post": "Link sa post na ito",
|
||||
"reply_to_post": "Tumugon sa post na ito",
|
||||
"sticky": "Malagkit",
|
||||
"closed": "Sarado",
|
||||
"reply": "Tumugon",
|
||||
"link": "Link"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Catalogue",
|
||||
"all": "Tous",
|
||||
"subscriptions": "Abonnements",
|
||||
"board": "Forum"
|
||||
"board": "Forum",
|
||||
"link_to_post": "Lien vers ce message",
|
||||
"reply_to_post": "Répondre à ce message",
|
||||
"sticky": "Épinglé",
|
||||
"closed": "Fermé",
|
||||
"reply": "Répondre",
|
||||
"link": "Lien"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "קטלוג",
|
||||
"all": "הכל",
|
||||
"subscriptions": "מנויים",
|
||||
"board": "פורום"
|
||||
"board": "פורום",
|
||||
"link_to_post": "קישור להודעה זו",
|
||||
"reply_to_post": "הגבה להודעה זו",
|
||||
"sticky": "מדבקה",
|
||||
"closed": "סגור",
|
||||
"reply": "הגב",
|
||||
"link": "קישור"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "कैटलॉग",
|
||||
"all": "सभी",
|
||||
"subscriptions": "सदस्यता",
|
||||
"board": "फोरम"
|
||||
"board": "फोरम",
|
||||
"link_to_post": "इस पोस्ट का लिंक",
|
||||
"reply_to_post": "इस पोस्ट का जवाब दें",
|
||||
"sticky": "चिपकाया",
|
||||
"closed": "बंद",
|
||||
"reply": "जवाब दें",
|
||||
"link": "लिंक"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Katalógus",
|
||||
"all": "Összes",
|
||||
"subscriptions": "Előfizetések",
|
||||
"board": "Fórum"
|
||||
"board": "Fórum",
|
||||
"link_to_post": "Hivatkozás erre a hozzászólásra",
|
||||
"reply_to_post": "Válasz erre a hozzászólásra",
|
||||
"sticky": "Rögzített",
|
||||
"closed": "Zárt",
|
||||
"reply": "Válaszolás",
|
||||
"link": "Hivatkozás"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Katalog",
|
||||
"all": "Semua",
|
||||
"subscriptions": "Langganan",
|
||||
"board": "Forum"
|
||||
"board": "Forum",
|
||||
"link_to_post": "Tautan ke pos ini",
|
||||
"reply_to_post": "Balas ke pos ini",
|
||||
"sticky": "Menempel",
|
||||
"closed": "Tutup",
|
||||
"reply": "Balas",
|
||||
"link": "Tautan"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Catalogo",
|
||||
"all": "Tutti",
|
||||
"subscriptions": "Iscrizioni",
|
||||
"board": "Forum"
|
||||
"board": "Forum",
|
||||
"link_to_post": "Link per questo post",
|
||||
"reply_to_post": "Commenta questo post",
|
||||
"sticky": "Incollato",
|
||||
"closed": "Chiuso",
|
||||
"reply": "Commenta",
|
||||
"link": "Link"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "カタログ",
|
||||
"all": "すべて",
|
||||
"subscriptions": "購読",
|
||||
"board": "掲示板"
|
||||
"board": "掲示板",
|
||||
"link_to_post": "この投稿へのリンク",
|
||||
"reply_to_post": "この投稿に返信",
|
||||
"sticky": "固定",
|
||||
"closed": "閉鎖",
|
||||
"reply": "返信",
|
||||
"link": "リンク"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "카탈로그",
|
||||
"all": "모두",
|
||||
"subscriptions": "구독",
|
||||
"board": "게시판"
|
||||
"board": "게시판",
|
||||
"link_to_post": "이 게시물에 대한 링크",
|
||||
"reply_to_post": "이 게시물에 답장하기",
|
||||
"sticky": "고정된",
|
||||
"closed": "닫힘",
|
||||
"reply": "답장",
|
||||
"link": "링크"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "कॅटलॉग",
|
||||
"all": "सर्व",
|
||||
"subscriptions": "सदस्यता",
|
||||
"board": "फोरम"
|
||||
"board": "फोरम",
|
||||
"link_to_post": "ह्या पोस्टचा लिंक",
|
||||
"reply_to_post": "या पोस्टवर प्रतिसाद द्या",
|
||||
"sticky": "स्टिकी",
|
||||
"closed": "बंद",
|
||||
"reply": "प्रतिसाद",
|
||||
"link": "लिंक"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Catalogus",
|
||||
"all": "Alles",
|
||||
"subscriptions": "Abonnementen",
|
||||
"board": "Forum"
|
||||
"board": "Forum",
|
||||
"link_to_post": "Link naar dit bericht",
|
||||
"reply_to_post": "Reageren op dit bericht",
|
||||
"sticky": "Vastgemaakt",
|
||||
"closed": "Gesloten",
|
||||
"reply": "Antwoord",
|
||||
"link": "Link"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Katalog",
|
||||
"all": "Alle",
|
||||
"subscriptions": "Abonnementer",
|
||||
"board": "Forum"
|
||||
"board": "Forum",
|
||||
"link_to_post": "Lenke til dette innlegget",
|
||||
"reply_to_post": "Svar på dette innlegget",
|
||||
"sticky": "Klistret",
|
||||
"closed": "Stengt",
|
||||
"reply": "Svar",
|
||||
"link": "Lenke"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Katalog",
|
||||
"all": "Wszystkie",
|
||||
"subscriptions": "Subskrypcje",
|
||||
"board": "Forum"
|
||||
"board": "Forum",
|
||||
"link_to_post": "Link do tego posta",
|
||||
"reply_to_post": "Odpowiedz na ten post",
|
||||
"sticky": "Przyklejony",
|
||||
"closed": "Zamknięty",
|
||||
"reply": "Odpowiedz",
|
||||
"link": "Link"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Catálogo",
|
||||
"all": "Todos",
|
||||
"subscriptions": "Inscrições",
|
||||
"board": "Fórum"
|
||||
"board": "Fórum",
|
||||
"link_to_post": "Link para esta postagem",
|
||||
"reply_to_post": "Responder a esta postagem",
|
||||
"sticky": "Fixado",
|
||||
"closed": "Fechado",
|
||||
"reply": "Responder",
|
||||
"link": "Link"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Catalog",
|
||||
"all": "Toate",
|
||||
"subscriptions": "Abonamente",
|
||||
"board": "Forum"
|
||||
"board": "Forum",
|
||||
"link_to_post": "Link către acest mesaj",
|
||||
"reply_to_post": "Răspunde la acest mesaj",
|
||||
"sticky": "Lipit",
|
||||
"closed": "Închis",
|
||||
"reply": "Răspunde",
|
||||
"link": "Legătură"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Каталог",
|
||||
"all": "Все",
|
||||
"subscriptions": "Подписки",
|
||||
"board": "Форум"
|
||||
"board": "Форум",
|
||||
"link_to_post": "Ссылка на это сообщение",
|
||||
"reply_to_post": "Ответить на это сообщение",
|
||||
"sticky": "Прикреплено",
|
||||
"closed": "Закрыто",
|
||||
"reply": "Ответить",
|
||||
"link": "Ссылка"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Katalog",
|
||||
"all": "Të gjitha",
|
||||
"subscriptions": "Abonimet",
|
||||
"board": "Forumi"
|
||||
"board": "Forumi",
|
||||
"link_to_post": "Link për këtë postim",
|
||||
"reply_to_post": "Përgjigju në këtë postim",
|
||||
"sticky": "Ngjitës",
|
||||
"closed": "Mbyllur",
|
||||
"reply": "Përgjigju",
|
||||
"link": "Lidhje"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Katalog",
|
||||
"all": "Allt",
|
||||
"subscriptions": "Prenumerationer",
|
||||
"board": "Forum"
|
||||
"board": "Forum",
|
||||
"link_to_post": "Länk till detta inlägg",
|
||||
"reply_to_post": "Svara på detta inlägg",
|
||||
"sticky": "Klistrad",
|
||||
"closed": "Stängd",
|
||||
"reply": "Svara",
|
||||
"link": "Länk"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "కాటలాగ్",
|
||||
"all": "అన్ని",
|
||||
"subscriptions": "సబ్స్క్రిప్షన్లు",
|
||||
"board": "ఫోరం"
|
||||
"board": "ఫోరం",
|
||||
"link_to_post": "ఈ పోస్టుకు లింక్",
|
||||
"reply_to_post": "ఈ పోస్టుకు మరియుని జవాబు ఇవ్వండి",
|
||||
"sticky": "స్టికీ",
|
||||
"closed": "మూసివేయబడినది",
|
||||
"reply": "స్పందించు",
|
||||
"link": "లింక్"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "แคตตาล็อก",
|
||||
"all": "ทั้งหมด",
|
||||
"subscriptions": "การสมัครสมาชิก",
|
||||
"board": "กระดาน"
|
||||
"board": "กระดาน",
|
||||
"link_to_post": "ลิงก์ไปยังโพสต์นี้",
|
||||
"reply_to_post": "ตอบกลับไปยังโพสต์นี้",
|
||||
"sticky": "ปักหมุด",
|
||||
"closed": "ปิด",
|
||||
"reply": "ตอบ",
|
||||
"link": "ลิงก์"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Katalog",
|
||||
"all": "Tümü",
|
||||
"subscriptions": "Abonelikler",
|
||||
"board": "Forum"
|
||||
"board": "Forum",
|
||||
"link_to_post": "Bu gönderiye bağlantı",
|
||||
"reply_to_post": "Bu gönderiye yanıtla",
|
||||
"sticky": "Yapışkan",
|
||||
"closed": "Kapalı",
|
||||
"reply": "Yanıtla",
|
||||
"link": "Bağlantı"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Каталог",
|
||||
"all": "Всі",
|
||||
"subscriptions": "Підписки",
|
||||
"board": "Форум"
|
||||
"board": "Форум",
|
||||
"link_to_post": "Посилання на цей допис",
|
||||
"reply_to_post": "Відповісти на це повідомлення",
|
||||
"sticky": "Прикріплений",
|
||||
"closed": "Закрито",
|
||||
"reply": "Відповісти",
|
||||
"link": "Посилання"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "کیٹالوگ",
|
||||
"all": "سب",
|
||||
"subscriptions": "سبسکرپشنز",
|
||||
"board": "پیغام بورڈ"
|
||||
"board": "پیغام بورڈ",
|
||||
"link_to_post": "اس پوسٹ کا لنک",
|
||||
"reply_to_post": "اس پوسٹ کا جواب دیں",
|
||||
"sticky": "چسپی",
|
||||
"closed": "بند",
|
||||
"reply": "جواب دیں",
|
||||
"link": "لنک"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "Catalog",
|
||||
"all": "Tất cả",
|
||||
"subscriptions": "Đăng ký",
|
||||
"board": "Diễn đàn"
|
||||
"board": "Diễn đàn",
|
||||
"link_to_post": "Liên kết đến bài viết này",
|
||||
"reply_to_post": "Trả lời bài viết này",
|
||||
"sticky": "Dán",
|
||||
"closed": "Đóng",
|
||||
"reply": "Trả lời",
|
||||
"link": "Liên kết"
|
||||
}
|
||||
@@ -29,5 +29,11 @@
|
||||
"catalog": "目录",
|
||||
"all": "所有",
|
||||
"subscriptions": "订阅",
|
||||
"board": "论坛"
|
||||
"board": "论坛",
|
||||
"link_to_post": "链接到此帖",
|
||||
"reply_to_post": "回复此帖",
|
||||
"sticky": "置顶",
|
||||
"closed": "关闭",
|
||||
"reply": "回复",
|
||||
"link": "链接"
|
||||
}
|
||||
@@ -9,3 +9,9 @@
|
||||
overflow-x: hidden;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.app {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
@@ -27,10 +27,6 @@
|
||||
|
||||
.content hr {
|
||||
width: 90%;
|
||||
/* revert to browser styling for burichan and futaba */
|
||||
border: var(--hr-border, revert);
|
||||
border-top: var(--hr-border-top, revert);
|
||||
height: var(--hr-height, revert);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -17,14 +17,15 @@ interface BoardBannerProps {
|
||||
address: string | undefined;
|
||||
}
|
||||
|
||||
const BoardBanner = ({ title, address }: BoardBannerProps) => {
|
||||
const BoardBanner = ({ address, title }: BoardBannerProps) => {
|
||||
const displayAddress = address && address.length > 10 && !address.includes('.') ? address.slice(0, 13).concat('...') : address;
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.bannerCnt}>
|
||||
<ImageBanner key={address} />
|
||||
</div>
|
||||
<div className={styles.boardTitle}>{title || `p/${address}`}</div>
|
||||
{title && <div className={styles.boardAddress}>p/{address}</div>}
|
||||
<div className={styles.boardTitle}>{title || `p/${displayAddress}`}</div>
|
||||
<div className={styles.boardAddress}>p/{address}</div>
|
||||
<hr />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,4 +17,8 @@
|
||||
.mobileBoardButtons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.subscribeButton {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,15 @@ export const MobileBoardButtons = ({ address }: BoardButtonsProps) => {
|
||||
export const DesktopBoardButtons = ({ address }: BoardButtonsProps) => {
|
||||
return (
|
||||
<div className={styles.desktopBoardButtons}>
|
||||
<SubscribeButton address={address} />
|
||||
<hr />
|
||||
[
|
||||
<OptionsButton />
|
||||
] [
|
||||
<CatalogButton />]
|
||||
<span className={styles.subscribeButton}>
|
||||
[
|
||||
<SubscribeButton address={address} />]
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
|
||||
.boardNavDesktop {
|
||||
padding: 5px;
|
||||
font-size: var(--boardnav-font-size);
|
||||
color: var(--boardnav-separator-color);
|
||||
}
|
||||
|
||||
.boardNavDesktop a {
|
||||
color: var(--link-internal-text-color);
|
||||
text-decoration: var(--link-internal-text-decoration);
|
||||
color: var(--boardnav-desktop-link-text-color);
|
||||
text-decoration: var(--boardnav-desktop-link-text-decoration);
|
||||
}
|
||||
|
||||
.boardNavDesktop a:hover {
|
||||
color: var(--link-internal-text-color-hover);
|
||||
text-decoration: var(--link-internal-text-decoration-hover);
|
||||
color: var(--boardnav-desktop-link-text-color-hover);
|
||||
text-decoration: var(--boardnav-desktop-link-text-decoration-hover);
|
||||
}
|
||||
|
||||
.navTopRight {
|
||||
@@ -29,10 +28,6 @@
|
||||
font-size: var(--boardnav-mobile-font-size);
|
||||
}
|
||||
|
||||
.boardNavMobile .pageJump a {
|
||||
color: var(--boardnav-mobile-button-text-color);
|
||||
}
|
||||
|
||||
.boardNavMobile strong {
|
||||
text-transform: capitalize;
|
||||
padding-right: 5px;
|
||||
@@ -54,13 +49,11 @@
|
||||
padding: 3px 5px 0 0;
|
||||
}
|
||||
|
||||
.pageJump span, .pageJump a {
|
||||
.pageJump a, .mobileSearchButton {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.pageJump a {
|
||||
color: var(--link-internal-text-color);
|
||||
text-decoration: var(--link-internal-text-decoration);
|
||||
color: var(--homepage-box-link-text-color);
|
||||
text-decoration: var(--homepage-box-link-text-decoration);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
|
||||
@@ -37,16 +37,24 @@ const BoardNavDesktop = ({ subplebbits }: BoardNavProps) => {
|
||||
const BoardNavMobile = ({ subplebbits, currentSubplebbit }: BoardNavProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const displaySubplebbitAddress = currentSubplebbit && currentSubplebbit.length > 30 ? currentSubplebbit.slice(0, 30).concat('...') : currentSubplebbit;
|
||||
|
||||
const currentSubplebbitIsInList = subplebbits.some((subplebbit: any) => subplebbit.address === currentSubplebbit);
|
||||
|
||||
const boardSelect = (
|
||||
<select value={currentSubplebbit} onChange={(e) => navigate(`/p/${e.target.value}`)}>
|
||||
<option value='all'>All</option>
|
||||
<option value='subscriptions'>Subscriptions</option>
|
||||
{subplebbits.map((subplebbit: any, index: number) => (
|
||||
<option key={index} value={subplebbit.address}>
|
||||
{subplebbit.address.includes('.') ? subplebbit.address : subplebbit.title || subplebbit.address.slice(0, 10).concat('...')}
|
||||
</option>
|
||||
))}
|
||||
<select value={currentSubplebbit || 'all'} onChange={(e) => navigate(`/p/${e.target.value}`)}>
|
||||
{!currentSubplebbitIsInList && currentSubplebbit && <option value={currentSubplebbit}>{displaySubplebbitAddress}</option>}
|
||||
<option value='all'>{t('all')}</option>
|
||||
<option value='subscriptions'>{t('subscriptions')}</option>
|
||||
{subplebbits.map((subplebbit: any, index: number) => {
|
||||
const { address, title } = subplebbit;
|
||||
const subplebbitAddress = address.includes('.') ? address : title || address.slice(0, 10).concat('...');
|
||||
return (
|
||||
<option key={index} value={subplebbit.address}>
|
||||
{subplebbitAddress}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
);
|
||||
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
min-width: 468px;
|
||||
margin-left: -4px;
|
||||
margin-bottom: 5px;
|
||||
/* revert to browser styling for burichan and futaba */
|
||||
border: var(--hr-border, revert);
|
||||
border-top: var(--hr-border-top, revert);
|
||||
height: var(--hr-height, revert);
|
||||
}
|
||||
|
||||
.blotter {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
export interface BoardStatsProps {
|
||||
address: string | undefined;
|
||||
createdAt: number;
|
||||
createdAt: number | undefined;
|
||||
}
|
||||
|
||||
const BoardStats = ({ address, createdAt }: BoardStatsProps) => {
|
||||
@@ -59,7 +59,7 @@ const BoardStats = ({ address, createdAt }: BoardStatsProps) => {
|
||||
</tr>
|
||||
<tr>
|
||||
<td className={styles.lowercase}>
|
||||
{unixToMMDDYYYY(createdAt)} {t('board_created')}
|
||||
{createdAt && unixToMMDDYYYY(createdAt)} {t('board_created')}
|
||||
{' / '}
|
||||
<Trans
|
||||
i18nKey='board_stats_all'
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
.audioEmbed {
|
||||
height: 240px !important;
|
||||
width: 700px !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.audioEmbed {
|
||||
max-height: 250px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.instagramEmbed {
|
||||
height: 420px !important;
|
||||
width: 360px !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.instagramEmbed {
|
||||
max-height: 420px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.redditEmbed {
|
||||
width: 500px !important;
|
||||
height: 520px !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.redditEmbed {
|
||||
width: 100% !important;
|
||||
height: 520px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tiktokEmbed {
|
||||
width: 400px !important;
|
||||
height: 780px !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tiktokEmbed {
|
||||
width: 100% !important;
|
||||
height: 70vh !important;
|
||||
}
|
||||
}
|
||||
|
||||
.videoEmbed {
|
||||
height: 450px !important;
|
||||
width: 800px !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.videoEmbed {
|
||||
max-height: 250px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.xEmbed {
|
||||
height: 580px !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.xEmbed {
|
||||
width: 100% !important;
|
||||
height: 50vh !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
import styles from './embed.module.css';
|
||||
|
||||
interface EmbedProps {
|
||||
url: string;
|
||||
}
|
||||
|
||||
const Embed = ({ url }: EmbedProps) => {
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
if (youtubeHosts.has(parsedUrl.host) || parsedUrl.host.startsWith('yt.')) {
|
||||
return <YoutubeEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (xHosts.has(parsedUrl.host)) {
|
||||
return <XEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (redditHosts.has(parsedUrl.host)) {
|
||||
return <RedditEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (twitchHosts.has(parsedUrl.host)) {
|
||||
return <TwitchEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (tiktokHosts.has(parsedUrl.host)) {
|
||||
return <TiktokEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (instagramHosts.has(parsedUrl.host)) {
|
||||
return <InstagramEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (odyseeHosts.has(parsedUrl.host)) {
|
||||
return <OdyseeEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (bitchuteHosts.has(parsedUrl.host)) {
|
||||
return <BitchuteEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (streamableHosts.has(parsedUrl.host)) {
|
||||
return <StreamableEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (spotifyHosts.has(parsedUrl.host)) {
|
||||
return <SpotifyEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
};
|
||||
|
||||
interface EmbedComponentProps {
|
||||
parsedUrl: URL;
|
||||
}
|
||||
|
||||
const youtubeHosts = new Set<string>(['youtube.com', 'www.youtube.com', 'youtu.be', 'www.youtu.be', 'm.youtube.com']);
|
||||
|
||||
const YoutubeEmbed = ({ parsedUrl }: EmbedComponentProps) => {
|
||||
let youtubeId = parsedUrl.searchParams.get('v');
|
||||
|
||||
if (!youtubeId && parsedUrl.host.includes('youtu.be')) {
|
||||
youtubeId = parsedUrl.pathname.substring(1);
|
||||
}
|
||||
|
||||
if (youtubeId) {
|
||||
return (
|
||||
<iframe
|
||||
className={styles.videoEmbed}
|
||||
height='100%'
|
||||
width='100%'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowFullScreen
|
||||
title={parsedUrl.href}
|
||||
src={`https://www.youtube-nocookie.com/embed/${youtubeId}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const xHosts = new Set<string>(['twitter.com', 'www.twitter.com', 'x.com', 'www.x.com']);
|
||||
|
||||
const XEmbed = ({ parsedUrl }: EmbedComponentProps) => {
|
||||
return (
|
||||
<iframe
|
||||
className={styles.xEmbed}
|
||||
height='100%'
|
||||
width='100%'
|
||||
referrerPolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
title={parsedUrl.href}
|
||||
srcDoc={`
|
||||
<blockquote class="twitter-tweet" data-theme="dark">
|
||||
<a href="${parsedUrl.href.replace('x.com', 'twitter.com')}"></a>
|
||||
</blockquote>
|
||||
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
|
||||
`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const redditHosts = new Set<string>(['reddit.com', 'www.reddit.com', 'old.reddit.com']);
|
||||
|
||||
const RedditEmbed = ({ parsedUrl }: EmbedComponentProps) => {
|
||||
return (
|
||||
<iframe
|
||||
className={styles.redditEmbed}
|
||||
height='100%'
|
||||
width='100%'
|
||||
referrerPolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
title={parsedUrl.href}
|
||||
srcDoc={`
|
||||
<style>
|
||||
/* fix reddit iframe being centered */
|
||||
iframe {
|
||||
margin: 0!important;
|
||||
}
|
||||
</style>
|
||||
<blockquote class="reddit-embed-bq" style="height:240px" data-embed-theme="dark">
|
||||
<a href="${parsedUrl.href}"></a>
|
||||
</blockquote>
|
||||
<script async src="https://embed.reddit.com/widgets.js" charset="UTF-8"></script>
|
||||
`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const twitchHosts = new Set<string>(['twitch.tv', 'www.twitch.tv']);
|
||||
|
||||
const TwitchEmbed = ({ parsedUrl }: EmbedComponentProps) => {
|
||||
let iframeUrl;
|
||||
if (parsedUrl.pathname.startsWith('/videos/')) {
|
||||
const videoId = parsedUrl.pathname.replace('/videos/', '');
|
||||
iframeUrl = `https://player.twitch.tv/?video=${videoId}&parent=${window.location.hostname}`;
|
||||
} else {
|
||||
const channel = parsedUrl.pathname.replaceAll('/', '');
|
||||
iframeUrl = `https://player.twitch.tv/?channel=${channel}&parent=${window.location.hostname}`;
|
||||
}
|
||||
return (
|
||||
<iframe
|
||||
className={styles.videoEmbed}
|
||||
height='100%'
|
||||
width='100%'
|
||||
referrerPolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowFullScreen
|
||||
title={parsedUrl.href}
|
||||
src={iframeUrl}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const tiktokHosts = new Set<string>(['tiktok.com', 'www.tiktok.com']);
|
||||
|
||||
const TiktokEmbed = ({ parsedUrl }: EmbedComponentProps) => {
|
||||
const videoId = parsedUrl.pathname.replace(/.+\/video\//, '').replaceAll('/', '');
|
||||
return (
|
||||
<iframe
|
||||
className={styles.tiktokEmbed}
|
||||
height='100%'
|
||||
width='100%'
|
||||
referrerPolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
title={parsedUrl.href}
|
||||
srcDoc={`
|
||||
<blockquote class="tiktok-embed" data-video-id="${videoId}">
|
||||
<a></a>
|
||||
</blockquote>
|
||||
<script async src="https://www.tiktok.com/embed.js"></script>
|
||||
`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const instagramHosts = new Set<string>(['instagram.com', 'www.instagram.com']);
|
||||
|
||||
const InstagramEmbed = ({ parsedUrl }: EmbedComponentProps) => {
|
||||
const pathNames = parsedUrl.pathname.replace(/\/+$/, '').split('/');
|
||||
const id = pathNames[pathNames.length - 1];
|
||||
return (
|
||||
<iframe
|
||||
className={styles.instagramEmbed}
|
||||
height='100%'
|
||||
width='100%'
|
||||
referrerPolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
title={parsedUrl.href}
|
||||
srcDoc={`
|
||||
<blockquote class="instagram-media">
|
||||
<a href="https://www.instagram.com/p/${id}/"></a>
|
||||
</blockquote>
|
||||
<script async src="//www.instagram.com/embed.js"></script>
|
||||
`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const odyseeHosts = new Set<string>(['odysee.com', 'www.odysee.com']);
|
||||
|
||||
const OdyseeEmbed = ({ parsedUrl }: EmbedComponentProps) => {
|
||||
const iframeUrl = `https://odysee.com/$/embed${parsedUrl.pathname}`;
|
||||
return (
|
||||
<iframe
|
||||
className={styles.videoEmbed}
|
||||
height='100%'
|
||||
width='100%'
|
||||
referrerPolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowFullScreen
|
||||
title={parsedUrl.href}
|
||||
src={iframeUrl}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const bitchuteHosts = new Set<string>(['bitchute.com', 'www.bitchute.com']);
|
||||
|
||||
const BitchuteEmbed = ({ parsedUrl }: EmbedComponentProps) => {
|
||||
const videoId = parsedUrl.pathname.replace(/\/video\//, '').replaceAll('/', '');
|
||||
return (
|
||||
<iframe
|
||||
className={styles.videoEmbed}
|
||||
height='100%'
|
||||
width='100%'
|
||||
referrerPolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowFullScreen
|
||||
title={parsedUrl.href}
|
||||
src={`https://www.bitchute.com/embed/${videoId}/`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const streamableHosts = new Set<string>(['streamable.com', 'www.streamable.com']);
|
||||
|
||||
const StreamableEmbed = ({ parsedUrl }: EmbedComponentProps) => {
|
||||
const videoId = parsedUrl.pathname.replaceAll('/', '');
|
||||
return (
|
||||
<iframe
|
||||
className={styles.videoEmbed}
|
||||
height='100%'
|
||||
width='100%'
|
||||
referrerPolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowFullScreen
|
||||
title={parsedUrl.href}
|
||||
src={`https://streamable.com/e/${videoId}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const spotifyHosts = new Set<string>(['spotify.com', 'www.spotify.com', 'open.spotify.com']);
|
||||
|
||||
const SpotifyEmbed = ({ parsedUrl }: EmbedComponentProps) => {
|
||||
const iframeUrl = `https://open.spotify.com/embed${parsedUrl.pathname}?theme=0`;
|
||||
return (
|
||||
<iframe
|
||||
className={styles.audioEmbed}
|
||||
height='100%'
|
||||
width='100%'
|
||||
referrerPolicy='no-referrer'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowFullScreen
|
||||
title={parsedUrl.href}
|
||||
src={iframeUrl}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const canEmbedHosts = new Set<string>([
|
||||
...youtubeHosts,
|
||||
...xHosts,
|
||||
...redditHosts,
|
||||
...twitchHosts,
|
||||
...tiktokHosts,
|
||||
...instagramHosts,
|
||||
...odyseeHosts,
|
||||
...bitchuteHosts,
|
||||
...streamableHosts,
|
||||
...spotifyHosts,
|
||||
]);
|
||||
|
||||
export const canEmbed = (parsedUrl: URL): boolean => canEmbedHosts.has(parsedUrl.host);
|
||||
|
||||
export default Embed;
|
||||
@@ -0,0 +1 @@
|
||||
export { default, canEmbed } from './embed';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './loading-ellipsis';
|
||||
@@ -0,0 +1,24 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
@keyframes ellipsis {
|
||||
to {
|
||||
width: 1.3em;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes ellipsis {
|
||||
to {
|
||||
width: 1.3em;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import styles from './loading-ellipsis.module.css';
|
||||
|
||||
interface LoadingEllipsisProps {
|
||||
string: string;
|
||||
}
|
||||
|
||||
const LoadingEllipsis = ({ string }: LoadingEllipsisProps) => {
|
||||
return <span className={styles.ellipsis}>{string}</span>;
|
||||
};
|
||||
|
||||
export default LoadingEllipsis;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './markdown';
|
||||
@@ -0,0 +1,25 @@
|
||||
.markdown a {
|
||||
color: var(--post-link-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown a:hover {
|
||||
color: var(--post-link-text-color-hover);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown ol {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.markdown ul {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.hrWrapper {
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
.hrWrapper hr {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useMemo } from 'react';
|
||||
import styles from './markdown.module.css';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import supersub from 'remark-supersub';
|
||||
import { visit } from 'unist-util-visit';
|
||||
|
||||
interface MarkdownProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
const MAX_LENGTH_FOR_GFM = 10000; // remarkGfm lags with large content
|
||||
|
||||
const blockquoteToGreentext = () => (tree: any) => {
|
||||
tree.children.forEach((node: any) => {
|
||||
if (node.type === 'blockquote') {
|
||||
node.children.forEach((child: any) => {
|
||||
if (child.type === 'paragraph' && child.children.length > 0) {
|
||||
const prefix = {
|
||||
type: 'text',
|
||||
value: '>',
|
||||
};
|
||||
child.children.unshift(prefix);
|
||||
}
|
||||
});
|
||||
node.type = 'div';
|
||||
node.data = {
|
||||
hName: 'div',
|
||||
hProperties: {
|
||||
className: 'greentext',
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const Markdown = ({ content }: MarkdownProps) => {
|
||||
const remarkPlugins: any[] = [[supersub]];
|
||||
|
||||
if (content.length <= MAX_LENGTH_FOR_GFM) {
|
||||
remarkPlugins.push([remarkGfm, { singleTilde: false }]);
|
||||
}
|
||||
|
||||
const customSchema = useMemo(
|
||||
() => ({
|
||||
...defaultSchema,
|
||||
tagNames: [...(defaultSchema.tagNames || []), 'div'],
|
||||
attributes: {
|
||||
...defaultSchema.attributes,
|
||||
div: ['className'],
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
remarkPlugins.push([blockquoteToGreentext]);
|
||||
|
||||
return (
|
||||
<span className={styles.markdown}>
|
||||
<ReactMarkdown
|
||||
children={content}
|
||||
remarkPlugins={remarkPlugins}
|
||||
rehypePlugins={[[rehypeSanitize, customSchema]]}
|
||||
components={{
|
||||
img: ({ src }) => <span>{src}</span>,
|
||||
video: ({ src }) => <span>{src}</span>,
|
||||
iframe: ({ src }) => <span>{src}</span>,
|
||||
source: ({ src }) => <span>{src}</span>,
|
||||
hr: () => (
|
||||
<div className={styles.hrWrapper}>
|
||||
<hr />
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Markdown;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './post';
|
||||
@@ -0,0 +1,96 @@
|
||||
.thread {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.hrWrapper hr {
|
||||
margin: 0; /* margin bugs Virtuoso scrolling */
|
||||
}
|
||||
|
||||
.hrWrapper {
|
||||
padding-top: 0.5em; /* instead of using margin, wrap with padding */
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.postDesktop .link a {
|
||||
color: var(--post-link-text-color);
|
||||
text-decoration: var(--post-link-text-decoration);
|
||||
}
|
||||
|
||||
.postDesktop .link a:hover {
|
||||
color: var(--post-link-text-color-hover);
|
||||
text-decoration: var(--post-link-text-decoration-hover);
|
||||
}
|
||||
|
||||
.postDesktop .postInfo {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.postDesktop .subject {
|
||||
color: var(--post-subject-text-color);
|
||||
font-weight: var(--post-subject-font-weight);
|
||||
}
|
||||
|
||||
.postDesktop .name {
|
||||
color: var(--post-name-text-color);
|
||||
font-weight: var(--post-name-font-weight);
|
||||
}
|
||||
|
||||
.postDesktop .postNumLink a, .postDesktop .postNumLink span {
|
||||
color: unset;
|
||||
text-decoration: unset;
|
||||
}
|
||||
|
||||
.postDesktop .postNumLink a:hover, .postDesktop .postNumLink span:hover {
|
||||
cursor: pointer;
|
||||
color: var(--button-desktop-text-color-hover);
|
||||
}
|
||||
|
||||
.postDesktop .stickyIconWrapper {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.postDesktop .closedIconWrapper {
|
||||
padding-left: 3px;
|
||||
}
|
||||
|
||||
.postDesktop .stickyIconWrapper img, .postDesktop .closedIconWrapper img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: none;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.postDesktop .replyButton {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.postDesktop .replyButton a {
|
||||
color: var(--button-desktop-text-color);
|
||||
text-decoration: var(--button-desktop-text-decoration);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.postDesktop .replyButton a:hover {
|
||||
cursor: pointer;
|
||||
color: var(--button-desktop-text-color-hover);
|
||||
}
|
||||
|
||||
.postMenuBtn {
|
||||
padding-left: 5px;
|
||||
color: var(--post-menu-button-color);
|
||||
}
|
||||
|
||||
.postMenuBtn:hover {
|
||||
cursor: pointer;
|
||||
color: var(--post-menu-button-color-hover);
|
||||
}
|
||||
|
||||
.postDesktop .postMessage {
|
||||
padding: 1em 40px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.postDesktop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Comment } from '@plebbit/plebbit-react-hooks';
|
||||
import styles from './post.module.css';
|
||||
import useReplies from '../../hooks/use-replies';
|
||||
import { getLinkMediaInfoMemoized } from '../../lib/utils/media-utils';
|
||||
import { getFormattedDate } from '../../lib/utils/time-utils';
|
||||
import Markdown from '../markdown';
|
||||
|
||||
const Post = ({ post }: Comment) => {
|
||||
const { t } = useTranslation();
|
||||
const { author, cid, content, link, locked, pinned, postCid, shortCid, subplebbitAddress, timestamp, title } = post || {};
|
||||
const { displayName, shortAddress } = author || {};
|
||||
const replies = useReplies(post);
|
||||
const linkMediaInfo = getLinkMediaInfoMemoized(link);
|
||||
|
||||
return (
|
||||
<div className={styles.thread}>
|
||||
<div className={styles.postContainer}>
|
||||
<div className={styles.postDesktop}>
|
||||
<div className={styles.hrWrapper}>
|
||||
<hr />
|
||||
</div>
|
||||
{linkMediaInfo?.url && (
|
||||
<div className={styles.link}>
|
||||
{t('link')}:{' '}
|
||||
<a href={linkMediaInfo?.url} target='_blank' rel='noopener noreferrer'>
|
||||
{linkMediaInfo.url.length > 30 ? linkMediaInfo?.url.slice(0, 30) + '...' : linkMediaInfo?.url}
|
||||
</a>{' '}
|
||||
({linkMediaInfo?.type})
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.postInfo}>
|
||||
{title && <span className={styles.subject}>{title.length > 75 ? title.slice(0, 75) + '...' : title}</span>}{' '}
|
||||
<span className={styles.nameBlock}>
|
||||
<span className={styles.name}>{displayName || 'Anonymous'} </span>
|
||||
<span className={styles.userAddress}>(u/{shortAddress}) </span>
|
||||
</span>
|
||||
<span className={styles.dateTime}>{getFormattedDate(timestamp)} </span>
|
||||
<span className={styles.postNum}>
|
||||
<span className={styles.postNumLink}>
|
||||
<Link to={`/p/${subplebbitAddress}/c/${cid}`} className={styles.linkToPost} title={t('link_to_post')}>
|
||||
c/
|
||||
</Link>
|
||||
<span className={styles.replyToPost} title={t('reply_to_post')}>
|
||||
{shortCid}
|
||||
</span>
|
||||
</span>
|
||||
{pinned && (
|
||||
<span className={styles.stickyIconWrapper}>
|
||||
<img src='assets/icons/sticky.gif' alt='' className={styles.stickyIcon} title={t('sticky')} />
|
||||
</span>
|
||||
)}
|
||||
{locked && (
|
||||
<span className={styles.closedIconWrapper}>
|
||||
<img src='assets/icons/closed.gif' alt='' className={styles.closedIcon} title={t('closed')} />
|
||||
</span>
|
||||
)}
|
||||
<span className={styles.replyButton}>
|
||||
[<Link to={`/p/${subplebbitAddress}/c/${postCid}`}>{t('reply')}</Link>]
|
||||
</span>
|
||||
</span>
|
||||
<span className={styles.postMenuBtn}>▶</span>
|
||||
</div>
|
||||
{content && (
|
||||
<div className={styles.postMessage}>
|
||||
<blockquote>
|
||||
<Markdown content={content} />
|
||||
</blockquote>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Post;
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useMemo } from 'react';
|
||||
import useStateString from './use-state-string';
|
||||
import { useSubplebbit, useSubplebbitsStates } from '@plebbit/plebbit-react-hooks';
|
||||
|
||||
const clientHosts: { [key: string]: string } = {};
|
||||
|
||||
const getClientHost = (clientUrl: string): string => {
|
||||
if (!clientHosts[clientUrl]) {
|
||||
try {
|
||||
clientHosts[clientUrl] = new URL(clientUrl).hostname || clientUrl;
|
||||
} catch (e) {
|
||||
clientHosts[clientUrl] = clientUrl;
|
||||
}
|
||||
}
|
||||
return clientHosts[clientUrl];
|
||||
};
|
||||
|
||||
const useFeedStateString = (subplebbitAddresses?: string[]): string | undefined => {
|
||||
// single subplebbit feed state string
|
||||
const subplebbitAddress = subplebbitAddresses?.length === 1 ? subplebbitAddresses[0] : undefined;
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const singleSubplebbitFeedStateString = useStateString(subplebbit);
|
||||
|
||||
// multiple subplebbit feed state string
|
||||
const { states } = useSubplebbitsStates({ subplebbitAddresses });
|
||||
|
||||
const multipleSubplebbitsFeedStateString = useMemo(() => {
|
||||
if (subplebbitAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. Resolving 2 addresses from infura.io, fetching 2 IPNS, 1 IPFS from cloudflare-ipfs.com, ipfs.io
|
||||
let stateString = '';
|
||||
|
||||
if (states['resolving-address']) {
|
||||
const { subplebbitAddresses, clientUrls } = states['resolving-address'];
|
||||
if (subplebbitAddresses.length && clientUrls.length) {
|
||||
stateString += `resolving ${subplebbitAddresses.length} ${subplebbitAddresses.length === 1 ? 'address' : 'addresses'} from ${clientUrls
|
||||
.map(getClientHost)
|
||||
.join(', ')}`;
|
||||
}
|
||||
}
|
||||
|
||||
// find all page client and sub addresses
|
||||
const pagesStatesClientHosts = new Set();
|
||||
const pagesStatesSubplebbitAddresses = new Set();
|
||||
for (const state in states) {
|
||||
if (state.match('page')) {
|
||||
states[state].clientUrls.forEach((clientUrl) => pagesStatesClientHosts.add(getClientHost(clientUrl)));
|
||||
states[state].subplebbitAddresses.forEach((subplebbitAddress) => pagesStatesSubplebbitAddresses.add(subplebbitAddress));
|
||||
}
|
||||
}
|
||||
|
||||
if (states['fetching-ipns'] || states['fetching-ipfs'] || pagesStatesSubplebbitAddresses.size) {
|
||||
// separate 2 different states using ', '
|
||||
if (stateString) {
|
||||
stateString += ', ';
|
||||
}
|
||||
|
||||
// find all client urls
|
||||
const clientHosts = new Set([...pagesStatesClientHosts]);
|
||||
states['fetching-ipns']?.clientUrls.forEach((clientUrl) => clientHosts.add(getClientHost(clientUrl)));
|
||||
states['fetching-ipfs']?.clientUrls.forEach((clientUrl) => clientHosts.add(getClientHost(clientUrl)));
|
||||
|
||||
if (clientHosts.size) {
|
||||
stateString += 'fetching ';
|
||||
if (states['fetching-ipns']) {
|
||||
stateString += `${states['fetching-ipns'].subplebbitAddresses.length} IPNS`;
|
||||
}
|
||||
if (states['fetching-ipfs']) {
|
||||
if (states['fetching-ipns']) {
|
||||
stateString += ', ';
|
||||
}
|
||||
stateString += `${states['fetching-ipfs'].subplebbitAddresses.length} IPFS`;
|
||||
}
|
||||
if (pagesStatesSubplebbitAddresses.size) {
|
||||
if (states['fetching-ipns'] || states['fetching-ipfs']) {
|
||||
stateString += ', ';
|
||||
}
|
||||
stateString += `${pagesStatesSubplebbitAddresses.size} ${pagesStatesSubplebbitAddresses.size === 1 ? 'page' : 'pages'}`;
|
||||
}
|
||||
stateString += ` from ${[...clientHosts].join(', ')}`;
|
||||
}
|
||||
}
|
||||
|
||||
// capitalize first letter
|
||||
stateString = stateString.charAt(0).toUpperCase() + stateString.slice(1);
|
||||
|
||||
// if string is empty, return undefined instead
|
||||
return stateString === '' ? undefined : stateString;
|
||||
}, [states, subplebbitAddress]);
|
||||
|
||||
if (singleSubplebbitFeedStateString) {
|
||||
return singleSubplebbitFeedStateString;
|
||||
}
|
||||
return multipleSubplebbitsFeedStateString;
|
||||
};
|
||||
|
||||
export default useFeedStateString;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import { Comment, useAccountComments } from '@plebbit/plebbit-react-hooks';
|
||||
|
||||
const useRepliesAndAccountReplies = (comment: Comment) => {
|
||||
// filter only the parent cid
|
||||
const filter = useCallback((accountComment: Comment) => accountComment.parentCid === (comment?.cid || 'n/a'), [comment?.cid]);
|
||||
const { accountComments } = useAccountComments({ filter });
|
||||
|
||||
// the account's replies have a delay before getting published, so get them locally from accountComments instead
|
||||
const accountRepliesNotYetPublished = useMemo(() => {
|
||||
const replies = comment?.replies?.pages?.topAll?.comments || [];
|
||||
const replyCids = new Set(replies.map((reply: Comment) => reply?.cid));
|
||||
// filter out the account comments already in comment.replies, so they don't appear twice
|
||||
return accountComments.filter((accountReply) => !replyCids.has(accountReply?.cid));
|
||||
}, [comment?.replies?.pages?.topAll?.comments, accountComments]);
|
||||
|
||||
const repliesAndNotYetPublishedReplies = useMemo(() => {
|
||||
return [
|
||||
// put the author's unpublished replies at the top, latest first (reverse)
|
||||
...accountRepliesNotYetPublished.reverse(),
|
||||
// put the published replies after,
|
||||
...(comment?.replies?.pages?.topAll?.comments || []),
|
||||
];
|
||||
}, [comment?.replies?.pages?.topAll?.comments, accountRepliesNotYetPublished]);
|
||||
|
||||
return repliesAndNotYetPublishedReplies;
|
||||
};
|
||||
|
||||
export default useRepliesAndAccountReplies;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useClientsStates } from '@plebbit/plebbit-react-hooks';
|
||||
|
||||
interface CommentOrSubplebbit {
|
||||
state?: string;
|
||||
publishingState?: string;
|
||||
updatingState?: string;
|
||||
}
|
||||
|
||||
interface States {
|
||||
[key: string]: string[];
|
||||
}
|
||||
|
||||
const clientHosts: { [key: string]: string } = {};
|
||||
|
||||
const getClientHost = (clientUrl: string): string => {
|
||||
if (!clientHosts[clientUrl]) {
|
||||
try {
|
||||
clientHosts[clientUrl] = new URL(clientUrl).hostname || clientUrl;
|
||||
} catch (e) {
|
||||
clientHosts[clientUrl] = clientUrl;
|
||||
}
|
||||
}
|
||||
return clientHosts[clientUrl];
|
||||
};
|
||||
|
||||
const useStateString = (commentOrSubplebbit: CommentOrSubplebbit): string | undefined => {
|
||||
const { states } = useClientsStates({ comment: commentOrSubplebbit }) as { states: States };
|
||||
return useMemo(() => {
|
||||
let stateString: string | undefined = '';
|
||||
|
||||
for (const state in states) {
|
||||
const clientUrls = states[state];
|
||||
const clientHosts = clientUrls.map((clientUrl) => getClientHost(clientUrl));
|
||||
|
||||
if (clientHosts.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stateString) {
|
||||
stateString += ', ';
|
||||
}
|
||||
|
||||
const formattedState = state.replaceAll('-', ' ').replace('ipfs', 'IPFS').replace('ipns', 'IPNS');
|
||||
stateString += `${formattedState} from ${clientHosts.join(', ')}`;
|
||||
}
|
||||
|
||||
if (!stateString && commentOrSubplebbit?.state !== 'succeeded') {
|
||||
if (commentOrSubplebbit?.publishingState && commentOrSubplebbit?.publishingState !== 'stopped' && commentOrSubplebbit?.publishingState !== 'succeeded') {
|
||||
stateString = commentOrSubplebbit.publishingState;
|
||||
} else if (commentOrSubplebbit?.updatingState !== 'stopped' && commentOrSubplebbit?.updatingState !== 'succeeded') {
|
||||
stateString = commentOrSubplebbit.updatingState;
|
||||
}
|
||||
if (stateString) {
|
||||
stateString = stateString.replaceAll('-', ' ').replace('ipfs', 'IPFS').replace('ipns', 'IPNS');
|
||||
}
|
||||
}
|
||||
|
||||
if (stateString) {
|
||||
stateString = stateString.charAt(0).toUpperCase() + stateString.slice(1);
|
||||
}
|
||||
|
||||
return stateString === '' ? undefined : stateString;
|
||||
}, [states, commentOrSubplebbit]);
|
||||
};
|
||||
|
||||
export default useStateString;
|
||||
+11
-1
@@ -7,11 +7,21 @@ body {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, code, form, fieldset, legend, input, textarea, p, blockquote, th, td, iframe {
|
||||
body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, code, form, fieldset, legend, input, textarea, p, th, td, iframe, blockquote {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: var(--hr-border, revert);
|
||||
border-top: var(--hr-border-top, revert);
|
||||
height: var(--hr-height, revert);
|
||||
}
|
||||
|
||||
.greentext {
|
||||
color: var(--post-greentext-color);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.button {
|
||||
text-transform: capitalize;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Comment } from '@plebbit/plebbit-react-hooks';
|
||||
import extName from 'ext-name';
|
||||
import { canEmbed } from '../../components/embed';
|
||||
import memoize from 'memoizee';
|
||||
import { isValidURL } from './url-utils';
|
||||
|
||||
export interface CommentMediaInfo {
|
||||
url: string;
|
||||
type: string;
|
||||
thumbnail?: string;
|
||||
patternThumbnailUrl?: string;
|
||||
}
|
||||
|
||||
export const getHasThumbnail = (commentMediaInfo: CommentMediaInfo | undefined, link: string | undefined): boolean => {
|
||||
const iframeThumbnail = commentMediaInfo?.patternThumbnailUrl || commentMediaInfo?.thumbnail;
|
||||
return link &&
|
||||
commentMediaInfo &&
|
||||
(commentMediaInfo.type === 'image' ||
|
||||
commentMediaInfo.type === 'video' ||
|
||||
commentMediaInfo.type === 'gif' ||
|
||||
(commentMediaInfo.type === 'webpage' && commentMediaInfo.thumbnail) ||
|
||||
(commentMediaInfo.type === 'iframe' && iframeThumbnail))
|
||||
? true
|
||||
: false;
|
||||
};
|
||||
|
||||
const getYouTubeVideoId = (url: URL): string | null => {
|
||||
if (url.host.includes('youtu.be')) {
|
||||
return url.pathname.slice(1);
|
||||
} else if (url.searchParams.has('v')) {
|
||||
return url.searchParams.get('v');
|
||||
} else if (url.host.startsWith('yt.')) {
|
||||
return url.searchParams.get('v');
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getPatternThumbnailUrl = (url: URL): string | undefined => {
|
||||
const videoId = getYouTubeVideoId(url);
|
||||
if (videoId) {
|
||||
return `https://img.youtube.com/vi/${videoId}/0.jpg`;
|
||||
}
|
||||
if (url.host.includes('streamable.com')) {
|
||||
const videoId = url.pathname.split('/')[1];
|
||||
return `https://cdn-cf-east.streamable.com/image/${videoId}.jpg`;
|
||||
}
|
||||
};
|
||||
|
||||
const getLinkMediaInfo = (link: string): CommentMediaInfo | undefined => {
|
||||
if (!isValidURL(link)) {
|
||||
return;
|
||||
}
|
||||
const url = new URL(link);
|
||||
let patternThumbnailUrl: string | undefined;
|
||||
let type: string = 'webpage';
|
||||
let mime: string | undefined;
|
||||
|
||||
// Check for common dynamic image URL patterns
|
||||
if (link.includes('/_next/image?')) {
|
||||
// Next.js Image component
|
||||
return { url: link, type: 'image' };
|
||||
}
|
||||
|
||||
try {
|
||||
mime = extName(url.pathname.slice(url.pathname.lastIndexOf('/') + 1))[0]?.mime;
|
||||
if (mime) {
|
||||
if (mime.startsWith('image')) {
|
||||
type = mime === 'image/gif' ? 'gif' : 'image';
|
||||
} else if (mime.startsWith('video')) {
|
||||
type = 'video';
|
||||
} else if (mime.startsWith('audio')) {
|
||||
type = 'audio';
|
||||
}
|
||||
}
|
||||
|
||||
if (canEmbed(url) || url.host.startsWith('yt.')) {
|
||||
type = 'iframe';
|
||||
patternThumbnailUrl = getPatternThumbnailUrl(url);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return { url: link, type, patternThumbnailUrl };
|
||||
};
|
||||
|
||||
const getCommentMediaInfo = (comment: Comment): CommentMediaInfo | undefined => {
|
||||
if (!comment?.thumbnailUrl && !comment?.link) {
|
||||
return;
|
||||
}
|
||||
const linkInfo = comment.link ? getLinkMediaInfo(comment.link) : undefined;
|
||||
if (linkInfo) {
|
||||
linkInfo.thumbnail = comment.thumbnailUrl || linkInfo.thumbnail;
|
||||
return linkInfo;
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
export const getCommentMediaInfoMemoized = memoize(getCommentMediaInfo, { max: 1000 });
|
||||
export const getLinkMediaInfoMemoized = memoize(getLinkMediaInfo, { max: 1000 });
|
||||
@@ -0,0 +1,33 @@
|
||||
export const getFormattedDate = (commentTimestamp: number) => {
|
||||
if (commentTimestamp === undefined || isNaN(commentTimestamp)) {
|
||||
return '';
|
||||
}
|
||||
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
||||
const string = new Intl.DateTimeFormat(locale, {
|
||||
hour12: false,
|
||||
year: '2-digit',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
weekday: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
}).format(new Date(commentTimestamp * 1000));
|
||||
if (locale.startsWith('ar')) {
|
||||
return string;
|
||||
}
|
||||
const items = string.split(/,* /);
|
||||
if (items.length === 3) {
|
||||
const itemIsNumber = [items[0][0].match(/[0-9]/), items[1][0].match(/[0-9]/)];
|
||||
if (itemIsNumber[0] && itemIsNumber[1]) {
|
||||
return `${items[0]}(${items[2]})${items[1]}`;
|
||||
}
|
||||
if (itemIsNumber[0] && !itemIsNumber[1]) {
|
||||
return `${items[0]}(${items[1]})${items[2]}`;
|
||||
}
|
||||
if (!itemIsNumber[0] && itemIsNumber[1]) {
|
||||
return `${items[1]}(${items[0]})${items[2]}`;
|
||||
}
|
||||
}
|
||||
return string;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
export const getHostname = (url: string) => {
|
||||
try {
|
||||
return new URL(url).hostname.replace(/^www\./, '');
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const isValidURL = (url: string) => {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const copyShareLinkToClipboard = (subplebbitAddress: string, cid: string) => {
|
||||
const shareLink = `https://pleb.bz/p/${subplebbitAddress}/c/${cid}`;
|
||||
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(shareLink);
|
||||
} else {
|
||||
alert('Your browser does not support clipboard API');
|
||||
}
|
||||
};
|
||||
+115
-39
@@ -8,6 +8,10 @@
|
||||
/* board nav */
|
||||
--boardnav-font-size: 9pt;
|
||||
--boardnav-separator-color: #b86;
|
||||
--boardnav-desktop-link-text-color: #800;
|
||||
--boardnav-desktop-link-text-color-hover: #e00;
|
||||
--boardnav-desktop-link-text-decoration: none;
|
||||
--boardnav-desktop-link-text-decoration-hover: underline;
|
||||
--boardnav-mobile-background-color: #f0e0d6;
|
||||
--boardnav-mobile-border-bottom: 2px solid #d9c5b7;
|
||||
--boardnav-mobile-font-size: 10px;
|
||||
@@ -46,17 +50,30 @@
|
||||
--homepage-box-bar-text-color: #800;
|
||||
--homepage-box-bar-text-color-hover: #c63;
|
||||
--homepage-box-bar-background-color: #fca;
|
||||
--homepage-infobox-link-text-color: #00f;
|
||||
--homepage-infobox-link-text-decoration: underline;
|
||||
--homepage-box-link-text-color: #800;
|
||||
--homepage-box-link-text-color-hover: #e00;
|
||||
--homepage-box-link-text-decoration: none;
|
||||
--homepage-box-link-text-decoration-hover: underline;
|
||||
|
||||
/* footer */
|
||||
--footer-item-background-color-desktop: #fed;
|
||||
|
||||
/* links */
|
||||
--link-external-text-color: #00f;
|
||||
--link-external-text-decoration: underline;
|
||||
--link-internal-text-color: #800;
|
||||
--link-internal-text-color-hover: #e00;
|
||||
--link-internal-text-decoration: none;
|
||||
--link-internal-text-decoration-hover: underline;
|
||||
--post-link-text-color: #00e;
|
||||
--post-link-text-decoration: underline;
|
||||
--post-link-text-color-hover: red;
|
||||
--post-link-text-decoration-hover: underline;
|
||||
|
||||
/* post */
|
||||
--post-subject-text-color: #cc1105;
|
||||
--post-subject-font-weight: 700;
|
||||
--post-name-text-color: #117743;
|
||||
--post-name-font-weight: 700;
|
||||
--post-menu-button-color: #000080;
|
||||
--post-menu-button-color-hover: red;
|
||||
--post-greentext-color: #789922;
|
||||
|
||||
/* post form */
|
||||
--post-form-toggle-font-size: 22px;
|
||||
@@ -75,6 +92,10 @@
|
||||
/* board nav */
|
||||
--boardnav-font-size: 9pt;
|
||||
--boardnav-separator-color: #89a;
|
||||
--boardnav-desktop-link-text-color: #34345c;
|
||||
--boardnav-desktop-link-text-color-hover: #d00;
|
||||
--boardnav-desktop-link-text-decoration: none;
|
||||
--boardnav-desktop-link-text-decoration-hover: none;
|
||||
--boardnav-mobile-background-color: #d6daf0;
|
||||
--boardnav-mobile-border-bottom: 2px solid #b7c5d9;
|
||||
--boardnav-mobile-font-size: 10px;
|
||||
@@ -96,6 +117,12 @@
|
||||
--homepage-box-bar-text-color: #000;
|
||||
--homepage-box-bar-text-color-hover: #fff;
|
||||
--homepage-box-bar-background-color: #98e;
|
||||
--homepage-infobox-link-text-color: #00f;
|
||||
--homepage-infobox-link-text-decoration: underline;
|
||||
--homepage-box-link-text-color: #34345c;
|
||||
--homepage-box-link-text-color-hover: #d00;
|
||||
--homepage-box-link-text-decoration: none;
|
||||
--homepage-box-link-text-decoration-hover: underline;
|
||||
|
||||
/* desktop buttons */
|
||||
--button-desktop-text-color: #34345c;
|
||||
@@ -117,12 +144,19 @@
|
||||
--hr-border-top: 1px solid #b7c5d9;
|
||||
|
||||
/* links */
|
||||
--link-external-text-color: #00f;
|
||||
--link-external-text-decoration: underline;
|
||||
--link-internal-text-color: #34345c;
|
||||
--link-internal-text-color-hover: #d00;
|
||||
--link-internal-text-decoration: none;
|
||||
--link-internal-text-decoration-hover: underline;
|
||||
--post-link-text-color: #34345c;
|
||||
--post-link-text-decoration: underline;
|
||||
--post-link-text-color-hover: #d00;
|
||||
--post-link-text-decoration-hover: underline;
|
||||
|
||||
/* post */
|
||||
--post-subject-text-color: #0f0c5d;
|
||||
--post-subject-font-weight: 700;
|
||||
--post-name-text-color: #117743;
|
||||
--post-name-font-weight: 700;
|
||||
--post-menu-button-color: #34345c;
|
||||
--post-menu-button-color-hover: #d00;
|
||||
--post-greentext-color: #789922;
|
||||
|
||||
/* post form */
|
||||
--post-form-toggle-font-size: 22px;
|
||||
@@ -137,19 +171,13 @@
|
||||
--body-font-family: times new roman, serif;
|
||||
--body-font-size: 12pt;
|
||||
|
||||
/* homepage */
|
||||
--homepage-infobox-text-color: #000;
|
||||
--homepage-infobox-bar-text-color: #fff;
|
||||
--homepage-infobox-bar-background-color: #800;
|
||||
--homepage-box-background-color: #fff;
|
||||
--homepage-box-border-color: #800;
|
||||
--homepage-box-bar-text-color: #800;
|
||||
--homepage-box-bar-text-color-hover: #c63;
|
||||
--homepage-box-bar-background-color: #fca;
|
||||
|
||||
/* board nav */
|
||||
--boardnav-font-size: 11pt;
|
||||
--boardnav-separator-color: none;
|
||||
--boardnav-desktop-link-text-color: #00e;
|
||||
--boardnav-desktop-link-text-color-hover: red;
|
||||
--boardnav-desktop-link-text-decoration: underline;
|
||||
--boardnav-desktop-link-text-decoration-hover: underline;
|
||||
--boardnav-mobile-background-color: #f0e0d6;
|
||||
--boardnav-mobile-border-bottom: 2px solid #d9c5b7;
|
||||
--boardnav-mobile-font-size: 11pt;
|
||||
@@ -176,12 +204,19 @@
|
||||
--footer-item-background-color-desktop: #fed;
|
||||
|
||||
/* links */
|
||||
--link-external-text-color: #00f;
|
||||
--link-external-text-decoration: underline;
|
||||
--link-internal-text-color: #00e;
|
||||
--link-internal-text-color-hover: #e00;
|
||||
--link-internal-text-decoration: underline;
|
||||
--link-internal-text-decoration-hover: underline;
|
||||
--post-link-text-color: #00e;
|
||||
--post-link-text-decoration: underline;
|
||||
--post-link-text-color-hover: red;
|
||||
--post-link-text-decoration-hover: underline;
|
||||
|
||||
/* post */
|
||||
--post-subject-text-color: #cc1105;
|
||||
--post-subject-font-weight: 700;
|
||||
--post-name-text-color: #117743;
|
||||
--post-name-font-weight: 700;
|
||||
--post-menu-button-color: #000080;
|
||||
--post-menu-button-color-hover: red;
|
||||
--post-greentext-color: #789922;
|
||||
|
||||
/* post form */
|
||||
--post-form-toggle-font-size: 22px;
|
||||
@@ -193,13 +228,17 @@
|
||||
--banner-image-border: 1px solid #000;
|
||||
--board-title-font-size: 32px;
|
||||
--board-title-text-color: #af0a0f;
|
||||
--board-title-font-family: Tahoma, sans-serif;
|
||||
--board-title-font-family: times new roman, serif;
|
||||
--board-address-font-size: 10pt;
|
||||
--board-address-text-color: #af0a0f;
|
||||
|
||||
/* board nav */
|
||||
--boardnav-font-size: 11pt;
|
||||
--boardnav-separator-color: none;
|
||||
--boardnav-desktop-link-text-color: #34345c;
|
||||
--boardnav-desktop-link-text-color-hover: #d00;
|
||||
--boardnav-desktop-link-text-decoration: underline;
|
||||
--boardnav-desktop-link-text-decoration-hover: underline;
|
||||
--boardnav-mobile-background-color: #d6daf0;
|
||||
--boardnav-mobile-border-bottom: 2px solid #b7c5d9;
|
||||
--boardnav-mobile-font-size: 11pt;
|
||||
@@ -239,13 +278,20 @@
|
||||
--footer-item-background-color-desktop: #eef2ff;
|
||||
|
||||
/* links */
|
||||
--link-external-text-color: #00f;
|
||||
--link-external-text-decoration: underline;
|
||||
--link-internal-text-color: #34345c;
|
||||
--link-internal-text-color-hover: #d00;
|
||||
--link-internal-text-decoration: underline;
|
||||
--link-internal-text-decoration-hover: underline;
|
||||
--post-link-text-color: #34345c;
|
||||
--post-link-text-decoration: underline;
|
||||
--post-link-text-color-hover: #d00;
|
||||
--post-link-text-decoration-hover: underline;
|
||||
|
||||
/* post */
|
||||
--post-subject-text-color: #0f0c5d;
|
||||
--post-subject-font-weight: 700;
|
||||
--post-name-text-color: #117743;
|
||||
--post-name-font-weight: 700;
|
||||
--post-menu-button-color: #34345c;
|
||||
--post-menu-button-color-hover: #d00;
|
||||
--post-greentext-color: #789922;
|
||||
|
||||
/* post form */
|
||||
--post-form-toggle-font-size: 22px;
|
||||
--post-form-toggle-font-weight: 700;
|
||||
@@ -272,6 +318,10 @@
|
||||
/* board nav */
|
||||
--boardnav-font-size: 9pt;
|
||||
--boardnav-separator-color: #c5c8c6;
|
||||
--boardnav-desktop-link-text-color: #81a2be;
|
||||
--boardnav-desktop-link-text-color-hover: #5f89ac;
|
||||
--boardnav-desktop-link-text-decoration: none;
|
||||
--boardnav-desktop-link-text-decoration-hover: none;
|
||||
--boardnav-mobile-background-color: #1d1f21;
|
||||
--boardnav-mobile-border-bottom: 2px solid #282a2e;
|
||||
--boardnav-mobile-font-size: 10px;
|
||||
@@ -300,8 +350,19 @@
|
||||
--hr-height: 0px;
|
||||
|
||||
/* links */
|
||||
--link-internal-text-color: #81a2be;
|
||||
--link-internal-text-color-hover: #5f89ac;
|
||||
--post-link-text-color: #81a2be;
|
||||
--post-link-text-decoration: underline;
|
||||
--post-link-text-color-hover: #5f89ac;
|
||||
--post-link-text-decoration-hover: underline;
|
||||
|
||||
/* post */
|
||||
--post-subject-text-color: #b294bb;
|
||||
--post-subject-font-weight: 700;
|
||||
--post-name-text-color: #c5c8c6;
|
||||
--post-name-font-weight: 700;
|
||||
--post-menu-button-color: #5F89AC;
|
||||
--post-menu-button-color-hover: #81a2be;
|
||||
--post-greentext-color: #b5bd68;
|
||||
|
||||
/* post form */
|
||||
--post-form-toggle-font-size: 22px;
|
||||
@@ -324,6 +385,10 @@
|
||||
/* board nav */
|
||||
--boardnav-font-size: 9pt;
|
||||
--boardnav-separator-color: #333;
|
||||
--boardnav-desktop-link-text-color: #f60;
|
||||
--boardnav-desktop-link-text-color-hover: #f30;
|
||||
--boardnav-desktop-link-text-decoration: none;
|
||||
--boardnav-desktop-link-text-decoration-hover: none;
|
||||
--boardnav-mobile-background-color: #ddd;
|
||||
--boardnav-mobile-border-bottom: 2px solid #ccc;
|
||||
--boardnav-mobile-font-size: 10px;
|
||||
@@ -352,8 +417,19 @@
|
||||
--hr-height: 0px;
|
||||
|
||||
/* links */
|
||||
--link-internal-text-color: #f60;
|
||||
--link-internal-text-color-hover: #f30;
|
||||
--post-link-text-color: #f60;
|
||||
--post-link-text-decoration: underline;
|
||||
--post-link-text-color-hover: #f30;
|
||||
--post-link-text-decoration-hover: underline;
|
||||
|
||||
/* post */
|
||||
--post-subject-text-color: #111;
|
||||
--post-subject-font-weight: 700;
|
||||
--post-name-text-color: #004a99;
|
||||
--post-name-font-weight: 700;
|
||||
--post-menu-button-color: #FF6600;
|
||||
--post-menu-button-color-hover: #FF3300;
|
||||
--post-greentext-color: #789922;
|
||||
|
||||
/* post form */
|
||||
--post-form-toggle-font-size: 22px;
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
}
|
||||
|
||||
.boxContent a, .boxContent a:visited{
|
||||
color: var(--link-external-text-color);
|
||||
color: var(--homepage-infobox-link-text-color);
|
||||
}
|
||||
|
||||
.searchBar {
|
||||
@@ -116,13 +116,13 @@
|
||||
|
||||
.list a {
|
||||
display: inline-block;
|
||||
color: var(--link-internal-text-color);
|
||||
text-decoration: var(--link-internal-text-decoration);
|
||||
color: var(--homepage-box-link-text-color);
|
||||
text-decoration: var(--homepage-box-link-text-decoration);
|
||||
}
|
||||
|
||||
.list a:hover {
|
||||
color: var(--link-internal-text-color-hover);
|
||||
text-decoration: var(--link-internal-text-decoration-hover);
|
||||
color: var(--homepage-box-link-text-color-hover);
|
||||
text-decoration: var(--homepage-box-link-text-decoration-hover);
|
||||
}
|
||||
|
||||
.boardsBoxContent {
|
||||
@@ -175,7 +175,7 @@
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: var(--link-external-text-color);
|
||||
color: var(--homepage-infobox-link-text-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.version {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content select {
|
||||
background-color: var(--select-background-color, revert);
|
||||
border: var(--select-border, revert);
|
||||
|
||||
@@ -2,6 +2,9 @@ import { useTranslation } from 'react-i18next';
|
||||
import useTheme from '../../hooks/use-theme';
|
||||
import styles from './settings.module.css';
|
||||
import { Link } from 'react-router-dom';
|
||||
import packageJson from '../../../package.json';
|
||||
const isElectron = window.isElectron === true;
|
||||
const commitRef = process.env.REACT_APP_COMMIT_REF;
|
||||
|
||||
// TODO: remove theme and languages selectors for debugging
|
||||
const ThemeSettings = () => {
|
||||
@@ -46,6 +49,26 @@ const LanguageSettings = () => {
|
||||
const Settings = () => {
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.version}>
|
||||
<a href={`https://github.com/plebbit/plebchan/releases/tag/v${packageJson.version}`} target='_blank' rel='noopener noreferrer'>
|
||||
v{packageJson.version}
|
||||
</a>
|
||||
{isElectron && (
|
||||
<a className={styles.fullNodeStats} href='http://localhost:5001/webui/' target='_blank' rel='noreferrer'>
|
||||
node stats
|
||||
</a>
|
||||
)}
|
||||
{commitRef && (
|
||||
<>
|
||||
{' '}
|
||||
(
|
||||
<a href={`https://github.com/plebbit/plebchan/commit/${commitRef}`} target='_blank' rel='noopener noreferrer'>
|
||||
{commitRef.slice(0, 7)}
|
||||
</a>
|
||||
)
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Link to='/'>Home</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,72 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useFeed, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styles from './subplebbit.module.css';
|
||||
import useFeedStateString from '../../hooks/use-feed-state-string';
|
||||
import LoadingEllipsis from '../../components/loading-ellipsis';
|
||||
import Post from '../../components/post';
|
||||
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||
|
||||
const Subplebbit = () => {
|
||||
return <div className={styles.content}></div>;
|
||||
const { t } = useTranslation();
|
||||
const { subplebbitAddress } = useParams<{ subplebbitAddress: string }>();
|
||||
const subplebbitAddresses = useMemo(() => [subplebbitAddress], [subplebbitAddress]) as string[];
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const { createdAt, description, roles, rules, shortAddress, state, title, updatedAt, settings } = subplebbit || {};
|
||||
const sortType = 'active';
|
||||
const { feed, hasMore, loadMore } = useFeed({ subplebbitAddresses, sortType });
|
||||
const loadingStateString = useFeedStateString(subplebbitAddresses) || t('loading');
|
||||
|
||||
const loadingString = <div className={styles.stateString}>{state === 'failed' ? state : <LoadingEllipsis string={loadingStateString} />}</div>;
|
||||
|
||||
useEffect(() => {
|
||||
document.title = title ? title : shortAddress;
|
||||
}, [title, shortAddress]);
|
||||
|
||||
const Footer = () => {
|
||||
let footerContent;
|
||||
if (feed.length === 0) {
|
||||
footerContent = t('no_posts');
|
||||
}
|
||||
if (hasMore || subplebbitAddresses.length === 0) {
|
||||
footerContent = loadingString;
|
||||
}
|
||||
return <div className={styles.footer}>{footerContent}</div>;
|
||||
};
|
||||
|
||||
// save the last Virtuoso state to restore it when navigating back
|
||||
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
|
||||
useEffect(() => {
|
||||
const setLastVirtuosoState = () => {
|
||||
virtuosoRef.current?.getState((snapshot: StateSnapshot) => {
|
||||
if (snapshot?.ranges?.length) {
|
||||
lastVirtuosoStates[subplebbitAddress + sortType] = snapshot;
|
||||
}
|
||||
});
|
||||
};
|
||||
window.addEventListener('scroll', setLastVirtuosoState);
|
||||
return () => window.removeEventListener('scroll', setLastVirtuosoState);
|
||||
}, [subplebbitAddress, sortType]);
|
||||
|
||||
const lastVirtuosoState = lastVirtuosoStates?.[subplebbitAddress + sortType];
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<Virtuoso
|
||||
increaseViewportBy={{ bottom: 1200, top: 600 }}
|
||||
totalCount={feed?.length || 0}
|
||||
data={feed}
|
||||
itemContent={(index, post) => <Post index={index} post={post} />}
|
||||
useWindowScroll={true}
|
||||
components={{ Footer }}
|
||||
endReached={loadMore}
|
||||
ref={virtuosoRef}
|
||||
restoreStateFrom={lastVirtuosoState}
|
||||
initialScrollTop={lastVirtuosoState?.scrollTop}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Subplebbit;
|
||||
|
||||
Reference in New Issue
Block a user