mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
@@ -110,7 +110,7 @@ Bitsocial communities can require users to solve one or more anti-spam challenge
|
|||||||
|
|
||||||
### Build
|
### Build
|
||||||
|
|
||||||
The Linux/Windows/macOS/Android build scripts are in [.github/workflows/release.yml](https://github.com/plebbit/5chan/blob/master/.github/workflows/release.yml)
|
The Linux/Windows/macOS/Android build scripts are in [.github/workflows/release.yml](https://github.com/bitsocialhq/5chan/blob/master/.github/workflows/release.yml)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const ipfsClientWindowsPath = path.join(ipfsClientsPath, 'win');
|
|||||||
const ipfsClientMacPath = path.join(ipfsClientsPath, 'mac');
|
const ipfsClientMacPath = path.join(ipfsClientsPath, 'mac');
|
||||||
const ipfsClientLinuxPath = path.join(ipfsClientsPath, 'linux');
|
const ipfsClientLinuxPath = path.join(ipfsClientsPath, 'linux');
|
||||||
|
|
||||||
// plebbit kubu download links https://github.com/plebbit/kubo/releases
|
// plebbit kubo download links https://github.com/plebbit/kubo/releases
|
||||||
// const ipfsClientVersion = '0.20.0'
|
// const ipfsClientVersion = '0.20.0'
|
||||||
// const ipfsClientWindowsUrl = `https://github.com/plebbit/kubo/releases/download/v${ipfsClientVersion}/ipfs-windows-amd64`
|
// const ipfsClientWindowsUrl = `https://github.com/plebbit/kubo/releases/download/v${ipfsClientVersion}/ipfs-windows-amd64`
|
||||||
// const ipfsClientMacUrl = `https://github.com/plebbit/kubo/releases/download/v${ipfsClientVersion}/ipfs-darwin-amd64`
|
// const ipfsClientMacUrl = `https://github.com/plebbit/kubo/releases/download/v${ipfsClientVersion}/ipfs-darwin-amd64`
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "في انتظار موافقة المشرف، غير مرئي للمستخدمين",
|
"pending_mod_approval": "في انتظار موافقة المشرف، غير مرئي للمستخدمين",
|
||||||
"copy_user_id": "نسخ معرف المستخدم",
|
"copy_user_id": "نسخ معرف المستخدم",
|
||||||
"copy_direct_link": "نسخ الرابط المباشر"
|
"copy_direct_link": "نسخ الرابط المباشر",
|
||||||
|
"posts_auto_update_info": "يتم تحديث المنشورات تلقائياً كل ~2 دقيقة. لا يمكن تعطيل هذا في الوقت الحالي.",
|
||||||
|
"board_address_tooltip": "عنوان عقدة الند للند لهذه اللوحة"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "মডারেটর অনুমোদনের জন্য অপেক্ষা করছে, ব্যবহারকারীদের কাছে দৃশ্যমান নয়",
|
"pending_mod_approval": "মডারেটর অনুমোদনের জন্য অপেক্ষা করছে, ব্যবহারকারীদের কাছে দৃশ্যমান নয়",
|
||||||
"copy_user_id": "ইউজার আইডি কপি করুন",
|
"copy_user_id": "ইউজার আইডি কপি করুন",
|
||||||
"copy_direct_link": "সরাসরি লিংক অনুলিপি করুন"
|
"copy_direct_link": "সরাসরি লিংক অনুলিপি করুন",
|
||||||
|
"posts_auto_update_info": "পোস্টগুলি প্রতি ~2 মিনিটে স্বয়ংক্রিয়ভাবে আপডেট হয়। বর্তমানে এটি অক্ষম করা সম্ভব নয়।",
|
||||||
|
"board_address_tooltip": "এই বোর্ডের পিয়ার-টু-পিয়ার নোডের ঠিকানা"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "Čekání na schválení moderatorem, není viditelné uživatelům",
|
"pending_mod_approval": "Čekání na schválení moderatorem, není viditelné uživatelům",
|
||||||
"copy_user_id": "kopírovat ID uživatele",
|
"copy_user_id": "kopírovat ID uživatele",
|
||||||
"copy_direct_link": "kopírovat přímý odkaz"
|
"copy_direct_link": "kopírovat přímý odkaz",
|
||||||
|
"posts_auto_update_info": "Příspěvky se aktualizují automaticky přibližně každých ~2 minut. V současné době není možné tuto funkci zakázat.",
|
||||||
|
"board_address_tooltip": "Adresa uzlu peer-to-peer této desky"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "Afventer moderatorgodkendelse, ikke synlig for brugere",
|
"pending_mod_approval": "Afventer moderatorgodkendelse, ikke synlig for brugere",
|
||||||
"copy_user_id": "kopier bruger-ID",
|
"copy_user_id": "kopier bruger-ID",
|
||||||
"copy_direct_link": "kopier direkte link"
|
"copy_direct_link": "kopier direkte link",
|
||||||
|
"posts_auto_update_info": "Indlæg opdateres automatisk hver ~2 minut. Det er ikke muligt at deaktivere dette i øjeblikket.",
|
||||||
|
"board_address_tooltip": "Adressen til dette boards peer-to-peer-node"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "Nein",
|
"no": "Nein",
|
||||||
"pending_mod_approval": "Ausstehende Moderatorgenehmigung, für Benutzer nicht sichtbar",
|
"pending_mod_approval": "Ausstehende Moderatorgenehmigung, für Benutzer nicht sichtbar",
|
||||||
"copy_user_id": "Benutzer-ID kopieren",
|
"copy_user_id": "Benutzer-ID kopieren",
|
||||||
"copy_direct_link": "Direktlink kopieren"
|
"copy_direct_link": "Direktlink kopieren",
|
||||||
|
"posts_auto_update_info": "Beiträge werden automatisch etwa alle ~2 Minuten aktualisiert. Es ist derzeit nicht möglich, dies zu deaktivieren.",
|
||||||
|
"board_address_tooltip": "Die Adresse des Peer-to-Peer-Knotens dieses Boards"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "Σε αναμονή έγκρισης διαχειριστή, δεν είναι ορατό στους χρήστες",
|
"pending_mod_approval": "Σε αναμονή έγκρισης διαχειριστή, δεν είναι ορατό στους χρήστες",
|
||||||
"copy_user_id": "αντιγραφή ID χρήστη",
|
"copy_user_id": "αντιγραφή ID χρήστη",
|
||||||
"copy_direct_link": "αντιγραφή άμεσου συνδέσμου"
|
"copy_direct_link": "αντιγραφή άμεσου συνδέσμου",
|
||||||
|
"posts_auto_update_info": "Οι δημοσιεύσεις ενημερώνονται αυτόματα περίπου κάθε ~2 λεπτά. Δεν είναι δυνατό να απενεργοποιήσετε αυτό τη στιγμή.",
|
||||||
|
"board_address_tooltip": "Η διεύθυνση του κόμβου peer-to-peer αυτού του πίνακα"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@
|
|||||||
"latest_stable_version": "You are on the latest stable version, 5chan v{{version}}.",
|
"latest_stable_version": "You are on the latest stable version, 5chan v{{version}}.",
|
||||||
"check": "check",
|
"check": "check",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
|
"posts_auto_update_info": "Posts update automatically every ~2 minutes. It's not possible to disable this at the moment.",
|
||||||
"style": "Style",
|
"style": "Style",
|
||||||
"interface_language": "interface language",
|
"interface_language": "interface language",
|
||||||
"delete_confirm": "Are you sure you want to delete {{value}}?",
|
"delete_confirm": "Are you sure you want to delete {{value}}?",
|
||||||
@@ -277,5 +278,6 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "Pending mod approval, not visible to users",
|
"pending_mod_approval": "Pending mod approval, not visible to users",
|
||||||
"copy_user_id": "Copy user ID",
|
"copy_user_id": "Copy user ID",
|
||||||
"copy_direct_link": "Copy direct link"
|
"copy_direct_link": "Copy direct link",
|
||||||
|
"board_address_tooltip": "The address of this board's peer-to-peer node"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "Pendiente de aprobación del moderador, no visible para los usuarios",
|
"pending_mod_approval": "Pendiente de aprobación del moderador, no visible para los usuarios",
|
||||||
"copy_user_id": "Copiar ID de usuario",
|
"copy_user_id": "Copiar ID de usuario",
|
||||||
"copy_direct_link": "Copiar enlace directo"
|
"copy_direct_link": "Copiar enlace directo",
|
||||||
|
"posts_auto_update_info": "Los posts se actualizan automáticamente cada ~2 minutos. Por el momento, no es posible desactivar esto.",
|
||||||
|
"board_address_tooltip": "La dirección del nodo peer-to-peer de este tablón"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "در انتظار تایید مدیر، برای کاربران قابل مشاهده نیست",
|
"pending_mod_approval": "در انتظار تایید مدیر، برای کاربران قابل مشاهده نیست",
|
||||||
"copy_user_id": "کپی شناسه کاربر",
|
"copy_user_id": "کپی شناسه کاربر",
|
||||||
"copy_direct_link": "رونوشت لینک مستقیم"
|
"copy_direct_link": "رونوشت لینک مستقیم",
|
||||||
|
"posts_auto_update_info": "پستها هر ~2 دقیقه بهطور خودکار بهروز میشوند. فعلاً امکان غیرفعال کردن آن وجود ندارد.",
|
||||||
|
"board_address_tooltip": "آدرس گره همتا-همتای این تخته"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "Odottaa moderaattorin hyväksyntää, ei näkyvissä käyttäjille",
|
"pending_mod_approval": "Odottaa moderaattorin hyväksyntää, ei näkyvissä käyttäjille",
|
||||||
"copy_user_id": "kopioi käyttäjätunnus",
|
"copy_user_id": "kopioi käyttäjätunnus",
|
||||||
"copy_direct_link": "kopioi suora linkki"
|
"copy_direct_link": "kopioi suora linkki",
|
||||||
|
"posts_auto_update_info": "Viestit päivittyvät automaattisesti noin 2 minuutin välein. Tätä ei ole mahdollista poistaa käytöstä tällä hetkellä.",
|
||||||
|
"board_address_tooltip": "Tämän laudan vertaisverkko-solmun osoite"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "Naghihintay ng pag-apruba ng moderator, hindi nakikita ng mga gumagamit",
|
"pending_mod_approval": "Naghihintay ng pag-apruba ng moderator, hindi nakikita ng mga gumagamit",
|
||||||
"copy_user_id": "kopyahin ang user ID",
|
"copy_user_id": "kopyahin ang user ID",
|
||||||
"copy_direct_link": "kopyahin ang direct link"
|
"copy_direct_link": "kopyahin ang direct link",
|
||||||
|
"posts_auto_update_info": "Ang mga post ay awtomatikong nag-update bawat ~2 minuto. Kasalukuyang hindi posible na i-disable ito.",
|
||||||
|
"board_address_tooltip": "Ang address ng peer-to-peer node ng board na ito"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "Non",
|
"no": "Non",
|
||||||
"pending_mod_approval": "En attente d'approbation du modérateur, non visible pour les utilisateurs",
|
"pending_mod_approval": "En attente d'approbation du modérateur, non visible pour les utilisateurs",
|
||||||
"copy_user_id": "Copier l'ID utilisateur",
|
"copy_user_id": "Copier l'ID utilisateur",
|
||||||
"copy_direct_link": "Copier le lien direct"
|
"copy_direct_link": "Copier le lien direct",
|
||||||
|
"posts_auto_update_info": "Les messages se mettent à jour automatiquement toutes les ~2 minutes. Il n'est pas possible de désactiver cela pour le moment.",
|
||||||
|
"board_address_tooltip": "L'adresse du nœud pair-à-pair de ce tableau"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "ממתין לאישור מנהל, לא ניתן לראות למשתמשים",
|
"pending_mod_approval": "ממתין לאישור מנהל, לא ניתן לראות למשתמשים",
|
||||||
"copy_user_id": "העתק מזהה משתמש",
|
"copy_user_id": "העתק מזהה משתמש",
|
||||||
"copy_direct_link": "העתק קישור ישיר"
|
"copy_direct_link": "העתק קישור ישיר",
|
||||||
|
"posts_auto_update_info": "הפוסטים מתעדכנים באופן אוטומטי בערך כל ~2 דקות. כרגע לא ניתן להשבית זאת.",
|
||||||
|
"board_address_tooltip": "כתובת צומת ה-P2P של הלוח הזה"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "मॉडरेटर की मंजूरी के लिए प्रतीक्षा में, उपयोगकर्ताओं को दिखाई नहीं देता",
|
"pending_mod_approval": "मॉडरेटर की मंजूरी के लिए प्रतीक्षा में, उपयोगकर्ताओं को दिखाई नहीं देता",
|
||||||
"copy_user_id": "यूज़र ID कॉपी करें",
|
"copy_user_id": "यूज़र ID कॉपी करें",
|
||||||
"copy_direct_link": "डायरेक्ट लिंक कॉपी करें"
|
"copy_direct_link": "डायरेक्ट लिंक कॉपी करें",
|
||||||
|
"posts_auto_update_info": "पोस्ट्स हर ~2 मिनट में स्वचालित रूप से अपडेट होते हैं। फिलहाल इसे अक्षम करना संभव नहीं है।",
|
||||||
|
"board_address_tooltip": "इस बोर्ड के पीयर-टू-पीयर नोड का पता"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "Moderátor jóváhagyása alatt áll, a felhasználók számára nem látható",
|
"pending_mod_approval": "Moderátor jóváhagyása alatt áll, a felhasználók számára nem látható",
|
||||||
"copy_user_id": "felhasználói ID másolása",
|
"copy_user_id": "felhasználói ID másolása",
|
||||||
"copy_direct_link": "közvetlen link másolása"
|
"copy_direct_link": "közvetlen link másolása",
|
||||||
|
"posts_auto_update_info": "A bejegyzések kb. 2 percenként automatikusan frissülnek. Jelenleg nem lehetséges ezt letiltani.",
|
||||||
|
"board_address_tooltip": "Ennek a táblának a peer-to-peer csomópontja címe"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "Menunggu persetujuan moderator, tidak terlihat oleh pengguna",
|
"pending_mod_approval": "Menunggu persetujuan moderator, tidak terlihat oleh pengguna",
|
||||||
"copy_user_id": "salin ID pengguna",
|
"copy_user_id": "salin ID pengguna",
|
||||||
"copy_direct_link": "salin tautan langsung"
|
"copy_direct_link": "salin tautan langsung",
|
||||||
|
"posts_auto_update_info": "Posting diperbarui secara otomatis setiap ~2 menit. Saat ini tidak mungkin untuk menonaktifkan ini.",
|
||||||
|
"board_address_tooltip": "Alamat node peer-to-peer papan ini"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "In attesa di approvazione del moderatore, non visibile agli utenti",
|
"pending_mod_approval": "In attesa di approvazione del moderatore, non visibile agli utenti",
|
||||||
"copy_user_id": "Copia ID utente",
|
"copy_user_id": "Copia ID utente",
|
||||||
"copy_direct_link": "Copia link diretto"
|
"copy_direct_link": "Copia link diretto",
|
||||||
|
"posts_auto_update_info": "I post si aggiornano automaticamente ogni ~2 minuti. Non è possibile disabilitare questa funzione al momento.",
|
||||||
|
"board_address_tooltip": "L'indirizzo del nodo peer-to-peer di questa bacheca"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "いいえ",
|
"no": "いいえ",
|
||||||
"pending_mod_approval": "モデレーターの承認待ち、ユーザーには表示されません",
|
"pending_mod_approval": "モデレーターの承認待ち、ユーザーには表示されません",
|
||||||
"copy_user_id": "ユーザーIDをコピー",
|
"copy_user_id": "ユーザーIDをコピー",
|
||||||
"copy_direct_link": "直接リンクをコピー"
|
"copy_direct_link": "直接リンクをコピー",
|
||||||
|
"posts_auto_update_info": "投稿は約2分ごとに自動更新されます。現在、これを無効にすることはできません。",
|
||||||
|
"board_address_tooltip": "この板のピアツーピアノードのアドレス"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "중재자 승인 대기 중, 사용자에게 표시되지 않음",
|
"pending_mod_approval": "중재자 승인 대기 중, 사용자에게 표시되지 않음",
|
||||||
"copy_user_id": "사용자 ID 복사",
|
"copy_user_id": "사용자 ID 복사",
|
||||||
"copy_direct_link": "직접 링크 복사"
|
"copy_direct_link": "직접 링크 복사",
|
||||||
|
"posts_auto_update_info": "게시물은 약 2분마다 자동으로 업데이트됩니다. 현재 이를 비활성화할 수 없습니다.",
|
||||||
|
"board_address_tooltip": "이 게시판의 P2P 노드 주소"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "संचालकाच्या मंजुरीची प्रतीक्षा, वापरकर्त्यांना दिसत नाही",
|
"pending_mod_approval": "संचालकाच्या मंजुरीची प्रतीक्षा, वापरकर्त्यांना दिसत नाही",
|
||||||
"copy_user_id": "यूजर ID कॉपी करा",
|
"copy_user_id": "यूजर ID कॉपी करा",
|
||||||
"copy_direct_link": "डायरेक्ट लिंक कॉपी करा"
|
"copy_direct_link": "डायरेक्ट लिंक कॉपी करा",
|
||||||
|
"posts_auto_update_info": "पोस्ट प्रत्येक ~2 मिनिटात स्वयंचलितपणे अपडेट होतात. सध्या हे अक्षम करणे शक्य नाही.",
|
||||||
|
"board_address_tooltip": "या बोर्डच्या पीअर-टू-पीअर नोडचा पत्ता"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "In afwachting van moderatorgoedkeuring, niet zichtbaar voor gebruikers",
|
"pending_mod_approval": "In afwachting van moderatorgoedkeuring, niet zichtbaar voor gebruikers",
|
||||||
"copy_user_id": "gebruikers-ID kopiëren",
|
"copy_user_id": "gebruikers-ID kopiëren",
|
||||||
"copy_direct_link": "directe link kopiëren"
|
"copy_direct_link": "directe link kopiëren",
|
||||||
|
"posts_auto_update_info": "Berichten worden automatisch ongeveer elke ~2 minuten bijgewerkt. Het is momenteel niet mogelijk dit uit te schakelen.",
|
||||||
|
"board_address_tooltip": "Het adres van de peer-to-peer-node van dit bord"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "Venter på moderatorgodkjenning, ikke synlig for brukere",
|
"pending_mod_approval": "Venter på moderatorgodkjenning, ikke synlig for brukere",
|
||||||
"copy_user_id": "kopier bruker-ID",
|
"copy_user_id": "kopier bruker-ID",
|
||||||
"copy_direct_link": "kopier direkte lenke"
|
"copy_direct_link": "kopier direkte lenke",
|
||||||
|
"posts_auto_update_info": "Innlegg oppdateres automatisk omtrent hver ~2 minutt. Det er ikke mulig å deaktivere dette for øyeblikket.",
|
||||||
|
"board_address_tooltip": "Adressen til denne tavlens peer-to-peer-node"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "Oczekiwanie na zatwierdzenie moderatora, niewidoczne dla użytkowników",
|
"pending_mod_approval": "Oczekiwanie na zatwierdzenie moderatora, niewidoczne dla użytkowników",
|
||||||
"copy_user_id": "skopiuj ID użytkownika",
|
"copy_user_id": "skopiuj ID użytkownika",
|
||||||
"copy_direct_link": "skopiuj bezpośredni link"
|
"copy_direct_link": "skopiuj bezpośredni link",
|
||||||
|
"posts_auto_update_info": "Posty aktualizują się automatycznie co ~2 minuty. Chwilowo nie jest możliwe wyłączenie tej funkcji.",
|
||||||
|
"board_address_tooltip": "Adres węzła peer-to-peer tej tablicy"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "Não",
|
"no": "Não",
|
||||||
"pending_mod_approval": "Aguardando aprovação do moderador, não visível para os usuários",
|
"pending_mod_approval": "Aguardando aprovação do moderador, não visível para os usuários",
|
||||||
"copy_user_id": "Copiar ID do usuário",
|
"copy_user_id": "Copiar ID do usuário",
|
||||||
"copy_direct_link": "Copiar link direto"
|
"copy_direct_link": "Copiar link direto",
|
||||||
|
"posts_auto_update_info": "Posts são atualizados automaticamente a cada ~2 minutos. Não é possível desabilitar isso no momento.",
|
||||||
|
"board_address_tooltip": "O endereço do nó peer-to-peer desta placa"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "În așteptarea aprobării moderatorului, nu este vizibil pentru utilizatori",
|
"pending_mod_approval": "În așteptarea aprobării moderatorului, nu este vizibil pentru utilizatori",
|
||||||
"copy_user_id": "copiați ID-ul utilizatorului",
|
"copy_user_id": "copiați ID-ul utilizatorului",
|
||||||
"copy_direct_link": "copiați linkul direct"
|
"copy_direct_link": "copiați linkul direct",
|
||||||
|
"posts_auto_update_info": "Postările se actualizează automat la fiecare ~2 minute. În prezent, nu este posibil să dezactivezi aceasta.",
|
||||||
|
"board_address_tooltip": "Adresa nodului peer-to-peer al acestei plăci"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "Нет",
|
"no": "Нет",
|
||||||
"pending_mod_approval": "Ожидает одобрения модератора, не видно пользователям",
|
"pending_mod_approval": "Ожидает одобрения модератора, не видно пользователям",
|
||||||
"copy_user_id": "копировать ID пользователя",
|
"copy_user_id": "копировать ID пользователя",
|
||||||
"copy_direct_link": "копировать прямую ссылку"
|
"copy_direct_link": "копировать прямую ссылку",
|
||||||
|
"posts_auto_update_info": "Посты обновляются автоматически примерно каждые ~2 минуты. В данный момент невозможно отключить эту функцию.",
|
||||||
|
"board_address_tooltip": "Адрес пирингового узла этой доски"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "Duke pritur miratimin e moderatorit, jo i dukshëm për përdoruesit",
|
"pending_mod_approval": "Duke pritur miratimin e moderatorit, jo i dukshëm për përdoruesit",
|
||||||
"copy_user_id": "kopjo ID e përdoruesit",
|
"copy_user_id": "kopjo ID e përdoruesit",
|
||||||
"copy_direct_link": "kopjo lidhjen e drejtpërdrejtë"
|
"copy_direct_link": "kopjo lidhjen e drejtpërdrejtë",
|
||||||
|
"posts_auto_update_info": "Postimet përditësohen automatikisht çdo ~2 minuta. Nuk është e mundur ta çaktivizoni këtë për momentin.",
|
||||||
|
"board_address_tooltip": "Adresa e nyjës peer-to-peer të kësaj tabele"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "Väntar på moderatorgodkännande, inte synligt för användare",
|
"pending_mod_approval": "Väntar på moderatorgodkännande, inte synligt för användare",
|
||||||
"copy_user_id": "kopiera användar-ID",
|
"copy_user_id": "kopiera användar-ID",
|
||||||
"copy_direct_link": "kopiera direktlänk"
|
"copy_direct_link": "kopiera direktlänk",
|
||||||
|
"posts_auto_update_info": "Inlägg uppdateras automatiskt ungefär var ~2:e minut. Det är för närvarande inte möjligt att inaktivera detta.",
|
||||||
|
"board_address_tooltip": "Adressen till denna tavlas peer-to-peer-nod"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "మాడరేటర్ ఆమోదం కోసం చేతనిస్తోంది, వినియోగదారులకు కనిపించవు",
|
"pending_mod_approval": "మాడరేటర్ ఆమోదం కోసం చేతనిస్తోంది, వినియోగదారులకు కనిపించవు",
|
||||||
"copy_user_id": "యూజర్ ID కాపీ చేయండి",
|
"copy_user_id": "యూజర్ ID కాపీ చేయండి",
|
||||||
"copy_direct_link": "డైరెక్ట్ లింక్ కాపీ చేయండి"
|
"copy_direct_link": "డైరెక్ట్ లింక్ కాపీ చేయండి",
|
||||||
|
"posts_auto_update_info": "పోస్టులు ప్రతి ~2 నిమిషాలకు స్వయంచాలకంగా నవీకరించబడతాయి. ప్రస్తుతం దీనిని నిలిపివేయడం సম్భవం కాదు.",
|
||||||
|
"board_address_tooltip": "ఈ బోర్డ్ యొక్క పీర్-టు-పీర్ నోడ్ యొక్క చిరునామా"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "รอการอนุมัติจากผู้ดูแล ไม่มองเห็นสำหรับผู้ใช้",
|
"pending_mod_approval": "รอการอนุมัติจากผู้ดูแล ไม่มองเห็นสำหรับผู้ใช้",
|
||||||
"copy_user_id": "คัดลอก ID ผู้ใช้",
|
"copy_user_id": "คัดลอก ID ผู้ใช้",
|
||||||
"copy_direct_link": "คัดลอกลิงก์โดยตรง"
|
"copy_direct_link": "คัดลอกลิงก์โดยตรง",
|
||||||
|
"posts_auto_update_info": "โพสต์จะอัปเดตโดยอัตโนมัติทุก ~2 นาที ไม่สามารถปิดการใช้งานนี้ได้ในขณะนี้",
|
||||||
|
"board_address_tooltip": "ที่อยู่โหนด P2P ของบอร์ดนี้"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "Moderatör onayı beklemede, kullanıcılara görünmüyor",
|
"pending_mod_approval": "Moderatör onayı beklemede, kullanıcılara görünmüyor",
|
||||||
"copy_user_id": "kullanıcı ID'sini kopyala",
|
"copy_user_id": "kullanıcı ID'sini kopyala",
|
||||||
"copy_direct_link": "doğrudan bağlantıyı kopyala"
|
"copy_direct_link": "doğrudan bağlantıyı kopyala",
|
||||||
|
"posts_auto_update_info": "Gönderiler her ~2 dakikada otomatik olarak güncellenir. Şu anda bunu devre dışı bırakmak mümkün değildir.",
|
||||||
|
"board_address_tooltip": "Bu panonun eşler arası düğüm adresi"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "Очікує затвердження модератора, не видно користувачам",
|
"pending_mod_approval": "Очікує затвердження модератора, не видно користувачам",
|
||||||
"copy_user_id": "копіювати ID користувача",
|
"copy_user_id": "копіювати ID користувача",
|
||||||
"copy_direct_link": "копіювати пряме посилання"
|
"copy_direct_link": "копіювати пряме посилання",
|
||||||
|
"posts_auto_update_info": "Дописи оновлюються автоматично приблизно кожні ~2 хвилини. В даний момент неможливо вимкнути цю функцію.",
|
||||||
|
"board_address_tooltip": "Адреса пірінгового вузла цієї дошки"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "ماڈریٹر کی منظوری کا انتظار، صارفین کے لیے نظر نہیں آتا",
|
"pending_mod_approval": "ماڈریٹر کی منظوری کا انتظار، صارفین کے لیے نظر نہیں آتا",
|
||||||
"copy_user_id": "صارف کی شناخت کاپی کریں",
|
"copy_user_id": "صارف کی شناخت کاپی کریں",
|
||||||
"copy_direct_link": "براہ راست لنک کاپی کریں"
|
"copy_direct_link": "براہ راست لنک کاپی کریں",
|
||||||
|
"posts_auto_update_info": "پوسٹس ہر ~2 منٹ میں خود بخود اپ ڈیٹ ہوتے ہیں۔ فی الوقت اس کو غیر فعال کرنا ممکن نہیں ہے۔",
|
||||||
|
"board_address_tooltip": "اس بورڈ کے پیر ٹو پیر نوڈ کا پتہ"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "No",
|
"no": "No",
|
||||||
"pending_mod_approval": "Chờ duyệt của người điều hành, không hiển thị cho người dùng",
|
"pending_mod_approval": "Chờ duyệt của người điều hành, không hiển thị cho người dùng",
|
||||||
"copy_user_id": "sao chép ID người dùng",
|
"copy_user_id": "sao chép ID người dùng",
|
||||||
"copy_direct_link": "sao chép liên kết trực tiếp"
|
"copy_direct_link": "sao chép liên kết trực tiếp",
|
||||||
|
"posts_auto_update_info": "Bài đăng được cập nhật tự động cứ ~2 phút một lần. Hiện tại không thể tắt tính năng này.",
|
||||||
|
"board_address_tooltip": "Địa chỉ nút peer-to-peer của bảng này"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,5 +277,7 @@
|
|||||||
"no": "否",
|
"no": "否",
|
||||||
"pending_mod_approval": "等待版主批准,用户不可见",
|
"pending_mod_approval": "等待版主批准,用户不可见",
|
||||||
"copy_user_id": "复制用户ID",
|
"copy_user_id": "复制用户ID",
|
||||||
"copy_direct_link": "复制直接链接"
|
"copy_direct_link": "复制直接链接",
|
||||||
|
"posts_auto_update_info": "帖子每约2分钟自动更新一次。目前无法禁用此功能。",
|
||||||
|
"board_address_tooltip": "此板块的点对点节点地址"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ SCRIPT="
|
|||||||
cd ~
|
cd ~
|
||||||
rm $FIVECHAN_HTML_NAME.zip
|
rm $FIVECHAN_HTML_NAME.zip
|
||||||
rm -fr $FIVECHAN_HTML_NAME
|
rm -fr $FIVECHAN_HTML_NAME
|
||||||
wget https://github.com/plebbit/5chan/releases/download/v$FIVECHAN_VERSION/$FIVECHAN_HTML_NAME.zip || exit
|
wget https://github.com/bitsocialhq/5chan/releases/download/v$FIVECHAN_VERSION/$FIVECHAN_HTML_NAME.zip || exit
|
||||||
|
|
||||||
# extract html
|
# extract html
|
||||||
unzip $FIVECHAN_HTML_NAME.zip || exit
|
unzip $FIVECHAN_HTML_NAME.zip || exit
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ if (remoteFiles.length) {
|
|||||||
files = remoteFiles;
|
files = remoteFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
const linkTo = (file) => `https://github.com/plebbit/5chan/releases/download/v${version}/${file}`;
|
const linkTo = (file) => `https://github.com/bitsocialhq/5chan/releases/download/v${version}/${file}`;
|
||||||
const has = (s, sub) => s.toLowerCase().includes(sub);
|
const has = (s, sub) => s.toLowerCase().includes(sub);
|
||||||
const isArm = (s) => has(s, 'arm64') || has(s, 'aarch64');
|
const isArm = (s) => has(s, 'arm64') || has(s, 'aarch64');
|
||||||
const isX64 = (s) => has(s, 'x64') || has(s, 'x86_64') || !isArm(s);
|
const isX64 = (s) => has(s, 'x64') || has(s, 'x86_64') || !isArm(s);
|
||||||
@@ -102,7 +102,7 @@ const htmlSection = section('Static HTML build', [htmlZip && `- 5chan-html (zip)
|
|||||||
const downloads = [macSection, winSection, linuxSection, androidSection, htmlSection].filter(Boolean).join('\n\n');
|
const downloads = [macSection, winSection, linuxSection, androidSection, htmlSection].filter(Boolean).join('\n\n');
|
||||||
|
|
||||||
// One-liner summary of what changed in this release. Update before each release.
|
// One-liner summary of what changed in this release. Update before each release.
|
||||||
const oneLinerDescription = 'This version adds backlinks for quoted posts, improves reply functionality with proper quote references, and includes several bug fixes.';
|
const oneLinerDescription = 'This version enables the Update button on the post page to refresh replies, adds an informational alert for the Auto checkbox, fixes inline quote link layout, and includes several bug fixes.';
|
||||||
|
|
||||||
const releaseBody = `${oneLinerDescription}
|
const releaseBody = `${oneLinerDescription}
|
||||||
|
|
||||||
|
|||||||
@@ -147,47 +147,32 @@ const RefreshButton = () => {
|
|||||||
|
|
||||||
const UpdateButton = () => {
|
const UpdateButton = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isMobile = useIsMobile();
|
const reset = useFeedResetStore((state) => state.reset);
|
||||||
|
|
||||||
const handleManualUpdate = () => {
|
|
||||||
window.alert('Manual updates are not available yet. Posts update automatically every ~2 minutes.');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<button className='button' onClick={() => reset?.()}>
|
||||||
{/* TODO: Implement update button once available in API */}
|
|
||||||
{isMobile ? (
|
|
||||||
<button className={`button ${styles.disabledButton}`} onClick={handleManualUpdate}>
|
|
||||||
{t('update')}
|
{t('update')}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
|
||||||
<button className={`button ${styles.disabledButton}`} onClick={handleManualUpdate}>
|
|
||||||
{t('update')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AutoButton = () => {
|
const AutoButton = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const handleAutoClick = () => {
|
||||||
const handleManualUpdate = () => {
|
window.alert(t('posts_auto_update_info'));
|
||||||
window.alert('Manual updates are not available yet. Posts update automatically every ~2 minutes.');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<button className='button' onClick={handleManualUpdate}>
|
<button className='button' onClick={handleAutoClick}>
|
||||||
<label>
|
<label>
|
||||||
<input type='checkbox' className={styles.autoCheckbox} checked disabled />
|
<input type='checkbox' className={styles.autoCheckbox} checked disabled />
|
||||||
{t('Auto')}
|
{t('Auto')}
|
||||||
</label>
|
</label>
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<label onClick={handleManualUpdate}>
|
<label onClick={handleAutoClick}>
|
||||||
{' '}
|
{' '}
|
||||||
<input type='checkbox' className={styles.autoCheckbox} checked disabled /> {t('Auto')}
|
<input type='checkbox' className={styles.autoCheckbox} checked disabled /> {t('Auto')}
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ const BoardHeader = () => {
|
|||||||
<span className={styles.clickableSubtitle} onClick={() => navigate('/subs/settings#subscriptions-settings')}>
|
<span className={styles.clickableSubtitle} onClick={() => navigate('/subs/settings#subscriptions-settings')}>
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</span>
|
</span>
|
||||||
|
) : !isInAllView && !isInModView && subtitle ? (
|
||||||
|
<span title={t('board_address_tooltip')}>{subtitle}</span>
|
||||||
) : (
|
) : (
|
||||||
subtitle
|
subtitle
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Fragment, useMemo, useState } from 'react';
|
import { Fragment, useCallback, useMemo, useState } from 'react';
|
||||||
import { useLocation, useParams } from 'react-router-dom';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { Comment, useComment } from '@plebbit/plebbit-react-hooks';
|
import { Comment, useComment } from '@plebbit/plebbit-react-hooks';
|
||||||
@@ -25,6 +25,41 @@ const QuotedCidLink = ({ cid, postCid }: { cid: string; postCid: string }) => {
|
|||||||
return <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotedComment} isOP={isOP} />;
|
return <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotedComment} isOP={isOP} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const useScopedCidToNumber = (cids: string[]) => {
|
||||||
|
const sortedUniqueCids = useMemo(() => {
|
||||||
|
const uniqueCids = new Set<string>();
|
||||||
|
for (const cid of cids) {
|
||||||
|
if (cid) {
|
||||||
|
uniqueCids.add(cid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...uniqueCids].sort();
|
||||||
|
}, [cids]);
|
||||||
|
|
||||||
|
// Subscribe only to CIDs this comment needs so unrelated thread updates do not rerender content.
|
||||||
|
const cidNumbersSignature = usePostNumberStore(
|
||||||
|
useCallback((state) => sortedUniqueCids.map((cid) => `${cid}:${state.cidToNumber[cid] ?? ''}`).join('|'), [sortedUniqueCids]),
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (sortedUniqueCids.length === 0) {
|
||||||
|
return {} as Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { cidToNumber } = usePostNumberStore.getState();
|
||||||
|
const nextCidToNumber: Record<string, number> = {};
|
||||||
|
|
||||||
|
for (const cid of sortedUniqueCids) {
|
||||||
|
const number = cidToNumber[cid];
|
||||||
|
if (typeof number === 'number') {
|
||||||
|
nextCidToNumber[cid] = number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextCidToNumber;
|
||||||
|
}, [sortedUniqueCids, cidNumbersSignature]);
|
||||||
|
};
|
||||||
|
|
||||||
const CommentContent = ({ comment: post }: { comment: Comment }) => {
|
const CommentContent = ({ comment: post }: { comment: Comment }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -59,7 +94,15 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => {
|
|||||||
return new Set([...matches].map((m) => parseInt(m[1], 10)));
|
return new Set([...matches].map((m) => parseInt(m[1], 10)));
|
||||||
}, [content]);
|
}, [content]);
|
||||||
|
|
||||||
const cidToNumber = usePostNumberStore((state) => state.cidToNumber);
|
const relevantQuotedCids = useMemo(() => {
|
||||||
|
const cids = quotedCids ? [...quotedCids] : [];
|
||||||
|
if (parentCid) {
|
||||||
|
cids.push(parentCid);
|
||||||
|
}
|
||||||
|
return cids;
|
||||||
|
}, [quotedCids, parentCid]);
|
||||||
|
|
||||||
|
const cidToNumber = useScopedCidToNumber(relevantQuotedCids);
|
||||||
const filteredQuotedCids = useMemo(() => {
|
const filteredQuotedCids = useMemo(() => {
|
||||||
if (!quotedCids?.length) return [];
|
if (!quotedCids?.length) return [];
|
||||||
return quotedCids.filter((cid: string) => {
|
return quotedCids.filter((cid: string) => {
|
||||||
@@ -71,13 +114,16 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => {
|
|||||||
const shouldShowReplyingToReply = isReplyingToReply && (parentCid ? !contentNumbers.has(cidToNumber[parentCid] ?? -1) : true);
|
const shouldShowReplyingToReply = isReplyingToReply && (parentCid ? !contentNumbers.has(cidToNumber[parentCid] ?? -1) : true);
|
||||||
|
|
||||||
const stateString = useStateString(post);
|
const stateString = useStateString(post);
|
||||||
|
const hasFailedState = state === 'failed';
|
||||||
|
|
||||||
const loadingString = <div className={styles.stateString}>{stateString !== 'Failed' ? <LoadingEllipsis string={stateString || t('loading')} /> : stateString}</div>;
|
const loadingString = (
|
||||||
|
<div className={styles.stateString}>{!hasFailedState ? <LoadingEllipsis string={stateString || t('loading')} /> : stateString || capitalize(t('failed'))}</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<blockquote className={`${styles.postMessage} ${!isReply && isMobile && styles.clampLines}`}>
|
<blockquote className={`${styles.postMessage} ${!isReply && isMobile && styles.clampLines}`}>
|
||||||
{isReply &&
|
{isReply &&
|
||||||
state !== 'failed' &&
|
!hasFailedState &&
|
||||||
!(deleted || removed) &&
|
!(deleted || removed) &&
|
||||||
(filteredQuotedCids.length > 0
|
(filteredQuotedCids.length > 0
|
||||||
? filteredQuotedCids.map((cid: string) => <QuotedCidLink key={cid} cid={cid} postCid={postCid} />)
|
? filteredQuotedCids.map((cid: string) => <QuotedCidLink key={cid} cid={cid} postCid={postCid} />)
|
||||||
@@ -163,7 +209,7 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => {
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!cid && state === 'pending' && stateString !== 'Failed' && (
|
{!cid && !hasFailedState && (
|
||||||
<>
|
<>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -25,13 +25,9 @@ const CreateBoardModal = () => {
|
|||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
<h3>Creating Your Board</h3>
|
<h3>Creating Your Board</h3>
|
||||||
<p>
|
<p>
|
||||||
Create a board using the{' '}
|
Create a board using the CLI:
|
||||||
<a href='https://plebbit.github.io/docs/learn/clients/5chan/create-a-board' target='_blank' rel='noopener noreferrer'>
|
<a href='https://github.com/bitsocialhq/bitsocial-cli' target='_blank' rel='noopener noreferrer'>
|
||||||
Seedit GUI client
|
bitsocial-cli
|
||||||
</a>{' '}
|
|
||||||
or{' '}
|
|
||||||
<a href='https://github.com/plebbit/plebbit-cli' target='_blank' rel='noopener noreferrer'>
|
|
||||||
plebbit-cli
|
|
||||||
</a>
|
</a>
|
||||||
. <strong>Build a following:</strong> Users can subscribe to your board via the "[Subscribe]" button, which adds it to their top bar. You can gain
|
. <strong>Build a following:</strong> Users can subscribe to your board via the "[Subscribe]" button, which adds it to their top bar. You can gain
|
||||||
subscribers through direct links, word of mouth, or search—no directory assignment or dev approval needed.
|
subscribers through direct links, word of mouth, or search—no directory assignment or dev approval needed.
|
||||||
@@ -42,7 +38,7 @@ const CreateBoardModal = () => {
|
|||||||
<h3>Submitting to a Directory</h3>
|
<h3>Submitting to a Directory</h3>
|
||||||
<p>
|
<p>
|
||||||
Open a PR editing{' '}
|
Open a PR editing{' '}
|
||||||
<a href='https://github.com/plebbit/lists/blob/master/5chan-directories.json' target='_blank' rel='noopener noreferrer'>
|
<a href='https://github.com/bitsocialhq/lists/blob/master/5chan-directories.json' target='_blank' rel='noopener noreferrer'>
|
||||||
5chan-directories.json
|
5chan-directories.json
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
with your board's title, address, and NSFW status. Devs will review and merge if approved. 99% uptime is required.
|
with your board's title, address, and NSFW status. Devs will review and merge if approved. 99% uptime is required.
|
||||||
|
|||||||
@@ -32,13 +32,9 @@ const DirectoryModal = () => {
|
|||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
<h3>Creating Your Board</h3>
|
<h3>Creating Your Board</h3>
|
||||||
<p>
|
<p>
|
||||||
<strong>Anyone can create a board</strong> using the{' '}
|
<strong>Anyone can create a board</strong> using the official CLI:{' '}
|
||||||
<a href='https://plebbit.github.io/docs/learn/clients/5chan/create-a-board' target='_blank' rel='noopener noreferrer'>
|
<a href='https://github.com/bitsocialhq/bitsocial-cli' target='_blank' rel='noopener noreferrer'>
|
||||||
Seedit GUI client
|
bitsocial-cli
|
||||||
</a>{' '}
|
|
||||||
or{' '}
|
|
||||||
<a href='https://github.com/plebbit/plebbit-cli' target='_blank' rel='noopener noreferrer'>
|
|
||||||
plebbit-cli
|
|
||||||
</a>
|
</a>
|
||||||
. Users can access it anytime via the search bar, direct links, or by subscribing with the "[Subscribe]" button—
|
. Users can access it anytime via the search bar, direct links, or by subscribing with the "[Subscribe]" button—
|
||||||
<strong>no directory assignment or dev approval needed</strong>. Directory boards are simply featured in homepage categories (like "Anime &
|
<strong>no directory assignment or dev approval needed</strong>. Directory boards are simply featured in homepage categories (like "Anime &
|
||||||
@@ -50,7 +46,7 @@ const DirectoryModal = () => {
|
|||||||
<h3>Directory Submission</h3>
|
<h3>Directory Submission</h3>
|
||||||
<p>
|
<p>
|
||||||
To submit your board, open a PR editing{' '}
|
To submit your board, open a PR editing{' '}
|
||||||
<a href='https://github.com/plebbit/lists/blob/master/5chan-directories.json' target='_blank' rel='noopener noreferrer'>
|
<a href='https://github.com/bitsocialhq/lists/blob/master/5chan-directories.json' target='_blank' rel='noopener noreferrer'>
|
||||||
5chan-directories.json
|
5chan-directories.json
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
with your board's title, address, and NSFW status. Requirements: 99% uptime (they're P2P nodes), active, well-moderated, and relevant to the
|
with your board's title, address, and NSFW status. Requirements: 99% uptime (they're P2P nodes), active, well-moderated, and relevant to the
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState, useMemo, useCallback } from 'react';
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { autoUpdate, FloatingFocusManager, offset, shift, useClick, useDismiss, useFloating, useId, useInteractions, useRole } from '@floating-ui/react';
|
import { autoUpdate, FloatingFocusManager, offset, shift, useClick, useDismiss, useFloating, useId, useInteractions, useRole } from '@floating-ui/react';
|
||||||
import {
|
import {
|
||||||
@@ -33,30 +33,22 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const { author, cid, content, deleted, locked, parentCid, pinned, postCid, reason, removed, spoiler, subplebbitAddress } = post || {};
|
const { author, cid, content, deleted, locked, parentCid, pinned, postCid, reason, removed, spoiler, subplebbitAddress } = post || {};
|
||||||
|
const authorDisplayName = post?.author?.displayName;
|
||||||
|
const modBanExpiresAt = post?.commentModeration?.author?.banExpiresAt;
|
||||||
const purged = post?.commentModeration?.purged ?? false;
|
const purged = post?.commentModeration?.purged ?? false;
|
||||||
const [isEditMenuOpen, setIsEditMenuOpen] = useState(false);
|
const [isEditMenuOpen, setIsEditMenuOpen] = useState(false);
|
||||||
const [isContentEditorOpen, setIsContentEditorOpen] = useState(false);
|
const [isContentEditorOpen, setIsContentEditorOpen] = useState(false);
|
||||||
|
|
||||||
const account = useAccount();
|
const account = useAccount();
|
||||||
const [signer, setSigner] = useState<any>(account?.signer);
|
|
||||||
|
|
||||||
const { isCommentAuthorMod, isAccountMod, isAccountCommentAuthor } = useAuthorPrivileges({
|
const { isCommentAuthorMod, isAccountMod, isAccountCommentAuthor } = useAuthorPrivileges({
|
||||||
commentAuthorAddress: author?.address,
|
commentAuthorAddress: author?.address,
|
||||||
subplebbitAddress,
|
subplebbitAddress,
|
||||||
postCid,
|
postCid,
|
||||||
});
|
});
|
||||||
|
const signer = isAccountCommentAuthor ? account?.signer : null;
|
||||||
const checkSigner = useCallback(() => {
|
const latestPostRef = useRef(post);
|
||||||
if (isAccountCommentAuthor) {
|
latestPostRef.current = post;
|
||||||
setSigner(account?.signer);
|
const onChallenge = useCallback((...args: any) => addChallenge([...args, latestPostRef.current]), []);
|
||||||
} else {
|
|
||||||
setSigner(null);
|
|
||||||
}
|
|
||||||
}, [isAccountCommentAuthor, account?.signer]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
checkSigner();
|
|
||||||
}, [checkSigner]);
|
|
||||||
|
|
||||||
const defaultPublishEditOptions = useMemo(() => {
|
const defaultPublishEditOptions = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
@@ -75,17 +67,17 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
|||||||
purged: purged ?? false,
|
purged: purged ?? false,
|
||||||
spoiler: spoiler ?? false,
|
spoiler: spoiler ?? false,
|
||||||
reason,
|
reason,
|
||||||
author: post?.commentModeration?.author?.banExpiresAt ? { banExpiresAt: post.commentModeration.author.banExpiresAt } : undefined,
|
author: modBanExpiresAt ? { banExpiresAt: modBanExpiresAt } : undefined,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
onChallenge: (...args: any) => addChallenge([...args, post]),
|
onChallenge,
|
||||||
onChallengeVerification: alertChallengeVerificationFailed,
|
onChallengeVerification: alertChallengeVerificationFailed,
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
console.warn(error);
|
console.warn(error);
|
||||||
alert('Comment edit failed. ' + error.message);
|
alert('Comment edit failed. ' + error.message);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [isAccountMod, isAccountCommentAuthor, cid, content, deleted, locked, pinned, reason, removed, purged, spoiler, subplebbitAddress, post]);
|
}, [isAccountMod, isAccountCommentAuthor, cid, content, deleted, locked, pinned, reason, removed, purged, spoiler, subplebbitAddress, modBanExpiresAt, onChallenge]);
|
||||||
|
|
||||||
const [publishCommentEditOptions, setPublishCommentEditOptions] = useState<PublishCommentEditOptions>(defaultPublishEditOptions);
|
const [publishCommentEditOptions, setPublishCommentEditOptions] = useState<PublishCommentEditOptions>(defaultPublishEditOptions);
|
||||||
|
|
||||||
@@ -94,19 +86,19 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
|||||||
commentCid: cid,
|
commentCid: cid,
|
||||||
subplebbitAddress,
|
subplebbitAddress,
|
||||||
signer,
|
signer,
|
||||||
author: signer?.address === author?.address ? { address: signer?.address, displayName: post?.author?.displayName } : account?.author,
|
author: signer?.address === author?.address ? { address: signer?.address, displayName: authorDisplayName } : account?.author,
|
||||||
content: publishCommentEditOptions.content,
|
content: publishCommentEditOptions.content,
|
||||||
deleted: publishCommentEditOptions.deleted,
|
deleted: publishCommentEditOptions.deleted,
|
||||||
reason: publishCommentEditOptions.reason,
|
reason: publishCommentEditOptions.reason,
|
||||||
spoiler: publishCommentEditOptions.spoiler,
|
spoiler: publishCommentEditOptions.spoiler,
|
||||||
onChallenge: (...args: any) => addChallenge([...args, post]),
|
onChallenge,
|
||||||
onChallengeVerification: alertChallengeVerificationFailed,
|
onChallengeVerification: alertChallengeVerificationFailed,
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
console.warn(error);
|
console.warn(error);
|
||||||
alert('Comment edit failed. ' + error.message);
|
alert('Comment edit failed. ' + error.message);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[publishCommentEditOptions, cid, subplebbitAddress, signer, post, account?.author, author?.address],
|
[publishCommentEditOptions, cid, subplebbitAddress, signer, account?.author, author?.address, authorDisplayName, onChallenge],
|
||||||
);
|
);
|
||||||
|
|
||||||
const modEditOptions = useMemo<PublishCommentModerationOptions>(
|
const modEditOptions = useMemo<PublishCommentModerationOptions>(
|
||||||
@@ -123,27 +115,31 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
|||||||
author: publishCommentEditOptions.commentModeration?.author,
|
author: publishCommentEditOptions.commentModeration?.author,
|
||||||
},
|
},
|
||||||
author: account?.author,
|
author: account?.author,
|
||||||
onChallenge: (...args: any) => addChallenge([...args, post]),
|
onChallenge,
|
||||||
onChallengeVerification: alertChallengeVerificationFailed,
|
onChallengeVerification: alertChallengeVerificationFailed,
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
console.warn(error);
|
console.warn(error);
|
||||||
alert('Comment moderation failed. ' + error.message);
|
alert('Comment moderation failed. ' + error.message);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[publishCommentEditOptions, cid, subplebbitAddress, post, account?.author, parentCid],
|
[publishCommentEditOptions, cid, subplebbitAddress, account?.author, parentCid, onChallenge],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { publishCommentEdit: publishAuthorEdit } = usePublishCommentEdit(authorEditOptions);
|
const { publishCommentEdit: publishAuthorEdit } = usePublishCommentEdit(authorEditOptions);
|
||||||
const { publishCommentModeration } = usePublishCommentModeration(modEditOptions);
|
const { publishCommentModeration } = usePublishCommentModeration(modEditOptions);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setPublishCommentEditOptions(defaultPublishEditOptions);
|
|
||||||
}, [defaultPublishEditOptions]);
|
|
||||||
|
|
||||||
const [banDuration, setBanDuration] = useState(() =>
|
const [banDuration, setBanDuration] = useState(() =>
|
||||||
publishCommentEditOptions.commentModeration?.author?.banExpiresAt ? timestampToDays(publishCommentEditOptions.commentModeration.author.banExpiresAt) : 1,
|
defaultPublishEditOptions.commentModeration?.author?.banExpiresAt ? timestampToDays(defaultPublishEditOptions.commentModeration.author.banExpiresAt) : 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const resetMenuState = () => {
|
||||||
|
setPublishCommentEditOptions(defaultPublishEditOptions);
|
||||||
|
setBanDuration(
|
||||||
|
defaultPublishEditOptions.commentModeration?.author?.banExpiresAt ? timestampToDays(defaultPublishEditOptions.commentModeration.author.banExpiresAt) : 1,
|
||||||
|
);
|
||||||
|
setIsContentEditorOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
const onCheckbox = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const onCheckbox = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { id, checked } = e.target;
|
const { id, checked } = e.target;
|
||||||
|
|
||||||
@@ -258,7 +254,12 @@ const EditMenu = ({ post }: { post: Comment }) => {
|
|||||||
type='checkbox'
|
type='checkbox'
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
if (cid && (isAccountCommentAuthor || isAccountMod)) {
|
if (cid && (isAccountCommentAuthor || isAccountMod)) {
|
||||||
setIsEditMenuOpen(!isEditMenuOpen);
|
if (!isEditMenuOpen) {
|
||||||
|
resetMenuState();
|
||||||
|
setIsEditMenuOpen(true);
|
||||||
|
} else {
|
||||||
|
setIsEditMenuOpen(false);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setIsEditMenuOpen(false);
|
setIsEditMenuOpen(false);
|
||||||
alert(parentCid ? t('cannot_edit_reply') : t('cannot_edit_thread'));
|
alert(parentCid ? t('cannot_edit_reply') : t('cannot_edit_thread'));
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ const NumberQuoteLink = ({ number, threadPostCid }: { number: number; threadPost
|
|||||||
return <span>{`>>${number}`}</span>;
|
return <span>{`>>${number}`}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={comment} isOP={isOP} />;
|
return <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={comment} isOP={isOP} showTrailingBreak={false} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAnchorLink = (children: React.ReactNode, href: string, threadPostCid?: string) => {
|
const renderAnchorLink = (children: React.ReactNode, href: string, threadPostCid?: string) => {
|
||||||
@@ -247,14 +247,18 @@ const renderAnchorLink = (children: React.ReactNode, href: string, threadPostCid
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Markdown = ({ content, title, postCid }: MarkdownProps) => {
|
const Markdown = ({ content, title, postCid }: MarkdownProps) => {
|
||||||
const remarkPlugins: any[] = [[supersub]];
|
const remarkPlugins = useMemo(() => {
|
||||||
|
const plugins: any[] = [[supersub]];
|
||||||
|
|
||||||
if (content && content.length <= MAX_LENGTH_FOR_GFM) {
|
if (content && content.length <= MAX_LENGTH_FOR_GFM) {
|
||||||
remarkPlugins.push([remarkGfm, { singleTilde: false }]);
|
plugins.push([remarkGfm, { singleTilde: false }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
remarkPlugins.push([blockquoteToGreentext]);
|
plugins.push([blockquoteToGreentext]);
|
||||||
remarkPlugins.push([spoilerTransform]);
|
plugins.push([spoilerTransform]);
|
||||||
|
|
||||||
|
return plugins;
|
||||||
|
}, [content]);
|
||||||
|
|
||||||
const customSchema = useMemo(
|
const customSchema = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -270,25 +274,18 @@ const Markdown = ({ content, title, postCid }: MarkdownProps) => {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const isInCatalogView = isCatalogView(useLocation().pathname, useParams());
|
const location = useLocation();
|
||||||
|
const params = useParams();
|
||||||
|
const isInCatalogView = isCatalogView(location.pathname, params);
|
||||||
|
|
||||||
|
const rehypePlugins = useMemo(() => [[rehypeRaw as any], [rehypeSanitize, customSchema]] as any[], [customSchema]);
|
||||||
|
|
||||||
// Preprocess content to convert plain text 5chan patterns to markdown links
|
// Preprocess content to convert plain text 5chan patterns to markdown links
|
||||||
const processedContent = preprocess5chanPatterns(content || '');
|
const processedContent = useMemo(() => preprocess5chanPatterns(content || ''), [content]);
|
||||||
|
|
||||||
return (
|
const components = useMemo(
|
||||||
<span className={styles.markdown}>
|
() =>
|
||||||
{isInCatalogView && title && (
|
({
|
||||||
<span>
|
|
||||||
<b>{title}</b>
|
|
||||||
{content ? ': ' : ''}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<ReactMarkdown
|
|
||||||
children={processedContent}
|
|
||||||
remarkPlugins={remarkPlugins}
|
|
||||||
rehypePlugins={[[rehypeRaw as any], [rehypeSanitize, customSchema]]}
|
|
||||||
components={
|
|
||||||
{
|
|
||||||
p: ({ children }) => <p className={isInCatalogView ? styles.inline : ''}>{children}</p>,
|
p: ({ children }) => <p className={isInCatalogView ? styles.inline : ''}>{children}</p>,
|
||||||
h1: ({ children }) => <p className={styles.header}>{children}</p>,
|
h1: ({ children }) => <p className={styles.header}>{children}</p>,
|
||||||
h2: ({ children }) => <p className={styles.header}>{children}</p>,
|
h2: ({ children }) => <p className={styles.header}>{children}</p>,
|
||||||
@@ -321,9 +318,19 @@ const Markdown = ({ content, title, postCid }: MarkdownProps) => {
|
|||||||
|
|
||||||
return renderAnchorLink(children, href || '', postCid);
|
return renderAnchorLink(children, href || '', postCid);
|
||||||
},
|
},
|
||||||
} as ExtendedComponents
|
}) as ExtendedComponents,
|
||||||
}
|
[isInCatalogView, postCid],
|
||||||
/>
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={styles.markdown}>
|
||||||
|
{isInCatalogView && title && (
|
||||||
|
<span>
|
||||||
|
<b>{title}</b>
|
||||||
|
{content ? ': ' : ''}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<ReactMarkdown children={processedContent} remarkPlugins={remarkPlugins} rehypePlugins={rehypePlugins} components={components} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
|||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom';
|
import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom';
|
||||||
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
||||||
import { Comment, useAuthorAvatar, useEditedComment, useReplies, useAccount } from '@plebbit/plebbit-react-hooks';
|
import { Comment, useAuthorAvatar, useEditedComment, useReplies, useAccount, useAccountComment } from '@plebbit/plebbit-react-hooks';
|
||||||
import Plebbit from '@plebbit/plebbit-js';
|
import Plebbit from '@plebbit/plebbit-js';
|
||||||
import styles from '../../views/post/post.module.css';
|
import styles from '../../views/post/post.module.css';
|
||||||
import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils';
|
import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils';
|
||||||
import { hashStringToColor, getTextColorForBackground } from '../../lib/utils/post-utils';
|
import { hashStringToColor, getTextColorForBackground } from '../../lib/utils/post-utils';
|
||||||
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
||||||
import { isValidURL, QUOTE_NUMBER_REGEX } from '../../lib/utils/url-utils';
|
import { isValidURL } from '../../lib/utils/url-utils';
|
||||||
import { isAllView, isModQueueView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
import { isAllView, isModQueueView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||||
import { formatUserIDForDisplay } from '../../lib/utils/string-utils';
|
import { formatUserIDForDisplay } from '../../lib/utils/string-utils';
|
||||||
import useModQueueStore from '../../stores/use-mod-queue-store';
|
import useModQueueStore from '../../stores/use-mod-queue-store';
|
||||||
@@ -38,9 +38,11 @@ import { shouldShowSnow } from '../../lib/snow';
|
|||||||
import useReplyModalStore from '../../stores/use-reply-modal-store';
|
import useReplyModalStore from '../../stores/use-reply-modal-store';
|
||||||
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
|
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
|
||||||
import useChallengesStore from '../../stores/use-challenges-store';
|
import useChallengesStore from '../../stores/use-challenges-store';
|
||||||
|
import useFeedResetStore from '../../stores/use-feed-reset-store';
|
||||||
import usePostNumberStore from '../../stores/use-post-number-store';
|
import usePostNumberStore from '../../stores/use-post-number-store';
|
||||||
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
|
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
|
||||||
import { usePublishCommentModeration } from '@plebbit/plebbit-react-hooks';
|
import { usePublishCommentModeration } from '@plebbit/plebbit-react-hooks';
|
||||||
|
import useQuotedByMap from '../../hooks/use-quoted-by-map';
|
||||||
|
|
||||||
const { addChallenge } = useChallengesStore.getState();
|
const { addChallenge } = useChallengesStore.getState();
|
||||||
|
|
||||||
@@ -76,15 +78,15 @@ const PostInfo = ({
|
|||||||
onApprove,
|
onApprove,
|
||||||
onReject,
|
onReject,
|
||||||
quotedByMap,
|
quotedByMap,
|
||||||
}: PostProps) => {
|
directRepliesByParentCid,
|
||||||
|
}: PostProps & { directRepliesByParentCid?: Map<string, Comment[]> }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { author, cid, deleted, locked, pinned, parentCid, postCid, reason, removed, state, subplebbitAddress, timestamp } = post || {};
|
const { author, cid, deleted, locked, pinned, parentCid, postCid, reason, removed, state, subplebbitAddress, timestamp } = post || {};
|
||||||
const title = post?.title?.trim();
|
const title = post?.title?.trim();
|
||||||
const { replies } = useReplies({ comment: post, accountComments: { newerThan: Infinity } });
|
|
||||||
const { address, shortAddress } = author || {};
|
const { address, shortAddress } = author || {};
|
||||||
const displayName = author?.displayName?.trim();
|
const displayName = author?.displayName?.trim();
|
||||||
const authorRole = roles?.[address]?.role?.replace('moderator', 'mod');
|
const authorRole = roles?.[address]?.role?.replace('moderator', 'mod');
|
||||||
const stateString = useStateString(post);
|
const hasFailedState = state === 'failed';
|
||||||
const isReply = parentCid;
|
const isReply = parentCid;
|
||||||
const { showOmittedReplies } = useShowOmittedReplies();
|
const { showOmittedReplies } = useShowOmittedReplies();
|
||||||
const { imageUrl: avatarImageUrl } = useAuthorAvatar({ author });
|
const { imageUrl: avatarImageUrl } = useAuthorAvatar({ author });
|
||||||
@@ -203,12 +205,18 @@ const PostInfo = ({
|
|||||||
const userIDBackgroundColor = hashStringToColor(userID);
|
const userIDBackgroundColor = hashStringToColor(userID);
|
||||||
const userIDTextColor = getTextColorForBackground(userIDBackgroundColor);
|
const userIDTextColor = getTextColorForBackground(userIDBackgroundColor);
|
||||||
|
|
||||||
const handleUserAddressClick = useAuthorAddressClick();
|
|
||||||
const numberOfPostsByAuthor = document.querySelectorAll(`[data-author-address="${shortAddress}"][data-post-cid="${postCid}"]`).length;
|
|
||||||
|
|
||||||
const pseudonymityMode = useSubplebbitField(subplebbitAddress, (sub) => sub?.features?.pseudonymityMode);
|
const pseudonymityMode = useSubplebbitField(subplebbitAddress, (sub) => sub?.features?.pseudonymityMode);
|
||||||
const showUserID = pseudonymityMode !== 'per-reply';
|
const showUserID = pseudonymityMode !== 'per-reply';
|
||||||
|
|
||||||
|
const handleUserAddressClick = useAuthorAddressClick();
|
||||||
|
const numberOfPostsByAuthor = useMemo(() => {
|
||||||
|
if (!showUserID || deleted || removed || !shortAddress || !postCid || typeof document === 'undefined') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return document.querySelectorAll(`[data-author-address="${shortAddress}"][data-post-cid="${postCid}"]`).length;
|
||||||
|
}, [showUserID, deleted, removed, shortAddress, postCid]);
|
||||||
|
|
||||||
const { hidden } = useHide(post);
|
const { hidden } = useHide(post);
|
||||||
|
|
||||||
const { openReplyModal } = useReplyModalStore();
|
const { openReplyModal } = useReplyModalStore();
|
||||||
@@ -327,9 +335,7 @@ const PostInfo = ({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<span>No.</span>
|
<span>No.</span>
|
||||||
<span className={styles.pendingCid}>
|
<span className={styles.pendingCid}>{hasFailedState ? capitalize(t('failed')) : capitalize(t('pending'))}</span>
|
||||||
{state === 'failed' || stateString === 'Failed' ? capitalize(t('failed')) : state === 'pending' ? capitalize(t('pending')) : ''}
|
|
||||||
</span>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{pinned && (
|
{pinned && (
|
||||||
@@ -419,18 +425,35 @@ const PostInfo = ({
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{!(removed || deleted) && !isModQueue && <PostMenuDesktop postMenu={postMenuProps} />}
|
{!(removed || deleted) && !isModQueue && <PostMenuDesktop postMenu={postMenuProps} />}
|
||||||
{cid &&
|
{cid && parentCid && <ReplyBacklinks post={post} quotedByMap={quotedByMap} directRepliesByParentCid={directRepliesByParentCid} />}
|
||||||
parentCid &&
|
{cid && !parentCid && <OpBacklinks cid={cid} quotedByMap={quotedByMap} />}
|
||||||
replies &&
|
</span>
|
||||||
replies.map(
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReplyBacklinks = ({
|
||||||
|
post,
|
||||||
|
quotedByMap,
|
||||||
|
directRepliesByParentCid,
|
||||||
|
}: {
|
||||||
|
post: Comment;
|
||||||
|
quotedByMap?: Map<string, Comment[]>;
|
||||||
|
directRepliesByParentCid?: Map<string, Comment[]>;
|
||||||
|
}) => {
|
||||||
|
const { cid, parentCid } = post || {};
|
||||||
|
if (!cid || !parentCid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const directReplies = directRepliesByParentCid?.get(cid) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{directReplies.map(
|
||||||
(reply: Comment, index: number) =>
|
(reply: Comment, index: number) =>
|
||||||
reply?.parentCid === cid &&
|
reply?.parentCid === cid && reply?.cid && !(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />,
|
||||||
reply?.cid &&
|
|
||||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />,
|
|
||||||
)}
|
)}
|
||||||
{cid &&
|
{quotedByMap
|
||||||
parentCid &&
|
|
||||||
quotedByMap
|
|
||||||
?.get(cid)
|
?.get(cid)
|
||||||
?.map(
|
?.map(
|
||||||
(reply: Comment, index: number) =>
|
(reply: Comment, index: number) =>
|
||||||
@@ -438,18 +461,20 @@ const PostInfo = ({
|
|||||||
reply?.cid &&
|
reply?.cid &&
|
||||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`qb-${index}`} isBacklinkReply={true} backlinkReply={reply} />,
|
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`qb-${index}`} isBacklinkReply={true} backlinkReply={reply} />,
|
||||||
)}
|
)}
|
||||||
{cid &&
|
</>
|
||||||
!parentCid &&
|
);
|
||||||
quotedByMap
|
};
|
||||||
|
|
||||||
|
const OpBacklinks = ({ cid, quotedByMap }: { cid: string; quotedByMap?: Map<string, Comment[]> }) => (
|
||||||
|
<>
|
||||||
|
{quotedByMap
|
||||||
?.get(cid)
|
?.get(cid)
|
||||||
?.map(
|
?.map(
|
||||||
(reply: Comment) =>
|
(reply: Comment) =>
|
||||||
reply?.cid && !(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`op-bl-${reply.cid}`} isBacklinkReply={true} backlinkReply={reply} />,
|
reply?.cid && !(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`op-bl-${reply.cid}`} isBacklinkReply={true} backlinkReply={reply} />,
|
||||||
)}
|
)}
|
||||||
</span>
|
</>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
interface PostMediaProps {
|
interface PostMediaProps {
|
||||||
commentMediaInfo: CommentMediaInfo | undefined;
|
commentMediaInfo: CommentMediaInfo | undefined;
|
||||||
@@ -548,10 +573,21 @@ const PostMedia = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Reply = ({ postReplyCount, reply, roles, threadNumber, quotedByMap }: PostProps) => {
|
const Reply = ({
|
||||||
let post = reply;
|
postReplyCount,
|
||||||
|
reply,
|
||||||
|
roles,
|
||||||
|
threadNumber,
|
||||||
|
quotedByMap,
|
||||||
|
directRepliesByParentCid,
|
||||||
|
}: PostProps & { directRepliesByParentCid?: Map<string, Comment[]> }) => {
|
||||||
|
const accountReply = useAccountComment({
|
||||||
|
commentIndex: typeof reply?.index === 'number' ? reply.index : undefined,
|
||||||
|
});
|
||||||
|
const hasReplyIndex = typeof reply?.index === 'number';
|
||||||
|
let post = hasReplyIndex && accountReply?.index === reply.index ? accountReply : reply;
|
||||||
// handle pending mod or author edit
|
// handle pending mod or author edit
|
||||||
const { editedComment } = useEditedComment({ comment: reply });
|
const { editedComment } = useEditedComment({ comment: post });
|
||||||
if (editedComment) {
|
if (editedComment) {
|
||||||
post = editedComment;
|
post = editedComment;
|
||||||
}
|
}
|
||||||
@@ -576,7 +612,15 @@ const Reply = ({ postReplyCount, reply, roles, threadNumber, quotedByMap }: Post
|
|||||||
<div className={styles.replyDesktop}>
|
<div className={styles.replyDesktop}>
|
||||||
<div className={styles.sideArrows}>{'>>'}</div>
|
<div className={styles.sideArrows}>{'>>'}</div>
|
||||||
<div className={`${styles.reply} ${isRouteLinkToReply && styles.highlight}`} data-cid={cid} data-author-address={author?.shortAddress} data-post-cid={postCid}>
|
<div className={`${styles.reply} ${isRouteLinkToReply && styles.highlight}`} data-cid={cid} data-author-address={author?.shortAddress} data-post-cid={postCid}>
|
||||||
<PostInfo post={post} postReplyCount={postReplyCount} roles={roles} isHidden={hidden} threadNumber={threadNumber} quotedByMap={quotedByMap} />
|
<PostInfo
|
||||||
|
post={post}
|
||||||
|
postReplyCount={postReplyCount}
|
||||||
|
roles={roles}
|
||||||
|
isHidden={hidden}
|
||||||
|
threadNumber={threadNumber}
|
||||||
|
quotedByMap={quotedByMap}
|
||||||
|
directRepliesByParentCid={directRepliesByParentCid}
|
||||||
|
/>
|
||||||
{link && !hidden && !(deleted || removed) && isValidURL(link) && (
|
{link && !hidden && !(deleted || removed) && isValidURL(link) && (
|
||||||
<PostMedia
|
<PostMedia
|
||||||
commentMediaInfo={commentMediaInfo}
|
commentMediaInfo={commentMediaInfo}
|
||||||
@@ -626,12 +670,28 @@ const PostDesktop = ({
|
|||||||
const { hidden, unhide, hide } = useHide({ cid });
|
const { hidden, unhide, hide } = useHide({ cid });
|
||||||
const isHidden = hidden && !isInPostPageView;
|
const isHidden = hidden && !isInPostPageView;
|
||||||
|
|
||||||
const { replies, hasMore, loadMore } = useReplies({ comment: post, flat: true, accountComments: { newerThan: Infinity } });
|
const repliesResult = useReplies({
|
||||||
|
comment: post,
|
||||||
|
sortType: 'old',
|
||||||
|
flat: true,
|
||||||
|
accountComments: { newerThan: Infinity, append: true },
|
||||||
|
});
|
||||||
|
const { replies, hasMore, loadMore } = repliesResult;
|
||||||
|
const updatedReplies = (repliesResult as { updatedReplies?: Comment[] }).updatedReplies;
|
||||||
|
const repliesForRender = updatedReplies?.length ? updatedReplies : replies || [];
|
||||||
|
const reset = (repliesResult as { reset?: () => Promise<void> }).reset;
|
||||||
|
const setResetFunction = useFeedResetStore((s) => s.setResetFunction);
|
||||||
|
useEffect(() => {
|
||||||
|
if ((isInPostPageView || isInPendingPostView) && reset) {
|
||||||
|
setResetFunction(() => {
|
||||||
|
reset();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isInPostPageView, isInPendingPostView, reset, setResetFunction]);
|
||||||
const registerComments = usePostNumberStore((s) => s.registerComments);
|
const registerComments = usePostNumberStore((s) => s.registerComments);
|
||||||
const numberToCid = usePostNumberStore((s) => s.numberToCid);
|
|
||||||
const prevCidsRef = useRef<string>('');
|
const prevCidsRef = useRef<string>('');
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const all = post ? [post, ...(replies || [])] : replies || [];
|
const all = post ? [post, ...repliesForRender] : repliesForRender;
|
||||||
if (!all.length) return;
|
if (!all.length) return;
|
||||||
const cidsKey = all
|
const cidsKey = all
|
||||||
.map((c) => c?.cid)
|
.map((c) => c?.cid)
|
||||||
@@ -641,53 +701,41 @@ const PostDesktop = ({
|
|||||||
if (cidsKey === prevCidsRef.current) return;
|
if (cidsKey === prevCidsRef.current) return;
|
||||||
prevCidsRef.current = cidsKey;
|
prevCidsRef.current = cidsKey;
|
||||||
registerComments(all);
|
registerComments(all);
|
||||||
}, [post, replies, registerComments]);
|
}, [post, repliesForRender, registerComments]);
|
||||||
const visiblelinksCount = useCountLinksInReplies(post, 5);
|
const visiblelinksCount = useCountLinksInReplies(post, 5);
|
||||||
const totalLinksCount = useCountLinksInReplies(post);
|
const totalLinksCount = useCountLinksInReplies(post);
|
||||||
const replyCount = replies?.length;
|
const replyCount = repliesForRender.length;
|
||||||
|
|
||||||
const repliesCount = pinned ? replyCount : replyCount - 5;
|
const repliesCount = pinned ? replyCount : replyCount - 5;
|
||||||
const linksCount = pinned ? totalLinksCount : totalLinksCount - visiblelinksCount;
|
const linksCount = pinned ? totalLinksCount : totalLinksCount - visiblelinksCount;
|
||||||
const { showOmittedReplies, setShowOmittedReplies } = useShowOmittedReplies();
|
const { showOmittedReplies, setShowOmittedReplies } = useShowOmittedReplies();
|
||||||
|
|
||||||
const stateString = useStateString(post) || t('downloading_board');
|
const stateString = useStateString(post) || t('downloading_board');
|
||||||
|
const hasFailedState = state === 'failed';
|
||||||
|
|
||||||
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||||
|
|
||||||
// Filter out deleted replies with no children for both virtuoso and non-virtuoso rendering
|
// Filter out deleted replies with no children for both virtuoso and non-virtuoso rendering
|
||||||
const filteredReplies = useMemo(() => (replies || []).filter((reply) => !(reply.deleted && (reply.replyCount === 0 || !reply.replyCount))), [replies]);
|
const filteredReplies = useMemo(() => repliesForRender.filter((reply) => !(reply.deleted && (reply.replyCount === 0 || !reply.replyCount))), [repliesForRender]);
|
||||||
|
const directRepliesByParentCid = useMemo(() => {
|
||||||
// Compute quotedByMap: map each quoted CID to array of replies that quote it
|
|
||||||
const quotedByMap = useMemo(() => {
|
|
||||||
const map = new Map<string, Comment[]>();
|
const map = new Map<string, Comment[]>();
|
||||||
for (const reply of filteredReplies) {
|
for (const reply of filteredReplies) {
|
||||||
const cidSet = new Set<string>();
|
const directParentCid = reply?.parentCid;
|
||||||
|
if (!directParentCid || !reply?.cid) {
|
||||||
if (reply.quotedCids?.length) {
|
continue;
|
||||||
for (const quotedCid of reply.quotedCids) {
|
|
||||||
cidSet.add(quotedCid);
|
|
||||||
}
|
}
|
||||||
}
|
const existingReplies = map.get(directParentCid);
|
||||||
|
if (existingReplies) {
|
||||||
if (reply.content) {
|
existingReplies.push(reply);
|
||||||
for (const match of reply.content.matchAll(QUOTE_NUMBER_REGEX)) {
|
} else {
|
||||||
const postNumber = parseInt(match[1], 10);
|
map.set(directParentCid, [reply]);
|
||||||
const quotedCid = numberToCid[postNumber];
|
|
||||||
if (quotedCid) {
|
|
||||||
cidSet.add(quotedCid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const quotedCid of cidSet) {
|
|
||||||
const arr = map.get(quotedCid);
|
|
||||||
if (arr) arr.push(reply);
|
|
||||||
else map.set(quotedCid, [reply]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}, [filteredReplies, numberToCid]);
|
}, [filteredReplies]);
|
||||||
|
|
||||||
|
const quotedByMap = useQuotedByMap(filteredReplies);
|
||||||
|
|
||||||
// Virtuoso scroll position management for infinite replies
|
// Virtuoso scroll position management for infinite replies
|
||||||
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
|
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
|
||||||
@@ -778,6 +826,7 @@ const PostDesktop = ({
|
|||||||
onApprove={onApprove}
|
onApprove={onApprove}
|
||||||
onReject={onReject}
|
onReject={onReject}
|
||||||
quotedByMap={quotedByMap}
|
quotedByMap={quotedByMap}
|
||||||
|
directRepliesByParentCid={directRepliesByParentCid}
|
||||||
/>
|
/>
|
||||||
{!isHidden && !content && !(deleted || removed) && <div className={styles.spacer} />}
|
{!isHidden && !content && !(deleted || removed) && <div className={styles.spacer} />}
|
||||||
{!isHidden && <CommentContent comment={post} />}
|
{!isHidden && <CommentContent comment={post} />}
|
||||||
@@ -815,7 +864,14 @@ const PostDesktop = ({
|
|||||||
data={filteredReplies}
|
data={filteredReplies}
|
||||||
itemContent={(index, reply) => (
|
itemContent={(index, reply) => (
|
||||||
<div className={styles.replyContainer}>
|
<div className={styles.replyContainer}>
|
||||||
<Reply reply={reply} roles={roles} postReplyCount={replyCount} threadNumber={post?.number} quotedByMap={quotedByMap} />
|
<Reply
|
||||||
|
reply={reply}
|
||||||
|
roles={roles}
|
||||||
|
postReplyCount={replyCount}
|
||||||
|
threadNumber={post?.number}
|
||||||
|
quotedByMap={quotedByMap}
|
||||||
|
directRepliesByParentCid={directRepliesByParentCid}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
useWindowScroll={true}
|
useWindowScroll={true}
|
||||||
@@ -834,7 +890,14 @@ const PostDesktop = ({
|
|||||||
!hasMore &&
|
!hasMore &&
|
||||||
filteredReplies.map((reply, index) => (
|
filteredReplies.map((reply, index) => (
|
||||||
<div key={index} className={styles.replyContainer}>
|
<div key={index} className={styles.replyContainer}>
|
||||||
<Reply reply={reply} roles={roles} postReplyCount={replyCount} threadNumber={post?.number} quotedByMap={quotedByMap} />
|
<Reply
|
||||||
|
reply={reply}
|
||||||
|
roles={roles}
|
||||||
|
postReplyCount={replyCount}
|
||||||
|
threadNumber={post?.number}
|
||||||
|
quotedByMap={quotedByMap}
|
||||||
|
directRepliesByParentCid={directRepliesByParentCid}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{/* Non-virtualized rendering for board view (last 5 replies or show omitted) */}
|
{/* Non-virtualized rendering for board view (last 5 replies or show omitted) */}
|
||||||
@@ -842,21 +905,28 @@ const PostDesktop = ({
|
|||||||
!showAllReplies &&
|
!showAllReplies &&
|
||||||
!(pinned && !isInPostPageView && !showOmittedReplies[cid]) &&
|
!(pinned && !isInPostPageView && !showOmittedReplies[cid]) &&
|
||||||
!isInPendingPostView &&
|
!isInPendingPostView &&
|
||||||
replies &&
|
repliesForRender &&
|
||||||
showReplies &&
|
showReplies &&
|
||||||
(showOmittedReplies[cid] ? filteredReplies : filteredReplies.slice(-5)).map((reply, index) => (
|
(showOmittedReplies[cid] ? filteredReplies : filteredReplies.slice(-5)).map((reply, index) => (
|
||||||
<div key={index} className={styles.replyContainer}>
|
<div key={index} className={styles.replyContainer}>
|
||||||
<Reply reply={reply} roles={roles} postReplyCount={replyCount} threadNumber={post?.number} quotedByMap={quotedByMap} />
|
<Reply
|
||||||
|
reply={reply}
|
||||||
|
roles={roles}
|
||||||
|
postReplyCount={replyCount}
|
||||||
|
threadNumber={post?.number}
|
||||||
|
quotedByMap={quotedByMap}
|
||||||
|
directRepliesByParentCid={directRepliesByParentCid}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{!isInPendingPostView && stateString && stateString !== 'Failed' && state !== 'succeeded' && isInPostPageView && !(!showReplies && !showAllReplies) ? (
|
{!isInPendingPostView && stateString && !hasFailedState && state !== 'succeeded' && isInPostPageView && !(!showReplies && !showAllReplies) ? (
|
||||||
<div className={styles.stateString}>
|
<div className={styles.stateString}>
|
||||||
<br />
|
<br />
|
||||||
<LoadingEllipsis string={stateString} />
|
<LoadingEllipsis string={stateString} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
state === 'failed' && <span className={styles.error}>{t('failed')}</span>
|
hasFailedState && <span className={styles.error}>{t('failed')}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,14 +2,13 @@ import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom';
|
import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom';
|
||||||
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
||||||
import { Comment, useAuthorAvatar, useEditedComment, useReplies, useAccount, usePublishCommentModeration } from '@plebbit/plebbit-react-hooks';
|
import { Comment, useAuthorAvatar, useEditedComment, useReplies, useAccount, usePublishCommentModeration, useAccountComment } from '@plebbit/plebbit-react-hooks';
|
||||||
import Plebbit from '@plebbit/plebbit-js';
|
import Plebbit from '@plebbit/plebbit-js';
|
||||||
import styles from '../../views/post/post.module.css';
|
import styles from '../../views/post/post.module.css';
|
||||||
import { shouldShowSnow } from '../../lib/snow';
|
import { shouldShowSnow } from '../../lib/snow';
|
||||||
import { getHasThumbnail } from '../../lib/utils/media-utils';
|
import { getHasThumbnail } from '../../lib/utils/media-utils';
|
||||||
import { getTextColorForBackground, hashStringToColor } from '../../lib/utils/post-utils';
|
import { getTextColorForBackground, hashStringToColor } from '../../lib/utils/post-utils';
|
||||||
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
||||||
import { QUOTE_NUMBER_REGEX } from '../../lib/utils/url-utils';
|
|
||||||
import { isAllView, isModQueueView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
import { isAllView, isModQueueView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||||
import { formatUserIDForDisplay } from '../../lib/utils/string-utils';
|
import { formatUserIDForDisplay } from '../../lib/utils/string-utils';
|
||||||
import useModQueueStore from '../../stores/use-mod-queue-store';
|
import useModQueueStore from '../../stores/use-mod-queue-store';
|
||||||
@@ -34,8 +33,10 @@ import { capitalize, lowerCase } from 'lodash';
|
|||||||
import useReplyModalStore from '../../stores/use-reply-modal-store';
|
import useReplyModalStore from '../../stores/use-reply-modal-store';
|
||||||
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
|
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
|
||||||
import useChallengesStore from '../../stores/use-challenges-store';
|
import useChallengesStore from '../../stores/use-challenges-store';
|
||||||
|
import useFeedResetStore from '../../stores/use-feed-reset-store';
|
||||||
import usePostNumberStore from '../../stores/use-post-number-store';
|
import usePostNumberStore from '../../stores/use-post-number-store';
|
||||||
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
|
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
|
||||||
|
import useQuotedByMap from '../../hooks/use-quoted-by-map';
|
||||||
|
|
||||||
const { addChallenge } = useChallengesStore.getState();
|
const { addChallenge } = useChallengesStore.getState();
|
||||||
|
|
||||||
@@ -167,15 +168,21 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
|||||||
const alertThresholdSeconds = getAlertThresholdSeconds();
|
const alertThresholdSeconds = getAlertThresholdSeconds();
|
||||||
const isOverThreshold = isAwaitingApproval && timeWaiting > alertThresholdSeconds;
|
const isOverThreshold = isAwaitingApproval && timeWaiting > alertThresholdSeconds;
|
||||||
|
|
||||||
const stateString = useStateString(post);
|
const hasFailedState = state === 'failed';
|
||||||
const postMenuProps = useMemo(() => selectPostMenuProps(post), [post]);
|
const postMenuProps = useMemo(() => selectPostMenuProps(post), [post]);
|
||||||
|
|
||||||
const handleUserAddressClick = useAuthorAddressClick();
|
|
||||||
const numberOfPostsByAuthor = document.querySelectorAll(`[data-author-address="${shortAddress}"][data-post-cid="${postCid}"]`).length;
|
|
||||||
|
|
||||||
const pseudonymityMode = useSubplebbitField(subplebbitAddress, (sub) => sub?.features?.pseudonymityMode);
|
const pseudonymityMode = useSubplebbitField(subplebbitAddress, (sub) => sub?.features?.pseudonymityMode);
|
||||||
const showUserID = pseudonymityMode !== 'per-reply';
|
const showUserID = pseudonymityMode !== 'per-reply';
|
||||||
|
|
||||||
|
const handleUserAddressClick = useAuthorAddressClick();
|
||||||
|
const numberOfPostsByAuthor = useMemo(() => {
|
||||||
|
if (!showUserID || deleted || removed || !shortAddress || !postCid || typeof document === 'undefined') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return document.querySelectorAll(`[data-author-address="${shortAddress}"][data-post-cid="${postCid}"]`).length;
|
||||||
|
}, [showUserID, deleted, removed, shortAddress, postCid]);
|
||||||
|
|
||||||
const userID = address && Plebbit.getShortAddress({ address }); // shortened to 8 chars for display; users can verify the full user ID via "Copy user ID" in the post menu to guard against spoofing
|
const userID = address && Plebbit.getShortAddress({ address }); // shortened to 8 chars for display; users can verify the full user ID via "Copy user ID" in the post menu to guard against spoofing
|
||||||
const userIDBackgroundColor = hashStringToColor(userID);
|
const userIDBackgroundColor = hashStringToColor(userID);
|
||||||
const userIDTextColor = getTextColorForBackground(userIDBackgroundColor);
|
const userIDTextColor = getTextColorForBackground(userIDBackgroundColor);
|
||||||
@@ -319,9 +326,7 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<span>No.</span>
|
<span>No.</span>
|
||||||
<span className={styles.pendingCid}>
|
<span className={styles.pendingCid}>{hasFailedState ? capitalize(t('failed')) : capitalize(t('pending'))}</span>
|
||||||
{state === 'failed' || stateString === 'Failed' ? capitalize(t('failed')) : state === 'pending' ? capitalize(t('pending')) : ''}
|
|
||||||
</span>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{shouldShowPendingApprovalButtons && (
|
{shouldShowPendingApprovalButtons && (
|
||||||
@@ -382,7 +387,15 @@ const PostMediaContent = ({ post, link }: { post: any; link: string }) => {
|
|||||||
|
|
||||||
const ReplyBacklinks = ({ post, quotedByMap }: PostProps) => {
|
const ReplyBacklinks = ({ post, quotedByMap }: PostProps) => {
|
||||||
const { cid, parentCid } = post || {};
|
const { cid, parentCid } = post || {};
|
||||||
const { replies } = useReplies({ comment: post, flat: true, accountComments: { newerThan: Infinity } });
|
const repliesResult = useReplies({
|
||||||
|
comment: post,
|
||||||
|
sortType: 'old',
|
||||||
|
flat: true,
|
||||||
|
accountComments: { newerThan: Infinity, append: true },
|
||||||
|
});
|
||||||
|
const { replies } = repliesResult;
|
||||||
|
const updatedReplies = (repliesResult as { updatedReplies?: Comment[] }).updatedReplies;
|
||||||
|
const repliesForRender = updatedReplies?.length ? updatedReplies : replies || [];
|
||||||
|
|
||||||
const opBacklinks =
|
const opBacklinks =
|
||||||
cid &&
|
cid &&
|
||||||
@@ -395,9 +408,9 @@ const ReplyBacklinks = ({ post, quotedByMap }: PostProps) => {
|
|||||||
)
|
)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
const replyBacklinks = cid && parentCid && ((replies?.length || 0) > 0 || quotedByMap?.get(cid)?.length) && (
|
const replyBacklinks = cid && parentCid && ((repliesForRender?.length || 0) > 0 || quotedByMap?.get(cid)?.length) && (
|
||||||
<>
|
<>
|
||||||
{replies?.map(
|
{repliesForRender?.map(
|
||||||
(reply: Comment, index: number) =>
|
(reply: Comment, index: number) =>
|
||||||
reply?.parentCid === cid && reply?.cid && !(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />,
|
reply?.parentCid === cid && reply?.cid && !(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />,
|
||||||
)}
|
)}
|
||||||
@@ -421,9 +434,13 @@ const ReplyBacklinks = ({ post, quotedByMap }: PostProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Reply = ({ postReplyCount, reply, roles, threadNumber, quotedByMap }: PostProps) => {
|
const Reply = ({ postReplyCount, reply, roles, threadNumber, quotedByMap }: PostProps) => {
|
||||||
let post = reply;
|
const accountReply = useAccountComment({
|
||||||
|
commentIndex: typeof reply?.index === 'number' ? reply.index : undefined,
|
||||||
|
});
|
||||||
|
const hasReplyIndex = typeof reply?.index === 'number';
|
||||||
|
let post = hasReplyIndex && accountReply?.index === reply.index ? accountReply : reply;
|
||||||
// handle pending mod or author edit
|
// handle pending mod or author edit
|
||||||
const { editedComment } = useEditedComment({ comment: reply });
|
const { editedComment } = useEditedComment({ comment: post });
|
||||||
if (editedComment) {
|
if (editedComment) {
|
||||||
post = editedComment;
|
post = editedComment;
|
||||||
}
|
}
|
||||||
@@ -476,12 +493,28 @@ const PostMobile = ({
|
|||||||
const directories = useDirectories();
|
const directories = useDirectories();
|
||||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, directories) : undefined;
|
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, directories) : undefined;
|
||||||
const linksCount = useCountLinksInReplies(post);
|
const linksCount = useCountLinksInReplies(post);
|
||||||
const { replies, hasMore, loadMore } = useReplies({ comment: post, accountComments: { newerThan: Infinity } });
|
const repliesResult = useReplies({
|
||||||
|
comment: post,
|
||||||
|
sortType: 'old',
|
||||||
|
flat: true,
|
||||||
|
accountComments: { newerThan: Infinity, append: true },
|
||||||
|
});
|
||||||
|
const { replies, hasMore, loadMore } = repliesResult;
|
||||||
|
const updatedReplies = (repliesResult as { updatedReplies?: Comment[] }).updatedReplies;
|
||||||
|
const repliesForRender = updatedReplies?.length ? updatedReplies : replies || [];
|
||||||
|
const reset = (repliesResult as { reset?: () => Promise<void> }).reset;
|
||||||
|
const setResetFunction = useFeedResetStore((s) => s.setResetFunction);
|
||||||
|
useEffect(() => {
|
||||||
|
if ((isInPostView || isInPendingPostView) && reset) {
|
||||||
|
setResetFunction(() => {
|
||||||
|
reset();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isInPostView, isInPendingPostView, reset, setResetFunction]);
|
||||||
const registerComments = usePostNumberStore((s) => s.registerComments);
|
const registerComments = usePostNumberStore((s) => s.registerComments);
|
||||||
const numberToCid = usePostNumberStore((s) => s.numberToCid);
|
|
||||||
const prevCidsRef = useRef<string>('');
|
const prevCidsRef = useRef<string>('');
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const all = post ? [post, ...(replies || [])] : replies || [];
|
const all = post ? [post, ...repliesForRender] : repliesForRender;
|
||||||
if (!all.length) return;
|
if (!all.length) return;
|
||||||
const cidsKey = all
|
const cidsKey = all
|
||||||
.map((c) => c?.cid)
|
.map((c) => c?.cid)
|
||||||
@@ -491,46 +524,18 @@ const PostMobile = ({
|
|||||||
if (cidsKey === prevCidsRef.current) return;
|
if (cidsKey === prevCidsRef.current) return;
|
||||||
prevCidsRef.current = cidsKey;
|
prevCidsRef.current = cidsKey;
|
||||||
registerComments(all);
|
registerComments(all);
|
||||||
}, [post, replies, registerComments]);
|
}, [post, repliesForRender, registerComments]);
|
||||||
|
|
||||||
const isInPostPageView = isPostPageView(location.pathname, params);
|
const isInPostPageView = isPostPageView(location.pathname, params);
|
||||||
const { hidden, unhide } = useHide({ cid });
|
const { hidden, unhide } = useHide({ cid });
|
||||||
|
|
||||||
const stateString = useStateString(post) || t('loading_post');
|
const stateString = useStateString(post) || t('loading_post');
|
||||||
|
const hasFailedState = state === 'failed';
|
||||||
|
|
||||||
// Filter out deleted replies with no children for both virtuoso and non-virtuoso rendering
|
// Filter out deleted replies with no children for both virtuoso and non-virtuoso rendering
|
||||||
const filteredReplies = useMemo(() => (replies || []).filter((reply) => !(reply.deleted && (reply.replyCount === 0 || !reply.replyCount))), [replies]);
|
const filteredReplies = useMemo(() => repliesForRender.filter((reply) => !(reply.deleted && (reply.replyCount === 0 || !reply.replyCount))), [repliesForRender]);
|
||||||
|
|
||||||
// Compute quotedByMap: map each quoted CID to array of replies that quote it
|
const quotedByMap = useQuotedByMap(filteredReplies);
|
||||||
const quotedByMap = useMemo(() => {
|
|
||||||
const map = new Map<string, Comment[]>();
|
|
||||||
for (const reply of filteredReplies) {
|
|
||||||
const cidSet = new Set<string>();
|
|
||||||
|
|
||||||
if (reply.quotedCids?.length) {
|
|
||||||
for (const quotedCid of reply.quotedCids) {
|
|
||||||
cidSet.add(quotedCid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reply.content) {
|
|
||||||
for (const match of reply.content.matchAll(QUOTE_NUMBER_REGEX)) {
|
|
||||||
const postNumber = parseInt(match[1], 10);
|
|
||||||
const quotedCid = numberToCid[postNumber];
|
|
||||||
if (quotedCid) {
|
|
||||||
cidSet.add(quotedCid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const quotedCid of cidSet) {
|
|
||||||
const arr = map.get(quotedCid);
|
|
||||||
if (arr) arr.push(reply);
|
|
||||||
else map.set(quotedCid, [reply]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}, [filteredReplies, numberToCid]);
|
|
||||||
|
|
||||||
// Virtuoso scroll position management for infinite replies
|
// Virtuoso scroll position management for infinite replies
|
||||||
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
|
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
|
||||||
@@ -674,7 +679,7 @@ const PostMobile = ({
|
|||||||
{!(pinned && !isInPostView) &&
|
{!(pinned && !isInPostView) &&
|
||||||
!showAllReplies &&
|
!showAllReplies &&
|
||||||
!isInPendingPostView &&
|
!isInPendingPostView &&
|
||||||
replies &&
|
repliesForRender &&
|
||||||
showReplies &&
|
showReplies &&
|
||||||
filteredReplies.slice(-5).map((reply, index) => (
|
filteredReplies.slice(-5).map((reply, index) => (
|
||||||
<div key={index} className={styles.replyContainer}>
|
<div key={index} className={styles.replyContainer}>
|
||||||
@@ -682,12 +687,12 @@ const PostMobile = ({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{!isInPendingPostView && stateString && stateString !== 'Failed' && state !== 'succeeded' && isInPostPageView && !(!showReplies && !showAllReplies) ? (
|
{!isInPendingPostView && stateString && !hasFailedState && state !== 'succeeded' && isInPostPageView && !(!showReplies && !showAllReplies) ? (
|
||||||
<div className={styles.stateString}>
|
<div className={styles.stateString}>
|
||||||
<LoadingEllipsis string={stateString} />
|
<LoadingEllipsis string={stateString} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
state === 'failed' && <span className={styles.error}>{t('failed')}</span>
|
hasFailedState && <span className={styles.error}>{t('failed')}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface ReplyQuotePreviewProps {
|
|||||||
isQuotelinkReply?: boolean;
|
isQuotelinkReply?: boolean;
|
||||||
quotelinkReply?: Comment;
|
quotelinkReply?: Comment;
|
||||||
isOP?: boolean;
|
isOP?: boolean;
|
||||||
|
showTrailingBreak?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleQuoteHover = (cid: string, onElementOutOfView: () => void) => {
|
const handleQuoteHover = (cid: string, onElementOutOfView: () => void) => {
|
||||||
@@ -70,7 +71,7 @@ const scrollToThreadCardTop = (threadCid: string) => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply, isOP }: ReplyQuotePreviewProps) => {
|
const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply, isOP, showTrailingBreak = true }: ReplyQuotePreviewProps) => {
|
||||||
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
|
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
|
||||||
const [outOfViewCid, setOutOfViewCid] = useState<string | null>(null);
|
const [outOfViewCid, setOutOfViewCid] = useState<string | null>(null);
|
||||||
const placementRef = useRef<Placement>('right');
|
const placementRef = useRef<Placement>('right');
|
||||||
@@ -194,7 +195,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
|
|||||||
{isOP && ' (OP)'}
|
{isOP && ' (OP)'}
|
||||||
{quotelinkReply?.author?.address === account?.author?.address && ' (You)'}
|
{quotelinkReply?.author?.address === account?.author?.address && ' (You)'}
|
||||||
</Link>
|
</Link>
|
||||||
<br />
|
{showTrailingBreak && <br />}
|
||||||
{hoveredCid === quotelinkReply?.cid &&
|
{hoveredCid === quotelinkReply?.cid &&
|
||||||
outOfViewCid === quotelinkReply?.cid &&
|
outOfViewCid === quotelinkReply?.cid &&
|
||||||
createPortal(
|
createPortal(
|
||||||
@@ -339,7 +340,7 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
|
|||||||
return isBacklinkReply ? replyBacklink : isQuotelinkReply && replyQuotelink;
|
return isBacklinkReply ? replyBacklink : isQuotelinkReply && replyQuotelink;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ReplyQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply, isOP }: ReplyQuotePreviewProps) => {
|
const ReplyQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply, isOP, showTrailingBreak }: ReplyQuotePreviewProps) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
return isMobile ? (
|
return isMobile ? (
|
||||||
@@ -351,6 +352,7 @@ const ReplyQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQ
|
|||||||
isBacklinkReply={isBacklinkReply}
|
isBacklinkReply={isBacklinkReply}
|
||||||
isQuotelinkReply={isQuotelinkReply}
|
isQuotelinkReply={isQuotelinkReply}
|
||||||
isOP={isOP}
|
isOP={isOP}
|
||||||
|
showTrailingBreak={showTrailingBreak}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,21 +22,21 @@ const CheckForUpdates = () => {
|
|||||||
const checkForUpdates = async () => {
|
const checkForUpdates = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const packageRes = await fetch('https://raw.githubusercontent.com/plebbit/5chan/master/package.json', { cache: 'no-cache' });
|
const packageRes = await fetch('https://raw.githubusercontent.com/bitsocialhq/5chan/master/package.json', { cache: 'no-cache' });
|
||||||
const packageData = await packageRes.json();
|
const packageData = await packageRes.json();
|
||||||
let updateAvailable = false;
|
let updateAvailable = false;
|
||||||
|
|
||||||
if (packageJson.version !== packageData.version) {
|
if (packageJson.version !== packageData.version) {
|
||||||
const newVersionText = t('new_stable_version', { newVersion: packageData.version, oldVersion: packageJson.version });
|
const newVersionText = t('new_stable_version', { newVersion: packageData.version, oldVersion: packageJson.version });
|
||||||
const updateActionText = isElectron
|
const updateActionText = isElectron
|
||||||
? t('download_latest_desktop', { link: 'https://github.com/plebbit/5chan/releases/latest', interpolation: { escapeValue: false } })
|
? t('download_latest_desktop', { link: 'https://github.com/bitsocialhq/5chan/releases/latest', interpolation: { escapeValue: false } })
|
||||||
: t('refresh_to_update');
|
: t('refresh_to_update');
|
||||||
alert(newVersionText + ' ' + updateActionText);
|
alert(newVersionText + ' ' + updateActionText);
|
||||||
updateAvailable = true;
|
updateAvailable = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commitRef && commitRef.length > 0) {
|
if (commitRef && commitRef.length > 0) {
|
||||||
const commitRes = await fetch('https://api.github.com/repos/plebbit/5chan/commits?per_page=1&sha=development', { cache: 'no-cache' });
|
const commitRes = await fetch('https://api.github.com/repos/bitsocialhq/5chan/commits?per_page=1&sha=development', { cache: 'no-cache' });
|
||||||
const commitData = await commitRes.json();
|
const commitData = await commitRes.json();
|
||||||
|
|
||||||
const latestCommitHash = commitData[0].sha;
|
const latestCommitHash = commitData[0].sha;
|
||||||
|
|||||||
@@ -138,13 +138,20 @@
|
|||||||
|
|
||||||
.editSubscriptionsLink {
|
.editSubscriptionsLink {
|
||||||
font-size: var(--body-font-size);
|
font-size: var(--body-font-size);
|
||||||
color: var(--link-color, #0000ee);
|
color: var(--post-link-text-color);
|
||||||
text-decoration: underline;
|
text-decoration: var(--post-content-link-text-decoration);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editSubscriptionsWrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.editSubscriptionsLink:hover {
|
.editSubscriptionsLink:hover {
|
||||||
color: var(--link-hover-color, #551a8b);
|
color: var(--post-link-text-color-hover);
|
||||||
|
text-decoration: var(--post-content-link-text-decoration-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbarEditFooter {
|
.topbarEditFooter {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import styles from './topbar-edit-modal.module.css';
|
|||||||
|
|
||||||
const TopbarEditModal = () => {
|
const TopbarEditModal = () => {
|
||||||
const { showModal, closeTopbarEditModal } = useTopbarEditModalStore();
|
const { showModal, closeTopbarEditModal } = useTopbarEditModalStore();
|
||||||
const { visibleDirectories, visibleSubscriptions, setDirectoryVisibility, setSubscriptionVisibility } = useTopbarVisibilityStore();
|
const { visibleDirectories, showSubscriptionsInTopbar, setDirectoryVisibility, setShowSubscriptionsInTopbar } = useTopbarVisibilityStore();
|
||||||
const account = useAccount();
|
const account = useAccount();
|
||||||
const subscriptions = useMemo(() => account?.subscriptions || [], [account?.subscriptions]);
|
const subscriptions = useMemo(() => account?.subscriptions || [], [account?.subscriptions]);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -37,17 +37,16 @@ const TopbarEditModal = () => {
|
|||||||
// Local state for text input (will be saved on Save click)
|
// Local state for text input (will be saved on Save click)
|
||||||
// Empty string means all directories visible (default), otherwise show only the specified codes
|
// Empty string means all directories visible (default), otherwise show only the specified codes
|
||||||
const [localDirectoryInput, setLocalDirectoryInput] = useState<string>(allDirectoriesVisible ? '' : directoriesToString(visibleDirectories));
|
const [localDirectoryInput, setLocalDirectoryInput] = useState<string>(allDirectoriesVisible ? '' : directoriesToString(visibleDirectories));
|
||||||
// Check if all subscriptions are visible (if any subscription is visible, show subscriptions is checked)
|
const [showSubscriptions, setShowSubscriptions] = useState<boolean>(showSubscriptionsInTopbar);
|
||||||
const [showSubscriptions, setShowSubscriptions] = useState<boolean>(subscriptions.length > 0 && subscriptions.some((addr: string) => visibleSubscriptions.has(addr)));
|
|
||||||
|
|
||||||
// Update local state when modal opens or store changes
|
// Sync local state when modal opens or store changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showModal) {
|
if (showModal) {
|
||||||
const allVisible = allBoardCodes.every((code) => visibleDirectories.has(code));
|
const allVisible = allBoardCodes.every((code) => visibleDirectories.has(code));
|
||||||
setLocalDirectoryInput(allVisible ? '' : directoriesToString(visibleDirectories));
|
setLocalDirectoryInput(allVisible ? '' : directoriesToString(visibleDirectories));
|
||||||
setShowSubscriptions(subscriptions.length > 0 && subscriptions.some((addr: string) => visibleSubscriptions.has(addr)));
|
setShowSubscriptions(showSubscriptionsInTopbar);
|
||||||
}
|
}
|
||||||
}, [showModal, visibleDirectories, visibleSubscriptions, allBoardCodes, subscriptions]);
|
}, [showModal, visibleDirectories, showSubscriptionsInTopbar, allBoardCodes]);
|
||||||
|
|
||||||
if (!showModal) {
|
if (!showModal) {
|
||||||
return null;
|
return null;
|
||||||
@@ -75,13 +74,7 @@ const TopbarEditModal = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply subscription visibility changes
|
setShowSubscriptionsInTopbar(showSubscriptions);
|
||||||
// If showSubscriptions is checked, make all subscriptions visible
|
|
||||||
// Otherwise, hide all subscriptions
|
|
||||||
subscriptions.forEach((address: string) => {
|
|
||||||
setSubscriptionVisibility(address, showSubscriptions);
|
|
||||||
});
|
|
||||||
|
|
||||||
closeTopbarEditModal();
|
closeTopbarEditModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -107,8 +100,9 @@ const TopbarEditModal = () => {
|
|||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
<div className={styles.checkboxItem}>
|
<div className={styles.checkboxItem}>
|
||||||
<input type='checkbox' id='show-subscriptions' checked={showSubscriptions} onChange={(e) => setShowSubscriptions(e.target.checked)} />
|
<input type='checkbox' id='show-subscriptions' checked={showSubscriptions} onChange={(e) => setShowSubscriptions(e.target.checked)} />
|
||||||
<label htmlFor='show-subscriptions'>
|
<label htmlFor='show-subscriptions'>show subscriptions</label>
|
||||||
show subscriptions (
|
<span className={styles.editSubscriptionsWrapper}>
|
||||||
|
(
|
||||||
<Link
|
<Link
|
||||||
to={location.pathname.replace(/\/$/, '') + '/settings#subscriptions-settings'}
|
to={location.pathname.replace(/\/$/, '') + '/settings#subscriptions-settings'}
|
||||||
className={styles.editSubscriptionsLink}
|
className={styles.editSubscriptionsLink}
|
||||||
@@ -120,7 +114,7 @@ const TopbarEditModal = () => {
|
|||||||
edit subscriptions
|
edit subscriptions
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
</label>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ const TopBarDesktop = () => {
|
|||||||
const { openCreateBoardModal } = useCreateBoardModalStore();
|
const { openCreateBoardModal } = useCreateBoardModalStore();
|
||||||
const { openTopbarEditModal } = useTopbarEditModalStore();
|
const { openTopbarEditModal } = useTopbarEditModalStore();
|
||||||
const { openDirectoryModal } = useDirectoryModalStore();
|
const { openDirectoryModal } = useDirectoryModalStore();
|
||||||
const { visibleDirectories, visibleSubscriptions } = useTopbarVisibilityStore();
|
const { visibleDirectories, showSubscriptionsInTopbar } = useTopbarVisibilityStore();
|
||||||
const directories = useDirectories();
|
const directories = useDirectories();
|
||||||
|
|
||||||
// Memoize allBoardCodes since it's derived from a constant
|
// Memoize allBoardCodes since it's derived from a constant
|
||||||
@@ -127,8 +127,8 @@ const TopBarDesktop = () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filter subscriptions to only show visible ones
|
// Show all subscriptions when enabled; no separate per-address tracking (avoids drift when subscribing from board-buttons)
|
||||||
const visibleSubscriptionAddresses = subscriptions.filter((address: string) => visibleSubscriptions.has(address));
|
const visibleSubscriptionAddresses = showSubscriptionsInTopbar ? subscriptions : [];
|
||||||
|
|
||||||
// Check if any directories are hidden
|
// Check if any directories are hidden
|
||||||
const hasHiddenDirectories = useMemo(() => {
|
const hasHiddenDirectories = useMemo(() => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const Version = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<a
|
<a
|
||||||
href={commitRef ? `https://github.com/plebbit/5chan/commit/${commitRef}` : `https://github.com/plebbit/5chan/releases/tag/v${version}`}
|
href={commitRef ? `https://github.com/bitsocialhq/5chan/commit/${commitRef}` : `https://github.com/bitsocialhq/5chan/releases/tag/v${version}`}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export interface DirectoriesState {
|
|||||||
error: Error | null;
|
error: Error | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GITHUB_URL = 'https://raw.githubusercontent.com/plebbit/lists/master/5chan-directories.json';
|
const GITHUB_URL = 'https://raw.githubusercontent.com/bitsocialhq/lists/master/5chan-directories.json';
|
||||||
const LOCALSTORAGE_KEY = '5chan-directories-cache';
|
const LOCALSTORAGE_KEY = '5chan-directories-cache';
|
||||||
const LOCALSTORAGE_TIMESTAMP_KEY = '5chan-directories-cache-timestamp';
|
const LOCALSTORAGE_TIMESTAMP_KEY = '5chan-directories-cache-timestamp';
|
||||||
const CACHE_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
const CACHE_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
|
import { Comment } from '@plebbit/plebbit-react-hooks';
|
||||||
|
import { QUOTE_NUMBER_REGEX } from '../lib/utils/url-utils';
|
||||||
|
import usePostNumberStore from '../stores/use-post-number-store';
|
||||||
|
|
||||||
|
interface ReplyQuoteTargets {
|
||||||
|
reply: Comment;
|
||||||
|
quotedPostNumbers: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const getReplyFingerprint = (reply: Comment) =>
|
||||||
|
`${reply?.cid ?? ''}|${reply?.deleted ? '1' : '0'}|${reply?.removed ? '1' : '0'}|${reply?.edit?.timestamp ?? ''}|${reply?.state ?? ''}`;
|
||||||
|
|
||||||
|
const areQuotedByMapsEquivalent = (previousMap: Map<string, Comment[]>, nextMap: Map<string, Comment[]>) => {
|
||||||
|
if (previousMap.size !== nextMap.size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [quotedCid, nextReplies] of nextMap) {
|
||||||
|
const previousReplies = previousMap.get(quotedCid);
|
||||||
|
if (!previousReplies || previousReplies.length !== nextReplies.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < nextReplies.length; i++) {
|
||||||
|
if (getReplyFingerprint(previousReplies[i]) !== getReplyFingerprint(nextReplies[i])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractReplyQuoteTargets = (replies: Comment[]) => {
|
||||||
|
const quotedPostNumbers = new Set<number>();
|
||||||
|
const replyQuoteTargets: ReplyQuoteTargets[] = [];
|
||||||
|
|
||||||
|
for (const reply of replies) {
|
||||||
|
const quotedPostNumbersForReply: number[] = [];
|
||||||
|
|
||||||
|
if (reply.content) {
|
||||||
|
for (const match of reply.content.matchAll(QUOTE_NUMBER_REGEX)) {
|
||||||
|
const postNumber = Number.parseInt(match[1], 10);
|
||||||
|
if (!Number.isNaN(postNumber)) {
|
||||||
|
quotedPostNumbersForReply.push(postNumber);
|
||||||
|
quotedPostNumbers.add(postNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
replyQuoteTargets.push({
|
||||||
|
reply,
|
||||||
|
quotedPostNumbers: quotedPostNumbersForReply,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
replyQuoteTargets,
|
||||||
|
quotedPostNumbers: [...quotedPostNumbers].sort((a, b) => a - b),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const useQuotedByMap = (replies: Comment[] = []) => {
|
||||||
|
const stableQuotedByMapRef = useRef<Map<string, Comment[]>>(new Map());
|
||||||
|
const { replyQuoteTargets, quotedPostNumbers } = useMemo(() => extractReplyQuoteTargets(replies), [replies]);
|
||||||
|
|
||||||
|
// Subscribe only to post numbers referenced in this thread to avoid unrelated global store churn.
|
||||||
|
const quotedNumbersSignature = usePostNumberStore(
|
||||||
|
useCallback((state) => quotedPostNumbers.map((postNumber) => `${postNumber}:${state.numberToCid[postNumber] ?? ''}`).join('|'), [quotedPostNumbers]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const quotedNumberToCid = useMemo(() => {
|
||||||
|
if (quotedPostNumbers.length === 0) {
|
||||||
|
return {} as Record<number, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { numberToCid } = usePostNumberStore.getState();
|
||||||
|
const nextQuotedNumberToCid: Record<number, string> = {};
|
||||||
|
|
||||||
|
for (const postNumber of quotedPostNumbers) {
|
||||||
|
const quotedCid = numberToCid[postNumber];
|
||||||
|
if (quotedCid) {
|
||||||
|
nextQuotedNumberToCid[postNumber] = quotedCid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextQuotedNumberToCid;
|
||||||
|
}, [quotedPostNumbers, quotedNumbersSignature]);
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const map = new Map<string, Comment[]>();
|
||||||
|
|
||||||
|
for (const { reply, quotedPostNumbers: quotedNumbersForReply } of replyQuoteTargets) {
|
||||||
|
const cidSet = new Set<string>();
|
||||||
|
|
||||||
|
if (reply.quotedCids?.length) {
|
||||||
|
for (const quotedCid of reply.quotedCids) {
|
||||||
|
cidSet.add(quotedCid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const postNumber of quotedNumbersForReply) {
|
||||||
|
const quotedCid = quotedNumberToCid[postNumber];
|
||||||
|
if (quotedCid) {
|
||||||
|
cidSet.add(quotedCid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const quotedCid of cidSet) {
|
||||||
|
const repliesQuotingCid = map.get(quotedCid);
|
||||||
|
if (repliesQuotingCid) {
|
||||||
|
repliesQuotingCid.push(reply);
|
||||||
|
} else {
|
||||||
|
map.set(quotedCid, [reply]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areQuotedByMapsEquivalent(stableQuotedByMapRef.current, map)) {
|
||||||
|
return stableQuotedByMapRef.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
stableQuotedByMapRef.current = map;
|
||||||
|
return map;
|
||||||
|
}, [replyQuoteTargets, quotedNumberToCid]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useQuotedByMap;
|
||||||
@@ -14,18 +14,28 @@ const usePostNumberStore = create<PostNumberState>((set) => ({
|
|||||||
if (!comments?.length) return;
|
if (!comments?.length) return;
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const nextNumberToCid = { ...state.numberToCid };
|
let nextNumberToCid = state.numberToCid;
|
||||||
const nextCidToNumber = { ...state.cidToNumber };
|
let nextCidToNumber = state.cidToNumber;
|
||||||
|
let hasUpdates = false;
|
||||||
|
|
||||||
for (const c of comments) {
|
for (const c of comments) {
|
||||||
const num = c?.number;
|
const num = c?.number;
|
||||||
const cid = c?.cid;
|
const cid = c?.cid;
|
||||||
if (typeof num === 'number' && cid) {
|
if (typeof num === 'number' && cid && (nextNumberToCid[num] !== cid || nextCidToNumber[cid] !== num)) {
|
||||||
|
if (!hasUpdates) {
|
||||||
|
nextNumberToCid = { ...state.numberToCid };
|
||||||
|
nextCidToNumber = { ...state.cidToNumber };
|
||||||
|
hasUpdates = true;
|
||||||
|
}
|
||||||
nextNumberToCid[num] = cid;
|
nextNumberToCid[num] = cid;
|
||||||
nextCidToNumber[cid] = num;
|
nextCidToNumber[cid] = num;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasUpdates) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
return { numberToCid: nextNumberToCid, cidToNumber: nextCidToNumber };
|
return { numberToCid: nextNumberToCid, cidToNumber: nextCidToNumber };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,13 +7,12 @@ const LOCALSTORAGE_KEY_SUBSCRIPTIONS = '5chan-topbar-subscriptions-visible';
|
|||||||
interface TopbarVisibilityState {
|
interface TopbarVisibilityState {
|
||||||
// Directory codes that are visible (all visible by default)
|
// Directory codes that are visible (all visible by default)
|
||||||
visibleDirectories: Set<string>;
|
visibleDirectories: Set<string>;
|
||||||
// Subscription addresses that are visible in topbar (all hidden by default)
|
// If true, show all account subscriptions in topbar (default: false)
|
||||||
visibleSubscriptions: Set<string>;
|
showSubscriptionsInTopbar: boolean;
|
||||||
// Actions
|
// Actions
|
||||||
toggleDirectory: (code: string) => void;
|
toggleDirectory: (code: string) => void;
|
||||||
toggleSubscription: (address: string) => void;
|
|
||||||
setDirectoryVisibility: (code: string, visible: boolean) => void;
|
setDirectoryVisibility: (code: string, visible: boolean) => void;
|
||||||
setSubscriptionVisibility: (address: string, visible: boolean) => void;
|
setShowSubscriptionsInTopbar: (show: boolean) => void;
|
||||||
// Initialize from localStorage
|
// Initialize from localStorage
|
||||||
initialize: () => void;
|
initialize: () => void;
|
||||||
}
|
}
|
||||||
@@ -40,15 +39,36 @@ const saveToLocalStorage = (key: string, set: Set<string>) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadShowSubscriptionsFromStorage = (): boolean => {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(LOCALSTORAGE_KEY_SUBSCRIPTIONS);
|
||||||
|
if (!stored) return false;
|
||||||
|
const parsed = JSON.parse(stored);
|
||||||
|
// Migrate from old format (array of addresses)
|
||||||
|
if (Array.isArray(parsed)) return parsed.length > 0;
|
||||||
|
if (typeof parsed === 'boolean') return parsed;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to load subscriptions visibility from localStorage:', e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveShowSubscriptionsToStorage = (show: boolean) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(LOCALSTORAGE_KEY_SUBSCRIPTIONS, JSON.stringify(show));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to save subscriptions visibility to localStorage:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const useTopbarVisibilityStore = create<TopbarVisibilityState>((set, _get) => {
|
const useTopbarVisibilityStore = create<TopbarVisibilityState>((set, _get) => {
|
||||||
// Initialize with all directories visible by default
|
// Initialize with all directories visible by default
|
||||||
const allBoardCodes = getAllBoardCodes();
|
const allBoardCodes = getAllBoardCodes();
|
||||||
const defaultVisibleDirectories = new Set(allBoardCodes);
|
const defaultVisibleDirectories = new Set(allBoardCodes);
|
||||||
const defaultVisibleSubscriptions = new Set<string>();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
visibleDirectories: loadFromLocalStorage(LOCALSTORAGE_KEY_DIRECTORIES, defaultVisibleDirectories),
|
visibleDirectories: loadFromLocalStorage(LOCALSTORAGE_KEY_DIRECTORIES, defaultVisibleDirectories),
|
||||||
visibleSubscriptions: loadFromLocalStorage(LOCALSTORAGE_KEY_SUBSCRIPTIONS, defaultVisibleSubscriptions),
|
showSubscriptionsInTopbar: loadShowSubscriptionsFromStorage(),
|
||||||
|
|
||||||
toggleDirectory: (code: string) => {
|
toggleDirectory: (code: string) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
@@ -63,19 +83,6 @@ const useTopbarVisibilityStore = create<TopbarVisibilityState>((set, _get) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleSubscription: (address: string) => {
|
|
||||||
set((state) => {
|
|
||||||
const newSet = new Set(state.visibleSubscriptions);
|
|
||||||
if (newSet.has(address)) {
|
|
||||||
newSet.delete(address);
|
|
||||||
} else {
|
|
||||||
newSet.add(address);
|
|
||||||
}
|
|
||||||
saveToLocalStorage(LOCALSTORAGE_KEY_SUBSCRIPTIONS, newSet);
|
|
||||||
return { visibleSubscriptions: newSet };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
setDirectoryVisibility: (code: string, visible: boolean) => {
|
setDirectoryVisibility: (code: string, visible: boolean) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newSet = new Set(state.visibleDirectories);
|
const newSet = new Set(state.visibleDirectories);
|
||||||
@@ -89,26 +96,17 @@ const useTopbarVisibilityStore = create<TopbarVisibilityState>((set, _get) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setSubscriptionVisibility: (address: string, visible: boolean) => {
|
setShowSubscriptionsInTopbar: (show: boolean) => {
|
||||||
set((state) => {
|
saveShowSubscriptionsToStorage(show);
|
||||||
const newSet = new Set(state.visibleSubscriptions);
|
set({ showSubscriptionsInTopbar: show });
|
||||||
if (visible) {
|
|
||||||
newSet.add(address);
|
|
||||||
} else {
|
|
||||||
newSet.delete(address);
|
|
||||||
}
|
|
||||||
saveToLocalStorage(LOCALSTORAGE_KEY_SUBSCRIPTIONS, newSet);
|
|
||||||
return { visibleSubscriptions: newSet };
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
initialize: () => {
|
initialize: () => {
|
||||||
// Load from localStorage on initialization
|
|
||||||
const directories = loadFromLocalStorage(LOCALSTORAGE_KEY_DIRECTORIES, defaultVisibleDirectories);
|
const directories = loadFromLocalStorage(LOCALSTORAGE_KEY_DIRECTORIES, defaultVisibleDirectories);
|
||||||
const subscriptions = loadFromLocalStorage(LOCALSTORAGE_KEY_SUBSCRIPTIONS, defaultVisibleSubscriptions);
|
const showSubscriptions = loadShowSubscriptionsFromStorage();
|
||||||
set({
|
set({
|
||||||
visibleDirectories: directories,
|
visibleDirectories: directories,
|
||||||
visibleSubscriptions: subscriptions,
|
showSubscriptionsInTopbar: showSubscriptions,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -132,8 +132,8 @@ const FAQ = () => {
|
|||||||
</dd>
|
</dd>
|
||||||
<dt id='uploadimage'>Can I upload an image?</dt>
|
<dt id='uploadimage'>Can I upload an image?</dt>
|
||||||
<dd>
|
<dd>
|
||||||
Yes, but only in the Android app, which you can download on <a href='https://github.com/plebbit/5chan/releases/latest'>GitHub</a>. The app is able to
|
Yes, but only in the Android app, which you can download on <a href='https://github.com/bitsocialhq/5chan/releases/latest'>GitHub</a>. The app is able
|
||||||
automatically upload media to image hosting services, like Imgur or catbox.moe, sharing your IP address with the image hosting service. This is not
|
to automatically upload media to image hosting services, like Imgur or catbox.moe, sharing your IP address with the image hosting service. This is not
|
||||||
possible in the browser, which can't make backend requests.
|
possible in the browser, which can't make backend requests.
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import DirectoryModal from '../../components/directory-modal';
|
|||||||
import { getBoardPath } from '../../lib/utils/route-utils';
|
import { getBoardPath } from '../../lib/utils/route-utils';
|
||||||
import { lowerCase } from 'lodash';
|
import { lowerCase } from 'lodash';
|
||||||
|
|
||||||
// https://github.com/plebbit/lists/blob/master/5chan-directories.json
|
// https://github.com/bitsocialhq/lists/blob/master/5chan-directories.json
|
||||||
|
|
||||||
const SearchBar = () => {
|
const SearchBar = () => {
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -126,7 +126,7 @@ export const Footer = () => {
|
|||||||
<>
|
<>
|
||||||
<ul className={styles.footer}>
|
<ul className={styles.footer}>
|
||||||
<li>
|
<li>
|
||||||
<a href='https://github.com/plebbit/5chan/blob/master/README.md' target='_blank' rel='noopener noreferrer'>
|
<a href='https://github.com/bitsocialhq/5chan/blob/master/README.md' target='_blank' rel='noopener noreferrer'>
|
||||||
{t('about')}
|
{t('about')}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -147,7 +147,7 @@ export const Footer = () => {
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href='https://github.com/plebbit/5chan' target='_blank' rel='noopener noreferrer'>
|
<a href='https://github.com/bitsocialhq/5chan' target='_blank' rel='noopener noreferrer'>
|
||||||
GitHub
|
GitHub
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -165,11 +165,11 @@ export const Footer = () => {
|
|||||||
<div className={styles.footerInfo}>
|
<div className={styles.footerInfo}>
|
||||||
<br />
|
<br />
|
||||||
<Version /> •{' '}
|
<Version /> •{' '}
|
||||||
<a href='https://github.com/plebbit/5chan/issues/new' target='_blank' rel='noopener noreferrer'>
|
<a href='https://github.com/bitsocialhq/5chan/issues/new' target='_blank' rel='noopener noreferrer'>
|
||||||
Feedback
|
Feedback
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
•{' '}
|
•{' '}
|
||||||
<a href='https://github.com/plebbit/5chan/graphs/contributors' target='_blank' rel='noopener noreferrer'>
|
<a href='https://github.com/bitsocialhq/5chan/graphs/contributors' target='_blank' rel='noopener noreferrer'>
|
||||||
Contact
|
Contact
|
||||||
</a>
|
</a>
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
Reference in New Issue
Block a user