mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
|
Before Width: | Height: | Size: 403 KiB After Width: | Height: | Size: 403 KiB |
|
Before Width: | Height: | Size: 344 KiB After Width: | Height: | Size: 344 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
@@ -147,5 +147,6 @@
|
|||||||
"optional": "اختياري",
|
"optional": "اختياري",
|
||||||
"close_thread": "اغلق الموضوع",
|
"close_thread": "اغلق الموضوع",
|
||||||
"view_spoiler": "عرض الحاضر",
|
"view_spoiler": "عرض الحاضر",
|
||||||
"filter": "تصفية"
|
"filter": "تصفية",
|
||||||
|
"edit": "تحرير"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "ঐচ্ছিক",
|
"optional": "ঐচ্ছিক",
|
||||||
"close_thread": "থ্রেড বন্ধ করুন",
|
"close_thread": "থ্রেড বন্ধ করুন",
|
||||||
"view_spoiler": "স্পোয়ালার দেখুন",
|
"view_spoiler": "স্পোয়ালার দেখুন",
|
||||||
"filter": "ফিল্টার"
|
"filter": "ফিল্টার",
|
||||||
|
"edit": "সম্পাদনা"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "volitelný",
|
"optional": "volitelný",
|
||||||
"close_thread": "uzavřít vlákno",
|
"close_thread": "uzavřít vlákno",
|
||||||
"view_spoiler": "zobrazit spoiler",
|
"view_spoiler": "zobrazit spoiler",
|
||||||
"filter": "filtr"
|
"filter": "filtr",
|
||||||
|
"edit": "upravit"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "valgfri",
|
"optional": "valgfri",
|
||||||
"close_thread": "luk tråd",
|
"close_thread": "luk tråd",
|
||||||
"view_spoiler": "vis spoiler",
|
"view_spoiler": "vis spoiler",
|
||||||
"filter": "filtrér"
|
"filter": "filtrér",
|
||||||
|
"edit": "rediger"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "optional",
|
"optional": "optional",
|
||||||
"close_thread": "Thread schließen",
|
"close_thread": "Thread schließen",
|
||||||
"view_spoiler": "Spoiler anzeigen",
|
"view_spoiler": "Spoiler anzeigen",
|
||||||
"filter": "filtern"
|
"filter": "filtern",
|
||||||
|
"edit": "bearbeiten"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "προαιρετικό",
|
"optional": "προαιρετικό",
|
||||||
"close_thread": "κλείσιμο νήματος",
|
"close_thread": "κλείσιμο νήματος",
|
||||||
"view_spoiler": "προβολή spoiler",
|
"view_spoiler": "προβολή spoiler",
|
||||||
"filter": "φίλτρο"
|
"filter": "φίλτρο",
|
||||||
|
"edit": "επεξεργασία"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "optional",
|
"optional": "optional",
|
||||||
"close_thread": "close thread",
|
"close_thread": "close thread",
|
||||||
"view_spoiler": "view spoiler",
|
"view_spoiler": "view spoiler",
|
||||||
"filter": "filter"
|
"filter": "filter",
|
||||||
|
"edit": "edit"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "opcional",
|
"optional": "opcional",
|
||||||
"close_thread": "cerrar hilo",
|
"close_thread": "cerrar hilo",
|
||||||
"view_spoiler": "ver spoiler",
|
"view_spoiler": "ver spoiler",
|
||||||
"filter": "filtrar"
|
"filter": "filtrar",
|
||||||
|
"edit": "editar"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "اختیاری",
|
"optional": "اختیاری",
|
||||||
"close_thread": "بستن موضوع",
|
"close_thread": "بستن موضوع",
|
||||||
"view_spoiler": "نمایش اسپویلر",
|
"view_spoiler": "نمایش اسپویلر",
|
||||||
"filter": "فیلتر"
|
"filter": "فیلتر",
|
||||||
|
"edit": "ویرایش"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "valinnainen",
|
"optional": "valinnainen",
|
||||||
"close_thread": "sulje keskustelu",
|
"close_thread": "sulje keskustelu",
|
||||||
"view_spoiler": "näytä spoiler",
|
"view_spoiler": "näytä spoiler",
|
||||||
"filter": "suodatin"
|
"filter": "suodatin",
|
||||||
|
"edit": "muokkaa"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "opsyonal",
|
"optional": "opsyonal",
|
||||||
"close_thread": "isara ang thread",
|
"close_thread": "isara ang thread",
|
||||||
"view_spoiler": "tingnan ang spoiler",
|
"view_spoiler": "tingnan ang spoiler",
|
||||||
"filter": "salain"
|
"filter": "salain",
|
||||||
|
"edit": "i-edit"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "facultatif",
|
"optional": "facultatif",
|
||||||
"close_thread": "fermer le fil",
|
"close_thread": "fermer le fil",
|
||||||
"view_spoiler": "voir le spoiler",
|
"view_spoiler": "voir le spoiler",
|
||||||
"filter": "filtre"
|
"filter": "filtre",
|
||||||
|
"edit": "modifier"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "אופציונלי",
|
"optional": "אופציונלי",
|
||||||
"close_thread": "סגור את הנושא",
|
"close_thread": "סגור את הנושא",
|
||||||
"view_spoiler": "הצג ספוילר",
|
"view_spoiler": "הצג ספוילר",
|
||||||
"filter": "סנן"
|
"filter": "סנן",
|
||||||
|
"edit": "ערוך"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "वैकल्पिक",
|
"optional": "वैकल्पिक",
|
||||||
"close_thread": "थ्रेड बंद करें",
|
"close_thread": "थ्रेड बंद करें",
|
||||||
"view_spoiler": "स्पॉइलर देखें",
|
"view_spoiler": "स्पॉइलर देखें",
|
||||||
"filter": "फ़िल्टर"
|
"filter": "फ़िल्टर",
|
||||||
|
"edit": "संपादित करें"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "opcionális",
|
"optional": "opcionális",
|
||||||
"close_thread": "szál bezárása",
|
"close_thread": "szál bezárása",
|
||||||
"view_spoiler": "spoiler megtekintése",
|
"view_spoiler": "spoiler megtekintése",
|
||||||
"filter": "szűrő"
|
"filter": "szűrő",
|
||||||
|
"edit": "szerkesztés"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "opsional",
|
"optional": "opsional",
|
||||||
"close_thread": "tutup utas",
|
"close_thread": "tutup utas",
|
||||||
"view_spoiler": "lihat spoiler",
|
"view_spoiler": "lihat spoiler",
|
||||||
"filter": "menyaring"
|
"filter": "menyaring",
|
||||||
|
"edit": "sunting"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "opzionale",
|
"optional": "opzionale",
|
||||||
"close_thread": "chiudi il thread",
|
"close_thread": "chiudi il thread",
|
||||||
"view_spoiler": "mostra spoiler",
|
"view_spoiler": "mostra spoiler",
|
||||||
"filter": "filtro"
|
"filter": "filtro",
|
||||||
|
"edit": "modifica"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "オプショナル",
|
"optional": "オプショナル",
|
||||||
"close_thread": "スレッドを閉じる",
|
"close_thread": "スレッドを閉じる",
|
||||||
"view_spoiler": "スポイラーを表示",
|
"view_spoiler": "スポイラーを表示",
|
||||||
"filter": "フィルター"
|
"filter": "フィルター",
|
||||||
|
"edit": "編集"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "선택",
|
"optional": "선택",
|
||||||
"close_thread": "스레드 닫기",
|
"close_thread": "스레드 닫기",
|
||||||
"view_spoiler": "스포일러 보기",
|
"view_spoiler": "스포일러 보기",
|
||||||
"filter": "필터"
|
"filter": "필터",
|
||||||
|
"edit": "편집"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "वैकल्पिक",
|
"optional": "वैकल्पिक",
|
||||||
"close_thread": "थ्रेड बंद करा",
|
"close_thread": "थ्रेड बंद करा",
|
||||||
"view_spoiler": "स्पॉईलर पहा",
|
"view_spoiler": "स्पॉईलर पहा",
|
||||||
"filter": "फिल्टर"
|
"filter": "फिल्टर",
|
||||||
|
"edit": "संपादित करा"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "optioneel",
|
"optional": "optioneel",
|
||||||
"close_thread": "draad sluiten",
|
"close_thread": "draad sluiten",
|
||||||
"view_spoiler": "bekijk spoiler",
|
"view_spoiler": "bekijk spoiler",
|
||||||
"filter": "filter"
|
"filter": "filter",
|
||||||
|
"edit": "bewerken"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "valgfritt",
|
"optional": "valgfritt",
|
||||||
"close_thread": "lukk tråd",
|
"close_thread": "lukk tråd",
|
||||||
"view_spoiler": "se spoiler",
|
"view_spoiler": "se spoiler",
|
||||||
"filter": "filter"
|
"filter": "filter",
|
||||||
|
"edit": "rediger"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "opcjonalnie",
|
"optional": "opcjonalnie",
|
||||||
"close_thread": "zamknij wątek",
|
"close_thread": "zamknij wątek",
|
||||||
"view_spoiler": "pokaż spoiler",
|
"view_spoiler": "pokaż spoiler",
|
||||||
"filter": "filtr"
|
"filter": "filtr",
|
||||||
|
"edit": "edytuj"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "opcional",
|
"optional": "opcional",
|
||||||
"close_thread": "fechar tópico",
|
"close_thread": "fechar tópico",
|
||||||
"view_spoiler": "ver spoiler",
|
"view_spoiler": "ver spoiler",
|
||||||
"filter": "filtro"
|
"filter": "filtro",
|
||||||
|
"edit": "editar"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "opțional",
|
"optional": "opțional",
|
||||||
"close_thread": "închide firul",
|
"close_thread": "închide firul",
|
||||||
"view_spoiler": "vezi spoilerul",
|
"view_spoiler": "vezi spoilerul",
|
||||||
"filter": "filtru"
|
"filter": "filtru",
|
||||||
|
"edit": "editați"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "необязательный",
|
"optional": "необязательный",
|
||||||
"close_thread": "закрыть тему",
|
"close_thread": "закрыть тему",
|
||||||
"view_spoiler": "посмотреть спойлер",
|
"view_spoiler": "посмотреть спойлер",
|
||||||
"filter": "фильтр"
|
"filter": "фильтр",
|
||||||
|
"edit": "редактировать"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "opsionale",
|
"optional": "opsionale",
|
||||||
"close_thread": "mbyll temën",
|
"close_thread": "mbyll temën",
|
||||||
"view_spoiler": "shikoni spoiler",
|
"view_spoiler": "shikoni spoiler",
|
||||||
"filter": "filtruar"
|
"filter": "filtruar",
|
||||||
|
"edit": "redakto"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "valfritt",
|
"optional": "valfritt",
|
||||||
"close_thread": "stäng tråd",
|
"close_thread": "stäng tråd",
|
||||||
"view_spoiler": "visa spoiler",
|
"view_spoiler": "visa spoiler",
|
||||||
"filter": "filter"
|
"filter": "filter",
|
||||||
|
"edit": "redigera"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "ఐచ్ఛికం",
|
"optional": "ఐచ్ఛికం",
|
||||||
"close_thread": "థ్రెడ్ను మూసేయండి",
|
"close_thread": "థ్రెడ్ను మూసేయండి",
|
||||||
"view_spoiler": "స్పోయ్లర్ వీక్షించండి",
|
"view_spoiler": "స్పోయ్లర్ వీక్షించండి",
|
||||||
"filter": "వడపోత"
|
"filter": "వడపోత",
|
||||||
|
"edit": "సవరించు"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "ไม่จำเป็น",
|
"optional": "ไม่จำเป็น",
|
||||||
"close_thread": "ปิดกระทู้",
|
"close_thread": "ปิดกระทู้",
|
||||||
"view_spoiler": "ดูสปอยล์เลอร์",
|
"view_spoiler": "ดูสปอยล์เลอร์",
|
||||||
"filter": "ตัวกรอง"
|
"filter": "ตัวกรอง",
|
||||||
|
"edit": "แก้ไข"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "isteğe bağlı",
|
"optional": "isteğe bağlı",
|
||||||
"close_thread": "konuyu kapat",
|
"close_thread": "konuyu kapat",
|
||||||
"view_spoiler": "spoileri görüntüle",
|
"view_spoiler": "spoileri görüntüle",
|
||||||
"filter": "filtre"
|
"filter": "filtre",
|
||||||
|
"edit": "düzenle"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "необов’язково",
|
"optional": "необов’язково",
|
||||||
"close_thread": "закрити тему",
|
"close_thread": "закрити тему",
|
||||||
"view_spoiler": "переглянути спойлер",
|
"view_spoiler": "переглянути спойлер",
|
||||||
"filter": "фільтр"
|
"filter": "фільтр",
|
||||||
|
"edit": "редагувати"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "اختیاری",
|
"optional": "اختیاری",
|
||||||
"close_thread": "موضوع بند کریں",
|
"close_thread": "موضوع بند کریں",
|
||||||
"view_spoiler": "اسپوائلر دیکھیں",
|
"view_spoiler": "اسپوائلر دیکھیں",
|
||||||
"filter": "فلٹر"
|
"filter": "فلٹر",
|
||||||
|
"edit": "ترمیم کریں"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "tùy chọn",
|
"optional": "tùy chọn",
|
||||||
"close_thread": "đóng chủ đề",
|
"close_thread": "đóng chủ đề",
|
||||||
"view_spoiler": "xem spoiler",
|
"view_spoiler": "xem spoiler",
|
||||||
"filter": "bộ lọc"
|
"filter": "bộ lọc",
|
||||||
|
"edit": "chỉnh sửa"
|
||||||
}
|
}
|
||||||
@@ -147,5 +147,6 @@
|
|||||||
"optional": "可选的",
|
"optional": "可选的",
|
||||||
"close_thread": "关闭主题",
|
"close_thread": "关闭主题",
|
||||||
"view_spoiler": "查看剧透",
|
"view_spoiler": "查看剧透",
|
||||||
"filter": "过滤器"
|
"filter": "过滤器",
|
||||||
|
"edit": "编辑"
|
||||||
}
|
}
|
||||||
+14
-12
@@ -35,18 +35,20 @@ const BoardLayout = () => {
|
|||||||
<div className={styles.boardLayout}>
|
<div className={styles.boardLayout}>
|
||||||
<TopBar />
|
<TopBar />
|
||||||
<BoardHeader />
|
<BoardHeader />
|
||||||
{isMobile ? (
|
{isMobile
|
||||||
<>
|
? subplebbitAddress && (
|
||||||
<PostForm key={key} />
|
<>
|
||||||
<MobileBoardButtons />
|
<PostForm key={key} />
|
||||||
</>
|
<MobileBoardButtons />
|
||||||
) : (
|
</>
|
||||||
<>
|
)
|
||||||
<PostForm key={key} />
|
: subplebbitAddress && (
|
||||||
{subplebbitAddress && <SubplebbitStats />}
|
<>
|
||||||
<DesktopBoardButtons />
|
<PostForm key={key} />
|
||||||
</>
|
<SubplebbitStats />
|
||||||
)}
|
<DesktopBoardButtons />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -89,18 +89,18 @@ export const MobileBoardButtons = () => {
|
|||||||
<div className={styles.mobileBoardButtons}>
|
<div className={styles.mobileBoardButtons}>
|
||||||
{isInPostView || isInPendingPostPage ? (
|
{isInPostView || isInPendingPostPage ? (
|
||||||
<>
|
<>
|
||||||
<ReturnButton address={subplebbitAddress} />
|
<ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />
|
||||||
<CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />
|
<CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />
|
||||||
<SubscribeButton address={subplebbitAddress} />
|
<SubscribeButton address={subplebbitAddress} />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{isInCatalogView ? (
|
{isInCatalogView ? (
|
||||||
<ReturnButton address={subplebbitAddress} />
|
<ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />
|
||||||
) : (
|
) : (
|
||||||
<CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />
|
<CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />
|
||||||
)}
|
)}
|
||||||
<SubscribeButton address={subplebbitAddress} />
|
{!(isInAllView || isInSubscriptionsView) && <SubscribeButton address={subplebbitAddress} />}
|
||||||
<RefreshButton />
|
<RefreshButton />
|
||||||
{isInCatalogView && (
|
{isInCatalogView && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
|||||||
import styles from './board-header.module.css';
|
import styles from './board-header.module.css';
|
||||||
import { useMultisubMetadata } from '../../hooks/use-default-subplebbits';
|
import { useMultisubMetadata } from '../../hooks/use-default-subplebbits';
|
||||||
|
|
||||||
const totalBanners = 57;
|
const totalBanners = 59;
|
||||||
|
|
||||||
const ImageBanner = () => {
|
const ImageBanner = () => {
|
||||||
const [imagePath] = useState(() => {
|
const [imagePath] = useState(() => {
|
||||||
|
|||||||
+29
-13
@@ -4,14 +4,6 @@
|
|||||||
filter: var(--filter80);
|
filter: var(--filter80);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
|
||||||
color: var(--text-info);
|
|
||||||
font-weight: bold;
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 0 4px 0 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
z-index: 7;
|
z-index: 7;
|
||||||
background-color: var(--challenge-modal-background-color);
|
background-color: var(--challenge-modal-background-color);
|
||||||
@@ -43,7 +35,8 @@
|
|||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menuItem input[type="text"] {
|
|
||||||
|
.menuItem input[type="text"], .editTextarea {
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
box-shadow: var(--box-shadow-input);
|
box-shadow: var(--box-shadow-input);
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
@@ -51,12 +44,12 @@
|
|||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menuItem input[type="text"], .menuItem input[type='number'] {
|
.menuItem input[type="text"], .menuItem input[type='number'], .editTextarea {
|
||||||
border: var(--post-form-field-input-border);
|
border: var(--post-form-field-input-border);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menuItem input[type="text"]:focus, .menuItem input[type='number']:focus {
|
.menuItem input[type="text"]:focus, .menuItem input[type='number']:focus, .editTextarea:focus {
|
||||||
border: var(--post-form-field-input-focus-border);
|
border: var(--post-form-field-input-focus-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,10 +58,14 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editTextarea {
|
||||||
|
width: calc(100% - 12px);
|
||||||
|
margin-left: 3px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.bottom button {
|
.bottom button {
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
cursor: pointer;
|
|
||||||
padding: 2px 6px 3px;
|
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
@@ -82,3 +79,22 @@
|
|||||||
width: 3.5em;
|
width: 3.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.bottom button {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.checkbox {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replyCheckbox {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
import { autoUpdate, flip, FloatingFocusManager, offset, shift, useClick, useDismiss, useFloating, useId, useInteractions, useRole } from '@floating-ui/react';
|
||||||
|
import { PublishCommentEditOptions, useComment, useEditedComment, usePublishCommentEdit } from '@plebbit/plebbit-react-hooks';
|
||||||
|
import styles from './edit-menu.module.css';
|
||||||
|
import { alertChallengeVerificationFailed } from '../../../lib/utils/challenge-utils';
|
||||||
|
import useChallengesStore from '../../../stores/use-challenges-store';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import useIsMobile from '../../../hooks/use-is-mobile';
|
||||||
|
|
||||||
|
const { addChallenge } = useChallengesStore.getState();
|
||||||
|
|
||||||
|
type EditMenuProps = {
|
||||||
|
commentCid: string;
|
||||||
|
isAccountMod?: boolean;
|
||||||
|
isAccountCommentAuthor?: boolean;
|
||||||
|
isCommentAuthorMod?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditMenu = ({ commentCid, isAccountMod, isAccountCommentAuthor, isCommentAuthorMod }: EditMenuProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
let post: any;
|
||||||
|
const comment = useComment({ commentCid });
|
||||||
|
const { editedComment } = useEditedComment({ comment });
|
||||||
|
if (editedComment) {
|
||||||
|
post = editedComment;
|
||||||
|
} else if (comment) {
|
||||||
|
post = comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { banExpiresAt, content, deleted, locked, parentCid, pinned, removed, spoiler, subplebbitAddress } = post || {};
|
||||||
|
const isReply = parentCid;
|
||||||
|
|
||||||
|
const [isEditMenuOpen, setIsEditMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const [isContentEditorOpen, setIsContentEditorOpen] = useState(false);
|
||||||
|
const contentEditorRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (contentEditorRef.current) {
|
||||||
|
contentEditorRef.current.focus();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const defaultPublishOptions: PublishCommentEditOptions = {
|
||||||
|
commentAuthor: { banExpiresAt },
|
||||||
|
commentCid,
|
||||||
|
content,
|
||||||
|
deleted,
|
||||||
|
locked,
|
||||||
|
pinned,
|
||||||
|
removed,
|
||||||
|
spoiler,
|
||||||
|
subplebbitAddress,
|
||||||
|
onChallenge: (...args: any) => addChallenge([...args, post]),
|
||||||
|
onChallengeVerification: alertChallengeVerificationFailed,
|
||||||
|
onError: (error: Error) => {
|
||||||
|
console.warn(error);
|
||||||
|
alert(error.message);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const [publishCommentEditOptions, setPublishCommentEditOptions] = useState(defaultPublishOptions);
|
||||||
|
const { publishCommentEdit } = usePublishCommentEdit(publishCommentEditOptions);
|
||||||
|
|
||||||
|
const [banDuration, setBanDuration] = useState(1);
|
||||||
|
|
||||||
|
const daysToTimestampInSeconds = (days: number) => {
|
||||||
|
const now = new Date();
|
||||||
|
now.setDate(now.getDate() + days);
|
||||||
|
return Math.floor(now.getTime() / 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCheckbox = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { id, checked } = e.target;
|
||||||
|
if (id === 'banUser') {
|
||||||
|
setPublishCommentEditOptions((state) => ({
|
||||||
|
...state,
|
||||||
|
commentAuthor: { ...state.commentAuthor, banExpiresAt: checked ? daysToTimestampInSeconds(banDuration) : undefined },
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
setPublishCommentEditOptions((state) => ({ ...state, [id]: checked }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBanDurationChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const days = parseInt(e.target.value, 10) || 1;
|
||||||
|
setBanDuration(days);
|
||||||
|
setPublishCommentEditOptions((state) => ({
|
||||||
|
...state,
|
||||||
|
commentAuthor: { ...state.commentAuthor, banExpiresAt: daysToTimestampInSeconds(days) },
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onReason = (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setPublishCommentEditOptions((state) => ({ ...state, reason: e.target.value ? e.target.value : undefined }));
|
||||||
|
|
||||||
|
const { refs, floatingStyles, context } = useFloating({
|
||||||
|
placement: 'bottom-start',
|
||||||
|
open: isEditMenuOpen,
|
||||||
|
onOpenChange: setIsEditMenuOpen,
|
||||||
|
middleware: [offset(2), flip({ fallbackAxisSideDirection: 'end' }), shift()],
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
});
|
||||||
|
|
||||||
|
const click = useClick(context);
|
||||||
|
const dismiss = useDismiss(context);
|
||||||
|
const role = useRole(context);
|
||||||
|
|
||||||
|
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, role]);
|
||||||
|
|
||||||
|
const headingId = useId();
|
||||||
|
|
||||||
|
const _publishCommentEdit = async () => {
|
||||||
|
try {
|
||||||
|
await publishCommentEdit();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.warn(error);
|
||||||
|
alert(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsEditMenuOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span className={`${styles.checkbox} ${isReply && styles.replyCheckbox}`} ref={refs.setReference} {...(commentCid && getReferenceProps())}>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
onChange={() => commentCid && setIsEditMenuOpen(!isEditMenuOpen)}
|
||||||
|
checked={isEditMenuOpen}
|
||||||
|
disabled={!isAccountCommentAuthor && !isAccountMod}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{isEditMenuOpen && (isAccountCommentAuthor || isAccountMod) && (
|
||||||
|
<FloatingFocusManager context={context} modal={false}>
|
||||||
|
<div className={styles.modal} ref={refs.setFloating} style={floatingStyles} aria-labelledby={headingId} {...getFloatingProps()}>
|
||||||
|
<div className={styles.editMenu}>
|
||||||
|
{isAccountCommentAuthor && (
|
||||||
|
<>
|
||||||
|
<div className={styles.menuItem}>
|
||||||
|
<label>
|
||||||
|
[
|
||||||
|
<input onChange={onCheckbox} checked={publishCommentEditOptions.deleted} type='checkbox' id='deleted' />
|
||||||
|
{_.capitalize(t('delete'))}? ]
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={styles.menuItem}>
|
||||||
|
<label>
|
||||||
|
[
|
||||||
|
<input type='checkbox' onChange={() => setIsContentEditorOpen(!isContentEditorOpen)} checked={isContentEditorOpen} />
|
||||||
|
{_.capitalize(t('edit'))}? ]
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{isContentEditorOpen && (
|
||||||
|
<div>
|
||||||
|
<textarea
|
||||||
|
className={styles.editTextarea}
|
||||||
|
value={publishCommentEditOptions.content}
|
||||||
|
ref={contentEditorRef}
|
||||||
|
onChange={(e) => setPublishCommentEditOptions((state) => ({ ...state, content: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isAccountMod && (
|
||||||
|
<>
|
||||||
|
<div className={styles.menuItem}>
|
||||||
|
<label>
|
||||||
|
[
|
||||||
|
<input onChange={onCheckbox} checked={publishCommentEditOptions.removed} type='checkbox' id='removed' />
|
||||||
|
{_.capitalize(t('remove'))}? ]
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{!isReply && (
|
||||||
|
<div className={styles.menuItem}>
|
||||||
|
[
|
||||||
|
<label>
|
||||||
|
<input onChange={onCheckbox} checked={publishCommentEditOptions.locked} type='checkbox' id='locked' />
|
||||||
|
{_.capitalize(t('close_thread'))}?
|
||||||
|
</label>
|
||||||
|
]
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.menuItem}>
|
||||||
|
[
|
||||||
|
<label>
|
||||||
|
<input onChange={onCheckbox} checked={publishCommentEditOptions.spoiler} type='checkbox' id='spoiler' />
|
||||||
|
{_.capitalize(t('spoiler'))}?
|
||||||
|
</label>
|
||||||
|
]
|
||||||
|
</div>
|
||||||
|
<div className={styles.menuItem}>
|
||||||
|
[
|
||||||
|
<label>
|
||||||
|
<input onChange={onCheckbox} checked={publishCommentEditOptions.pinned} type='checkbox' id='pinned' />
|
||||||
|
{_.capitalize(t('sticky'))}?
|
||||||
|
</label>
|
||||||
|
]
|
||||||
|
</div>
|
||||||
|
{!isCommentAuthorMod && (
|
||||||
|
<div className={styles.menuItem}>
|
||||||
|
[
|
||||||
|
<label>
|
||||||
|
<input onChange={onCheckbox} checked={!!publishCommentEditOptions.commentAuthor?.banExpiresAt} type='checkbox' id='banUser' />
|
||||||
|
<Trans
|
||||||
|
i18nKey='ban_user_for'
|
||||||
|
shouldUnescape={true}
|
||||||
|
components={{
|
||||||
|
1: <input className={styles.banInput} onChange={onBanDurationChange} type='number' min={1} max={100} defaultValue={banDuration} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
?
|
||||||
|
</label>
|
||||||
|
]
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className={`${styles.menuItem} ${styles.menuReason}`}>
|
||||||
|
{_.capitalize(t('reason'))}? ({t('optional')})
|
||||||
|
<input type='text' onChange={onReason} defaultValue={post?.reason} size={14} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.bottom}>
|
||||||
|
<button className={isMobile ? 'button' : ''} onClick={_publishCommentEdit}>
|
||||||
|
{t('save')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FloatingFocusManager>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditMenu;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './edit-menu';
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from './mod-menu';
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
|
||||||
import { autoUpdate, flip, FloatingFocusManager, offset, shift, useClick, useDismiss, useFloating, useId, useInteractions, useRole } from '@floating-ui/react';
|
|
||||||
import { PublishCommentEditOptions, useComment, useEditedComment, usePublishCommentEdit } from '@plebbit/plebbit-react-hooks';
|
|
||||||
import styles from './mod-menu.module.css';
|
|
||||||
import { alertChallengeVerificationFailed } from '../../../lib/utils/challenge-utils';
|
|
||||||
import useChallengesStore from '../../../stores/use-challenges-store';
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
const { addChallenge } = useChallengesStore.getState();
|
|
||||||
|
|
||||||
type ModMenuProps = {
|
|
||||||
cid: string;
|
|
||||||
isCommentAuthorMod?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ModMenu = ({ cid, isCommentAuthorMod }: ModMenuProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
let post: any;
|
|
||||||
const comment = useComment({ commentCid: cid });
|
|
||||||
const { editedComment } = useEditedComment({ comment });
|
|
||||||
if (editedComment) {
|
|
||||||
post = editedComment;
|
|
||||||
} else if (comment) {
|
|
||||||
post = comment;
|
|
||||||
}
|
|
||||||
const isReply = post?.parentCid;
|
|
||||||
const [isModMenuOpen, setIsModMenuOpen] = useState(false);
|
|
||||||
|
|
||||||
const defaultPublishOptions: PublishCommentEditOptions = {
|
|
||||||
removed: post?.removed,
|
|
||||||
locked: post?.locked,
|
|
||||||
spoiler: post?.spoiler,
|
|
||||||
pinned: post?.pinned,
|
|
||||||
commentAuthor: { banExpiresAt: post?.banExpiresAt },
|
|
||||||
commentCid: post?.cid,
|
|
||||||
subplebbitAddress: post?.subplebbitAddress,
|
|
||||||
onChallenge: (...args: any) => addChallenge([...args, post]),
|
|
||||||
onChallengeVerification: alertChallengeVerificationFailed,
|
|
||||||
onError: (error: Error) => {
|
|
||||||
console.warn(error);
|
|
||||||
alert(error.message);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const [publishCommentEditOptions, setPublishCommentEditOptions] = useState(defaultPublishOptions);
|
|
||||||
const { publishCommentEdit } = usePublishCommentEdit(publishCommentEditOptions);
|
|
||||||
|
|
||||||
const [banDuration, setBanDuration] = useState(1);
|
|
||||||
|
|
||||||
const daysToTimestampInSeconds = (days: number) => {
|
|
||||||
const now = new Date();
|
|
||||||
now.setDate(now.getDate() + days);
|
|
||||||
return Math.floor(now.getTime() / 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCheckbox = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const { id, checked } = e.target;
|
|
||||||
if (id === 'banUser') {
|
|
||||||
setPublishCommentEditOptions((state) => ({
|
|
||||||
...state,
|
|
||||||
commentAuthor: { ...state.commentAuthor, banExpiresAt: checked ? daysToTimestampInSeconds(banDuration) : undefined },
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
setPublishCommentEditOptions((state) => ({ ...state, [id]: checked }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onBanDurationChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const days = parseInt(e.target.value, 10) || 1;
|
|
||||||
setBanDuration(days);
|
|
||||||
setPublishCommentEditOptions((state) => ({
|
|
||||||
...state,
|
|
||||||
commentAuthor: { ...state.commentAuthor, banExpiresAt: daysToTimestampInSeconds(days) },
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onReason = (e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
setPublishCommentEditOptions((state) => ({ ...state, reason: e.target.value ? e.target.value : undefined }));
|
|
||||||
|
|
||||||
const { refs, floatingStyles, context } = useFloating({
|
|
||||||
placement: 'bottom-start',
|
|
||||||
open: isModMenuOpen,
|
|
||||||
onOpenChange: setIsModMenuOpen,
|
|
||||||
middleware: [offset(2), flip({ fallbackAxisSideDirection: 'end' }), shift()],
|
|
||||||
whileElementsMounted: autoUpdate,
|
|
||||||
});
|
|
||||||
|
|
||||||
const click = useClick(context);
|
|
||||||
const dismiss = useDismiss(context);
|
|
||||||
const role = useRole(context);
|
|
||||||
|
|
||||||
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, role]);
|
|
||||||
|
|
||||||
const headingId = useId();
|
|
||||||
|
|
||||||
const handleSaveClick = async () => {
|
|
||||||
await publishCommentEdit();
|
|
||||||
setIsModMenuOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span className={styles.checkbox} ref={refs.setReference} {...(cid && getReferenceProps())}>
|
|
||||||
<input type='checkbox' onChange={() => cid && setIsModMenuOpen(!isModMenuOpen)} checked={isModMenuOpen} />
|
|
||||||
</span>
|
|
||||||
{isModMenuOpen && (
|
|
||||||
<FloatingFocusManager context={context} modal={false}>
|
|
||||||
<div className={styles.modal} ref={refs.setFloating} style={floatingStyles} aria-labelledby={headingId} {...getFloatingProps()}>
|
|
||||||
<div className={styles.ModMenu}>
|
|
||||||
<div className={styles.menuItem}>
|
|
||||||
<label>
|
|
||||||
[
|
|
||||||
<input onChange={onCheckbox} checked={publishCommentEditOptions.removed} type='checkbox' id='removed' />
|
|
||||||
{_.capitalize(t('remove'))}? ]
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{!isReply && (
|
|
||||||
<div className={styles.menuItem}>
|
|
||||||
[
|
|
||||||
<label>
|
|
||||||
<input onChange={onCheckbox} checked={publishCommentEditOptions.locked} type='checkbox' id='locked' />
|
|
||||||
{_.capitalize(t('close_thread'))}?
|
|
||||||
</label>
|
|
||||||
]
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={styles.menuItem}>
|
|
||||||
[
|
|
||||||
<label>
|
|
||||||
<input onChange={onCheckbox} checked={publishCommentEditOptions.spoiler} type='checkbox' id='spoiler' />
|
|
||||||
{_.capitalize(t('spoiler'))}?
|
|
||||||
</label>
|
|
||||||
]
|
|
||||||
</div>
|
|
||||||
<div className={styles.menuItem}>
|
|
||||||
[
|
|
||||||
<label>
|
|
||||||
<input onChange={onCheckbox} checked={publishCommentEditOptions.pinned} type='checkbox' id='pinned' />
|
|
||||||
{_.capitalize(t('sticky'))}?
|
|
||||||
</label>
|
|
||||||
]
|
|
||||||
</div>
|
|
||||||
{!isCommentAuthorMod && (
|
|
||||||
<div className={styles.menuItem}>
|
|
||||||
[
|
|
||||||
<label>
|
|
||||||
<input onChange={onCheckbox} checked={!!publishCommentEditOptions.commentAuthor?.banExpiresAt} type='checkbox' id='banUser' />
|
|
||||||
<Trans
|
|
||||||
i18nKey='ban_user_for'
|
|
||||||
shouldUnescape={true}
|
|
||||||
components={{ 1: <input className={styles.banInput} onChange={onBanDurationChange} type='number' min={1} max={100} defaultValue={banDuration} /> }}
|
|
||||||
/>
|
|
||||||
?
|
|
||||||
</label>
|
|
||||||
]
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={`${styles.menuItem} ${styles.menuReason}`}>
|
|
||||||
{_.capitalize(t('reason'))}?
|
|
||||||
<input type='text' onChange={onReason} defaultValue={post?.reason} size={14} />
|
|
||||||
</div>
|
|
||||||
<div className={styles.bottom}>
|
|
||||||
<button onClick={handleSaveClick}>{t('save')}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</FloatingFocusManager>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModMenu;
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { useLocation, useParams } from 'react-router-dom';
|
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||||
import { Link } from 'react-router-dom';
|
import { Comment, useAccount, useBlock, useComment } from '@plebbit/plebbit-react-hooks';
|
||||||
import { useAccount, useBlock, useEditedComment } from '@plebbit/plebbit-react-hooks';
|
|
||||||
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
|
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
|
||||||
import styles from '../post.module.css';
|
import styles from '../post.module.css';
|
||||||
import { getCommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail } from '../../../lib/utils/media-utils';
|
import { getCommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail } from '../../../lib/utils/media-utils';
|
||||||
@@ -10,6 +9,7 @@ import { getFormattedDate } from '../../../lib/utils/time-utils';
|
|||||||
import { isValidURL } from '../../../lib/utils/url-utils';
|
import { isValidURL } from '../../../lib/utils/url-utils';
|
||||||
import { isAllView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../../lib/utils/view-utils';
|
import { isAllView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../../lib/utils/view-utils';
|
||||||
import useCountLinksInReplies from '../../../hooks/use-count-links-in-replies';
|
import useCountLinksInReplies from '../../../hooks/use-count-links-in-replies';
|
||||||
|
import useEditCommentPrivileges from '../../../hooks/use-edit-comment-privileges';
|
||||||
import useReplies from '../../../hooks/use-replies';
|
import useReplies from '../../../hooks/use-replies';
|
||||||
import useStateString from '../../../hooks/use-state-string';
|
import useStateString from '../../../hooks/use-state-string';
|
||||||
import CommentMedia from '../../comment-media';
|
import CommentMedia from '../../comment-media';
|
||||||
@@ -17,13 +17,15 @@ import { canEmbed } from '../../embed';
|
|||||||
import LoadingEllipsis from '../../loading-ellipsis';
|
import LoadingEllipsis from '../../loading-ellipsis';
|
||||||
import Markdown from '../../markdown';
|
import Markdown from '../../markdown';
|
||||||
import PostMenuDesktop from './post-menu-desktop/';
|
import PostMenuDesktop from './post-menu-desktop/';
|
||||||
|
import EditMenu from '../edit-menu/edit-menu';
|
||||||
|
import ReplyQuotePreview from '../reply-quote-preview';
|
||||||
import { PostProps } from '../post';
|
import { PostProps } from '../post';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import ModMenu from '../mod-menu';
|
|
||||||
|
|
||||||
const PostInfo = ({ openReplyModal, post, roles, isHidden }: PostProps) => {
|
const PostInfo = ({ openReplyModal, post, roles, isHidden }: PostProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { author, cid, locked, pinned, parentCid, postCid, shortCid, state, subplebbitAddress, timestamp, title } = post || {};
|
const { author, cid, locked, pinned, parentCid, postCid, replyCount, shortCid, state, subplebbitAddress, timestamp, title } = post || {};
|
||||||
|
const replies = useReplies(post);
|
||||||
const { address, displayName, shortAddress } = author || {};
|
const { address, displayName, shortAddress } = author || {};
|
||||||
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
||||||
const stateString = useStateString(post);
|
const stateString = useStateString(post);
|
||||||
@@ -39,13 +41,14 @@ const PostInfo = ({ openReplyModal, post, roles, isHidden }: PostProps) => {
|
|||||||
const authorRole = roles?.[address]?.role;
|
const authorRole = roles?.[address]?.role;
|
||||||
const displayTitle = title && title.length > 75 ? title?.slice(0, 75) + '...' : title;
|
const displayTitle = title && title.length > 75 ? title?.slice(0, 75) + '...' : title;
|
||||||
|
|
||||||
// pending reply by account is not yet published
|
|
||||||
const account = useAccount();
|
const account = useAccount();
|
||||||
const accountShortAddress = account?.author?.shortAddress;
|
const accountShortAddress = account?.author?.shortAddress; // if reply by account is pending, it doesn't have an author yet
|
||||||
|
|
||||||
|
const { isCommentAuthorMod, isAccountMod, isAccountCommentAuthor } = useEditCommentPrivileges({ commentAuthorAddress: address, subplebbitAddress });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.postInfo}>
|
<div className={styles.postInfo}>
|
||||||
{!isHidden && <ModMenu cid={cid} />}
|
{!isHidden && <EditMenu commentCid={cid} isAccountCommentAuthor={isAccountCommentAuthor} isAccountMod={isAccountMod} isCommentAuthorMod={isCommentAuthorMod} />}
|
||||||
{title && <span className={styles.subject}>{displayTitle} </span>}
|
{title && <span className={styles.subject}>{displayTitle} </span>}
|
||||||
<span className={styles.nameBlock}>
|
<span className={styles.nameBlock}>
|
||||||
<span className={`${styles.name} ${(isDescription || isRules || authorRole) && styles.capcodeMod}`}>
|
<span className={`${styles.name} ${(isDescription || isRules || authorRole) && styles.capcodeMod}`}>
|
||||||
@@ -102,6 +105,10 @@ const PostInfo = ({ openReplyModal, post, roles, isHidden }: PostProps) => {
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<PostMenuDesktop post={post} />
|
<PostMenuDesktop post={post} />
|
||||||
|
{replyCount > 0 &&
|
||||||
|
parentCid &&
|
||||||
|
replies &&
|
||||||
|
replies.map((reply: Comment, index: number) => reply?.parentCid === cid && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -170,16 +177,11 @@ const PostMessage = ({ post }: PostProps) => {
|
|||||||
<div className={`${styles.stateString} ${styles.ellipsis}`}>{stateString !== 'Failed' ? <LoadingEllipsis string={stateString} /> : stateString}</div>
|
<div className={`${styles.stateString} ${styles.ellipsis}`}>{stateString !== 'Failed' ? <LoadingEllipsis string={stateString} /> : stateString}</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const quotelinkReply = useComment({ commentCid: parentCid });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<blockquote className={styles.postMessage}>
|
<blockquote className={styles.postMessage}>
|
||||||
{isReply && isReplyingToReply && (
|
{isReply && isReplyingToReply && <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotelinkReply} />}
|
||||||
<>
|
|
||||||
<Link to={`/p/${subplebbitAddress}/c/${parentCid}`} className={styles.quoteLink}>
|
|
||||||
{`c/${parentCid && Plebbit.getShortCid(parentCid)}`}
|
|
||||||
</Link>
|
|
||||||
<br />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{removed ? (
|
{removed ? (
|
||||||
<span className={styles.removedContent}>({t('this_post_was_removed')})</span>
|
<span className={styles.removedContent}>({t('this_post_was_removed')})</span>
|
||||||
) : deleted ? (
|
) : deleted ? (
|
||||||
@@ -210,15 +212,9 @@ const PostMessage = ({ post }: PostProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PostDesktop = ({ openReplyModal, post, roles, showAllReplies }: PostProps) => {
|
const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies = true }: PostProps) => {
|
||||||
// handle pending mod or author edit
|
|
||||||
const { editedComment } = useEditedComment({ comment: post });
|
|
||||||
if (editedComment) {
|
|
||||||
post = editedComment;
|
|
||||||
}
|
|
||||||
const { cid, content, link, pinned, replyCount, subplebbitAddress } = post || {};
|
const { cid, content, link, pinned, replyCount, subplebbitAddress } = post || {};
|
||||||
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
||||||
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isInPendingPostView = isPendingPostView(location.pathname, params);
|
const isInPendingPostView = isPendingPostView(location.pathname, params);
|
||||||
@@ -233,11 +229,24 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies }: PostProps)
|
|||||||
const repliesCount = pinned ? replyCount : replyCount - 5;
|
const repliesCount = pinned ? replyCount : replyCount - 5;
|
||||||
const linksCount = pinned ? totallinksCount : totallinksCount - visiblelinksCount;
|
const linksCount = pinned ? totallinksCount : totallinksCount - visiblelinksCount;
|
||||||
|
|
||||||
|
// scroll to reply if pathname is reply permalink (backlink)
|
||||||
|
const replyRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||||
|
useEffect(() => {
|
||||||
|
const replyIndex = replies.findIndex((reply) => location.pathname.startsWith(`/p/${subplebbitAddress}/c/${reply?.cid}`));
|
||||||
|
if (replyIndex !== -1 && replyRefs.current[replyIndex]) {
|
||||||
|
replyRefs.current[replyIndex]?.scrollIntoView();
|
||||||
|
}
|
||||||
|
}, [location.pathname, replies, subplebbitAddress]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.postDesktop}>
|
<div className={styles.postDesktop}>
|
||||||
<div className={styles.hrWrapper}>
|
{showAllReplies ? (
|
||||||
<hr />
|
<div className={styles.hrWrapper}>
|
||||||
</div>
|
<hr />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.replyQuotePreviewSpacer} />
|
||||||
|
)}
|
||||||
<div className={isHidden ? styles.postDesktopBlocked : ''}>
|
<div className={isHidden ? styles.postDesktopBlocked : ''}>
|
||||||
{!isInPostPageView && !isDescription && !isRules && (
|
{!isInPostPageView && !isDescription && !isRules && (
|
||||||
<span className={styles.hideButtonWrapper}>
|
<span className={styles.hideButtonWrapper}>
|
||||||
@@ -271,18 +280,22 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies }: PostProps)
|
|||||||
!isDescription &&
|
!isDescription &&
|
||||||
!isRules &&
|
!isRules &&
|
||||||
replies &&
|
replies &&
|
||||||
(showAllReplies ? replies : replies.slice(-5)).map((reply, index) => (
|
showReplies &&
|
||||||
<div key={index} className={styles.replyContainer}>
|
(showAllReplies ? replies : replies.slice(-5)).map((reply, index) => {
|
||||||
<div className={styles.replyDesktop}>
|
const isRouteLinkToReply = location.pathname.startsWith(`/p/${subplebbitAddress}/c/${reply?.cid}`);
|
||||||
<div className={styles.sideArrows}>{'>>'}</div>
|
return (
|
||||||
<div className={styles.reply}>
|
<div key={index} className={styles.replyContainer} ref={(el) => (replyRefs.current[index] = el)}>
|
||||||
<PostInfo openReplyModal={openReplyModal} post={reply} roles={roles} />
|
<div className={styles.replyDesktop}>
|
||||||
{reply.link && isValidURL(reply.link) && <PostMedia post={reply} />}
|
<div className={styles.sideArrows}>{'>>'}</div>
|
||||||
{reply.content && <PostMessage post={reply} />}
|
<div className={`${styles.reply} ${isRouteLinkToReply && styles.highlight}`}>
|
||||||
|
<PostInfo openReplyModal={openReplyModal} post={reply} roles={roles} />
|
||||||
|
{reply.link && isValidURL(reply.link) && <PostMedia post={reply} />}
|
||||||
|
{reply.content && <PostMessage post={reply} />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { autoUpdate, flip, FloatingFocusManager, offset, shift, useClick, useDismiss, useFloating, useId, useInteractions, useRole } from '@floating-ui/react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Comment } from '@plebbit/plebbit-react-hooks';
|
import { Comment } from '@plebbit/plebbit-react-hooks';
|
||||||
|
import { autoUpdate, flip, FloatingFocusManager, offset, shift, useClick, useDismiss, useFloating, useId, useInteractions, useRole } from '@floating-ui/react';
|
||||||
import styles from './post-menu-mobile.module.css';
|
import styles from './post-menu-mobile.module.css';
|
||||||
import { getCommentMediaInfo } from '../../../../lib/utils/media-utils';
|
import { getCommentMediaInfo } from '../../../../lib/utils/media-utils';
|
||||||
import { copyShareLinkToClipboard, isValidURL } from '../../../../lib/utils/url-utils';
|
import { copyShareLinkToClipboard, isValidURL } from '../../../../lib/utils/url-utils';
|
||||||
|
import useEditCommentPrivileges from '../../../../hooks/use-edit-comment-privileges';
|
||||||
|
import EditMenu from '../../edit-menu/edit-menu';
|
||||||
|
|
||||||
interface PostMenuMobileProps {
|
interface PostMenuMobileProps {
|
||||||
cid: string;
|
cid: string;
|
||||||
@@ -62,7 +64,8 @@ const ViewOnButtons = ({ cid, isDescription, isRules, subplebbitAddress, onClose
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PostMenuMobile = ({ post }: { post: Comment }) => {
|
const PostMenuMobile = ({ post }: { post: Comment }) => {
|
||||||
const { cid, isDescription, isRules, link, subplebbitAddress } = post || {};
|
const { author, cid, isDescription, isRules, link, subplebbitAddress } = post || {};
|
||||||
|
const { isCommentAuthorMod, isAccountMod, isAccountCommentAuthor } = useEditCommentPrivileges({ commentAuthorAddress: author?.address, subplebbitAddress });
|
||||||
const commentMediaInfo = getCommentMediaInfo(post);
|
const commentMediaInfo = getCommentMediaInfo(post);
|
||||||
const { thumbnail, type, url } = commentMediaInfo || {};
|
const { thumbnail, type, url } = commentMediaInfo || {};
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
@@ -98,6 +101,9 @@ const PostMenuMobile = ({ post }: { post: Comment }) => {
|
|||||||
</FloatingFocusManager>,
|
</FloatingFocusManager>,
|
||||||
document.body,
|
document.body,
|
||||||
)}
|
)}
|
||||||
|
{(isAccountMod || isAccountCommentAuthor) && (
|
||||||
|
<EditMenu commentCid={cid} isAccountCommentAuthor={isAccountCommentAuthor} isAccountMod={isAccountMod} isCommentAuthorMod={isCommentAuthorMod} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||||
import { useAccount, useEditedComment } from '@plebbit/plebbit-react-hooks';
|
import { Comment, useAccount, useComment } from '@plebbit/plebbit-react-hooks';
|
||||||
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
|
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
|
||||||
import styles from '../post.module.css';
|
import styles from '../post.module.css';
|
||||||
import { getCommentMediaInfo, getHasThumbnail } from '../../../lib/utils/media-utils';
|
import { getCommentMediaInfo, getHasThumbnail } from '../../../lib/utils/media-utils';
|
||||||
@@ -13,6 +13,7 @@ import useStateString from '../../../hooks/use-state-string';
|
|||||||
import CommentMedia from '../../comment-media';
|
import CommentMedia from '../../comment-media';
|
||||||
import LoadingEllipsis from '../../loading-ellipsis';
|
import LoadingEllipsis from '../../loading-ellipsis';
|
||||||
import Markdown from '../../markdown';
|
import Markdown from '../../markdown';
|
||||||
|
import ReplyQuotePreview from '../reply-quote-preview';
|
||||||
import PostMenuMobile from './post-menu-mobile';
|
import PostMenuMobile from './post-menu-mobile';
|
||||||
import { PostProps } from '../post';
|
import { PostProps } from '../post';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
@@ -99,7 +100,23 @@ const PostInfoAndMedia = ({ openReplyModal, post, roles }: PostProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ReplyBacklinks = ({ post }: PostProps) => {
|
||||||
|
const { cid, parentCid, replyCount } = post || {};
|
||||||
|
const replies = useReplies(post);
|
||||||
|
|
||||||
|
return (
|
||||||
|
replyCount > 0 &&
|
||||||
|
parentCid &&
|
||||||
|
replies && (
|
||||||
|
<div className={styles.mobileReplyBacklinks}>
|
||||||
|
{replies.map((reply: Comment, index: number) => reply?.parentCid === cid && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const PostMessageMobile = ({ post }: PostProps) => {
|
const PostMessageMobile = ({ post }: PostProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { cid, content, deleted, parentCid, postCid, reason, removed, state, subplebbitAddress } = post || {};
|
const { cid, content, deleted, parentCid, postCid, reason, removed, state, subplebbitAddress } = post || {};
|
||||||
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -117,21 +134,16 @@ const PostMessageMobile = ({ post }: PostProps) => {
|
|||||||
<div className={`${styles.stateString} ${styles.ellipsis}`}>{stateString !== 'Failed' ? <LoadingEllipsis string={stateString} /> : stateString}</div>
|
<div className={`${styles.stateString} ${styles.ellipsis}`}>{stateString !== 'Failed' ? <LoadingEllipsis string={stateString} /> : stateString}</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const quotelinkReply = useComment({ commentCid: parentCid });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
content && (
|
content && (
|
||||||
<blockquote className={`${styles.postMessage} ${!isReply && styles.clampLines}`}>
|
<blockquote className={`${styles.postMessage} ${!isReply && styles.clampLines}`}>
|
||||||
{isReply && isReplyingToReply && (
|
{isReply && isReplyingToReply && <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotelinkReply} />}
|
||||||
<>
|
|
||||||
<Link to={`/p/${subplebbitAddress}/c/${parentCid}`} className={styles.quoteLink}>
|
|
||||||
{`c/${parentCid && Plebbit.getShortCid(parentCid)}`}
|
|
||||||
</Link>
|
|
||||||
<br />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{removed ? (
|
{removed ? (
|
||||||
<span className={styles.removedContent}>(THIS POST WAS REMOVED)</span>
|
<span className={styles.removedContent}>({t('this_post_was_removed')})</span>
|
||||||
) : deleted ? (
|
) : deleted ? (
|
||||||
<span className={styles.removedContent}>User deleted this post.</span>
|
<span className={styles.removedContent}>{t('user_deleted_this_post')}</span>
|
||||||
) : (
|
) : (
|
||||||
<Markdown content={displayContent} />
|
<Markdown content={displayContent} />
|
||||||
)}
|
)}
|
||||||
@@ -159,32 +171,36 @@ const PostMessageMobile = ({ post }: PostProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PostMobile = ({ openReplyModal, post, roles, showAllReplies }: PostProps) => {
|
const PostMobile = ({ openReplyModal, post, roles, showAllReplies, showReplies = true }: PostProps) => {
|
||||||
// handle pending mod or author edit
|
|
||||||
const { editedComment } = useEditedComment({ comment: post });
|
|
||||||
if (editedComment) {
|
|
||||||
post = editedComment;
|
|
||||||
}
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { cid, content, pinned, replyCount, subplebbitAddress } = post || {};
|
const { cid, content, pinned, replyCount, subplebbitAddress } = post || {};
|
||||||
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
||||||
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isInAllView = isAllView(location.pathname);
|
const isInAllView = isAllView(location.pathname);
|
||||||
const isInPendingPostView = isPendingPostView(location.pathname, params);
|
const isInPendingPostView = isPendingPostView(location.pathname, params);
|
||||||
const isInPostView = isPostPageView(location.pathname, params);
|
const isInPostView = isPostPageView(location.pathname, params);
|
||||||
|
|
||||||
const linksCount = useCountLinksInReplies(post);
|
const linksCount = useCountLinksInReplies(post);
|
||||||
|
|
||||||
const replies = useReplies(post);
|
const replies = useReplies(post);
|
||||||
|
|
||||||
|
const replyRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const replyIndex = replies.findIndex((reply) => location.pathname.startsWith(`/p/${subplebbitAddress}/c/${reply?.cid}`));
|
||||||
|
|
||||||
|
if (replyIndex !== -1 && replyRefs.current[replyIndex]) {
|
||||||
|
replyRefs.current[replyIndex]?.scrollIntoView();
|
||||||
|
}
|
||||||
|
}, [location.pathname, replies, subplebbitAddress]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.postMobile}>
|
<div className={styles.postMobile}>
|
||||||
<div className={styles.hrWrapper}>
|
{showAllReplies && (
|
||||||
<hr />
|
<div className={styles.hrWrapper}>
|
||||||
</div>
|
<hr />
|
||||||
<div className={styles.thread}>
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={showAllReplies ? styles.thread : styles.quotePreview}>
|
||||||
<div className={styles.postContainer}>
|
<div className={styles.postContainer}>
|
||||||
<div className={styles.postOp}>
|
<div className={styles.postOp}>
|
||||||
<PostInfoAndMedia openReplyModal={openReplyModal} post={post} roles={roles} />
|
<PostInfoAndMedia openReplyModal={openReplyModal} post={post} roles={roles} />
|
||||||
@@ -210,18 +226,23 @@ const PostMobile = ({ openReplyModal, post, roles, showAllReplies }: PostProps)
|
|||||||
!isDescription &&
|
!isDescription &&
|
||||||
!isRules &&
|
!isRules &&
|
||||||
replies &&
|
replies &&
|
||||||
(showAllReplies ? replies : replies.slice(-5)).map((reply) => (
|
showReplies &&
|
||||||
<div key={reply.cid} className={styles.replyContainer}>
|
(showAllReplies ? replies : replies.slice(-5)).map((reply, index) => {
|
||||||
<div className={styles.replyMobile}>
|
const isRouteLinkToReply = location.pathname.startsWith(`/p/${subplebbitAddress}/c/${reply?.cid}`);
|
||||||
<div className={styles.reply}>
|
return (
|
||||||
<div className={styles.replyContainer}>
|
<div key={index} className={styles.replyContainer} ref={(el) => (replyRefs.current[index] = el)}>
|
||||||
<PostInfoAndMedia openReplyModal={openReplyModal} post={reply} roles={roles} />
|
<div className={styles.replyMobile}>
|
||||||
{reply.content && <PostMessageMobile post={reply} />}
|
<div className={styles.reply}>
|
||||||
|
<div className={`${styles.replyContainer} ${isRouteLinkToReply && styles.highlight}`}>
|
||||||
|
<PostInfoAndMedia openReplyModal={openReplyModal} post={reply} roles={roles} />
|
||||||
|
{reply.content && <PostMessageMobile post={reply} />}
|
||||||
|
<ReplyBacklinks post={reply} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -211,6 +211,10 @@
|
|||||||
padding-bottom: 30px;
|
padding-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quotePreview {
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.postMobile .postContainer {
|
.postMobile .postContainer {
|
||||||
background-color: var(--post-mobile-background-color);
|
background-color: var(--post-mobile-background-color);
|
||||||
}
|
}
|
||||||
@@ -272,7 +276,6 @@
|
|||||||
.postMobile .postMessage {
|
.postMobile .postMessage {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-size: var(--post-mobile-content-font-size);
|
font-size: var(--post-mobile-content-font-size);
|
||||||
word-break: break-all;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.postMobile .postLink {
|
.postMobile .postLink {
|
||||||
@@ -338,6 +341,7 @@
|
|||||||
.quoteLink:hover {
|
.quoteLink:hover {
|
||||||
color: var(--post-quotelink-text-color-hover);
|
color: var(--post-quotelink-text-color-hover);
|
||||||
text-decoration: var(--post-quotelink-text-decoration-hover);
|
text-decoration: var(--post-quotelink-text-decoration-hover);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.replyMobile {
|
.replyMobile {
|
||||||
@@ -389,11 +393,58 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.removedContent {
|
.removedContent {
|
||||||
|
text-transform: uppercase;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.backlink, .backlinkHash {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backlink {
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backlink, .backlinkHash {
|
||||||
|
color: var(--post-backlink-text-color);
|
||||||
|
text-decoration: var(--post-backlink-text-decoration);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backlink:hover, .backlinkHash:hover {
|
||||||
|
color: var(--post-backlink-text-color-hover);
|
||||||
|
text-decoration: var(--post-backlink-text-decoration-hover);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileReplyBacklinks {
|
||||||
|
padding: 0 0;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
line-height: 2;
|
||||||
|
background-color: var(--reply-backlink-mobile-background-color);
|
||||||
|
border-top: var(--reply-backlink-mobile-border-top);
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
background: var(--reply-highlight-background-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replyQuotePreview {
|
||||||
|
background: var(--quote-preview-background);
|
||||||
|
border: var(--quote-preview-border);
|
||||||
|
border-right: var(--quote-preview-border-right);
|
||||||
|
border-bottom: var(--quote-preview-border-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
.postDesktop .replyQuotePreviewSpacer {
|
||||||
|
padding: 1.5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
|
.replyQuotePreview {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.postDesktop {
|
.postDesktop {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -404,6 +455,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
|
.replyQuotePreview {
|
||||||
|
padding-right: 7px;
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.postMobile {
|
.postMobile {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ export interface PostProps {
|
|||||||
reply?: any;
|
reply?: any;
|
||||||
roles?: Role[];
|
roles?: Role[];
|
||||||
showAllReplies?: boolean;
|
showAllReplies?: boolean;
|
||||||
|
showReplies?: boolean;
|
||||||
openReplyModal?: (cid: string) => void;
|
openReplyModal?: (cid: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Post = ({ post, showAllReplies = false, openReplyModal }: PostProps) => {
|
const Post = ({ post, showAllReplies = false, showReplies = true, openReplyModal }: PostProps) => {
|
||||||
const subplebbit = useSubplebbit({ subplebbitAddress: post?.subplebbitAddress });
|
const subplebbit = useSubplebbit({ subplebbitAddress: post?.subplebbitAddress });
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
@@ -22,9 +23,9 @@ const Post = ({ post, showAllReplies = false, openReplyModal }: PostProps) => {
|
|||||||
<div className={styles.thread}>
|
<div className={styles.thread}>
|
||||||
<div className={styles.postContainer}>
|
<div className={styles.postContainer}>
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<PostMobile post={post} roles={subplebbit?.roles} showAllReplies={showAllReplies} openReplyModal={openReplyModal} />
|
<PostMobile post={post} roles={subplebbit?.roles} showAllReplies={showAllReplies} showReplies={showReplies} openReplyModal={openReplyModal} />
|
||||||
) : (
|
) : (
|
||||||
<PostDesktop post={post} roles={subplebbit?.roles} showAllReplies={showAllReplies} openReplyModal={openReplyModal} />
|
<PostDesktop post={post} roles={subplebbit?.roles} showAllReplies={showAllReplies} showReplies={showReplies} openReplyModal={openReplyModal} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './reply-quote-preview';
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Comment } from '@plebbit/plebbit-react-hooks';
|
||||||
|
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
|
||||||
|
import { useFloating, offset, shift, size, autoUpdate, Placement } from '@floating-ui/react';
|
||||||
|
import useIsMobile from '../../../hooks/use-is-mobile';
|
||||||
|
import styles from '../post.module.css';
|
||||||
|
import Post from '../post';
|
||||||
|
|
||||||
|
interface ReplyQuotePreviewProps {
|
||||||
|
isBacklinkReply?: boolean;
|
||||||
|
backlinkReply?: Comment;
|
||||||
|
isQuotelinkReply?: boolean;
|
||||||
|
quotelinkReply?: Comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply }: ReplyQuotePreviewProps) => {
|
||||||
|
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
|
||||||
|
const placementRef = useRef<Placement>('right');
|
||||||
|
const availableWidthRef = useRef<number>(0);
|
||||||
|
|
||||||
|
const { refs, floatingStyles, update } = useFloating({
|
||||||
|
placement: placementRef.current,
|
||||||
|
middleware: [
|
||||||
|
shift({ padding: 10 }),
|
||||||
|
offset({ mainAxis: placementRef.current === 'right' ? 8 : 4 }),
|
||||||
|
size({
|
||||||
|
apply({ availableWidth, elements }) {
|
||||||
|
availableWidthRef.current = availableWidth;
|
||||||
|
if (availableWidth >= 250) {
|
||||||
|
elements.floating.style.maxWidth = `${availableWidth - 12}px`;
|
||||||
|
} else if (placementRef.current === 'right') {
|
||||||
|
placementRef.current = 'left';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
const availableWidth = availableWidthRef.current;
|
||||||
|
if (availableWidth >= 250) {
|
||||||
|
placementRef.current = 'right';
|
||||||
|
} else {
|
||||||
|
placementRef.current = 'left';
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
};
|
||||||
|
}, [update]);
|
||||||
|
|
||||||
|
const replyBacklink = (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
className={styles.backlink}
|
||||||
|
to={`/p/${backlinkReply?.subplebbitAddress}/c/${backlinkReply?.cid}`}
|
||||||
|
ref={refs.setReference}
|
||||||
|
onMouseOver={() => setHoveredCid(backlinkReply?.cid)}
|
||||||
|
onMouseLeave={() => setHoveredCid(null)}
|
||||||
|
>
|
||||||
|
c/{backlinkReply?.shortCid}
|
||||||
|
</Link>
|
||||||
|
{hoveredCid === backlinkReply?.cid &&
|
||||||
|
createPortal(
|
||||||
|
<div className={styles.replyQuotePreview} ref={refs.setFloating} style={floatingStyles}>
|
||||||
|
<Post post={backlinkReply} showReplies={false} />
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const replyQuotelink = (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
to={`/p/${quotelinkReply?.subplebbitAddress}/c/${quotelinkReply?.cid}`}
|
||||||
|
ref={refs.setReference}
|
||||||
|
className={styles.quoteLink}
|
||||||
|
onMouseOver={() => setHoveredCid(quotelinkReply?.cid)}
|
||||||
|
onMouseLeave={() => setHoveredCid(null)}
|
||||||
|
>
|
||||||
|
{`c/${quotelinkReply?.cid && Plebbit.getShortCid(quotelinkReply.cid)}`}
|
||||||
|
</Link>
|
||||||
|
<br />
|
||||||
|
{hoveredCid === quotelinkReply?.cid &&
|
||||||
|
createPortal(
|
||||||
|
<div className={styles.replyQuotePreview} ref={refs.setFloating} style={floatingStyles}>
|
||||||
|
<Post post={quotelinkReply} showReplies={false} />
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return isBacklinkReply ? replyBacklink : isQuotelinkReply && replyQuotelink;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply }: ReplyQuotePreviewProps) => {
|
||||||
|
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { refs, floatingStyles, update } = useFloating({
|
||||||
|
placement: 'bottom',
|
||||||
|
middleware: [shift({ padding: isBacklinkReply ? 5 : 10 })],
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('resize', () => update());
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', () => update());
|
||||||
|
};
|
||||||
|
}, [update]);
|
||||||
|
|
||||||
|
const replyBacklink = (
|
||||||
|
<>
|
||||||
|
<span className={styles.backlink} ref={refs.setReference} onMouseOver={() => setHoveredCid(backlinkReply?.cid)} onMouseLeave={() => setHoveredCid(null)}>
|
||||||
|
c/{backlinkReply?.shortCid}{' '}
|
||||||
|
</span>
|
||||||
|
<Link to={`/p/${backlinkReply?.subplebbitAddress}/c/${backlinkReply?.cid}`} className={styles.backlinkHash}>
|
||||||
|
#
|
||||||
|
</Link>
|
||||||
|
{hoveredCid === backlinkReply?.cid &&
|
||||||
|
createPortal(
|
||||||
|
<div className={styles.replyQuotePreview} ref={refs.setFloating} style={floatingStyles}>
|
||||||
|
<Post post={backlinkReply} showReplies={false} />
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const replyQuotelink = (
|
||||||
|
<>
|
||||||
|
<span ref={refs.setReference} className={styles.quoteLink} onMouseOver={() => setHoveredCid(quotelinkReply?.cid)} onMouseLeave={() => setHoveredCid(null)}>
|
||||||
|
c/{quotelinkReply?.shortCid}{' '}
|
||||||
|
</span>
|
||||||
|
<Link className={styles.quoteLink} to={`/p/${quotelinkReply?.subplebbitAddress}/c/${quotelinkReply?.cid}`}>
|
||||||
|
#
|
||||||
|
</Link>
|
||||||
|
<br />
|
||||||
|
{hoveredCid === quotelinkReply?.cid &&
|
||||||
|
createPortal(
|
||||||
|
<div className={styles.replyQuotePreview} ref={refs.setFloating} style={floatingStyles}>
|
||||||
|
<Post post={quotelinkReply} showReplies={false} />
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return isBacklinkReply ? replyBacklink : isQuotelinkReply && replyQuotelink;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReplyQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply }: ReplyQuotePreviewProps) => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
return isMobile ? (
|
||||||
|
<MobileQuotePreview backlinkReply={backlinkReply} quotelinkReply={quotelinkReply} isBacklinkReply={isBacklinkReply} isQuotelinkReply={isQuotelinkReply} />
|
||||||
|
) : (
|
||||||
|
<DesktopQuotePreview backlinkReply={backlinkReply} quotelinkReply={quotelinkReply} isBacklinkReply={isBacklinkReply} isQuotelinkReply={isQuotelinkReply} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReplyQuotePreview;
|
||||||
@@ -93,7 +93,7 @@ const ReplyModal = ({ closeModal, parentCid, scrollY }: ReplyModalProps) => {
|
|||||||
<input
|
<input
|
||||||
type='text'
|
type='text'
|
||||||
defaultValue={displayName}
|
defaultValue={displayName}
|
||||||
placeholder={displayName ? undefined : _.capitalize(t('anonymous'))}
|
placeholder={displayName ? undefined : _.capitalize(t('name'))}
|
||||||
onChange={(e) => setAccount({ ...account, author: { ...account?.author, displayName: e.target.value } })}
|
onChange={(e) => setAccount({ ...account, author: { ...account?.author, displayName: e.target.value } })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { useAccount, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||||
|
|
||||||
|
interface EditCommentPrivileges {
|
||||||
|
commentAuthorAddress: string;
|
||||||
|
subplebbitAddress: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useEditCommentPrivileges = ({ commentAuthorAddress, subplebbitAddress }: EditCommentPrivileges) => {
|
||||||
|
const accountAuthorAddress = useAccount()?.author?.address;
|
||||||
|
const { roles } = useSubplebbit({ subplebbitAddress }) || {};
|
||||||
|
|
||||||
|
const commentAuthorRole = roles?.[commentAuthorAddress]?.role;
|
||||||
|
const isCommentAuthorMod = commentAuthorRole === 'admin' || commentAuthorRole === 'owner' || commentAuthorRole === 'moderator';
|
||||||
|
const accountAuthorRole = roles?.[accountAuthorAddress]?.role;
|
||||||
|
const isAccountMod = accountAuthorRole === 'admin' || accountAuthorRole === 'owner' || accountAuthorRole === 'moderator';
|
||||||
|
const isAccountCommentAuthor = accountAuthorAddress === commentAuthorAddress;
|
||||||
|
|
||||||
|
return { isCommentAuthorMod, isAccountMod, isAccountCommentAuthor };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useEditCommentPrivileges;
|
||||||
@@ -1,4 +1,12 @@
|
|||||||
:root .yotsuba {
|
:root .yotsuba {
|
||||||
|
/* backlink */
|
||||||
|
--post-backlink-text-color: navy;
|
||||||
|
--post-backlink-text-decoration: underline;
|
||||||
|
--post-backlink-text-color-hover: red;
|
||||||
|
--post-backlink-text-decoration-hover: underline;
|
||||||
|
--reply-backlink-mobile-background-color: #ead6ca;
|
||||||
|
--reply-backlink-mobile-border-top: 1px solid #d9bfb7;
|
||||||
|
|
||||||
/* board banner */
|
/* board banner */
|
||||||
--banner-image-border: 1px solid #800;
|
--banner-image-border: 1px solid #800;
|
||||||
--board-title-font-size: 28px;
|
--board-title-font-size: 28px;
|
||||||
@@ -144,12 +152,19 @@
|
|||||||
--post-quotelink-text-color-hover: red;
|
--post-quotelink-text-color-hover: red;
|
||||||
--post-quotelink-text-decoration: underline;
|
--post-quotelink-text-decoration: underline;
|
||||||
--post-quotelink-text-decoration-hover: underline;
|
--post-quotelink-text-decoration-hover: underline;
|
||||||
|
--quote-preview-background: #f0e0d6;
|
||||||
|
--quote-preview-border: 1px solid #d9bfb7;
|
||||||
|
--quote-preview-border-right: 2px solid #d9bfb7;
|
||||||
|
--quote-preview-border-bottom: 2px solid #d9bfb7;
|
||||||
|
|
||||||
/* reply desktop */
|
/* reply desktop */
|
||||||
--side-arrows-color: #e0bfb7;
|
--side-arrows-color: #e0bfb7;
|
||||||
--reply-desktop-background-color: #f0e0d6;
|
--reply-desktop-background-color: #f0e0d6;
|
||||||
--reply-desktop-border: 1px solid #d9bfb7;
|
--reply-desktop-border: 1px solid #d9bfb7;
|
||||||
|
|
||||||
|
/* reply highlight */
|
||||||
|
--reply-highlight-background-color: #f0c0b0;
|
||||||
|
|
||||||
/* reply modal */
|
/* reply modal */
|
||||||
--reply-modal-field-input-border: 1px solid #aaa;
|
--reply-modal-field-input-border: 1px solid #aaa;
|
||||||
--reply-modal-field-input-border-focus: 1px solid #ea8;
|
--reply-modal-field-input-border-focus: 1px solid #ea8;
|
||||||
@@ -183,6 +198,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root .yotsuba-b {
|
:root .yotsuba-b {
|
||||||
|
/* backlink */
|
||||||
|
--post-backlink-text-color: #34345C;
|
||||||
|
--post-backlink-text-decoration: underline;
|
||||||
|
--post-backlink-text-color-hover: #dd0000;
|
||||||
|
--post-backlink-text-decoration-hover: underline;
|
||||||
|
--reply-backlink-mobile-background-color: #c9cde8;
|
||||||
|
--reply-backlink-mobile-border-top: 1px solid #b7c5d9;
|
||||||
|
|
||||||
/* board banner */
|
/* board banner */
|
||||||
--banner-image-border: 1px solid #000;
|
--banner-image-border: 1px solid #000;
|
||||||
--board-title-font-size: 28px;
|
--board-title-font-size: 28px;
|
||||||
@@ -328,12 +351,19 @@
|
|||||||
--post-quotelink-text-color-hover: #d00;
|
--post-quotelink-text-color-hover: #d00;
|
||||||
--post-quotelink-text-decoration: underline;
|
--post-quotelink-text-decoration: underline;
|
||||||
--post-quotelink-text-decoration-hover: underline;
|
--post-quotelink-text-decoration-hover: underline;
|
||||||
|
--quote-preview-background: #d6daf0;
|
||||||
|
--quote-preview-border: 1px solid #b7c5d9;
|
||||||
|
--quote-preview-border-right: 2px solid #b7c5d9;
|
||||||
|
--quote-preview-border-bottom: 2px solid #b7c5d9;
|
||||||
|
|
||||||
/* reply desktop */
|
/* reply desktop */
|
||||||
--side-arrows-color: #b7c5d9;
|
--side-arrows-color: #b7c5d9;
|
||||||
--reply-desktop-background-color: #d6daf0;
|
--reply-desktop-background-color: #d6daf0;
|
||||||
--reply-desktop-border: 1px solid #b7c5d9;
|
--reply-desktop-border: 1px solid #b7c5d9;
|
||||||
|
|
||||||
|
/* reply highlight */
|
||||||
|
--reply-highlight-background-color: #d6bad0;
|
||||||
|
|
||||||
/* reply modal */
|
/* reply modal */
|
||||||
--reply-modal-field-input-border: 1px solid #aaa;
|
--reply-modal-field-input-border: 1px solid #aaa;
|
||||||
--reply-modal-field-input-border-focus: 1px solid #98e;
|
--reply-modal-field-input-border-focus: 1px solid #98e;
|
||||||
@@ -367,6 +397,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root .futaba {
|
:root .futaba {
|
||||||
|
/* backlink */
|
||||||
|
--post-backlink-text-color: navy;
|
||||||
|
--post-backlink-text-decoration: underline;
|
||||||
|
--post-backlink-text-color-hover: red;
|
||||||
|
--post-backlink-text-decoration-hover: underline;
|
||||||
|
--reply-backlink-mobile-background-color: #ead6ca;
|
||||||
|
--reply-backlink-mobile-border-top: 1px solid #d9bfb7;
|
||||||
|
|
||||||
/* body */
|
/* body */
|
||||||
--body-background-base-color: #ffe;
|
--body-background-base-color: #ffe;
|
||||||
--body-background-pattern-image: none;
|
--body-background-pattern-image: none;
|
||||||
@@ -483,12 +521,19 @@
|
|||||||
--post-quotelink-text-color-hover: red;
|
--post-quotelink-text-color-hover: red;
|
||||||
--post-quotelink-text-decoration: underline;
|
--post-quotelink-text-decoration: underline;
|
||||||
--post-quotelink-text-decoration-hover: underline;
|
--post-quotelink-text-decoration-hover: underline;
|
||||||
|
--quote-preview-background: #f0e0d6;
|
||||||
|
--quote-preview-border: 1px solid rgba(0,0,0,.2);
|
||||||
|
--quote-preview-border-right: 2px solid rgba(0,0,0,.2);
|
||||||
|
--quote-preview-border-bottom: 2px solid rgba(0,0,0,.2);
|
||||||
|
|
||||||
/* reply desktop */
|
/* reply desktop */
|
||||||
--side-arrows-color: #e0bfb7;
|
--side-arrows-color: #e0bfb7;
|
||||||
--reply-desktop-background-color: #f0e0d6;
|
--reply-desktop-background-color: #f0e0d6;
|
||||||
--reply-desktop-border: 1px solid #d9bfb7;
|
--reply-desktop-border: 1px solid #d9bfb7;
|
||||||
|
|
||||||
|
/* reply highlight */
|
||||||
|
--reply-highlight-background-color: #f0c0b0;
|
||||||
|
|
||||||
/* settings modal */
|
/* settings modal */
|
||||||
--settings-modal-background-color: #f0e0d6;
|
--settings-modal-background-color: #f0e0d6;
|
||||||
--settings-modal-border-top: 1px solid rgba(0, 0, 0, 0.20);
|
--settings-modal-border-top: 1px solid rgba(0, 0, 0, 0.20);
|
||||||
@@ -517,6 +562,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root .burichan {
|
:root .burichan {
|
||||||
|
/* backlink */
|
||||||
|
--post-backlink-text-color: #34345C;
|
||||||
|
--post-backlink-text-decoration: underline;
|
||||||
|
--post-backlink-text-color-hover: #dd0000;
|
||||||
|
--post-backlink-text-decoration-hover: underline;
|
||||||
|
--reply-backlink-mobile-background-color: #c9cde8;
|
||||||
|
--reply-backlink-mobile-border-top: 1px solid #b7c5d9;
|
||||||
|
|
||||||
/* board banner */
|
/* board banner */
|
||||||
--banner-image-border: 1px solid #000;
|
--banner-image-border: 1px solid #000;
|
||||||
--board-title-font-size: 32px;
|
--board-title-font-size: 32px;
|
||||||
@@ -647,12 +700,19 @@
|
|||||||
--post-quotelink-text-color-hover: #d00;
|
--post-quotelink-text-color-hover: #d00;
|
||||||
--post-quotelink-text-decoration: underline;
|
--post-quotelink-text-decoration: underline;
|
||||||
--post-quotelink-text-decoration-hover: underline;
|
--post-quotelink-text-decoration-hover: underline;
|
||||||
|
--quote-preview-background: #d6daf0;
|
||||||
|
--quote-preview-border: 1px solid rgba(0,0,0,.2);
|
||||||
|
--quote-preview-border-right: 2px solid rgba(0,0,0,.2);
|
||||||
|
--quote-preview-border-bottom: 2px solid rgba(0,0,0,.2);
|
||||||
|
|
||||||
/* reply desktop */
|
/* reply desktop */
|
||||||
--side-arrows-color: #b7c5d9;
|
--side-arrows-color: #b7c5d9;
|
||||||
--reply-desktop-background-color: #d6daf0;
|
--reply-desktop-background-color: #d6daf0;
|
||||||
--reply-desktop-border: 1px solid #b7c5d9;
|
--reply-desktop-border: 1px solid #b7c5d9;
|
||||||
|
|
||||||
|
/* reply highlight */
|
||||||
|
--reply-highlight-background-color: #d6bad0;
|
||||||
|
|
||||||
/* settings modal */
|
/* settings modal */
|
||||||
--settings-modal-background-color: #d6daf0;
|
--settings-modal-background-color: #d6daf0;
|
||||||
--settings-modal-border-top: 1px solid rgba(0, 0, 0, 0.20);
|
--settings-modal-border-top: 1px solid rgba(0, 0, 0, 0.20);
|
||||||
@@ -681,6 +741,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root .tomorrow {
|
:root .tomorrow {
|
||||||
|
/* backlink */
|
||||||
|
--post-backlink-text-color: #5f89ac;
|
||||||
|
--post-backlink-text-decoration: underline;
|
||||||
|
--post-backlink-text-color-hover: #81a2be;
|
||||||
|
--post-backlink-text-decoration-hover: underline;
|
||||||
|
--reply-backlink-mobile-background-color: #212326;
|
||||||
|
--reply-backlink-mobile-border-top: 1px solid #2D2F33;
|
||||||
|
|
||||||
/* body */
|
/* body */
|
||||||
--body-background-base-color: #1d1f21;
|
--body-background-base-color: #1d1f21;
|
||||||
--body-background-pattern-image: none;
|
--body-background-pattern-image: none;
|
||||||
@@ -822,6 +890,10 @@
|
|||||||
--post-quotelink-text-color-hover: #81a2be;
|
--post-quotelink-text-color-hover: #81a2be;
|
||||||
--post-quotelink-text-decoration: underline;
|
--post-quotelink-text-decoration: underline;
|
||||||
--post-quotelink-text-decoration-hover: underline;
|
--post-quotelink-text-decoration-hover: underline;
|
||||||
|
--quote-preview-background: #282a2e;
|
||||||
|
--quote-preview-border: 1px solid #333;
|
||||||
|
--quote-preview-border-right: 1px solid #333;
|
||||||
|
--quote-preview-border-bottom: 1px solid #333;
|
||||||
|
|
||||||
/* reply desktop */
|
/* reply desktop */
|
||||||
--side-arrows-color: #c5c8c6;
|
--side-arrows-color: #c5c8c6;
|
||||||
@@ -833,6 +905,9 @@
|
|||||||
--reply-modal-field-border-focus: 1px solid #757575;
|
--reply-modal-field-border-focus: 1px solid #757575;
|
||||||
--reply-modal-field-input-border-focus: 1px solid #757575;
|
--reply-modal-field-input-border-focus: 1px solid #757575;
|
||||||
|
|
||||||
|
/* reply highlight */
|
||||||
|
--reply-highlight-background-color: #1d1d21;
|
||||||
|
|
||||||
/* select */
|
/* select */
|
||||||
--select-border: 1px solid #575a60;
|
--select-border: 1px solid #575a60;
|
||||||
--select-background-color: #282a2e;
|
--select-background-color: #282a2e;
|
||||||
@@ -868,6 +943,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root .photon {
|
:root .photon {
|
||||||
|
/* backlink */
|
||||||
|
--post-backlink-text-color: #f60;
|
||||||
|
--post-backlink-text-decoration: underline;
|
||||||
|
--post-backlink-text-color-hover: #f30;
|
||||||
|
--post-backlink-text-decoration-hover: underline;
|
||||||
|
--reply-backlink-mobile-background-color: #ddd;
|
||||||
|
--reply-backlink-mobile-border-top: 1px solid #ccc;
|
||||||
|
|
||||||
/* body */
|
/* body */
|
||||||
--body-background-base-color: #eee;
|
--body-background-base-color: #eee;
|
||||||
--body-background-pattern-image: none;
|
--body-background-pattern-image: none;
|
||||||
@@ -995,12 +1078,19 @@
|
|||||||
--post-quotelink-text-color-hover: #f30;
|
--post-quotelink-text-color-hover: #f30;
|
||||||
--post-quotelink-text-decoration: underline;
|
--post-quotelink-text-decoration: underline;
|
||||||
--post-quotelink-text-decoration-hover: underline;
|
--post-quotelink-text-decoration-hover: underline;
|
||||||
|
--quote-preview-background: #ddd;
|
||||||
|
--quote-preview-border: 1px solid #ccc;
|
||||||
|
--quote-preview-border-right: 1px solid #ccc;
|
||||||
|
--quote-preview-border-bottom: 1px solid #ccc;
|
||||||
|
|
||||||
/* reply desktop */
|
/* reply desktop */
|
||||||
--side-arrows-color: #333;
|
--side-arrows-color: #333;
|
||||||
--reply-desktop-background-color: #ddd;
|
--reply-desktop-background-color: #ddd;
|
||||||
--reply-desktop-border: 1px solid #ccc;
|
--reply-desktop-border: 1px solid #ccc;
|
||||||
|
|
||||||
|
/* reply highlight */
|
||||||
|
--reply-highlight-background-color: #ccc;
|
||||||
|
|
||||||
/* reply modal */
|
/* reply modal */
|
||||||
--reply-modal-field-input-border: 1px solid #aaa;
|
--reply-modal-field-input-border: 1px solid #aaa;
|
||||||
--reply-modal-field-input-border-focus: 1px solid #ea8;
|
--reply-modal-field-input-border-focus: 1px solid #ea8;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Subplebbit, useAccount, useAccountSubplebbits } from '@plebbit/plebbit-react-hooks';
|
import { Subplebbit, useAccount, useAccountSubplebbits } from '@plebbit/plebbit-react-hooks';
|
||||||
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
|
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
|
||||||
import useHomeFiltersStore from '../../../stores/use-home-filters';
|
import useHomeFiltersStore from '../../../stores/use-home-filters-store';
|
||||||
import { useMultisubMetadata } from '../../../hooks/use-default-subplebbits';
|
import { useMultisubMetadata } from '../../../hooks/use-default-subplebbits';
|
||||||
import styles from '../home.module.css';
|
import styles from '../home.module.css';
|
||||||
import { nsfwTags } from '../home';
|
import { nsfwTags } from '../home';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import useHomeFiltersStore from '../../../stores/use-home-filters';
|
import useHomeFiltersStore from '../../../stores/use-home-filters-store';
|
||||||
import styles from '../home.module.css';
|
import styles from '../home.module.css';
|
||||||
|
|
||||||
const BoxModal = ({ isBoardsBoxModal }: { isBoardsBoxModal: boolean }) => {
|
const BoxModal = ({ isBoardsBoxModal }: { isBoardsBoxModal: boolean }) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Comment, Subplebbit } from '@plebbit/plebbit-react-hooks';
|
import { Comment, Subplebbit } from '@plebbit/plebbit-react-hooks';
|
||||||
import styles from '../home.module.css';
|
import styles from '../home.module.css';
|
||||||
import usePopularPosts from '../../../hooks/use-popular-posts';
|
import usePopularPosts from '../../../hooks/use-popular-posts';
|
||||||
import useHomeFiltersStore from '../../../stores/use-home-filters';
|
import useHomeFiltersStore from '../../../stores/use-home-filters-store';
|
||||||
import { getCommentMediaInfo } from '../../../lib/utils/media-utils';
|
import { getCommentMediaInfo } from '../../../lib/utils/media-utils';
|
||||||
import { CatalogPostMedia } from '../../../components/catalog-row';
|
import { CatalogPostMedia } from '../../../components/catalog-row';
|
||||||
import LoadingEllipsis from '../../../components/loading-ellipsis';
|
import LoadingEllipsis from '../../../components/loading-ellipsis';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
import { Comment, useComment, useEditedComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||||
import { useLocation, useParams } from 'react-router-dom';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
import styles from './post-page.module.css';
|
import styles from './post-page.module.css';
|
||||||
import { isDescriptionView, isRulesView, isSettingsView } from '../../lib/utils/view-utils';
|
import { isDescriptionView, isRulesView, isSettingsView } from '../../lib/utils/view-utils';
|
||||||
@@ -25,7 +25,21 @@ const PostPage = () => {
|
|||||||
|
|
||||||
const { activeCid, closeModal, openReplyModal, showReplyModal, scrollY } = useReplyModal();
|
const { activeCid, closeModal, openReplyModal, showReplyModal, scrollY } = useReplyModal();
|
||||||
|
|
||||||
const post = useComment({ commentCid });
|
const comment: Comment = useComment({ commentCid });
|
||||||
|
// if the comment is a reply, get the parent comment
|
||||||
|
const postComment = useComment({ commentCid: comment?.postCid });
|
||||||
|
let post;
|
||||||
|
if (comment.parentCid) {
|
||||||
|
post = postComment;
|
||||||
|
} else {
|
||||||
|
post = comment;
|
||||||
|
}
|
||||||
|
// handle pending mod or author edit
|
||||||
|
const { editedComment } = useEditedComment({ comment: post });
|
||||||
|
if (editedComment) {
|
||||||
|
post = editedComment;
|
||||||
|
}
|
||||||
|
|
||||||
const { deleted, locked, removed } = post || {};
|
const { deleted, locked, removed } = post || {};
|
||||||
const isThreadClosed = deleted || locked || removed;
|
const isThreadClosed = deleted || locked || removed;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user