mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
@@ -153,5 +153,8 @@
|
||||
"commitizen": {
|
||||
"path": "./node_modules/cz-conventional-changelog"
|
||||
}
|
||||
},
|
||||
"resolutions": {
|
||||
"skia-canvas": "1.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "GIF متحرك",
|
||||
"hide_avatars": "إخفاء الصور الرمزية",
|
||||
"anon_mode": "وضع مجهول",
|
||||
"anon_mode_description": "يتم استخدام معرف مستخدم مختلف تلقائيًا في كل موضوع",
|
||||
"anon_mode_description": "استخدم معرف مستخدم مختلف في كل موضوع",
|
||||
"pending": "قيد الانتظار",
|
||||
"failed": "فشل",
|
||||
"add": "أضف",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "অ্যানিমেটেড GIF",
|
||||
"hide_avatars": "অ্যাভাটার লুকান",
|
||||
"anon_mode": "অজ্ঞাত মোড",
|
||||
"anon_mode_description": "প্রতিটি থ্রেডে স্বয়ংক্রিয়ভাবে একটি ভিন্ন ব্যবহারকারীর আইডি ব্যবহার করুন",
|
||||
"anon_mode_description": "প্রতিটি থ্রেডে একটি ভিন্ন ব্যবহারকারী আইডি ব্যবহার করুন",
|
||||
"pending": "অপেক্ষমাণ",
|
||||
"failed": "ব্যর্থ",
|
||||
"add": "যোগ করুন",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "Animovaný GIF",
|
||||
"hide_avatars": "Skrýt avatary",
|
||||
"anon_mode": "Anonymní režim",
|
||||
"anon_mode_description": "Automaticky používejte jiný uživatelský ID v každém vlákně",
|
||||
"anon_mode_description": "Použijte různé uživatelské ID v každém vlákně",
|
||||
"pending": "Čeká na schválení",
|
||||
"failed": "Nepodařilo se",
|
||||
"add": "Přidat",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "Animeret GIF",
|
||||
"hide_avatars": "Skjul avatarer",
|
||||
"anon_mode": "Anonim tilstand",
|
||||
"anon_mode_description": "Brug automatisk en anden bruger-ID i hver tråd",
|
||||
"anon_mode_description": "Brug et forskelligt bruger-ID i hver tråd",
|
||||
"pending": "Afventer",
|
||||
"failed": "Mislykkedes",
|
||||
"add": "Tilføj",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "Animiertes GIF",
|
||||
"hide_avatars": "Avatare ausblenden",
|
||||
"anon_mode": "Anonymer Modus",
|
||||
"anon_mode_description": "Verwenden Sie automatisch eine andere Benutzer-ID in jedem Thread",
|
||||
"anon_mode_description": "Verwenden Sie eine andere Benutzer-ID in jedem Thread",
|
||||
"pending": "Ausstehend",
|
||||
"failed": "Fehlgeschlagen",
|
||||
"add": "Hinzufügen",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "Κινούμενο GIF",
|
||||
"hide_avatars": "Απόκρυψη avatars",
|
||||
"anon_mode": "Λειτουργία ανωνυμίας",
|
||||
"anon_mode_description": "Χρησιμοποιήστε αυτόματα διαφορετικό αναγνωριστικό χρήστη σε κάθε νήμα",
|
||||
"anon_mode_description": "Χρησιμοποιήστε ένα διαφορετικό αναγνωριστικό χρήστη σε κάθε θέμα",
|
||||
"pending": "Σε εκκρεμότητα",
|
||||
"failed": "Απέτυχε",
|
||||
"add": "Προσθήκη",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "Animated GIF",
|
||||
"hide_avatars": "Hide avatars",
|
||||
"anon_mode": "Anon mode",
|
||||
"anon_mode_description": "Automatically use a different user ID in each thread",
|
||||
"anon_mode_description": "Use a different user ID in each thread",
|
||||
"pending": "Pending",
|
||||
"failed": "Failed",
|
||||
"add": "Add",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "GIF Animado",
|
||||
"hide_avatars": "Ocultar avatares",
|
||||
"anon_mode": "Modo anónimo",
|
||||
"anon_mode_description": "Utiliza automáticamente un ID de usuario diferente en cada hilo",
|
||||
"anon_mode_description": "Usa un ID de usuario diferente en cada hilo",
|
||||
"pending": "Pendiente",
|
||||
"failed": "Fallido",
|
||||
"add": "Añadir",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "GIF متحرک",
|
||||
"hide_avatars": "پنهان کردن آواتارها",
|
||||
"anon_mode": "حالت ناشناس",
|
||||
"anon_mode_description": "به طور خودکار از یک شناسه کاربری متفاوت در هر موضوع استفاده کنید",
|
||||
"anon_mode_description": "در هر رشته یک شناسه کاربری مختلف استفاده کنید",
|
||||
"pending": "در انتظار",
|
||||
"failed": "ناموفق",
|
||||
"add": "اضافه کردن",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "Animoitu GIF",
|
||||
"hide_avatars": "Piilota avatarit",
|
||||
"anon_mode": "Anonyymi tila",
|
||||
"anon_mode_description": "Käytä automaattisesti eri käyttäjä-ID:tä jokaisessa keskustelussa",
|
||||
"anon_mode_description": "Käytä eri käyttäjä-ID:tä jokaisessa ketjussa",
|
||||
"pending": "Odottaa",
|
||||
"failed": "Epäonnistui",
|
||||
"add": "Lisää",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "Animated GIF",
|
||||
"hide_avatars": "Itago ang mga avatar",
|
||||
"anon_mode": "Anong mode",
|
||||
"anon_mode_description": "Awtomatikong gamitin ang ibang user ID sa bawat thread",
|
||||
"anon_mode_description": "Gumamit ng iba't ibang user ID sa bawat thread",
|
||||
"pending": "Naghihintay",
|
||||
"failed": "Nabigo",
|
||||
"add": "Idagdag",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "GIF Animé",
|
||||
"hide_avatars": "Masquer les avatars",
|
||||
"anon_mode": "Mode anonyme",
|
||||
"anon_mode_description": "Utilisez automatiquement un identifiant utilisateur différent dans chaque fil",
|
||||
"anon_mode_description": "Utilisez un ID utilisateur différent dans chaque fil",
|
||||
"pending": "En attente",
|
||||
"failed": "Échoué",
|
||||
"add": "Ajouter",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "GIF מונפש",
|
||||
"hide_avatars": "הסתר את התמונות האישיות",
|
||||
"anon_mode": "מצב אנונימי",
|
||||
"anon_mode_description": "השתמש אוטומטית במזהה משתמש שונה בכל שרשור",
|
||||
"anon_mode_description": "השתמש במזהה משתמש שונה בכל שרשור",
|
||||
"pending": "ממתין",
|
||||
"failed": "נכשל",
|
||||
"add": "הוסף",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "एनिमेटेड GIF",
|
||||
"hide_avatars": "अवतार छिपाएँ",
|
||||
"anon_mode": "अनाम मोड",
|
||||
"anon_mode_description": "प्रत्येक थ्रेड में स्वचालित रूप से एक अलग उपयोगकर्ता आईडी का उपयोग करें",
|
||||
"anon_mode_description": "प्रत्येक थ्रेड में एक अलग उपयोगकर्ता आईडी का उपयोग करें",
|
||||
"pending": "लंबित",
|
||||
"failed": "असफल",
|
||||
"add": "जोड़ें",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "Animált GIF",
|
||||
"hide_avatars": "Avatarok elrejtése",
|
||||
"anon_mode": "Anonim mód",
|
||||
"anon_mode_description": "Automatikusan használjon más felhasználói azonosítót minden szálban",
|
||||
"anon_mode_description": "Használjon különböző felhasználói azonosítót minden szálban",
|
||||
"pending": "Függőben",
|
||||
"failed": "Sikertelen",
|
||||
"add": "Hozzáad",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "GIF Animasi",
|
||||
"hide_avatars": "Sembunyikan avatar",
|
||||
"anon_mode": "Mode anonim",
|
||||
"anon_mode_description": "Secara otomatis gunakan ID pengguna yang berbeda di setiap utas",
|
||||
"anon_mode_description": "Gunakan ID pengguna yang berbeda di setiap thread",
|
||||
"pending": "Tertunda",
|
||||
"failed": "Gagal",
|
||||
"add": "Tambahkan",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "GIF Animata",
|
||||
"hide_avatars": "Nascondi avatar",
|
||||
"anon_mode": "Modalità anonima",
|
||||
"anon_mode_description": "Usa automaticamente un ID utente diverso in ogni thread",
|
||||
"anon_mode_description": "Usa un ID utente diverso per ogni thread",
|
||||
"pending": "In attesa",
|
||||
"failed": "Fallito",
|
||||
"add": "Aggiungi",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "アニメーションGIF",
|
||||
"hide_avatars": "アバターを隠す",
|
||||
"anon_mode": "匿名モード",
|
||||
"anon_mode_description": "各スレッドで自動的に異なるユーザーIDを使用します",
|
||||
"anon_mode_description": "各スレッドで異なるユーザーIDを使用する",
|
||||
"pending": "保留中",
|
||||
"failed": "失敗",
|
||||
"add": "追加",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "애니메이션 GIF",
|
||||
"hide_avatars": "아바타 숨기기",
|
||||
"anon_mode": "익명 모드",
|
||||
"anon_mode_description": "각 스레드에서 자동으로 다른 사용자 ID를 사용합니다",
|
||||
"anon_mode_description": "각 스레드에서 다른 사용자 ID를 사용하세요",
|
||||
"pending": "대기 중",
|
||||
"failed": "실패",
|
||||
"add": "추가",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "एनिमेटेड GIF",
|
||||
"hide_avatars": "अवतार लपवा",
|
||||
"anon_mode": "अनाम मोड",
|
||||
"anon_mode_description": "प्रत्येक थ्रेडमध्ये स्वयंचलितपणे वेगळा वापरकर्ता ID वापरा",
|
||||
"anon_mode_description": "प्रत्येक थ्रेडमध्ये एक वेगवेगळा वापरकर्ता ID वापरा",
|
||||
"pending": "प्रलंबित",
|
||||
"failed": "असफल",
|
||||
"add": "अॅड करा",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "Geanimeerde GIF",
|
||||
"hide_avatars": "Avatars verbergen",
|
||||
"anon_mode": "Anoniem modus",
|
||||
"anon_mode_description": "Gebruik automatisch een ander gebruikers-ID in elk thread",
|
||||
"anon_mode_description": "Gebruik een ander gebruikers-ID in elk draad",
|
||||
"pending": "In afwachting",
|
||||
"failed": "Mislukt",
|
||||
"add": "Toevoegen",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "Animasjons GIF",
|
||||
"hide_avatars": "Skjul avatarer",
|
||||
"anon_mode": "Anonim modus",
|
||||
"anon_mode_description": "Bruk automatisk en annen bruker-ID i hver tråd",
|
||||
"anon_mode_description": "Bruk et annet bruker-ID i hver tråd",
|
||||
"pending": "Venter",
|
||||
"failed": "Feilet",
|
||||
"add": "Legg til",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "Animowany GIF",
|
||||
"hide_avatars": "Ukryj awatary",
|
||||
"anon_mode": "Tryb anonimowy",
|
||||
"anon_mode_description": "Automatycznie używaj innego identyfikatora użytkownika w każdym wątku",
|
||||
"anon_mode_description": "Użyj innego identyfikatora użytkownika w każdym wątku",
|
||||
"pending": "Oczekujące",
|
||||
"failed": "Nieudane",
|
||||
"add": "Dodaj",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "GIF Animado",
|
||||
"hide_avatars": "Ocultar avatares",
|
||||
"anon_mode": "Modo anônimo",
|
||||
"anon_mode_description": "Use automaticamente um ID de usuário diferente em cada thread",
|
||||
"anon_mode_description": "Use um ID de usuário diferente em cada thread",
|
||||
"pending": "Pendente",
|
||||
"failed": "Falhou",
|
||||
"add": "Adicionar",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "GIF Animat",
|
||||
"hide_avatars": "Ascunde avatarele",
|
||||
"anon_mode": "Mod anonim",
|
||||
"anon_mode_description": "Utilizați automat un ID de utilizator diferit în fiecare fir",
|
||||
"anon_mode_description": "Utilizați un ID de utilizator diferit în fiecare fir",
|
||||
"pending": "În așteptare",
|
||||
"failed": "Eșuat",
|
||||
"add": "Adaugă",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "Анимированный GIF",
|
||||
"hide_avatars": "Скрыть аватары",
|
||||
"anon_mode": "Анонимный режим",
|
||||
"anon_mode_description": "Автоматически используйте другой идентификатор пользователя в каждом потоке",
|
||||
"anon_mode_description": "Используйте другой идентификатор пользователя в каждом потоке",
|
||||
"pending": "В ожидании",
|
||||
"failed": "Не удалось",
|
||||
"add": "Добавить",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "GIF i Animuar",
|
||||
"hide_avatars": "Fsheh avatarët",
|
||||
"anon_mode": "Mënyra anonime",
|
||||
"anon_mode_description": "Përdorni automatikisht një ID përdoruesi të ndryshëm në çdo temë",
|
||||
"anon_mode_description": "Përdorni një ID të ndryshme përdoruesi në çdo temë",
|
||||
"pending": "Në pritje",
|
||||
"failed": "Dështuar",
|
||||
"add": "Shto",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "Animerad GIF",
|
||||
"hide_avatars": "Dölj avatarer",
|
||||
"anon_mode": "Anonymt läge",
|
||||
"anon_mode_description": "Använd automatiskt ett annat användar-ID i varje tråd",
|
||||
"anon_mode_description": "Använd ett annat användar-ID i varje tråd",
|
||||
"pending": "Väntande",
|
||||
"failed": "Misslyckades",
|
||||
"add": "Lägg till",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "అనిమేటెడ్ GIF",
|
||||
"hide_avatars": "అవతార్లను దాచు",
|
||||
"anon_mode": "అనానిమస్ మోడ్",
|
||||
"anon_mode_description": "ప్రతి థ్రెడ్లో ఆటోమేటిక్గా ఒక వేరు వినియోగదారుడి ID ఉపయోగించండి",
|
||||
"anon_mode_description": "ప్రతి థ్రెడ్లో భిన్నమైన యూజర్ ID ఉపయోగించండి",
|
||||
"pending": "అనుమతించబడాలి",
|
||||
"failed": "విజయవంతం కాలేదు",
|
||||
"add": "చేర్చండి",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "GIF ที่เคลื่อนไหว",
|
||||
"hide_avatars": "ซ่อนอวาตาร์",
|
||||
"anon_mode": "โหมดนิรนาม",
|
||||
"anon_mode_description": "ใช้ ID ผู้ใช้ที่แตกต่างกันโดยอัตโนมัติในแต่ละเธรด",
|
||||
"anon_mode_description": "ใช้ ID ผู้ใช้ที่แตกต่างกันในแต่ละกระทู้",
|
||||
"pending": "รอดำเนินการ",
|
||||
"failed": "ล้มเหลว",
|
||||
"add": "เพิ่ม",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "Animasyonlu GIF",
|
||||
"hide_avatars": "Avatarları gizle",
|
||||
"anon_mode": "Gizli mod",
|
||||
"anon_mode_description": "Her iplikte otomatik olarak farklı bir kullanıcı ID'si kullanın",
|
||||
"anon_mode_description": "Her başlıkta farklı bir kullanıcı ID'si kullanın",
|
||||
"pending": "Beklemede",
|
||||
"failed": "Başarısız",
|
||||
"add": "Ekle",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "Анімований GIF",
|
||||
"hide_avatars": "Сховати аватари",
|
||||
"anon_mode": "Режим анонімності",
|
||||
"anon_mode_description": "Автоматично використовуйте інший ідентифікатор користувача в кожній гілці",
|
||||
"anon_mode_description": "Використовуйте інший ідентифікатор користувача в кожній темі",
|
||||
"pending": "Очікує",
|
||||
"failed": "Не вдалося",
|
||||
"add": "Додати",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "اینیمیٹڈ GIF",
|
||||
"hide_avatars": "اوٹار چھپائیں",
|
||||
"anon_mode": "خفیہ موڈ",
|
||||
"anon_mode_description": "ہر تھریڈ میں خود بخود مختلف صارف آئی ڈی کا استعمال کریں",
|
||||
"anon_mode_description": "ہر تھریڈ میں ایک مختلف صارف آئی ڈی استعمال کریں",
|
||||
"pending": "منتظر",
|
||||
"failed": "ناکام",
|
||||
"add": "شامل کریں",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "GIF Hoạt Hình",
|
||||
"hide_avatars": "Ẩn avatar",
|
||||
"anon_mode": "Chế độ ẩn danh",
|
||||
"anon_mode_description": "Tự động sử dụng ID người dùng khác nhau trong mỗi chủ đề",
|
||||
"anon_mode_description": "Sử dụng ID người dùng khác nhau trong từng chủ đề",
|
||||
"pending": "Đang chờ",
|
||||
"failed": "Thất bại",
|
||||
"add": "Thêm",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"animated_gif": "动画GIF",
|
||||
"hide_avatars": "隐藏头像",
|
||||
"anon_mode": "匿名模式",
|
||||
"anon_mode_description": "在每个线程中自动使用不同的用户 ID",
|
||||
"anon_mode_description": "在每个线程中使用不同的用户 ID",
|
||||
"pending": "待处理",
|
||||
"failed": "失败",
|
||||
"add": "添加",
|
||||
|
||||
@@ -29,10 +29,6 @@
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.disabledButton {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
.disabledButton:hover {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useAccountComment, useComment, useSubscribe } from '@plebbit/plebbit-react-hooks';
|
||||
import { isAllView, isCatalogView, isDescriptionView, isPendingPostView, isPostPageView, isRulesView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||
import { isAllView, isCatalogView, isDescriptionView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||
import useCatalogStyleStore from '../../stores/use-catalog-style-store';
|
||||
import useFeedResetStore from '../../stores/use-feed-reset-store';
|
||||
import useSortingStore from '../../stores/use-sorting-store';
|
||||
@@ -96,11 +96,11 @@ const UpdateButton = () => {
|
||||
<>
|
||||
{/* TODO: Implement update button once available in API */}
|
||||
{isMobile ? (
|
||||
<button className={`button ${styles.disabledButton}`} disabled>
|
||||
<button className={`button ${styles.disabledButton}`} onClick={() => alert('Coming soon')}>
|
||||
{t('update')}
|
||||
</button>
|
||||
) : (
|
||||
<button className={`button ${styles.disabledButton}`} disabled>
|
||||
<button className={`button ${styles.disabledButton}`} onClick={() => alert('Coming soon')}>
|
||||
{t('update')}
|
||||
</button>
|
||||
)}
|
||||
@@ -293,19 +293,18 @@ const PostPageStats = () => {
|
||||
const params = useParams();
|
||||
const location = useLocation();
|
||||
const isInDescriptionView = isDescriptionView(location.pathname, params);
|
||||
const isInRulesView = isRulesView(location.pathname, params);
|
||||
|
||||
const comment = useComment({ commentCid: params?.commentCid });
|
||||
const { closed, pinned, replyCount } = comment || {};
|
||||
const linkCount = useCountLinksInReplies(comment);
|
||||
|
||||
const displayReplyCount = replyCount !== undefined ? replyCount.toString() : '?';
|
||||
const replyCountTooltip = replyCount !== undefined ? _.capitalize(t('replies')) : t('loading');
|
||||
const displayReplyCount = replyCount !== undefined ? replyCount.toString() : isInDescriptionView ? 1 : '?';
|
||||
const replyCountTooltip = replyCount !== undefined || isInDescriptionView ? _.capitalize(t('replies')) : t('loading');
|
||||
|
||||
return (
|
||||
<span>
|
||||
{(pinned || isInDescriptionView || isInRulesView) && `${_.capitalize(t('sticky'))} / `}
|
||||
{(closed || isInDescriptionView || isInRulesView) && `${_.capitalize(t('closed'))} / `}
|
||||
{(pinned || isInDescriptionView) && `${_.capitalize(t('sticky'))} / `}
|
||||
{(closed || isInDescriptionView) && `${_.capitalize(t('closed'))} / `}
|
||||
<Tooltip children={displayReplyCount} content={replyCountTooltip} /> / <Tooltip children={linkCount?.toString()} content={_.capitalize(t('links'))} />
|
||||
</span>
|
||||
);
|
||||
@@ -379,7 +378,7 @@ export const DesktopBoardButtons = () => {
|
||||
{(isInAllView || isInSubscriptionsView) && (
|
||||
<TimeFilter isInAllView={isInAllView} isInCatalogView={isInCatalogView} isInSubscriptionsView={isInSubscriptionsView} />
|
||||
)}
|
||||
{(isInCatalogView || isInAllView || isInSubscriptionsView) && (
|
||||
{isInCatalogView && (
|
||||
<>
|
||||
[
|
||||
<CatalogFilters />]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||
import { Comment, useAccount, useAuthorAvatar, useComment, useEditedComment } from '@plebbit/plebbit-react-hooks';
|
||||
import { Comment, useAccount, useAuthorAvatar, useComment, useEditedComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
|
||||
import styles from '../../views/post/post.module.css';
|
||||
import { getCommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils';
|
||||
@@ -274,7 +274,7 @@ const PostMedia = ({ post }: PostProps) => {
|
||||
};
|
||||
|
||||
const PostMessage = ({ post }: PostProps) => {
|
||||
const { cid, content, deleted, edit, original, parentCid, postCid, reason, removed, state } = post || {};
|
||||
const { cid, content, deleted, edit, isRules, original, parentCid, postCid, reason, removed, state } = post || {};
|
||||
// TODO: commentAuthor is not yet available outside of editedComment, wait for API to be updated
|
||||
// const banned = !!post?.commentAuthor?.banExpiresAt;
|
||||
const { t } = useTranslation();
|
||||
@@ -297,7 +297,7 @@ const PostMessage = ({ post }: PostProps) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<blockquote className={styles.postMessage}>
|
||||
<blockquote className={`${styles.postMessage} ${isRules && styles.rulesMessage}`}>
|
||||
{isReply && !(removed || deleted) && state !== 'failed' && isReplyingToReply && <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotelinkReply} />}
|
||||
{removed ? (
|
||||
reason ? (
|
||||
@@ -433,6 +433,17 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies
|
||||
|
||||
const stateString = useStateString(post);
|
||||
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
|
||||
const subplebbitRulesReply = {
|
||||
isRules: true,
|
||||
subplebbitAddress,
|
||||
timestamp: subplebbit?.createdAt,
|
||||
author: { displayName: `## ${t('board_mods')}` },
|
||||
content: `${subplebbit?.rules?.map((rule: string, index: number) => `${index + 1}. ${rule}`).join('\n')}`,
|
||||
replyCount: 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.postDesktop}>
|
||||
{showReplies ? (
|
||||
@@ -477,8 +488,6 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies
|
||||
{!isHidden &&
|
||||
!(pinned && !isInPostPageView && !showOmittedReplies[cid]) &&
|
||||
!isInPendingPostView &&
|
||||
!isDescription &&
|
||||
!isRules &&
|
||||
replies &&
|
||||
showReplies &&
|
||||
(showAllReplies || showOmittedReplies[cid] ? replies : replies.slice(-5)).map((reply, index) => (
|
||||
@@ -486,6 +495,11 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies
|
||||
<Reply openReplyModal={openReplyModal} reply={reply} roles={roles} postReplyCount={replyCount} />
|
||||
</div>
|
||||
))}
|
||||
{isDescription && subplebbit?.rules && subplebbit?.rules.length > 0 && (
|
||||
<div className={styles.replyContainer}>
|
||||
<Reply reply={subplebbitRulesReply} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isInPendingPostView &&
|
||||
(stateString && stateString !== 'Failed' && state !== 'succeeded' ? (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||
import { Comment, useAccount, useAuthorAvatar, useComment, useEditedComment } from '@plebbit/plebbit-react-hooks';
|
||||
import { Comment, useAccount, useAuthorAvatar, useComment, useEditedComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
|
||||
import styles from '../../views/post/post.module.css';
|
||||
import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
|
||||
@@ -200,7 +200,7 @@ const ReplyBacklinks = ({ post }: PostProps) => {
|
||||
|
||||
const PostMessageMobile = ({ post }: PostProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { cid, content, deleted, edit, original, parentCid, postCid, reason, removed, state } = post || {};
|
||||
const { cid, content, deleted, edit, isRules, original, parentCid, postCid, reason, removed, state } = post || {};
|
||||
// TODO: commentAuthor is not available outside of editedComment, update when available
|
||||
// const banned = !!post?.commentAuthor?.banExpiresAt;
|
||||
const [showOriginal, setShowOriginal] = useState(false);
|
||||
@@ -223,7 +223,7 @@ const PostMessageMobile = ({ post }: PostProps) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<blockquote className={`${styles.postMessage} ${!isReply && styles.clampLines}`}>
|
||||
<blockquote className={`${styles.postMessage} ${!isReply && styles.clampLines} ${isRules && styles.rulesMessage}`}>
|
||||
{isReply && !(removed || deleted) && state !== 'failed' && isReplyingToReply && <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotelinkReply} />}
|
||||
{removed ? (
|
||||
reason ? (
|
||||
@@ -353,6 +353,17 @@ const PostMobile = ({ openReplyModal, post, roles, showAllReplies, showReplies =
|
||||
|
||||
const stateString = useStateString(post);
|
||||
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const showRules = isDescription && subplebbit?.rules && subplebbit?.rules.length > 0;
|
||||
const subplebbitRulesReply = {
|
||||
isRules: true,
|
||||
subplebbitAddress,
|
||||
timestamp: subplebbit?.createdAt,
|
||||
author: { displayName: `## ${t('board_mods')}` },
|
||||
content: `${subplebbit?.rules?.map((rule: string, index: number) => `${index + 1}. ${rule}`).join('\n')}`,
|
||||
replyCount: 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{hidden && !isInPostPageView ? (
|
||||
@@ -394,8 +405,6 @@ const PostMobile = ({ openReplyModal, post, roles, showAllReplies, showReplies =
|
||||
</div>
|
||||
{!(pinned && !isInPostView) &&
|
||||
!isInPendingPostView &&
|
||||
!isDescription &&
|
||||
!isRules &&
|
||||
replies &&
|
||||
showReplies &&
|
||||
(showAllReplies ? replies : replies.slice(-5)).map((reply, index) => (
|
||||
@@ -403,6 +412,11 @@ const PostMobile = ({ openReplyModal, post, roles, showAllReplies, showReplies =
|
||||
<Reply openReplyModal={openReplyModal} postReplyCount={replyCount} reply={reply} roles={roles} />
|
||||
</div>
|
||||
))}
|
||||
{showRules && (
|
||||
<div className={styles.replyContainer}>
|
||||
<Reply reply={subplebbitRulesReply} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isInPendingPostView &&
|
||||
(stateString && stateString !== 'Failed' && state !== 'succeeded' ? (
|
||||
|
||||
@@ -5,6 +5,8 @@ import useTheme from '../../../hooks/use-theme';
|
||||
import packageJson from '../../../../package.json';
|
||||
import styles from './interface-settings.module.css';
|
||||
import _ from 'lodash';
|
||||
import useInterfaceSettingsStore from '../../../stores/use-interface-settings-store';
|
||||
import useCatalogFiltersStore from '../../../stores/use-catalog-filters-store';
|
||||
|
||||
const commitRef = process.env.REACT_APP_COMMIT_REF;
|
||||
const isElectron = window.isElectron === true;
|
||||
@@ -110,6 +112,8 @@ const InterfaceLanguage = () => {
|
||||
const InterfaceSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const { hideAvatars, setHideAvatars } = useAvatarVisibilityStore();
|
||||
const { hideGoreBoards, setHideGoreBoards, hideAdultBoards, setHideAdultBoards, hideThreadsWithoutImages, setHideThreadsWithoutImages } = useInterfaceSettingsStore();
|
||||
const { setShowTextOnlyThreads } = useCatalogFiltersStore();
|
||||
|
||||
const handleHideAvatarsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setHideAvatars(e.target.checked);
|
||||
@@ -142,6 +146,29 @@ const InterfaceSettings = () => {
|
||||
<input type='checkbox' checked={hideAvatars} onChange={handleHideAvatarsChange} /> {_.capitalize(t('hide_avatars'))}
|
||||
</label>
|
||||
</div>
|
||||
<div className={styles.setting}>
|
||||
<label>
|
||||
<input type='checkbox' checked={hideGoreBoards} onChange={(e) => setHideGoreBoards(e.target.checked)} /> {_.capitalize(t('hide_gore_boards'))}
|
||||
</label>
|
||||
</div>
|
||||
<div className={styles.setting}>
|
||||
<label>
|
||||
<input type='checkbox' checked={hideAdultBoards} onChange={(e) => setHideAdultBoards(e.target.checked)} /> {_.capitalize(t('hide_adult_boards'))}
|
||||
</label>
|
||||
</div>
|
||||
<div className={styles.setting}>
|
||||
<label>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={hideThreadsWithoutImages}
|
||||
onChange={(e) => {
|
||||
setHideThreadsWithoutImages(e.target.checked);
|
||||
setShowTextOnlyThreads(!e.target.checked);
|
||||
}}
|
||||
/>
|
||||
{_.capitalize(t('hide_threads_without_images'))}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ const SubplebbitDescription = ({ avatarUrl, createdAt, description, shortAddress
|
||||
author: { displayName: `## ${t('board_mods')}` },
|
||||
content: isInAllView ? multisubMetadata?.description : description,
|
||||
link: avatarUrl,
|
||||
replyCount: 0,
|
||||
replyCount: 1,
|
||||
title: t('welcome_to_board', {
|
||||
board: isInAllView ? multisubMetadata?.title : title || `p/${shortAddress}`,
|
||||
interpolation: { escapeValue: false },
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
}
|
||||
|
||||
.disabledButton {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ const TopBarDesktop = () => {
|
||||
to='boards/create'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
alert('Coming soon');
|
||||
}}
|
||||
>
|
||||
Create
|
||||
@@ -101,6 +102,7 @@ const TopBarDesktop = () => {
|
||||
to='boards'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
alert('Coming soon');
|
||||
}}
|
||||
>
|
||||
Vote
|
||||
|
||||
@@ -2,17 +2,17 @@ import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams, useLocation } from 'react-router-dom';
|
||||
import { useAccountComments, Subplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import useInterfaceSettingsStore from '../stores/use-interface-settings-store';
|
||||
import { getCommentMediaInfo, getHasThumbnail } from '../lib/utils/media-utils';
|
||||
import { isAllView } from '../lib/utils/view-utils';
|
||||
import { useMultisubMetadata } from './use-default-subplebbits';
|
||||
import useCatalogFiltersStore from '../stores/use-catalog-filters-store';
|
||||
import _ from 'lodash';
|
||||
import { getCommentMediaInfo, getHasThumbnail } from '../lib/utils/media-utils';
|
||||
|
||||
const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolean, subplebbit: Subplebbit) => {
|
||||
const { t } = useTranslation();
|
||||
const { address, createdAt, description, rules, shortAddress, suggested, title } = subplebbit || {};
|
||||
const { avatarUrl } = suggested || {};
|
||||
const { showTextOnlyThreads } = useCatalogFiltersStore();
|
||||
const { hideThreadsWithoutImages } = useInterfaceSettingsStore();
|
||||
|
||||
const location = useLocation();
|
||||
const isInAllView = isAllView(location.pathname, useParams());
|
||||
@@ -41,7 +41,7 @@ const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolea
|
||||
!deleted &&
|
||||
!removed &&
|
||||
state === 'succeeded' &&
|
||||
(showTextOnlyThreads || (!showTextOnlyThreads && isMediaShowed)) &&
|
||||
(!hideThreadsWithoutImages || (hideThreadsWithoutImages && isMediaShowed)) &&
|
||||
cid &&
|
||||
cid === postCid &&
|
||||
subplebbitAddress === address &&
|
||||
@@ -63,7 +63,7 @@ const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolea
|
||||
}
|
||||
|
||||
// add subplebbit description and rules as fake posts at the top of the feed
|
||||
if ((description && description.length > 0 && (showTextOnlyThreads || (!showTextOnlyThreads && suggested?.avatarUrl))) || (isInAllView && showTextOnlyThreads)) {
|
||||
if (description && description.length > 0) {
|
||||
_feed.unshift({
|
||||
isDescription: true,
|
||||
subplebbitAddress: address,
|
||||
@@ -77,7 +77,8 @@ const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolea
|
||||
});
|
||||
}
|
||||
|
||||
if (rules && rules.length > 0 && showTextOnlyThreads) {
|
||||
// rules are shown in description thread if both are set
|
||||
if (rules && rules.length > 0 && !description) {
|
||||
_feed.unshift({
|
||||
isRules: true,
|
||||
subplebbitAddress: address,
|
||||
@@ -91,23 +92,7 @@ const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolea
|
||||
}
|
||||
|
||||
return _feed;
|
||||
}, [
|
||||
accountComments,
|
||||
feed,
|
||||
description,
|
||||
rules,
|
||||
address,
|
||||
isFeedLoaded,
|
||||
createdAt,
|
||||
title,
|
||||
shortAddress,
|
||||
avatarUrl,
|
||||
t,
|
||||
isInAllView,
|
||||
multisub,
|
||||
showTextOnlyThreads,
|
||||
suggested?.avatarUrl,
|
||||
]);
|
||||
}, [accountComments, feed, description, rules, address, isFeedLoaded, createdAt, title, shortAddress, avatarUrl, t, isInAllView, multisub, hideThreadsWithoutImages]);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
const rows = [];
|
||||
|
||||
@@ -53,6 +53,11 @@ const useReplies = (comment: Comment) => {
|
||||
});
|
||||
}, [flattenedReplies, accountRepliesNotYetPublished]);
|
||||
|
||||
// if the comment is a fake comment for description or rules, return an empty array
|
||||
if (comment?.isDescription || comment?.isRules) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return repliesAndNotYetPublishedReplies;
|
||||
};
|
||||
|
||||
|
||||
@@ -37,3 +37,20 @@ export const formatMarkdown = (content: string): string => {
|
||||
}
|
||||
return md;
|
||||
};
|
||||
|
||||
export function removeMarkdown(md: string): string {
|
||||
return md
|
||||
.replace(/\[([^\]]*?)\][\[\(].*?[\]\)]/g, '$1') // Remove links
|
||||
.replace(/[*_~`]/g, '') // Remove emphasis and code markers
|
||||
.replace(/^#+\s*(.+)$/gm, '$1') // Remove headers
|
||||
.replace(/^\s*[-*+]\s+/gm, '') // Remove list markers
|
||||
.replace(/^\s*>\s+/gm, '') // Remove blockquotes
|
||||
.replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, '') // Remove reference-style links
|
||||
.replace(/^(\n)?\s{0,}#{1,6}\s*( (.+))? +#+$|^(\n)?\s{0,}#{1,6}\s*( (.+))?$/gm, '$1$3$4$6') // Remove atx-style headers
|
||||
.replace(/([\*]+)(\S)(.*?\S)??\1/g, '$2$3') // Remove * emphasis
|
||||
.replace(/(^|\W)([_]+)(\S)(.*?\S)??\2($|\W)/g, '$1$3$4$5') // Remove _ emphasis
|
||||
.replace(/(`{3,})(.*?)\1/gm, '$2') // Remove code blocks
|
||||
.replace(/`(.+?)`/g, '$1') // Remove inline code
|
||||
.replace(/~(.*?)~/g, '$1') // Remove strike through
|
||||
.trim();
|
||||
}
|
||||
|
||||
@@ -9,10 +9,6 @@ interface FilterItem {
|
||||
}
|
||||
|
||||
interface CatalogFiltersStore {
|
||||
showAdultBoards: boolean;
|
||||
setShowAdultBoards: (value: boolean) => void;
|
||||
showGoreBoards: boolean;
|
||||
setShowGoreBoards: (value: boolean) => void;
|
||||
showTextOnlyThreads: boolean;
|
||||
setShowTextOnlyThreads: (value: boolean) => void;
|
||||
filterText: string;
|
||||
@@ -35,10 +31,6 @@ const useCatalogFiltersStore = create(
|
||||
set({ showTextOnlyThreads: value, filteredCount: 0 });
|
||||
get().updateFilter();
|
||||
},
|
||||
showAdultBoards: false,
|
||||
setShowAdultBoards: (value: boolean) => set({ showAdultBoards: value }),
|
||||
showGoreBoards: false,
|
||||
setShowGoreBoards: (value: boolean) => set({ showGoreBoards: value }),
|
||||
filterText: '',
|
||||
setFilterText: (value: string) => set({ filterText: value }),
|
||||
filterItems: [],
|
||||
@@ -58,28 +50,28 @@ const useCatalogFiltersStore = create(
|
||||
filter: (comment: Comment) => {
|
||||
const { showTextOnlyThreads, filterItems } = state;
|
||||
|
||||
const shouldShow = (() => {
|
||||
if (!showTextOnlyThreads && !getHasThumbnail(getCommentMediaInfo(comment), comment?.link)) {
|
||||
return false;
|
||||
const hasThumbnail = getHasThumbnail(getCommentMediaInfo(comment), comment?.link);
|
||||
const title = comment?.title?.toLowerCase() || '';
|
||||
const content = comment?.content?.toLowerCase() || '';
|
||||
|
||||
const matchesFilterItems = filterItems
|
||||
.filter((item) => item.enabled)
|
||||
.some((item) => {
|
||||
const text = item.text.toLowerCase();
|
||||
return title.includes(text) || content.includes(text);
|
||||
});
|
||||
|
||||
const shouldShow = showTextOnlyThreads || hasThumbnail;
|
||||
|
||||
if (!shouldShow || matchesFilterItems) {
|
||||
if (matchesFilterItems && !filteredCids.has(comment.cid)) {
|
||||
filteredCids.add(comment.cid);
|
||||
set((state) => ({ filteredCount: state.filteredCount + 1 }));
|
||||
}
|
||||
|
||||
const title = comment?.title?.toLowerCase() || '';
|
||||
const content = comment?.content?.toLowerCase() || '';
|
||||
|
||||
return !filterItems
|
||||
.filter((item) => item.enabled)
|
||||
.some((item) => {
|
||||
const text = item.text.toLowerCase();
|
||||
return title.includes(text) || content.includes(text);
|
||||
});
|
||||
})();
|
||||
|
||||
if (!shouldShow && !filteredCids.has(comment.cid)) {
|
||||
filteredCids.add(comment.cid);
|
||||
set((state) => ({ filteredCount: state.filteredCount + 1 }));
|
||||
return false;
|
||||
}
|
||||
|
||||
return shouldShow;
|
||||
return true;
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
interface InterfaceSettingsStore {
|
||||
hideGoreBoards: boolean;
|
||||
setHideGoreBoards: (value: boolean) => void;
|
||||
hideAdultBoards: boolean;
|
||||
setHideAdultBoards: (value: boolean) => void;
|
||||
hideThreadsWithoutImages: boolean;
|
||||
setHideThreadsWithoutImages: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const useInterfaceSettingsStore = create(
|
||||
persist<InterfaceSettingsStore>(
|
||||
(set) => ({
|
||||
hideGoreBoards: true,
|
||||
setHideGoreBoards: (value: boolean) => set({ hideGoreBoards: value }),
|
||||
hideAdultBoards: true,
|
||||
setHideAdultBoards: (value: boolean) => set({ hideAdultBoards: value }),
|
||||
hideThreadsWithoutImages: true,
|
||||
setHideThreadsWithoutImages: (value: boolean) => set({ hideThreadsWithoutImages: value }),
|
||||
}),
|
||||
{
|
||||
name: 'interface-settings-storage',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export default useInterfaceSettingsStore;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { useAccount, useAccountComments, useBlock, useFeed, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { Comment, useAccount, useAccountComments, useBlock, useFeed, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styles from './board.module.css';
|
||||
@@ -17,13 +17,23 @@ import ReplyModal from '../../components/reply-modal';
|
||||
import SettingsModal from '../../components/settings-modal';
|
||||
import SubplebbitDescription from '../../components/subplebbit-description';
|
||||
import SubplebbitRules from '../../components/subplebbit-rules';
|
||||
import useInterfaceSettingsStore from '../../stores/use-interface-settings-store';
|
||||
import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
|
||||
|
||||
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||
|
||||
const threadsWithoutImagesFilter = (comment: Comment) => {
|
||||
if (!getHasThumbnail(getCommentMediaInfo(comment), comment?.link)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const Board = () => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const { subplebbitAddress } = useParams<{ subplebbitAddress: string }>();
|
||||
const { hideThreadsWithoutImages } = useInterfaceSettingsStore();
|
||||
|
||||
const isInAllView = isAllView(location.pathname, useParams());
|
||||
const defaultSubplebbitAddresses = useDefaultSubplebbitAddresses();
|
||||
@@ -51,8 +61,9 @@ const Board = () => {
|
||||
sortType,
|
||||
postsPerPage: isInAllView || isInSubscriptionsView ? 5 : 25,
|
||||
...(isInAllView || isInSubscriptionsView ? { newerThan: timeFilterSeconds } : {}),
|
||||
filter: hideThreadsWithoutImages ? threadsWithoutImagesFilter : undefined,
|
||||
}),
|
||||
[subplebbitAddresses, sortType, timeFilterSeconds, isInAllView, isInSubscriptionsView],
|
||||
[subplebbitAddresses, sortType, timeFilterSeconds, isInAllView, isInSubscriptionsView, hideThreadsWithoutImages],
|
||||
);
|
||||
|
||||
const { feed, hasMore, loadMore, reset } = useFeed(feedOptions);
|
||||
@@ -75,12 +86,13 @@ const Board = () => {
|
||||
!removed &&
|
||||
state === 'succeeded' &&
|
||||
cid &&
|
||||
(hideThreadsWithoutImages ? getHasThumbnail(getCommentMediaInfo(comment), comment?.link) : true) &&
|
||||
cid === postCid &&
|
||||
comment?.subplebbitAddress === subplebbitAddress &&
|
||||
!feed.some((post) => post.cid === cid)
|
||||
);
|
||||
}),
|
||||
[accountComments, subplebbitAddress, feed],
|
||||
[accountComments, subplebbitAddress, feed, hideThreadsWithoutImages],
|
||||
);
|
||||
|
||||
// show newest account comment at the top of the feed but after pinned posts
|
||||
@@ -180,7 +192,7 @@ const Board = () => {
|
||||
)}
|
||||
{feed.length !== 0 ? (
|
||||
<>
|
||||
{rules && rules.length > 0 && <SubplebbitRules subplebbitAddress={subplebbitAddress} createdAt={createdAt} rules={rules} />}
|
||||
{rules && !description && rules.length > 0 && <SubplebbitRules subplebbitAddress={subplebbitAddress} createdAt={createdAt} rules={rules} />}
|
||||
<Virtuoso
|
||||
increaseViewportBy={{ bottom: 1200, top: 1200 }}
|
||||
totalCount={combinedFeed.length}
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { isAllView, isCatalogView } from '../../../lib/utils/view-utils';
|
||||
import useCatalogFiltersStore from '../../../stores/use-catalog-filters-store';
|
||||
import styles from './catalog-filters.module.css';
|
||||
|
||||
const FiltersTable = ({
|
||||
localFilterItems,
|
||||
setLocalFilterItems,
|
||||
onSave,
|
||||
}: {
|
||||
localFilterItems: any[];
|
||||
setLocalFilterItems: React.Dispatch<React.SetStateAction<any[]>>;
|
||||
onSave: () => void;
|
||||
}) => {
|
||||
const FiltersTable = ({ onSave }: { onSave: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { saveAndApplyFilters } = useCatalogFiltersStore();
|
||||
const { filterItems, saveAndApplyFilters } = useCatalogFiltersStore();
|
||||
|
||||
const [localFilterItems, setLocalFilterItems] = useState(filterItems);
|
||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
|
||||
const handleAddFilter = useCallback(() => {
|
||||
setLocalFilterItems((prev) => [...prev, { text: '', enabled: true }]);
|
||||
}, [setLocalFilterItems]);
|
||||
setLocalFilterItems((prev) => {
|
||||
const newIndex = prev.length;
|
||||
setTimeout(() => {
|
||||
inputRefs.current[newIndex]?.focus();
|
||||
}, 0);
|
||||
return [...prev, { text: '', enabled: true }];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
const nonEmptyFilters = localFilterItems.filter((item) => item.text.trim() !== '');
|
||||
@@ -27,31 +26,22 @@ const FiltersTable = ({
|
||||
onSave();
|
||||
}, [saveAndApplyFilters, localFilterItems, onSave]);
|
||||
|
||||
const updateLocalFilterItem = useCallback(
|
||||
(index: number, item: any) => {
|
||||
setLocalFilterItems((prev) => prev.map((f, i) => (i === index ? item : f)));
|
||||
},
|
||||
[setLocalFilterItems],
|
||||
);
|
||||
const updateLocalFilterItem = useCallback((index: number, item: any) => {
|
||||
setLocalFilterItems((prev) => prev.map((f, i) => (i === index ? item : f)));
|
||||
}, []);
|
||||
|
||||
const removeLocalFilterItem = useCallback(
|
||||
(index: number) => {
|
||||
setLocalFilterItems((prev) => prev.filter((_, i) => i !== index));
|
||||
},
|
||||
[setLocalFilterItems],
|
||||
);
|
||||
const removeLocalFilterItem = useCallback((index: number) => {
|
||||
setLocalFilterItems((prev) => prev.filter((_, i) => i !== index));
|
||||
}, []);
|
||||
|
||||
const moveLocalFilterItemUp = useCallback(
|
||||
(index: number) => {
|
||||
if (index === 0) return;
|
||||
setLocalFilterItems((prev) => {
|
||||
const newItems = [...prev];
|
||||
[newItems[index - 1], newItems[index]] = [newItems[index], newItems[index - 1]];
|
||||
return newItems;
|
||||
});
|
||||
},
|
||||
[setLocalFilterItems],
|
||||
);
|
||||
const moveLocalFilterItemUp = useCallback((index: number) => {
|
||||
if (index === 0) return;
|
||||
setLocalFilterItems((prev) => {
|
||||
const newItems = [...prev];
|
||||
[newItems[index - 1], newItems[index]] = [newItems[index], newItems[index - 1]];
|
||||
return newItems;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<table className={styles.filtersTable}>
|
||||
@@ -87,6 +77,7 @@ const FiltersTable = ({
|
||||
spellCheck='false'
|
||||
value={item.text}
|
||||
onChange={(e) => updateLocalFilterItem(index, { ...item, text: e.target.value })}
|
||||
ref={(el) => (inputRefs.current[index] = el)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
@@ -115,37 +106,6 @@ const FiltersTable = ({
|
||||
|
||||
const FiltersModal = ({ closeModal }: { closeModal: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { showAdultBoards, setShowAdultBoards, showGoreBoards, setShowGoreBoards, showTextOnlyThreads, setShowTextOnlyThreads, filterItems, saveAndApplyFilters } =
|
||||
useCatalogFiltersStore();
|
||||
|
||||
const [localShowAdultBoards, setLocalShowAdultBoards] = useState(showAdultBoards);
|
||||
const [localShowGoreBoards, setLocalShowGoreBoards] = useState(showGoreBoards);
|
||||
const [localShowTextOnlyThreads, setLocalShowTextOnlyThreads] = useState(showTextOnlyThreads);
|
||||
const [localFilterItems, setLocalFilterItems] = useState(filterItems);
|
||||
|
||||
const location = useLocation();
|
||||
const params = useParams();
|
||||
const isInCatalogView = isCatalogView(location.pathname, params);
|
||||
const isInAllView = isAllView(location.pathname, params);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
setShowAdultBoards(localShowAdultBoards);
|
||||
setShowGoreBoards(localShowGoreBoards);
|
||||
setShowTextOnlyThreads(localShowTextOnlyThreads);
|
||||
const nonEmptyFilters = localFilterItems.filter((item) => item.text.trim() !== '');
|
||||
saveAndApplyFilters(nonEmptyFilters);
|
||||
closeModal();
|
||||
}, [
|
||||
localShowAdultBoards,
|
||||
localShowGoreBoards,
|
||||
localShowTextOnlyThreads,
|
||||
localFilterItems,
|
||||
setShowAdultBoards,
|
||||
setShowGoreBoards,
|
||||
setShowTextOnlyThreads,
|
||||
saveAndApplyFilters,
|
||||
closeModal,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -155,33 +115,7 @@ const FiltersModal = ({ closeModal }: { closeModal: () => void }) => {
|
||||
<span className={styles.title}>{t('filters')}</span>
|
||||
<span className={styles.closeButton} title='close' onClick={closeModal} />
|
||||
</div>
|
||||
<div className={styles.filters}>
|
||||
{isInCatalogView && (
|
||||
<div>
|
||||
<label className='capitalize'>
|
||||
<input type='checkbox' checked={!localShowTextOnlyThreads} onChange={(e) => setLocalShowTextOnlyThreads(!e.target.checked)} />
|
||||
{t('hide_threads_without_images')}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{isInAllView && (
|
||||
<div className={styles.nsfwLabels}>
|
||||
<div>
|
||||
<label>
|
||||
<input type='checkbox' checked={!localShowGoreBoards} onChange={(e) => setLocalShowGoreBoards(!e.target.checked)} />
|
||||
hide gore content
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type='checkbox' checked={!localShowAdultBoards} onChange={(e) => setLocalShowAdultBoards(!e.target.checked)} />
|
||||
hide adult content
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isInCatalogView && <FiltersTable localFilterItems={localFilterItems} setLocalFilterItems={setLocalFilterItems} onSave={handleSave} />}
|
||||
<FiltersTable onSave={closeModal} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@ import CatalogRow from '../../components/catalog-row';
|
||||
import LoadingEllipsis from '../../components/loading-ellipsis';
|
||||
import SettingsModal from '../../components/settings-modal';
|
||||
import styles from './catalog.module.css';
|
||||
import useInterfaceSettingsStore from '../../stores/use-interface-settings-store';
|
||||
|
||||
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||
|
||||
@@ -27,7 +28,8 @@ const Catalog = () => {
|
||||
|
||||
const isInAllView = isAllView(location.pathname, useParams());
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const { filter, showAdultBoards, showGoreBoards } = useCatalogFiltersStore();
|
||||
const { hideAdultBoards, hideGoreBoards } = useInterfaceSettingsStore();
|
||||
const { filter } = useCatalogFiltersStore();
|
||||
|
||||
const account = useAccount();
|
||||
const subscriptions = account?.subscriptions;
|
||||
@@ -40,7 +42,7 @@ const Catalog = () => {
|
||||
const hasGoreTag = tags?.includes('gore');
|
||||
const hasAdultTag = tags?.includes('adult');
|
||||
|
||||
if ((hasGoreTag && !showGoreBoards) || (hasAdultTag && !showAdultBoards)) {
|
||||
if ((hasGoreTag && hideGoreBoards) || (hasAdultTag && hideAdultBoards)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -54,7 +56,7 @@ const Catalog = () => {
|
||||
return subscriptions || [];
|
||||
}
|
||||
return [subplebbitAddress];
|
||||
}, [isInAllView, isInSubscriptionsView, subplebbitAddress, defaultSubplebbits, subscriptions, showAdultBoards, showGoreBoards]);
|
||||
}, [isInAllView, isInSubscriptionsView, subplebbitAddress, defaultSubplebbits, subscriptions, hideAdultBoards, hideGoreBoards]);
|
||||
|
||||
const { imageSize } = useCatalogStyleStore();
|
||||
const columnWidth = imageSize === 'Large' ? 270 : 180;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { CatalogPostMedia } from '../../../components/catalog-row';
|
||||
import LoadingEllipsis from '../../../components/loading-ellipsis';
|
||||
import BoxModal from '../box-modal';
|
||||
import { nsfwTags } from '../home';
|
||||
import { removeMarkdown } from '../../../lib/utils/post-utils';
|
||||
|
||||
interface PopularThreadProps {
|
||||
post: Comment;
|
||||
@@ -17,11 +18,16 @@ interface PopularThreadProps {
|
||||
boardShortAddress: string;
|
||||
}
|
||||
|
||||
const PopularThreadCard = ({ post, boardTitle, boardShortAddress }: PopularThreadProps) => {
|
||||
const { cid, subplebbitAddress, title } = post || {};
|
||||
const commentMediaInfo = getCommentMediaInfo(post);
|
||||
const ContentPreview = ({ content, maxLength = 99 }: { content: string; maxLength?: number }) => {
|
||||
const plainText = removeMarkdown(content).trim().replaceAll(' ', '').replace(/\n\n/g, '\n').replaceAll('\n\n', '');
|
||||
const truncatedText = plainText.length > maxLength ? `${plainText.substring(0, maxLength).trim()}...` : plainText;
|
||||
|
||||
let content = post?.content?.replace(/\n\n/g, '\n');
|
||||
return truncatedText;
|
||||
};
|
||||
|
||||
const PopularThreadCard = ({ post, boardTitle, boardShortAddress }: PopularThreadProps) => {
|
||||
const { cid, content, subplebbitAddress, title } = post || {};
|
||||
const commentMediaInfo = getCommentMediaInfo(post);
|
||||
|
||||
return (
|
||||
<div className={styles.popularThread} key={cid}>
|
||||
@@ -38,7 +44,7 @@ const PopularThreadCard = ({ post, boardTitle, boardShortAddress }: PopularThrea
|
||||
{content && ': '}
|
||||
</>
|
||||
)}
|
||||
{content && (content.length > 99 ? `${content.substring(0, 99).trim()}...` : `${content}`)}
|
||||
{content && <ContentPreview content={content} maxLength={99} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -163,6 +163,10 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rulesMessage {
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.postDesktop .abbr {
|
||||
color: var(--post-mobile-abbr-text-color);
|
||||
font-size: var(--post-mobile-abbr-font-size);
|
||||
|
||||
Reference in New Issue
Block a user