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": "اختياري",
"close_thread": "اغلق الموضوع",
"view_spoiler": "عرض الحاضر",
"filter": "تصفية"
"filter": "تصفية",
"edit": "تحرير"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "ঐচ্ছিক",
"close_thread": "থ্রেড বন্ধ করুন",
"view_spoiler": "স্পোয়ালার দেখুন",
"filter": "ফিল্টার"
"filter": "ফিল্টার",
"edit": "সম্পাদনা"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "volitelný",
"close_thread": "uzavřít vlákno",
"view_spoiler": "zobrazit spoiler",
"filter": "filtr"
"filter": "filtr",
"edit": "upravit"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "valgfri",
"close_thread": "luk tråd",
"view_spoiler": "vis spoiler",
"filter": "filtrér"
"filter": "filtrér",
"edit": "rediger"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "optional",
"close_thread": "Thread schließen",
"view_spoiler": "Spoiler anzeigen",
"filter": "filtern"
"filter": "filtern",
"edit": "bearbeiten"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "προαιρετικό",
"close_thread": "κλείσιμο νήματος",
"view_spoiler": "προβολή spoiler",
"filter": "φίλτρο"
"filter": "φίλτρο",
"edit": "επεξεργασία"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "optional",
"close_thread": "close thread",
"view_spoiler": "view spoiler",
"filter": "filter"
"filter": "filter",
"edit": "edit"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "opcional",
"close_thread": "cerrar hilo",
"view_spoiler": "ver spoiler",
"filter": "filtrar"
"filter": "filtrar",
"edit": "editar"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "اختیاری",
"close_thread": "بستن موضوع",
"view_spoiler": "نمایش اسپویلر",
"filter": "فیلتر"
"filter": "فیلتر",
"edit": "ویرایش"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "valinnainen",
"close_thread": "sulje keskustelu",
"view_spoiler": "näytä spoiler",
"filter": "suodatin"
"filter": "suodatin",
"edit": "muokkaa"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "opsyonal",
"close_thread": "isara ang thread",
"view_spoiler": "tingnan ang spoiler",
"filter": "salain"
"filter": "salain",
"edit": "i-edit"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "facultatif",
"close_thread": "fermer le fil",
"view_spoiler": "voir le spoiler",
"filter": "filtre"
"filter": "filtre",
"edit": "modifier"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "אופציונלי",
"close_thread": "סגור את הנושא",
"view_spoiler": "הצג ספוילר",
"filter": "סנן"
"filter": "סנן",
"edit": "ערוך"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "वैकल्पिक",
"close_thread": "थ्रेड बंद करें",
"view_spoiler": "स्पॉइलर देखें",
"filter": "फ़िल्टर"
"filter": "फ़िल्टर",
"edit": "संपादित करें"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "opsional",
"close_thread": "tutup utas",
"view_spoiler": "lihat spoiler",
"filter": "menyaring"
"filter": "menyaring",
"edit": "sunting"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "opzionale",
"close_thread": "chiudi il thread",
"view_spoiler": "mostra spoiler",
"filter": "filtro"
"filter": "filtro",
"edit": "modifica"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "オプショナル",
"close_thread": "スレッドを閉じる",
"view_spoiler": "スポイラーを表示",
"filter": "フィルター"
"filter": "フィルター",
"edit": "編集"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "선택",
"close_thread": "스레드 닫기",
"view_spoiler": "스포일러 보기",
"filter": "필터"
"filter": "필터",
"edit": "편집"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "वैकल्पिक",
"close_thread": "थ्रेड बंद करा",
"view_spoiler": "स्पॉईलर पहा",
"filter": "फिल्टर"
"filter": "फिल्टर",
"edit": "संपादित करा"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "optioneel",
"close_thread": "draad sluiten",
"view_spoiler": "bekijk spoiler",
"filter": "filter"
"filter": "filter",
"edit": "bewerken"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "valgfritt",
"close_thread": "lukk tråd",
"view_spoiler": "se spoiler",
"filter": "filter"
"filter": "filter",
"edit": "rediger"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "opcjonalnie",
"close_thread": "zamknij wątek",
"view_spoiler": "pokaż spoiler",
"filter": "filtr"
"filter": "filtr",
"edit": "edytuj"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "opcional",
"close_thread": "fechar tópico",
"view_spoiler": "ver spoiler",
"filter": "filtro"
"filter": "filtro",
"edit": "editar"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "opțional",
"close_thread": "închide firul",
"view_spoiler": "vezi spoilerul",
"filter": "filtru"
"filter": "filtru",
"edit": "editați"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "необязательный",
"close_thread": "закрыть тему",
"view_spoiler": "посмотреть спойлер",
"filter": "фильтр"
"filter": "фильтр",
"edit": "редактировать"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "opsionale",
"close_thread": "mbyll temën",
"view_spoiler": "shikoni spoiler",
"filter": "filtruar"
"filter": "filtruar",
"edit": "redakto"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "valfritt",
"close_thread": "stäng tråd",
"view_spoiler": "visa spoiler",
"filter": "filter"
"filter": "filter",
"edit": "redigera"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "ఐచ్ఛికం",
"close_thread": "థ్రెడ్‌ను మూసేయండి",
"view_spoiler": "స్పోయ్లర్ వీక్షించండి",
"filter": "వడపోత"
"filter": "వడపోత",
"edit": "సవరించు"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "ไม่จำเป็น",
"close_thread": "ปิดกระทู้",
"view_spoiler": "ดูสปอยล์เลอร์",
"filter": "ตัวกรอง"
"filter": "ตัวกรอง",
"edit": "แก้ไข"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "необов’язково",
"close_thread": "закрити тему",
"view_spoiler": "переглянути спойлер",
"filter": "фільтр"
"filter": "фільтр",
"edit": "редагувати"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "اختیاری",
"close_thread": "موضوع بند کریں",
"view_spoiler": "اسپوائلر دیکھیں",
"filter": "فلٹر"
"filter": "فلٹر",
"edit": "ترمیم کریں"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -147,5 +147,6 @@
"optional": "可选的",
"close_thread": "关闭主题",
"view_spoiler": "查看剧透",
"filter": "过滤器"
"filter": "过滤器",
"edit": "编辑"
}
+14 -12
View File
@@ -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 && (
<>
+1 -1
View File
@@ -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(() => {
@@ -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;
}
}
+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 { 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} />
)}
</>
);
};
+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 { 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>
);
+57 -1
View File
@@ -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;
}
+4 -3
View File
@@ -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;
+1 -1
View File
@@ -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>
+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;
+93 -3
View File
@@ -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;
+1 -1
View File
@@ -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 -1
View File
@@ -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';
+16 -2
View File
@@ -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;