Merge pull request #385 from plebbit/development

Development
This commit is contained in:
Tom (plebeius.eth)
2024-06-06 17:36:43 +02:00
committed by GitHub
62 changed files with 837 additions and 326 deletions

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

+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "اختياري", "optional": "اختياري",
"close_thread": "اغلق الموضوع", "close_thread": "اغلق الموضوع",
"view_spoiler": "عرض الحاضر", "view_spoiler": "عرض الحاضر",
"filter": "تصفية" "filter": "تصفية",
"edit": "تحرير"
} }
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "ঐচ্ছিক", "optional": "ঐচ্ছিক",
"close_thread": "থ্রেড বন্ধ করুন", "close_thread": "থ্রেড বন্ধ করুন",
"view_spoiler": "স্পোয়ালার দেখুন", "view_spoiler": "স্পোয়ালার দেখুন",
"filter": "ফিল্টার" "filter": "ফিল্টার",
"edit": "সম্পাদনা"
} }
+2 -1
View File
@@ -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"
} }
+2 -1
View File
@@ -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"
} }
+2 -1
View File
@@ -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"
} }
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "προαιρετικό", "optional": "προαιρετικό",
"close_thread": "κλείσιμο νήματος", "close_thread": "κλείσιμο νήματος",
"view_spoiler": "προβολή spoiler", "view_spoiler": "προβολή spoiler",
"filter": "φίλτρο" "filter": "φίλτρο",
"edit": "επεξεργασία"
} }
+2 -1
View File
@@ -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"
} }
+2 -1
View File
@@ -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"
} }
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "اختیاری", "optional": "اختیاری",
"close_thread": "بستن موضوع", "close_thread": "بستن موضوع",
"view_spoiler": "نمایش اسپویلر", "view_spoiler": "نمایش اسپویلر",
"filter": "فیلتر" "filter": "فیلتر",
"edit": "ویرایش"
} }
+2 -1
View File
@@ -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"
} }
+2 -1
View File
@@ -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"
} }
+2 -1
View File
@@ -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"
} }
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "אופציונלי", "optional": "אופציונלי",
"close_thread": "סגור את הנושא", "close_thread": "סגור את הנושא",
"view_spoiler": "הצג ספוילר", "view_spoiler": "הצג ספוילר",
"filter": "סנן" "filter": "סנן",
"edit": "ערוך"
} }
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "वैकल्पिक", "optional": "वैकल्पिक",
"close_thread": "थ्रेड बंद करें", "close_thread": "थ्रेड बंद करें",
"view_spoiler": "स्पॉइलर देखें", "view_spoiler": "स्पॉइलर देखें",
"filter": "फ़िल्टर" "filter": "फ़िल्टर",
"edit": "संपादित करें"
} }
+2 -1
View File
@@ -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"
} }
+2 -1
View File
@@ -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"
} }
+2 -1
View File
@@ -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"
} }
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "オプショナル", "optional": "オプショナル",
"close_thread": "スレッドを閉じる", "close_thread": "スレッドを閉じる",
"view_spoiler": "スポイラーを表示", "view_spoiler": "スポイラーを表示",
"filter": "フィルター" "filter": "フィルター",
"edit": "編集"
} }
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "선택", "optional": "선택",
"close_thread": "스레드 닫기", "close_thread": "스레드 닫기",
"view_spoiler": "스포일러 보기", "view_spoiler": "스포일러 보기",
"filter": "필터" "filter": "필터",
"edit": "편집"
} }
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "वैकल्पिक", "optional": "वैकल्पिक",
"close_thread": "थ्रेड बंद करा", "close_thread": "थ्रेड बंद करा",
"view_spoiler": "स्पॉईलर पहा", "view_spoiler": "स्पॉईलर पहा",
"filter": "फिल्टर" "filter": "फिल्टर",
"edit": "संपादित करा"
} }
+2 -1
View File
@@ -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"
} }
+2 -1
View File
@@ -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"
} }
+2 -1
View File
@@ -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"
} }
+2 -1
View File
@@ -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"
} }
+2 -1
View File
@@ -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"
} }
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "необязательный", "optional": "необязательный",
"close_thread": "закрыть тему", "close_thread": "закрыть тему",
"view_spoiler": "посмотреть спойлер", "view_spoiler": "посмотреть спойлер",
"filter": "фильтр" "filter": "фильтр",
"edit": "редактировать"
} }
+2 -1
View File
@@ -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"
} }
+2 -1
View File
@@ -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"
} }
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "ఐచ్ఛికం", "optional": "ఐచ్ఛికం",
"close_thread": "థ్రెడ్‌ను మూసేయండి", "close_thread": "థ్రెడ్‌ను మూసేయండి",
"view_spoiler": "స్పోయ్లర్ వీక్షించండి", "view_spoiler": "స్పోయ్లర్ వీక్షించండి",
"filter": "వడపోత" "filter": "వడపోత",
"edit": "సవరించు"
} }
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "ไม่จำเป็น", "optional": "ไม่จำเป็น",
"close_thread": "ปิดกระทู้", "close_thread": "ปิดกระทู้",
"view_spoiler": "ดูสปอยล์เลอร์", "view_spoiler": "ดูสปอยล์เลอร์",
"filter": "ตัวกรอง" "filter": "ตัวกรอง",
"edit": "แก้ไข"
} }
+2 -1
View File
@@ -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"
} }
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "необов’язково", "optional": "необов’язково",
"close_thread": "закрити тему", "close_thread": "закрити тему",
"view_spoiler": "переглянути спойлер", "view_spoiler": "переглянути спойлер",
"filter": "фільтр" "filter": "фільтр",
"edit": "редагувати"
} }
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "اختیاری", "optional": "اختیاری",
"close_thread": "موضوع بند کریں", "close_thread": "موضوع بند کریں",
"view_spoiler": "اسپوائلر دیکھیں", "view_spoiler": "اسپوائلر دیکھیں",
"filter": "فلٹر" "filter": "فلٹر",
"edit": "ترمیم کریں"
} }
+2 -1
View File
@@ -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"
} }
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "可选的", "optional": "可选的",
"close_thread": "关闭主题", "close_thread": "关闭主题",
"view_spoiler": "查看剧透", "view_spoiler": "查看剧透",
"filter": "过滤器" "filter": "过滤器",
"edit": "编辑"
} }
+14 -12
View File
@@ -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 && (
<> <>
+1 -1
View File
@@ -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(() => {
@@ -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;
}
}
+239
View File
@@ -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;
+1
View File
@@ -0,0 +1 @@
export { default } from './edit-menu';
-1
View File
@@ -1 +0,0 @@
export { default } from './mod-menu';
-175
View File
@@ -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} />
)}
</> </>
); );
}; };
+55 -34
View File
@@ -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>
); );
+57 -1
View File
@@ -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;
} }
+4 -3
View File
@@ -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;
+1 -1
View File
@@ -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>
+21
View File
@@ -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;
+90
View File
@@ -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;
+1 -1
View File
@@ -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 -1
View File
@@ -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';
+16 -2
View File
@@ -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;