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": "اختياري",
|
||||
"close_thread": "اغلق الموضوع",
|
||||
"view_spoiler": "عرض الحاضر",
|
||||
"filter": "تصفية"
|
||||
"filter": "تصفية",
|
||||
"edit": "تحرير"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "ঐচ্ছিক",
|
||||
"close_thread": "থ্রেড বন্ধ করুন",
|
||||
"view_spoiler": "স্পোয়ালার দেখুন",
|
||||
"filter": "ফিল্টার"
|
||||
"filter": "ফিল্টার",
|
||||
"edit": "সম্পাদনা"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "volitelný",
|
||||
"close_thread": "uzavřít vlákno",
|
||||
"view_spoiler": "zobrazit spoiler",
|
||||
"filter": "filtr"
|
||||
"filter": "filtr",
|
||||
"edit": "upravit"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "valgfri",
|
||||
"close_thread": "luk tråd",
|
||||
"view_spoiler": "vis spoiler",
|
||||
"filter": "filtrér"
|
||||
"filter": "filtrér",
|
||||
"edit": "rediger"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "optional",
|
||||
"close_thread": "Thread schließen",
|
||||
"view_spoiler": "Spoiler anzeigen",
|
||||
"filter": "filtern"
|
||||
"filter": "filtern",
|
||||
"edit": "bearbeiten"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "προαιρετικό",
|
||||
"close_thread": "κλείσιμο νήματος",
|
||||
"view_spoiler": "προβολή spoiler",
|
||||
"filter": "φίλτρο"
|
||||
"filter": "φίλτρο",
|
||||
"edit": "επεξεργασία"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "optional",
|
||||
"close_thread": "close thread",
|
||||
"view_spoiler": "view spoiler",
|
||||
"filter": "filter"
|
||||
"filter": "filter",
|
||||
"edit": "edit"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "opcional",
|
||||
"close_thread": "cerrar hilo",
|
||||
"view_spoiler": "ver spoiler",
|
||||
"filter": "filtrar"
|
||||
"filter": "filtrar",
|
||||
"edit": "editar"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "اختیاری",
|
||||
"close_thread": "بستن موضوع",
|
||||
"view_spoiler": "نمایش اسپویلر",
|
||||
"filter": "فیلتر"
|
||||
"filter": "فیلتر",
|
||||
"edit": "ویرایش"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "valinnainen",
|
||||
"close_thread": "sulje keskustelu",
|
||||
"view_spoiler": "näytä spoiler",
|
||||
"filter": "suodatin"
|
||||
"filter": "suodatin",
|
||||
"edit": "muokkaa"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "opsyonal",
|
||||
"close_thread": "isara ang thread",
|
||||
"view_spoiler": "tingnan ang spoiler",
|
||||
"filter": "salain"
|
||||
"filter": "salain",
|
||||
"edit": "i-edit"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "facultatif",
|
||||
"close_thread": "fermer le fil",
|
||||
"view_spoiler": "voir le spoiler",
|
||||
"filter": "filtre"
|
||||
"filter": "filtre",
|
||||
"edit": "modifier"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "אופציונלי",
|
||||
"close_thread": "סגור את הנושא",
|
||||
"view_spoiler": "הצג ספוילר",
|
||||
"filter": "סנן"
|
||||
"filter": "סנן",
|
||||
"edit": "ערוך"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "वैकल्पिक",
|
||||
"close_thread": "थ्रेड बंद करें",
|
||||
"view_spoiler": "स्पॉइलर देखें",
|
||||
"filter": "फ़िल्टर"
|
||||
"filter": "फ़िल्टर",
|
||||
"edit": "संपादित करें"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "opcionális",
|
||||
"close_thread": "szál bezárása",
|
||||
"view_spoiler": "spoiler megtekintése",
|
||||
"filter": "szűrő"
|
||||
"filter": "szűrő",
|
||||
"edit": "szerkesztés"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "opsional",
|
||||
"close_thread": "tutup utas",
|
||||
"view_spoiler": "lihat spoiler",
|
||||
"filter": "menyaring"
|
||||
"filter": "menyaring",
|
||||
"edit": "sunting"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "opzionale",
|
||||
"close_thread": "chiudi il thread",
|
||||
"view_spoiler": "mostra spoiler",
|
||||
"filter": "filtro"
|
||||
"filter": "filtro",
|
||||
"edit": "modifica"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "オプショナル",
|
||||
"close_thread": "スレッドを閉じる",
|
||||
"view_spoiler": "スポイラーを表示",
|
||||
"filter": "フィルター"
|
||||
"filter": "フィルター",
|
||||
"edit": "編集"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "선택",
|
||||
"close_thread": "스레드 닫기",
|
||||
"view_spoiler": "스포일러 보기",
|
||||
"filter": "필터"
|
||||
"filter": "필터",
|
||||
"edit": "편집"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "वैकल्पिक",
|
||||
"close_thread": "थ्रेड बंद करा",
|
||||
"view_spoiler": "स्पॉईलर पहा",
|
||||
"filter": "फिल्टर"
|
||||
"filter": "फिल्टर",
|
||||
"edit": "संपादित करा"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "optioneel",
|
||||
"close_thread": "draad sluiten",
|
||||
"view_spoiler": "bekijk spoiler",
|
||||
"filter": "filter"
|
||||
"filter": "filter",
|
||||
"edit": "bewerken"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "valgfritt",
|
||||
"close_thread": "lukk tråd",
|
||||
"view_spoiler": "se spoiler",
|
||||
"filter": "filter"
|
||||
"filter": "filter",
|
||||
"edit": "rediger"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "opcjonalnie",
|
||||
"close_thread": "zamknij wątek",
|
||||
"view_spoiler": "pokaż spoiler",
|
||||
"filter": "filtr"
|
||||
"filter": "filtr",
|
||||
"edit": "edytuj"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "opcional",
|
||||
"close_thread": "fechar tópico",
|
||||
"view_spoiler": "ver spoiler",
|
||||
"filter": "filtro"
|
||||
"filter": "filtro",
|
||||
"edit": "editar"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "opțional",
|
||||
"close_thread": "închide firul",
|
||||
"view_spoiler": "vezi spoilerul",
|
||||
"filter": "filtru"
|
||||
"filter": "filtru",
|
||||
"edit": "editați"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "необязательный",
|
||||
"close_thread": "закрыть тему",
|
||||
"view_spoiler": "посмотреть спойлер",
|
||||
"filter": "фильтр"
|
||||
"filter": "фильтр",
|
||||
"edit": "редактировать"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "opsionale",
|
||||
"close_thread": "mbyll temën",
|
||||
"view_spoiler": "shikoni spoiler",
|
||||
"filter": "filtruar"
|
||||
"filter": "filtruar",
|
||||
"edit": "redakto"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "valfritt",
|
||||
"close_thread": "stäng tråd",
|
||||
"view_spoiler": "visa spoiler",
|
||||
"filter": "filter"
|
||||
"filter": "filter",
|
||||
"edit": "redigera"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "ఐచ్ఛికం",
|
||||
"close_thread": "థ్రెడ్ను మూసేయండి",
|
||||
"view_spoiler": "స్పోయ్లర్ వీక్షించండి",
|
||||
"filter": "వడపోత"
|
||||
"filter": "వడపోత",
|
||||
"edit": "సవరించు"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "ไม่จำเป็น",
|
||||
"close_thread": "ปิดกระทู้",
|
||||
"view_spoiler": "ดูสปอยล์เลอร์",
|
||||
"filter": "ตัวกรอง"
|
||||
"filter": "ตัวกรอง",
|
||||
"edit": "แก้ไข"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "isteğe bağlı",
|
||||
"close_thread": "konuyu kapat",
|
||||
"view_spoiler": "spoileri görüntüle",
|
||||
"filter": "filtre"
|
||||
"filter": "filtre",
|
||||
"edit": "düzenle"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "необов’язково",
|
||||
"close_thread": "закрити тему",
|
||||
"view_spoiler": "переглянути спойлер",
|
||||
"filter": "фільтр"
|
||||
"filter": "фільтр",
|
||||
"edit": "редагувати"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "اختیاری",
|
||||
"close_thread": "موضوع بند کریں",
|
||||
"view_spoiler": "اسپوائلر دیکھیں",
|
||||
"filter": "فلٹر"
|
||||
"filter": "فلٹر",
|
||||
"edit": "ترمیم کریں"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "tùy chọn",
|
||||
"close_thread": "đóng chủ đề",
|
||||
"view_spoiler": "xem spoiler",
|
||||
"filter": "bộ lọc"
|
||||
"filter": "bộ lọc",
|
||||
"edit": "chỉnh sửa"
|
||||
}
|
||||
@@ -147,5 +147,6 @@
|
||||
"optional": "可选的",
|
||||
"close_thread": "关闭主题",
|
||||
"view_spoiler": "查看剧透",
|
||||
"filter": "过滤器"
|
||||
"filter": "过滤器",
|
||||
"edit": "编辑"
|
||||
}
|
||||
+14
-12
@@ -35,18 +35,20 @@ const BoardLayout = () => {
|
||||
<div className={styles.boardLayout}>
|
||||
<TopBar />
|
||||
<BoardHeader />
|
||||
{isMobile ? (
|
||||
<>
|
||||
<PostForm key={key} />
|
||||
<MobileBoardButtons />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PostForm key={key} />
|
||||
{subplebbitAddress && <SubplebbitStats />}
|
||||
<DesktopBoardButtons />
|
||||
</>
|
||||
)}
|
||||
{isMobile
|
||||
? subplebbitAddress && (
|
||||
<>
|
||||
<PostForm key={key} />
|
||||
<MobileBoardButtons />
|
||||
</>
|
||||
)
|
||||
: subplebbitAddress && (
|
||||
<>
|
||||
<PostForm key={key} />
|
||||
<SubplebbitStats />
|
||||
<DesktopBoardButtons />
|
||||
</>
|
||||
)}
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -89,18 +89,18 @@ export const MobileBoardButtons = () => {
|
||||
<div className={styles.mobileBoardButtons}>
|
||||
{isInPostView || isInPendingPostPage ? (
|
||||
<>
|
||||
<ReturnButton address={subplebbitAddress} />
|
||||
<ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />
|
||||
<CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />
|
||||
<SubscribeButton address={subplebbitAddress} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{isInCatalogView ? (
|
||||
<ReturnButton address={subplebbitAddress} />
|
||||
<ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />
|
||||
) : (
|
||||
<CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />
|
||||
)}
|
||||
<SubscribeButton address={subplebbitAddress} />
|
||||
{!(isInAllView || isInSubscriptionsView) && <SubscribeButton address={subplebbitAddress} />}
|
||||
<RefreshButton />
|
||||
{isInCatalogView && (
|
||||
<>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||
import styles from './board-header.module.css';
|
||||
import { useMultisubMetadata } from '../../hooks/use-default-subplebbits';
|
||||
|
||||
const totalBanners = 57;
|
||||
const totalBanners = 59;
|
||||
|
||||
const ImageBanner = () => {
|
||||
const [imagePath] = useState(() => {
|
||||
|
||||
+29
-13
@@ -4,14 +4,6 @@
|
||||
filter: var(--filter80);
|
||||
}
|
||||
|
||||
.button {
|
||||
color: var(--text-info);
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
padding: 0 4px 0 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal {
|
||||
z-index: 7;
|
||||
background-color: var(--challenge-modal-background-color);
|
||||
@@ -43,7 +35,8 @@
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.menuItem input[type="text"] {
|
||||
|
||||
.menuItem input[type="text"], .editTextarea {
|
||||
padding: 2px;
|
||||
box-shadow: var(--box-shadow-input);
|
||||
margin-top: 2px;
|
||||
@@ -51,12 +44,12 @@
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -65,10 +58,14 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.editTextarea {
|
||||
width: calc(100% - 12px);
|
||||
margin-left: 3px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.bottom button {
|
||||
text-transform: capitalize;
|
||||
cursor: pointer;
|
||||
padding: 2px 6px 3px;
|
||||
margin-left: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
@@ -81,4 +78,23 @@
|
||||
.banInput {
|
||||
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 { useLocation, useParams } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAccount, useBlock, useEditedComment } from '@plebbit/plebbit-react-hooks';
|
||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||
import { Comment, useAccount, useBlock, useComment } from '@plebbit/plebbit-react-hooks';
|
||||
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
|
||||
import styles from '../post.module.css';
|
||||
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 { isAllView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../../lib/utils/view-utils';
|
||||
import useCountLinksInReplies from '../../../hooks/use-count-links-in-replies';
|
||||
import useEditCommentPrivileges from '../../../hooks/use-edit-comment-privileges';
|
||||
import useReplies from '../../../hooks/use-replies';
|
||||
import useStateString from '../../../hooks/use-state-string';
|
||||
import CommentMedia from '../../comment-media';
|
||||
@@ -17,13 +17,15 @@ import { canEmbed } from '../../embed';
|
||||
import LoadingEllipsis from '../../loading-ellipsis';
|
||||
import Markdown from '../../markdown';
|
||||
import PostMenuDesktop from './post-menu-desktop/';
|
||||
import EditMenu from '../edit-menu/edit-menu';
|
||||
import ReplyQuotePreview from '../reply-quote-preview';
|
||||
import { PostProps } from '../post';
|
||||
import _ from 'lodash';
|
||||
import ModMenu from '../mod-menu';
|
||||
|
||||
const PostInfo = ({ openReplyModal, post, roles, isHidden }: PostProps) => {
|
||||
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 { isDescription, isRules } = post || {}; // custom properties, not from api
|
||||
const stateString = useStateString(post);
|
||||
@@ -39,13 +41,14 @@ const PostInfo = ({ openReplyModal, post, roles, isHidden }: PostProps) => {
|
||||
const authorRole = roles?.[address]?.role;
|
||||
const displayTitle = title && title.length > 75 ? title?.slice(0, 75) + '...' : title;
|
||||
|
||||
// pending reply by account is not yet published
|
||||
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 (
|
||||
<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>}
|
||||
<span className={styles.nameBlock}>
|
||||
<span className={`${styles.name} ${(isDescription || isRules || authorRole) && styles.capcodeMod}`}>
|
||||
@@ -102,6 +105,10 @@ const PostInfo = ({ openReplyModal, post, roles, isHidden }: PostProps) => {
|
||||
)}
|
||||
</span>
|
||||
<PostMenuDesktop post={post} />
|
||||
{replyCount > 0 &&
|
||||
parentCid &&
|
||||
replies &&
|
||||
replies.map((reply: Comment, index: number) => reply?.parentCid === cid && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -170,16 +177,11 @@ const PostMessage = ({ post }: PostProps) => {
|
||||
<div className={`${styles.stateString} ${styles.ellipsis}`}>{stateString !== 'Failed' ? <LoadingEllipsis string={stateString} /> : stateString}</div>
|
||||
);
|
||||
|
||||
const quotelinkReply = useComment({ commentCid: parentCid });
|
||||
|
||||
return (
|
||||
<blockquote className={styles.postMessage}>
|
||||
{isReply && isReplyingToReply && (
|
||||
<>
|
||||
<Link to={`/p/${subplebbitAddress}/c/${parentCid}`} className={styles.quoteLink}>
|
||||
{`c/${parentCid && Plebbit.getShortCid(parentCid)}`}
|
||||
</Link>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
{isReply && isReplyingToReply && <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotelinkReply} />}
|
||||
{removed ? (
|
||||
<span className={styles.removedContent}>({t('this_post_was_removed')})</span>
|
||||
) : deleted ? (
|
||||
@@ -210,15 +212,9 @@ const PostMessage = ({ post }: PostProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const PostDesktop = ({ openReplyModal, post, roles, showAllReplies }: PostProps) => {
|
||||
// handle pending mod or author edit
|
||||
const { editedComment } = useEditedComment({ comment: post });
|
||||
if (editedComment) {
|
||||
post = editedComment;
|
||||
}
|
||||
const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies = true }: PostProps) => {
|
||||
const { cid, content, link, pinned, replyCount, subplebbitAddress } = post || {};
|
||||
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
||||
|
||||
const params = useParams();
|
||||
const location = useLocation();
|
||||
const isInPendingPostView = isPendingPostView(location.pathname, params);
|
||||
@@ -233,11 +229,24 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies }: PostProps)
|
||||
const repliesCount = pinned ? replyCount : replyCount - 5;
|
||||
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 (
|
||||
<div className={styles.postDesktop}>
|
||||
<div className={styles.hrWrapper}>
|
||||
<hr />
|
||||
</div>
|
||||
{showAllReplies ? (
|
||||
<div className={styles.hrWrapper}>
|
||||
<hr />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.replyQuotePreviewSpacer} />
|
||||
)}
|
||||
<div className={isHidden ? styles.postDesktopBlocked : ''}>
|
||||
{!isInPostPageView && !isDescription && !isRules && (
|
||||
<span className={styles.hideButtonWrapper}>
|
||||
@@ -271,18 +280,22 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies }: PostProps)
|
||||
!isDescription &&
|
||||
!isRules &&
|
||||
replies &&
|
||||
(showAllReplies ? replies : replies.slice(-5)).map((reply, index) => (
|
||||
<div key={index} className={styles.replyContainer}>
|
||||
<div className={styles.replyDesktop}>
|
||||
<div className={styles.sideArrows}>{'>>'}</div>
|
||||
<div className={styles.reply}>
|
||||
<PostInfo openReplyModal={openReplyModal} post={reply} roles={roles} />
|
||||
{reply.link && isValidURL(reply.link) && <PostMedia post={reply} />}
|
||||
{reply.content && <PostMessage post={reply} />}
|
||||
showReplies &&
|
||||
(showAllReplies ? replies : replies.slice(-5)).map((reply, index) => {
|
||||
const isRouteLinkToReply = location.pathname.startsWith(`/p/${subplebbitAddress}/c/${reply?.cid}`);
|
||||
return (
|
||||
<div key={index} className={styles.replyContainer} ref={(el) => (replyRefs.current[index] = el)}>
|
||||
<div className={styles.replyDesktop}>
|
||||
<div className={styles.sideArrows}>{'>>'}</div>
|
||||
<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>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
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 { 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 { getCommentMediaInfo } from '../../../../lib/utils/media-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 {
|
||||
cid: string;
|
||||
@@ -62,7 +64,8 @@ const ViewOnButtons = ({ cid, isDescription, isRules, subplebbitAddress, onClose
|
||||
};
|
||||
|
||||
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 { thumbnail, type, url } = commentMediaInfo || {};
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
@@ -98,6 +101,9 @@ const PostMenuMobile = ({ post }: { post: Comment }) => {
|
||||
</FloatingFocusManager>,
|
||||
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 { 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 styles from '../post.module.css';
|
||||
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 LoadingEllipsis from '../../loading-ellipsis';
|
||||
import Markdown from '../../markdown';
|
||||
import ReplyQuotePreview from '../reply-quote-preview';
|
||||
import PostMenuMobile from './post-menu-mobile';
|
||||
import { PostProps } from '../post';
|
||||
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 { t } = useTranslation();
|
||||
const { cid, content, deleted, parentCid, postCid, reason, removed, state, subplebbitAddress } = post || {};
|
||||
|
||||
const params = useParams();
|
||||
@@ -117,21 +134,16 @@ const PostMessageMobile = ({ post }: PostProps) => {
|
||||
<div className={`${styles.stateString} ${styles.ellipsis}`}>{stateString !== 'Failed' ? <LoadingEllipsis string={stateString} /> : stateString}</div>
|
||||
);
|
||||
|
||||
const quotelinkReply = useComment({ commentCid: parentCid });
|
||||
|
||||
return (
|
||||
content && (
|
||||
<blockquote className={`${styles.postMessage} ${!isReply && styles.clampLines}`}>
|
||||
{isReply && isReplyingToReply && (
|
||||
<>
|
||||
<Link to={`/p/${subplebbitAddress}/c/${parentCid}`} className={styles.quoteLink}>
|
||||
{`c/${parentCid && Plebbit.getShortCid(parentCid)}`}
|
||||
</Link>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
{isReply && isReplyingToReply && <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotelinkReply} />}
|
||||
{removed ? (
|
||||
<span className={styles.removedContent}>(THIS POST WAS REMOVED)</span>
|
||||
<span className={styles.removedContent}>({t('this_post_was_removed')})</span>
|
||||
) : deleted ? (
|
||||
<span className={styles.removedContent}>User deleted this post.</span>
|
||||
<span className={styles.removedContent}>{t('user_deleted_this_post')}</span>
|
||||
) : (
|
||||
<Markdown content={displayContent} />
|
||||
)}
|
||||
@@ -159,32 +171,36 @@ const PostMessageMobile = ({ post }: PostProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const PostMobile = ({ openReplyModal, post, roles, showAllReplies }: PostProps) => {
|
||||
// handle pending mod or author edit
|
||||
const { editedComment } = useEditedComment({ comment: post });
|
||||
if (editedComment) {
|
||||
post = editedComment;
|
||||
}
|
||||
const PostMobile = ({ openReplyModal, post, roles, showAllReplies, showReplies = true }: PostProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { cid, content, pinned, replyCount, subplebbitAddress } = post || {};
|
||||
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
||||
|
||||
const params = useParams();
|
||||
const location = useLocation();
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
const isInPendingPostView = isPendingPostView(location.pathname, params);
|
||||
const isInPostView = isPostPageView(location.pathname, params);
|
||||
|
||||
const linksCount = useCountLinksInReplies(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 (
|
||||
<div className={styles.postMobile}>
|
||||
<div className={styles.hrWrapper}>
|
||||
<hr />
|
||||
</div>
|
||||
<div className={styles.thread}>
|
||||
{showAllReplies && (
|
||||
<div className={styles.hrWrapper}>
|
||||
<hr />
|
||||
</div>
|
||||
)}
|
||||
<div className={showAllReplies ? styles.thread : styles.quotePreview}>
|
||||
<div className={styles.postContainer}>
|
||||
<div className={styles.postOp}>
|
||||
<PostInfoAndMedia openReplyModal={openReplyModal} post={post} roles={roles} />
|
||||
@@ -210,18 +226,23 @@ const PostMobile = ({ openReplyModal, post, roles, showAllReplies }: PostProps)
|
||||
!isDescription &&
|
||||
!isRules &&
|
||||
replies &&
|
||||
(showAllReplies ? replies : replies.slice(-5)).map((reply) => (
|
||||
<div key={reply.cid} className={styles.replyContainer}>
|
||||
<div className={styles.replyMobile}>
|
||||
<div className={styles.reply}>
|
||||
<div className={styles.replyContainer}>
|
||||
<PostInfoAndMedia openReplyModal={openReplyModal} post={reply} roles={roles} />
|
||||
{reply.content && <PostMessageMobile post={reply} />}
|
||||
showReplies &&
|
||||
(showAllReplies ? replies : replies.slice(-5)).map((reply, index) => {
|
||||
const isRouteLinkToReply = location.pathname.startsWith(`/p/${subplebbitAddress}/c/${reply?.cid}`);
|
||||
return (
|
||||
<div key={index} className={styles.replyContainer} ref={(el) => (replyRefs.current[index] = el)}>
|
||||
<div className={styles.replyMobile}>
|
||||
<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>
|
||||
);
|
||||
|
||||
@@ -211,6 +211,10 @@
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.quotePreview {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.postMobile .postContainer {
|
||||
background-color: var(--post-mobile-background-color);
|
||||
}
|
||||
@@ -272,7 +276,6 @@
|
||||
.postMobile .postMessage {
|
||||
padding: 10px;
|
||||
font-size: var(--post-mobile-content-font-size);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.postMobile .postLink {
|
||||
@@ -338,6 +341,7 @@
|
||||
.quoteLink:hover {
|
||||
color: var(--post-quotelink-text-color-hover);
|
||||
text-decoration: var(--post-quotelink-text-decoration-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.replyMobile {
|
||||
@@ -389,11 +393,58 @@
|
||||
}
|
||||
|
||||
.removedContent {
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
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) {
|
||||
.replyQuotePreview {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.postDesktop {
|
||||
display: none;
|
||||
}
|
||||
@@ -404,6 +455,11 @@
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.replyQuotePreview {
|
||||
padding-right: 7px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.postMobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -11,10 +11,11 @@ export interface PostProps {
|
||||
reply?: any;
|
||||
roles?: Role[];
|
||||
showAllReplies?: boolean;
|
||||
showReplies?: boolean;
|
||||
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 isMobile = useIsMobile();
|
||||
|
||||
@@ -22,9 +23,9 @@ const Post = ({ post, showAllReplies = false, openReplyModal }: PostProps) => {
|
||||
<div className={styles.thread}>
|
||||
<div className={styles.postContainer}>
|
||||
{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>
|
||||
|
||||
@@ -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
|
||||
type='text'
|
||||
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 } })}
|
||||
/>
|
||||
</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;
|
||||
+93
-3
@@ -1,4 +1,12 @@
|
||||
: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 */
|
||||
--banner-image-border: 1px solid #800;
|
||||
--board-title-font-size: 28px;
|
||||
@@ -143,13 +151,20 @@
|
||||
--post-quotelink-text-color: navy;
|
||||
--post-quotelink-text-color-hover: red;
|
||||
--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 */
|
||||
--side-arrows-color: #e0bfb7;
|
||||
--reply-desktop-background-color: #f0e0d6;
|
||||
--reply-desktop-border: 1px solid #d9bfb7;
|
||||
|
||||
/* reply highlight */
|
||||
--reply-highlight-background-color: #f0c0b0;
|
||||
|
||||
/* reply modal */
|
||||
--reply-modal-field-input-border: 1px solid #aaa;
|
||||
--reply-modal-field-input-border-focus: 1px solid #ea8;
|
||||
@@ -183,6 +198,14 @@
|
||||
}
|
||||
|
||||
: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 */
|
||||
--banner-image-border: 1px solid #000;
|
||||
--board-title-font-size: 28px;
|
||||
@@ -327,13 +350,20 @@
|
||||
--post-quotelink-text-color: #d00;
|
||||
--post-quotelink-text-color-hover: #d00;
|
||||
--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 */
|
||||
--side-arrows-color: #b7c5d9;
|
||||
--reply-desktop-background-color: #d6daf0;
|
||||
--reply-desktop-border: 1px solid #b7c5d9;
|
||||
|
||||
/* reply highlight */
|
||||
--reply-highlight-background-color: #d6bad0;
|
||||
|
||||
/* reply modal */
|
||||
--reply-modal-field-input-border: 1px solid #aaa;
|
||||
--reply-modal-field-input-border-focus: 1px solid #98e;
|
||||
@@ -367,6 +397,14 @@
|
||||
}
|
||||
|
||||
: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-background-base-color: #ffe;
|
||||
--body-background-pattern-image: none;
|
||||
@@ -483,12 +521,19 @@
|
||||
--post-quotelink-text-color-hover: red;
|
||||
--post-quotelink-text-decoration: 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 */
|
||||
--side-arrows-color: #e0bfb7;
|
||||
--reply-desktop-background-color: #f0e0d6;
|
||||
--reply-desktop-border: 1px solid #d9bfb7;
|
||||
|
||||
/* reply highlight */
|
||||
--reply-highlight-background-color: #f0c0b0;
|
||||
|
||||
/* settings modal */
|
||||
--settings-modal-background-color: #f0e0d6;
|
||||
--settings-modal-border-top: 1px solid rgba(0, 0, 0, 0.20);
|
||||
@@ -517,6 +562,14 @@
|
||||
}
|
||||
|
||||
: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 */
|
||||
--banner-image-border: 1px solid #000;
|
||||
--board-title-font-size: 32px;
|
||||
@@ -646,13 +699,20 @@
|
||||
--post-quotelink-text-color: #d00;
|
||||
--post-quotelink-text-color-hover: #d00;
|
||||
--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 */
|
||||
--side-arrows-color: #b7c5d9;
|
||||
--reply-desktop-background-color: #d6daf0;
|
||||
--reply-desktop-border: 1px solid #b7c5d9;
|
||||
|
||||
/* reply highlight */
|
||||
--reply-highlight-background-color: #d6bad0;
|
||||
|
||||
/* settings modal */
|
||||
--settings-modal-background-color: #d6daf0;
|
||||
--settings-modal-border-top: 1px solid rgba(0, 0, 0, 0.20);
|
||||
@@ -681,6 +741,14 @@
|
||||
}
|
||||
|
||||
: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-background-base-color: #1d1f21;
|
||||
--body-background-pattern-image: none;
|
||||
@@ -822,6 +890,10 @@
|
||||
--post-quotelink-text-color-hover: #81a2be;
|
||||
--post-quotelink-text-decoration: 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 */
|
||||
--side-arrows-color: #c5c8c6;
|
||||
@@ -833,6 +905,9 @@
|
||||
--reply-modal-field-border-focus: 1px solid #757575;
|
||||
--reply-modal-field-input-border-focus: 1px solid #757575;
|
||||
|
||||
/* reply highlight */
|
||||
--reply-highlight-background-color: #1d1d21;
|
||||
|
||||
/* select */
|
||||
--select-border: 1px solid #575a60;
|
||||
--select-background-color: #282a2e;
|
||||
@@ -868,6 +943,14 @@
|
||||
}
|
||||
|
||||
: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-background-base-color: #eee;
|
||||
--body-background-pattern-image: none;
|
||||
@@ -995,12 +1078,19 @@
|
||||
--post-quotelink-text-color-hover: #f30;
|
||||
--post-quotelink-text-decoration: 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 */
|
||||
--side-arrows-color: #333;
|
||||
--reply-desktop-background-color: #ddd;
|
||||
--reply-desktop-border: 1px solid #ccc;
|
||||
|
||||
/* reply highlight */
|
||||
--reply-highlight-background-color: #ccc;
|
||||
|
||||
/* reply modal */
|
||||
--reply-modal-field-input-border: 1px solid #aaa;
|
||||
--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 { Subplebbit, useAccount, useAccountSubplebbits } from '@plebbit/plebbit-react-hooks';
|
||||
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 styles from '../home.module.css';
|
||||
import { nsfwTags } from '../home';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
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';
|
||||
|
||||
const BoxModal = ({ isBoardsBoxModal }: { isBoardsBoxModal: boolean }) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Comment, Subplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import styles from '../home.module.css';
|
||||
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 { CatalogPostMedia } from '../../../components/catalog-row';
|
||||
import LoadingEllipsis from '../../../components/loading-ellipsis';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
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 styles from './post-page.module.css';
|
||||
import { isDescriptionView, isRulesView, isSettingsView } from '../../lib/utils/view-utils';
|
||||
@@ -25,7 +25,21 @@ const PostPage = () => {
|
||||
|
||||
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 isThreadClosed = deleted || locked || removed;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user