mirror of
https://github.com/plebbit/seedit.git
synced 2026-05-24 00:29:58 -04:00
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>إنشاء</1> حساب جديد",
|
||||
"wallet_number": "المحفظة #{{index}}",
|
||||
"view_more": "عرض المزيد",
|
||||
"submit_community": "قدم مجتمعك"
|
||||
"submit_community": "قدم مجتمعك",
|
||||
"hide_avatars_from_replies": "إخفاء الصور الرمزية من الردود"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>নতুন তৈরি করুন</1> অ্যাকাউন্ট",
|
||||
"wallet_number": "ওয়ালেট #{{index}}",
|
||||
"view_more": "আরও দেখুন",
|
||||
"submit_community": "আপনার সম্প্রদায় জমা দিন"
|
||||
"submit_community": "আপনার সম্প্রদায় জমা দিন",
|
||||
"hide_avatars_from_replies": "প্রতিক্রিয়া থেকে অ্যাভাটারগুলি লুকান"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>vytvořit</1> nový účet",
|
||||
"wallet_number": "peněženka #{{index}}",
|
||||
"view_more": "zobrazit více",
|
||||
"submit_community": "Odešlete svou komunitu"
|
||||
"submit_community": "Odešlete svou komunitu",
|
||||
"hide_avatars_from_replies": "Skrýt avatary v odpovědích"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>oprette</1> en ny konto",
|
||||
"wallet_number": "tegneboks #{{index}}",
|
||||
"view_more": "se mere",
|
||||
"submit_community": "Indsend dit fællesskab"
|
||||
"submit_community": "Indsend dit fællesskab",
|
||||
"hide_avatars_from_replies": "Skjul avatarer fra svar"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>erstelle</1> ein neues Konto",
|
||||
"wallet_number": "Wallet #{{index}}",
|
||||
"view_more": "mehr anzeigen",
|
||||
"submit_community": "Reichen Sie Ihre Gemeinschaft ein"
|
||||
"submit_community": "Reichen Sie Ihre Gemeinschaft ein",
|
||||
"hide_avatars_from_replies": "Avatare in Antworten ausblenden"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>δημιουργία</1> νέου λογαριασμού",
|
||||
"wallet_number": "πορτοφόλι #{{index}}",
|
||||
"view_more": "δείτε περισσότερα",
|
||||
"submit_community": "Υποβάλετε την κοινότητά σας"
|
||||
"submit_community": "Υποβάλετε την κοινότητά σας",
|
||||
"hide_avatars_from_replies": "Απόκρυψη avatar από τις απαντήσεις"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>create</1> a new account",
|
||||
"wallet_number": "wallet #{{index}}",
|
||||
"view_more": "view more",
|
||||
"submit_community": "Submit your community"
|
||||
"submit_community": "Submit your community",
|
||||
"hide_avatars_from_replies": "Hide avatars from replies"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>crear</1> una nueva cuenta",
|
||||
"wallet_number": "billetera #{{index}}",
|
||||
"view_more": "ver más",
|
||||
"submit_community": "Envía tu comunidad"
|
||||
"submit_community": "Envía tu comunidad",
|
||||
"hide_avatars_from_replies": "Ocultar avatares de las respuestas"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>ایجاد</1> یک حساب جدید",
|
||||
"wallet_number": "کیف پول #{{index}}",
|
||||
"view_more": "مشاهده بیشتر",
|
||||
"submit_community": "جامعه خود را ارسال کنید"
|
||||
"submit_community": "جامعه خود را ارسال کنید",
|
||||
"hide_avatars_from_replies": "تصاویر پروفایل را از پاسخها پنهان کن"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>luo</1> uusi tili",
|
||||
"wallet_number": "lompakko #{{index}}",
|
||||
"view_more": "näytä lisää",
|
||||
"submit_community": "Lähetä yhteisösi"
|
||||
"submit_community": "Lähetä yhteisösi",
|
||||
"hide_avatars_from_replies": "Piilota avatarit vastauksista"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>lumikha</1> ng bagong account",
|
||||
"wallet_number": "wallet #{{index}}",
|
||||
"view_more": "tingnan pa",
|
||||
"submit_community": "I-submit ang iyong komunidad"
|
||||
"submit_community": "I-submit ang iyong komunidad",
|
||||
"hide_avatars_from_replies": "Itago ang mga avatar mula sa mga sagot"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>créer</1> un nouveau compte",
|
||||
"wallet_number": "portefeuille #{{index}}",
|
||||
"view_more": "voir plus",
|
||||
"submit_community": "Soumettez votre communauté"
|
||||
"submit_community": "Soumettez votre communauté",
|
||||
"hide_avatars_from_replies": "Cacher les avatars des réponses"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>צור</1> חשבון חדש",
|
||||
"wallet_number": "ארנק #{{index}}",
|
||||
"view_more": "ראה עוד",
|
||||
"submit_community": "שלח את הקהילה שלך"
|
||||
"submit_community": "שלח את הקהילה שלך",
|
||||
"hide_avatars_from_replies": "הסתרת תמונות פרופיל בתשובות"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>नया बनाएं</1> खाता",
|
||||
"wallet_number": "वॉलेट #{{index}}",
|
||||
"view_more": "और देखें",
|
||||
"submit_community": "अपनी समुदाय सबमिट करें"
|
||||
"submit_community": "अपनी समुदाय सबमिट करें",
|
||||
"hide_avatars_from_replies": "उत्तर से अवतार छुपाएं"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>hozzon létre</1> egy új fiókot",
|
||||
"wallet_number": "tárca #{{index}}",
|
||||
"view_more": "több megtekintése",
|
||||
"submit_community": "Küldje el közösségét"
|
||||
"submit_community": "Küldje el közösségét",
|
||||
"hide_avatars_from_replies": "Rejtse el az avatarokat a válaszokból"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>buat</1> akun baru",
|
||||
"wallet_number": "dompet #{{index}}",
|
||||
"view_more": "lihat lebih banyak",
|
||||
"submit_community": "Kirim komunitas Anda"
|
||||
"submit_community": "Kirim komunitas Anda",
|
||||
"hide_avatars_from_replies": "Sembunyikan avatar dari balasan"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>crea</1> un nuovo account",
|
||||
"wallet_number": "wallet #{{index}}",
|
||||
"view_more": "altri",
|
||||
"submit_community": "Invia la tua comunità"
|
||||
"submit_community": "Invia la tua comunità",
|
||||
"hide_avatars_from_replies": "Nascondi avatar da commenti"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>新しい</1>アカウントを作成",
|
||||
"wallet_number": "ウォレット #{{index}}",
|
||||
"view_more": "もっと見る",
|
||||
"submit_community": "コミュニティを提出してください"
|
||||
"submit_community": "コミュニティを提出してください",
|
||||
"hide_avatars_from_replies": "返信からアバターを非表示にする"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>새로 만들기</1> 계정",
|
||||
"wallet_number": "지갑 #{{index}}",
|
||||
"view_more": "더보기",
|
||||
"submit_community": "커뮤니티를 제출하세요"
|
||||
"submit_community": "커뮤니티를 제출하세요",
|
||||
"hide_avatars_from_replies": "답글에서 아바타 숨기기"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>नवीन तयार करा</1> खाता",
|
||||
"wallet_number": "वॉलेट #{{index}}",
|
||||
"view_more": "अधिक पाहा",
|
||||
"submit_community": "तुमच्या समुदायाला सबमिट करा"
|
||||
"submit_community": "तुमच्या समुदायाला सबमिट करा",
|
||||
"hide_avatars_from_replies": "उत्तरांमधून अवतार लपवा"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>maak</1> een nieuw account aan",
|
||||
"wallet_number": "portemonnee #{{index}}",
|
||||
"view_more": "meer bekijken",
|
||||
"submit_community": "Dien uw gemeenschap in"
|
||||
"submit_community": "Dien uw gemeenschap in",
|
||||
"hide_avatars_from_replies": "Verberg avatars van reacties"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>opprett</1> en ny konto",
|
||||
"wallet_number": "lommebok #{{index}}",
|
||||
"view_more": "vis mer",
|
||||
"submit_community": "Send inn ditt fellesskap"
|
||||
"submit_community": "Send inn ditt fellesskap",
|
||||
"hide_avatars_from_replies": "Skjul avatarer fra svar"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>stwórz</1> nowe konto",
|
||||
"wallet_number": "portfel #{{index}}",
|
||||
"view_more": "zobacz więcej",
|
||||
"submit_community": "Prześlij swoją społeczność"
|
||||
"submit_community": "Prześlij swoją społeczność",
|
||||
"hide_avatars_from_replies": "Ukryj avatary w odpowiedziach"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>criar</1> uma nova conta",
|
||||
"wallet_number": "carteira #{{index}}",
|
||||
"view_more": "ver mais",
|
||||
"submit_community": "Envie sua comunidade"
|
||||
"submit_community": "Envie sua comunidade",
|
||||
"hide_avatars_from_replies": "Esconder avatares das respostas"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>creați</1> un cont nou",
|
||||
"wallet_number": "portofel #{{index}}",
|
||||
"view_more": "vezi mai mult",
|
||||
"submit_community": "Trimite comunitatea ta"
|
||||
"submit_community": "Trimite comunitatea ta",
|
||||
"hide_avatars_from_replies": "Ascunde avatarele din răspunsuri"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>создать</1> новую учетную запись",
|
||||
"wallet_number": "кошелек #{{index}}",
|
||||
"view_more": "смотреть больше",
|
||||
"submit_community": "Отправьте ваше сообщество"
|
||||
"submit_community": "Отправьте ваше сообщество",
|
||||
"hide_avatars_from_replies": "Скрыть аватары в ответах"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>krijo</1> një llogari të re",
|
||||
"wallet_number": "portofol #{{index}}",
|
||||
"view_more": "shiko më shumë",
|
||||
"submit_community": "Dorëzo komunitetin tuaj"
|
||||
"submit_community": "Dorëzo komunitetin tuaj",
|
||||
"hide_avatars_from_replies": "Fshih avatarët nga përgjigjet"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>skapa</1> ett nytt konto",
|
||||
"wallet_number": "plånbok #{{index}}",
|
||||
"view_more": "visa mer",
|
||||
"submit_community": "Skicka in ditt samhälle"
|
||||
"submit_community": "Skicka in ditt samhälle",
|
||||
"hide_avatars_from_replies": "Dölj avatarer från svar"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>తరువాత</1> కొత్త ఖాతా",
|
||||
"wallet_number": "వాలెట్ #{{index}}",
|
||||
"view_more": "మరింత చూడండి",
|
||||
"submit_community": "మీ కమ్యూనిటి పంపించండి"
|
||||
"submit_community": "మీ కమ్యూనిటి పంపించండి",
|
||||
"hide_avatars_from_replies": "ప్రతిస్పందనల నుండి అవతార్లను దాచు"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>สร้าง</1> บัญชีใหม่",
|
||||
"wallet_number": "กระเป๋าเงิน #{{index}}",
|
||||
"view_more": "ดูเพิ่มเติม",
|
||||
"submit_community": "ส่งชุมชนของคุณ"
|
||||
"submit_community": "ส่งชุมชนของคุณ",
|
||||
"hide_avatars_from_replies": "ซ่อนอวาตาร์จากคำตอบ"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>oluştur</1> yeni bir hesap",
|
||||
"wallet_number": "cüzdan #{{index}}",
|
||||
"view_more": "daha fazla görüntüle",
|
||||
"submit_community": "Topluluğunuzu gönderin"
|
||||
"submit_community": "Topluluğunuzu gönderin",
|
||||
"hide_avatars_from_replies": "Yanıtlardan avatarları gizle"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>створити</1> новий обліковий запис",
|
||||
"wallet_number": "гаманець #{{index}}",
|
||||
"view_more": "переглянути більше",
|
||||
"submit_community": "Надішліть своє співтовариство"
|
||||
"submit_community": "Надішліть своє співтовариство",
|
||||
"hide_avatars_from_replies": "Сховати аватари в відповідях"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>نیا بنائیں</1> اکاؤنٹ",
|
||||
"wallet_number": "والٹ #{{index}}",
|
||||
"view_more": "مزید دیکھیں",
|
||||
"submit_community": "اپنی کمیونٹی جمع کروائیں"
|
||||
"submit_community": "اپنی کمیونٹی جمع کروائیں",
|
||||
"hide_avatars_from_replies": "جوابوں سے اوتار چھپائیں"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>tạo</1> tài khoản mới",
|
||||
"wallet_number": "ví #{{index}}",
|
||||
"view_more": "xem thêm",
|
||||
"submit_community": "Gửi cộng đồng của bạn"
|
||||
"submit_community": "Gửi cộng đồng của bạn",
|
||||
"hide_avatars_from_replies": "Ẩn avatar trong các câu trả lời"
|
||||
}
|
||||
@@ -322,5 +322,6 @@
|
||||
"create_new_account": "<1>创建</1>新账户",
|
||||
"wallet_number": "钱包 #{{index}}",
|
||||
"view_more": "查看更多",
|
||||
"submit_community": "提交您的社区"
|
||||
"submit_community": "提交您的社区",
|
||||
"hide_avatars_from_replies": "隐藏回复中的头像"
|
||||
}
|
||||
@@ -41,8 +41,7 @@
|
||||
width: 84px;
|
||||
max-height: 26px;
|
||||
margin-top: 16px;
|
||||
margin-right: 5px;
|
||||
padding-right: 1ex;
|
||||
padding-right: 5px;
|
||||
filter: var(--filter90);
|
||||
}
|
||||
|
||||
|
||||
@@ -364,7 +364,7 @@ const Header = () => {
|
||||
(isInProfileView && !isInHomeAboutView) ||
|
||||
(isInAllView && !isInAllAboutView) ||
|
||||
(isInAuthorView && !isInHomeAboutView);
|
||||
const logoSrc = isInSubplebbitView ? suggested?.avatarUrl : 'assets/logo/seedit.png';
|
||||
const logoSrc = isInSubplebbitView && suggested?.avatarUrl ? suggested?.avatarUrl : 'assets/logo/seedit.png';
|
||||
const logoIsAvatar = isInSubplebbitView && suggested?.avatarUrl;
|
||||
const logoLink = isInSubplebbitView ? `/p/${params.subplebbitAddress}` : isInProfileView ? '/profile' : '/';
|
||||
|
||||
@@ -377,10 +377,10 @@ const Header = () => {
|
||||
>
|
||||
<div className={styles.logoContainer}>
|
||||
<Link to={logoLink} className={styles.logoLink}>
|
||||
{(logoIsAvatar || (!isInSubplebbitView && !isInProfileView && !isInAuthorView)) && (
|
||||
{(logoIsAvatar || (!isInSubplebbitView && !isInProfileView && !isInAuthorView) || (isInSubplebbitView && !suggested?.avatarUrl)) && (
|
||||
<img className={`${logoIsAvatar ? styles.avatar : styles.logo}`} src={logoSrc} alt='' />
|
||||
)}
|
||||
{!isInSubplebbitView && !isInProfileView && !isInAuthorView && (
|
||||
{((!isInSubplebbitView && !isInProfileView && !isInAuthorView) || (isInSubplebbitView && !suggested?.avatarUrl)) && (
|
||||
<img src={`assets/logo/seedit-text-${theme === 'dark' ? 'dark' : 'light'}.svg`} className={styles.logoText} alt='' />
|
||||
)}
|
||||
</Link>
|
||||
|
||||
@@ -1,21 +1,52 @@
|
||||
.ellipsis:after {
|
||||
overflow: hidden;
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
display: inline-block;
|
||||
vertical-align: text-bottom;
|
||||
-webkit-animation: ellipsis steps(4,end) 1500ms infinite;
|
||||
animation: ellipsis steps(4,end) 1500ms infinite;
|
||||
content: "\2026"; /* ascii code for the ellipsis character */
|
||||
width: 0px;
|
||||
width: 4ch;
|
||||
}
|
||||
|
||||
.ellipsis:after {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
-webkit-animation: ellipsis 1500ms steps(4, end) infinite;
|
||||
animation: ellipsis 1500ms steps(4, end) infinite;
|
||||
content: "";
|
||||
}
|
||||
|
||||
@keyframes ellipsis {
|
||||
to {
|
||||
width: 1.25em;
|
||||
0% {
|
||||
content: "";
|
||||
}
|
||||
25% {
|
||||
content: ".";
|
||||
}
|
||||
50% {
|
||||
content: "..";
|
||||
}
|
||||
75% {
|
||||
content: "...";
|
||||
}
|
||||
100% {
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes ellipsis {
|
||||
to {
|
||||
width: 1.25em;
|
||||
0% {
|
||||
content: "";
|
||||
}
|
||||
25% {
|
||||
content: ".";
|
||||
}
|
||||
50% {
|
||||
content: "..";
|
||||
}
|
||||
75% {
|
||||
content: "...";
|
||||
}
|
||||
100% {
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,20 @@ interface LoadingEllipsisProps {
|
||||
}
|
||||
|
||||
const LoadingEllipsis = ({ string }: LoadingEllipsisProps) => {
|
||||
return <span className={styles.ellipsis}>{string}</span>;
|
||||
const words = string.split(' ');
|
||||
const lastWord = words.pop();
|
||||
const restOfString = words.join(' ');
|
||||
|
||||
return (
|
||||
<span>
|
||||
{restOfString}
|
||||
{restOfString && ' '}
|
||||
<span className={styles.nowrap}>
|
||||
{lastWord}
|
||||
<span className={styles.ellipsis} />
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingEllipsis;
|
||||
|
||||
@@ -62,4 +62,8 @@
|
||||
.banInput {
|
||||
width: 3.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.modal .menuItem label {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import styles from './post.module.css';
|
||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||
import { Comment, useAuthorAddress, useBlock, useComment, useEditedComment, useSubplebbit, useSubscribe } from '@plebbit/plebbit-react-hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { isAllView, isPostView, isProfileHiddenView, isSubplebbitView } from '../../lib/utils/view-utils';
|
||||
import { CommentMediaInfo, fetchWebpageThumbnailIfNeeded, getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
|
||||
import { getHasThumbnail } from '../../lib/utils/media-utils';
|
||||
import { getPostScore } from '../../lib/utils/post-utils';
|
||||
import { getHostname } from '../../lib/utils/url-utils';
|
||||
import { getFormattedTimeAgo, formatLocalizedUTCTimestamp } from '../../lib/utils/time-utils';
|
||||
@@ -17,6 +17,7 @@ import Thumbnail from './thumbnail';
|
||||
import useDownvote from '../../hooks/use-downvote';
|
||||
import useUpvote from '../../hooks/use-upvote';
|
||||
import _ from 'lodash';
|
||||
import { useCommentMediaInfo } from '../../hooks/use-comment-media-info';
|
||||
|
||||
interface PostAuthorProps {
|
||||
authorAddress: string;
|
||||
@@ -70,33 +71,6 @@ interface PostProps {
|
||||
post: Comment | undefined;
|
||||
}
|
||||
|
||||
const ThumbnailLoader = ({ post }: PostProps) => {
|
||||
const { cid } = post || {};
|
||||
// Reset state by remounting component when post changes
|
||||
return useThumbnailContent(cid, post);
|
||||
};
|
||||
|
||||
const useThumbnailContent = (key: string, post: any) => {
|
||||
const [commentMediaInfo, setCommentMediaInfo] = useState<CommentMediaInfo | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
const loadThumbnail = async () => {
|
||||
const initialInfo = getCommentMediaInfo(post);
|
||||
// some sites have CORS access, so the thumbnail can be fetched client-side, which is helpful if subplebbit.settings.fetchThumbnailUrls is false
|
||||
if (initialInfo?.type === 'webpage' && !initialInfo.thumbnail) {
|
||||
const newMediaInfo = await fetchWebpageThumbnailIfNeeded(initialInfo);
|
||||
setCommentMediaInfo(newMediaInfo);
|
||||
} else {
|
||||
setCommentMediaInfo(initialInfo);
|
||||
}
|
||||
};
|
||||
|
||||
loadThumbnail();
|
||||
}, [post]);
|
||||
|
||||
return commentMediaInfo;
|
||||
};
|
||||
|
||||
const Post = ({ index, post = {} }: PostProps) => {
|
||||
// handle single comment thread
|
||||
const op = useComment({ commentCid: post?.parentCid ? post?.postCid : '' });
|
||||
@@ -147,7 +121,7 @@ const Post = ({ index, post = {} }: PostProps) => {
|
||||
const isInProfileHiddenView = isProfileHiddenView(location.pathname);
|
||||
const isInSubplebbitView = isSubplebbitView(location.pathname, params);
|
||||
|
||||
const commentMediaInfo = ThumbnailLoader({ post });
|
||||
const commentMediaInfo = useCommentMediaInfo(post);
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(isInPostView);
|
||||
const toggleExpanded = () => setIsExpanded(!isExpanded);
|
||||
|
||||
@@ -87,6 +87,7 @@ const ReplyForm = ({ cid, isReplyingToReply, hideReplyForm, subplebbitAddress, p
|
||||
const { t } = useTranslation();
|
||||
const [showOptions, setShowOptions] = useState(false);
|
||||
const [showFormattingHelp, setShowFormattingHelp] = useState(false);
|
||||
const [isTextareaFocused, setIsTextareaFocused] = useState(false);
|
||||
const { setContent, resetContent, replyIndex, publishReply } = usePublishReply({ cid, subplebbitAddress, postCid });
|
||||
|
||||
const mdContainerClass = isReplyingToReply ? `${styles.mdContainer} ${styles.mdContainerReplying}` : styles.mdContainer;
|
||||
@@ -149,7 +150,7 @@ const ReplyForm = ({ cid, isReplyingToReply, hideReplyForm, subplebbitAddress, p
|
||||
return (
|
||||
<div className={mdContainerClass}>
|
||||
<div className={styles.md}>
|
||||
{isOffline && <div className={styles.infobar}>{offlineTitle}</div>}
|
||||
{isOffline && isTextareaFocused && <div className={styles.infobar}>{offlineTitle}</div>}
|
||||
<div className={styles.options}>
|
||||
<span className={urlClass}>
|
||||
{t('media_url')}: <input className={`${styles.url} ${urlClass}`} ref={urlRef} onChange={(e) => setContent.link(e.target.value)} />
|
||||
@@ -160,7 +161,13 @@ const ReplyForm = ({ cid, isReplyingToReply, hideReplyForm, subplebbitAddress, p
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
<textarea className={styles.textarea} ref={textRef} onChange={(e) => setContent.content(e.target.value)} />
|
||||
<textarea
|
||||
className={styles.textarea}
|
||||
ref={textRef}
|
||||
onChange={(e) => setContent.content(e.target.value)}
|
||||
onFocus={() => setIsTextareaFocused(true)}
|
||||
onBlur={() => setIsTextareaFocused(false)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.bottomArea}>
|
||||
<button className={styles.save} onClick={onPublish}>
|
||||
|
||||
@@ -67,10 +67,17 @@
|
||||
}
|
||||
|
||||
.tagline {
|
||||
display: inline-block;
|
||||
color: var(--text);
|
||||
font-size: x-small;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.tagline {
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.expand {
|
||||
margin-right: 3px;
|
||||
padding: 1px;
|
||||
@@ -220,6 +227,7 @@
|
||||
|
||||
.collapsedEntry {
|
||||
padding-left: 25px;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
.parent {
|
||||
@@ -354,4 +362,11 @@
|
||||
.md {
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.stamp {
|
||||
font-family: inherit;
|
||||
font-size: x-small;
|
||||
line-height: normal;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { Comment, useAccountComment, useAuthorAddress, useAuthorAvatar, useBlock, useComment, useEditedComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { flattenCommentsPages } from '@plebbit/plebbit-react-hooks/dist/lib/utils';
|
||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styles from './reply.module.css';
|
||||
import { useCommentMediaInfo } from '../../hooks/use-comment-media-info';
|
||||
import useReplies from '../../hooks/use-replies';
|
||||
import { CommentMediaInfo, fetchWebpageThumbnailIfNeeded, getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
|
||||
import { CommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
|
||||
import { formatLocalizedUTCTimestamp, getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
||||
import CommentEditForm from '../comment-edit-form';
|
||||
import LoadingEllipsis from '../loading-ellipsis/';
|
||||
@@ -23,6 +24,7 @@ import { isInboxView, isPostContextView, isPostView } from '../../lib/utils/view
|
||||
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
|
||||
import Markdown from '../markdown';
|
||||
import { getHostname } from '../../lib/utils/url-utils';
|
||||
import useAvatarVisibilityStore from '../../stores/use-avatar-visibility-store';
|
||||
|
||||
interface ReplyAuthorProps {
|
||||
address: string;
|
||||
@@ -38,20 +40,20 @@ interface ReplyAuthorProps {
|
||||
|
||||
const ReplyAuthor = ({ address, authorRole, cid, deleted, displayName, imageUrl, isAvatarDefined, removed, shortAuthorAddress }: ReplyAuthorProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { hideAvatars } = useAvatarVisibilityStore();
|
||||
const isAuthorAdmin = authorRole === 'admin';
|
||||
const isAuthorOwner = authorRole === 'owner';
|
||||
const isAuthorModerator = authorRole === 'moderator';
|
||||
const authorRoleInitial = (isAuthorOwner && 'O') || (isAuthorAdmin && 'A') || (isAuthorModerator && 'M') || '';
|
||||
const moderatorClass = `${isAuthorOwner ? styles.owner : isAuthorAdmin ? styles.admin : isAuthorModerator ? styles.moderator : ''}`;
|
||||
const shortDisplayName = displayName?.length > 20 ? displayName?.slice(0, 20) + '...' : displayName;
|
||||
|
||||
return (
|
||||
<>
|
||||
{removed || deleted ? (
|
||||
<span className={styles.removedUsername}>[{removed ? t('removed') : deleted ? t('deleted') : ''}]</span>
|
||||
) : (
|
||||
<>
|
||||
{isAvatarDefined && (
|
||||
{!hideAvatars && isAvatarDefined && (
|
||||
<span className={styles.authorAvatar}>
|
||||
<img src={imageUrl} alt='' />
|
||||
</span>
|
||||
@@ -299,21 +301,7 @@ const Reply = ({ cidOfReplyWithContext, depth = 0, isSingleComment, isSingleRepl
|
||||
const showCommentEditForm = () => setIsEditing(true);
|
||||
const hideCommentEditForm = () => setIsEditing(false);
|
||||
|
||||
// some sites have CORS access, so the thumbnail can be fetched client-side, which is helpful if subplebbit.settings.fetchThumbnailUrls is false
|
||||
const initialCommentMediaInfo = useMemo(() => getCommentMediaInfo(reply), [reply]);
|
||||
const [commentMediaInfo, setCommentMediaInfo] = useState(initialCommentMediaInfo);
|
||||
|
||||
const fetchThumbnail = useCallback(async () => {
|
||||
if (initialCommentMediaInfo?.type === 'webpage' && !initialCommentMediaInfo.thumbnail) {
|
||||
const newMediaInfo = await fetchWebpageThumbnailIfNeeded(initialCommentMediaInfo);
|
||||
setCommentMediaInfo(newMediaInfo);
|
||||
}
|
||||
}, [initialCommentMediaInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchThumbnail();
|
||||
}, [fetchThumbnail]);
|
||||
|
||||
const commentMediaInfo = useCommentMediaInfo(reply);
|
||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
@@ -329,16 +317,6 @@ const Reply = ({ cidOfReplyWithContext, depth = 0, isSingleComment, isSingleRepl
|
||||
const [upvoted, upvote] = useUpvote(reply);
|
||||
const [downvoted, downvote] = useDownvote(reply);
|
||||
|
||||
const stateLabel = (
|
||||
<span className={styles.stateLabel}>
|
||||
{state === 'failed' && <Label color='red' text={t('failed')} />}
|
||||
{cid === undefined && state !== 'failed' && <Label color='yellow' text={t('pending')} />}
|
||||
{editState === 'failed' && <Label color='red' text={t('failed_edit')} />}
|
||||
{editState === 'pending' && <Label color='yellow' text={t('pending_edit')} />}
|
||||
{spoiler && <Label color='black' text={t('spoiler')} />}
|
||||
</span>
|
||||
);
|
||||
|
||||
const unnestedReplies = useMemo(() => flattenCommentsPages(reply.replies), [reply.replies]);
|
||||
const childrenCount = unnestedReplies.length;
|
||||
const childrenString = childrenCount === 1 ? t('child', { childrenCount }) : t('children', { childrenCount });
|
||||
@@ -357,6 +335,16 @@ const Reply = ({ cidOfReplyWithContext, depth = 0, isSingleComment, isSingleRepl
|
||||
setCollapsed(!collapsed);
|
||||
};
|
||||
|
||||
const stateLabel = (
|
||||
<span className={`${styles.stateLabel} ${collapsed ? styles.collapsedStateLabel : ''}`}>
|
||||
{state === 'failed' && <Label color='red' text={t('failed')} />}
|
||||
{cid === undefined && state !== 'failed' && <Label color='yellow' text={t('pending')} />}
|
||||
{editState === 'failed' && <Label color='red' text={t('failed_edit')} />}
|
||||
{editState === 'pending' && <Label color='yellow' text={t('pending_edit')} />}
|
||||
{spoiler && <Label color='black' text={t('spoiler')} />}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.reply}>
|
||||
{isSingleReply && !isInInboxView && <ParentLink postCid={cid ? postCid : parentOfPendingReply?.postCid} />}
|
||||
@@ -392,15 +380,19 @@ const Reply = ({ cidOfReplyWithContext, depth = 0, isSingleComment, isSingleRepl
|
||||
{edit && <span className={styles.timeEdited}> {t('edited_timestamp', { timestamp: getFormattedTimeAgo(edit.timestamp) })}</span>}
|
||||
</span>{' '}
|
||||
{pinned && <span className={styles.pinned}>- {t('stickied_comment')}</span>}
|
||||
{collapsed && <span className={styles.children}> ({childrenString})</span>}
|
||||
{stateLabel}{' '}
|
||||
{collapsed && (
|
||||
<>
|
||||
<span className={styles.children}>({childrenString})</span> {stateLabel} {state === 'pending' && loadingString}
|
||||
</>
|
||||
)}
|
||||
{!collapsed && stateLabel}
|
||||
{!collapsed && flair && (
|
||||
<>
|
||||
{' '}
|
||||
<Flair flair={flair} />
|
||||
</>
|
||||
)}
|
||||
{state === 'pending' && loadingString}
|
||||
{state === 'pending' && !collapsed && <> {loadingString}</>}
|
||||
</p>
|
||||
)}
|
||||
{isInInboxView && (
|
||||
|
||||
20
src/hooks/use-comment-media-info.ts
Normal file
20
src/hooks/use-comment-media-info.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Comment } from '@plebbit/plebbit-react-hooks';
|
||||
import { getCommentMediaInfo, fetchWebpageThumbnailIfNeeded } from '../lib/utils/media-utils';
|
||||
|
||||
export const useCommentMediaInfo = (comment: Comment) => {
|
||||
// some sites have CORS access, so the thumbnail can be fetched client-side, which is helpful if subplebbit.settings.fetchThumbnailUrls is false
|
||||
const initialCommentMediaInfo = useMemo(() => getCommentMediaInfo(comment), [comment]);
|
||||
const [commentMediaInfo, setCommentMediaInfo] = useState(initialCommentMediaInfo);
|
||||
const fetchThumbnail = useCallback(async () => {
|
||||
if (initialCommentMediaInfo?.type === 'webpage' && !initialCommentMediaInfo.thumbnail) {
|
||||
const newMediaInfo = await fetchWebpageThumbnailIfNeeded(initialCommentMediaInfo);
|
||||
setCommentMediaInfo(newMediaInfo);
|
||||
}
|
||||
}, [initialCommentMediaInfo]);
|
||||
useEffect(() => {
|
||||
fetchThumbnail();
|
||||
}, [fetchThumbnail]);
|
||||
|
||||
return commentMediaInfo;
|
||||
};
|
||||
21
src/stores/use-avatar-visibility-store.ts
Normal file
21
src/stores/use-avatar-visibility-store.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
interface AvatarVisibilityState {
|
||||
hideAvatars: boolean;
|
||||
setHideAvatars: (hide: boolean) => void;
|
||||
}
|
||||
|
||||
const useAvatarVisibilityStore = create<AvatarVisibilityState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
hideAvatars: false,
|
||||
setHideAvatars: (hide) => set({ hideAvatars: hide }),
|
||||
}),
|
||||
{
|
||||
name: 'avatar-visibility-storage',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export default useAvatarVisibilityStore;
|
||||
@@ -96,7 +96,7 @@ const FAQ = () => {
|
||||
Seedit desktop
|
||||
</a>
|
||||
, which automatically runs a full node and lets you create communities with the ease of a reddit-like graphical user interface. You can also open a remote
|
||||
connection to an <HashLink to='/settings/plebbit-options#nodeRpc'>RPC node</HashLink> to create communities even on web and on mobile devices. In the near
|
||||
connection to an <HashLink to='/settings/plebbit-options#plebbitRpc'>RPC node</HashLink> to create communities even on web and on mobile devices. In the near
|
||||
future, we plan to include a public RPC connection turned on by default, so you can create communities on any device without running your own node.
|
||||
</p>
|
||||
<hr />
|
||||
|
||||
@@ -62,7 +62,7 @@ const All = () => {
|
||||
footerContent = (
|
||||
<>
|
||||
{subplebbitAddressesWithNewerPosts.length > 0 ? (
|
||||
<div className={styles.stateString}>
|
||||
<div className={styles.morePostsSuggestion}>
|
||||
<Trans
|
||||
i18nKey='newer_posts_available'
|
||||
components={{
|
||||
@@ -74,7 +74,7 @@ const All = () => {
|
||||
showMorePostsSuggestion &&
|
||||
monthlyFeed.length > feed.length &&
|
||||
(weeklyFeed.length > feed.length ? (
|
||||
<div className={styles.stateString}>
|
||||
<div className={styles.morePostsSuggestion}>
|
||||
<Trans
|
||||
i18nKey='more_posts_last_week'
|
||||
values={{ currentTimeFilterName }}
|
||||
@@ -84,7 +84,7 @@ const All = () => {
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.stateString}>
|
||||
<div className={styles.morePostsSuggestion}>
|
||||
<Trans
|
||||
i18nKey='more_posts_last_month'
|
||||
values={{ currentTimeFilterName }}
|
||||
|
||||
@@ -74,4 +74,18 @@
|
||||
|
||||
.copyMessage a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.hideAvatarsCheckbox {
|
||||
padding-top: 10px;
|
||||
display: inline-block;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.hideAvatarsCheckbox label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hideAvatarsCheckbox input {
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
|
||||
import { setAccount, useAccount, useAuthorAvatar } from '@plebbit/plebbit-react-hooks';
|
||||
import styles from './avatar-settings.module.css';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import useAvatarVisibilityStore from '../../../stores/use-avatar-visibility-store';
|
||||
|
||||
interface AvatarSettingsProps {
|
||||
areSettingsShown?: boolean;
|
||||
@@ -57,6 +58,7 @@ const AvatarSettings = () => {
|
||||
const [tokenId, setTokenId] = useState(account?.author?.avatar?.id);
|
||||
const [timestamp, setTimestamp] = useState(account?.author?.avatar?.timestamp);
|
||||
const [signature, setSignature] = useState(account?.author?.avatar?.signature?.signature);
|
||||
const { hideAvatars, setHideAvatars } = useAvatarVisibilityStore();
|
||||
|
||||
const getNftMessageToSign = (authorAddress: string, timestamp: number, tokenAddress: string, tokenId: string) => {
|
||||
let messageToSign: any = {};
|
||||
@@ -205,6 +207,12 @@ const AvatarSettings = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.hideAvatarsCheckbox}>
|
||||
<label>
|
||||
<input type='checkbox' checked={hideAvatars} onChange={(e) => setHideAvatars(e.target.checked)} />
|
||||
{t('hide_avatars_from_replies')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ interface SettingsProps {
|
||||
maticRpcRef?: RefObject<HTMLTextAreaElement>;
|
||||
avaxRpcRef?: RefObject<HTMLTextAreaElement>;
|
||||
plebbitRpcRef?: RefObject<HTMLInputElement>;
|
||||
nodeDataPathRef?: RefObject<HTMLInputElement>;
|
||||
plebbitDataPathRef?: RefObject<HTMLInputElement>;
|
||||
}
|
||||
|
||||
const IPFSGatewaysSettings = ({ ipfsGatewayUrlsRef, mediaIpfsGatewayUrlRef }: SettingsProps) => {
|
||||
@@ -129,21 +129,23 @@ const PlebbitRPCSettings = ({ plebbitRpcRef }: SettingsProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const NodeDataPathSettings = ({ nodeDataPathRef }: SettingsProps) => {
|
||||
const PlebbitDataPathSettings = ({ plebbitDataPathRef }: SettingsProps) => {
|
||||
const plebbitRpc = usePlebbitRpcSettings();
|
||||
const { plebbitRpcSettings } = plebbitRpc || {};
|
||||
const isConnectedToRpc = plebbitRpc?.state === 'succeeded';
|
||||
const path = plebbitRpcSettings?.plebbitOptions?.dataPath || '';
|
||||
|
||||
return (
|
||||
<div className={styles.nodeDataPathSettings}>
|
||||
<div className={styles.plebbitDataPathSettings}>
|
||||
<div>
|
||||
<input type='text' defaultValue={path} disabled={!isConnectedToRpc} ref={nodeDataPathRef} />
|
||||
<input type='text' defaultValue={path} disabled={!isConnectedToRpc} ref={plebbitDataPathRef} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const isElectron = window.isElectron === true;
|
||||
|
||||
const PlebbitOptions = () => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
@@ -158,7 +160,7 @@ const PlebbitOptions = () => {
|
||||
const maticRpcRef = useRef<HTMLTextAreaElement>(null);
|
||||
const avaxRpcRef = useRef<HTMLTextAreaElement>(null);
|
||||
const plebbitRpcRef = useRef<HTMLInputElement>(null);
|
||||
const nodeDataPathRef = useRef<HTMLInputElement>(null);
|
||||
const plebbitDataPathRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleSave = async () => {
|
||||
const ipfsGatewayUrls = ipfsGatewayUrlsRef.current?.value.split('\n').map((url) => url.trim());
|
||||
@@ -169,7 +171,7 @@ const PlebbitOptions = () => {
|
||||
const maticRpcUrls = maticRpcRef.current?.value.split('\n').map((url) => url.trim());
|
||||
const avaxRpcUrls = avaxRpcRef.current?.value.split('\n').map((url) => url.trim());
|
||||
const plebbitRpcClientsOptions = plebbitRpcRef.current?.value.trim();
|
||||
const dataPath = nodeDataPathRef.current?.value.trim();
|
||||
const dataPath = plebbitDataPathRef.current?.value.trim();
|
||||
|
||||
const chainProviders = {
|
||||
eth: {
|
||||
@@ -234,18 +236,20 @@ const PlebbitOptions = () => {
|
||||
<BlockchainProvidersSettings ethRpcRef={ethRpcRef} solRpcRef={solRpcRef} maticRpcRef={maticRpcRef} avaxRpcRef={avaxRpcRef} />
|
||||
</span>
|
||||
</div>
|
||||
<div className={`${styles.category} ${location.hash === '#nodeRpc' ? styles.highlightedSetting : ''}`} id='nodeRpc'>
|
||||
<span className={styles.categoryTitle}>node rpc</span>
|
||||
<div className={`${styles.category} ${location.hash === '#plebbitRpc' ? styles.highlightedSetting : ''}`} id='plebbitRpc'>
|
||||
<span className={styles.categoryTitle}>plebbit rpc</span>
|
||||
<span className={styles.categorySettings}>
|
||||
<PlebbitRPCSettings plebbitRpcRef={plebbitRpcRef} />
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.category}>
|
||||
<span className={styles.categoryTitle}>node data path</span>
|
||||
<span className={styles.categorySettings}>
|
||||
<NodeDataPathSettings nodeDataPathRef={nodeDataPathRef} />
|
||||
</span>
|
||||
</div>
|
||||
{isElectron && (
|
||||
<div className={styles.category}>
|
||||
<span className={styles.categoryTitle}>plebbit data path</span>
|
||||
<span className={styles.categorySettings}>
|
||||
<PlebbitDataPathSettings plebbitDataPathRef={plebbitDataPathRef} />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<button className={styles.saveOptions} onClick={handleSave}>
|
||||
{t('save_options')}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user