diff --git a/src/components/comment-edit-form/comment-edit-form.tsx b/src/components/comment-edit-form/comment-edit-form.tsx
index 6b10cd13..c213c699 100644
--- a/src/components/comment-edit-form/comment-edit-form.tsx
+++ b/src/components/comment-edit-form/comment-edit-form.tsx
@@ -34,7 +34,7 @@ const CommentEditForm = ({ commentCid, hideCommentEditForm }: CommentEditFormPro
const defaultPublishOptions: PublishCommentEditOptions = {
commentCid,
content,
- edit: { reason: edit?.reason },
+ reason: edit?.reason ?? '',
spoiler,
subplebbitAddress,
onChallenge: (...args: any) => addChallenge([...args, post]),
@@ -82,8 +82,8 @@ const CommentEditForm = ({ commentCid, hideCommentEditForm }: CommentEditFormPro
{t('edit_reason')}:{' '}
setPublishCommentEditOptions((state) => ({ ...state, edit: { reason: e.target.value } }))}
+ value={publishCommentEditOptions.reason}
+ onChange={(e) => setPublishCommentEditOptions((state) => ({ ...state, reason: e.target.value }))}
/>
setShowFormattingHelp(!showFormattingHelp)}>
diff --git a/src/components/markdown/markdown.module.css b/src/components/markdown/markdown.module.css
index 244e5c13..b0f60d3c 100644
--- a/src/components/markdown/markdown.module.css
+++ b/src/components/markdown/markdown.module.css
@@ -5,11 +5,13 @@
.markdown ol {
padding-left: 40px;
white-space: normal;
+ padding-bottom: 5px;
}
.markdown ul {
padding-left: 40px;
white-space: normal;
+ padding-bottom: 5px;
}
.markdown blockquote {
diff --git a/src/components/markdown/markdown.tsx b/src/components/markdown/markdown.tsx
index 1af16018..29f199df 100644
--- a/src/components/markdown/markdown.tsx
+++ b/src/components/markdown/markdown.tsx
@@ -24,6 +24,11 @@ const Markdown = ({ content }: MarkdownProps) => {
remarkPlugins={remarkPlugins}
rehypePlugins={[[rehypeSanitize]]}
components={{
+ a: ({ children, href }) => (
+
+ {children}
+
+ ),
img: ({ src }) => {src},
video: ({ src }) => {src},
iframe: ({ src }) => {src},
diff --git a/src/components/post/comment-tools/comment-tools.tsx b/src/components/post/comment-tools/comment-tools.tsx
index 1dddda96..84e2e35a 100644
--- a/src/components/post/comment-tools/comment-tools.tsx
+++ b/src/components/post/comment-tools/comment-tools.tsx
@@ -263,67 +263,69 @@ const CommentTools = ({
const isInInboxView = isInboxView(useLocation().pathname);
return (
-
- {isReply ? (
- isSingleReply ? (
-
+ (!(deleted || removed) || ((deleted || removed) && (isAuthor || isAccountMod))) && (
+
+ {isReply ? (
+ isSingleReply ? (
+
+ ) : (
+
+ )
) : (
-
- )
- ) : (
- <>
-
-
- >
- )}
-
+ <>
+
+
+ >
+ )}
+
+ )
);
};
diff --git a/src/components/post/post.tsx b/src/components/post/post.tsx
index 7c517d96..74348fcf 100644
--- a/src/components/post/post.tsx
+++ b/src/components/post/post.tsx
@@ -70,6 +70,33 @@ interface PostProps {
post: Comment | undefined;
}
+const ThumbnailLoader = ({ post }: PostProps) => {
+ const { cid } = post || {};
+ // Reset state by remounting component when post changes
+ return useThumbnailContent(cid, post);
+};
+
+const useThumbnailContent = (key: string, post: any) => {
+ const [commentMediaInfo, setCommentMediaInfo] = useState();
+
+ useEffect(() => {
+ const loadThumbnail = async () => {
+ const initialInfo = getCommentMediaInfo(post);
+ // some sites have CORS access, so the thumbnail can be fetched client-side, which is helpful if subplebbit.settings.fetchThumbnailUrls is false
+ if (initialInfo?.type === 'webpage' && !initialInfo.thumbnail) {
+ const newMediaInfo = await fetchWebpageThumbnailIfNeeded(initialInfo);
+ setCommentMediaInfo(newMediaInfo);
+ } else {
+ setCommentMediaInfo(initialInfo);
+ }
+ };
+
+ loadThumbnail();
+ }, [post]);
+
+ return commentMediaInfo;
+};
+
const Post = ({ index, post = {} }: PostProps) => {
// handle single comment thread
const op = useComment({ commentCid: post?.parentCid ? post?.postCid : '' });
@@ -120,26 +147,7 @@ const Post = ({ index, post = {} }: PostProps) => {
const isInProfileHiddenView = isProfileHiddenView(location.pathname);
const isInSubplebbitView = isSubplebbitView(location.pathname, params);
- // some sites have CORS access, so the thumbnail can be fetched client-side, which is helpful if subplebbit.settings.fetchThumbnailUrls is false
- const [commentMediaInfo, setCommentMediaInfo] = useState();
-
- useEffect(() => {
- const loadThumbnail = async () => {
- const initialInfo = getCommentMediaInfo(post);
-
- if (initialInfo?.type === 'webpage' && !initialInfo.thumbnail) {
- const newMediaInfo = await fetchWebpageThumbnailIfNeeded(initialInfo);
- setCommentMediaInfo(newMediaInfo);
- } else {
- setCommentMediaInfo(initialInfo);
- }
- };
-
- loadThumbnail();
- return () => {
- setCommentMediaInfo(undefined);
- };
- }, [post]);
+ const commentMediaInfo = ThumbnailLoader({ post });
const [isExpanded, setIsExpanded] = useState(isInPostView);
const toggleExpanded = () => setIsExpanded(!isExpanded);
diff --git a/src/components/post/thumbnail/thumbnail.tsx b/src/components/post/thumbnail/thumbnail.tsx
index 5b6bac2c..2878a232 100644
--- a/src/components/post/thumbnail/thumbnail.tsx
+++ b/src/components/post/thumbnail/thumbnail.tsx
@@ -18,7 +18,6 @@ interface ThumbnailProps {
const Thumbnail = ({ cid, commentMediaInfo, expanded = false, isReply = false, link, linkHeight, linkWidth, subplebbitAddress, toggleExpanded }: ThumbnailProps) => {
const iframeThumbnail = commentMediaInfo?.patternThumbnailUrl || commentMediaInfo?.thumbnail;
let displayWidth, displayHeight, hasLinkDimensions;
- const routeOrLink = isReply || commentMediaInfo?.type === 'webpage' ? link : `/p/${subplebbitAddress}/c/${cid}`;
const thumbnailClass = expanded ? styles.thumbnailHidden : styles.thumbnailVisible;
if (linkWidth && linkHeight) {
@@ -52,17 +51,23 @@ const Thumbnail = ({ cid, commentMediaInfo, expanded = false, isReply = false, l
return (
- {
- if (e.button === 0 && isReply) {
- e.preventDefault();
- toggleExpanded && toggleExpanded();
- }
- }}
- >
- {mediaComponent}
-
+ {isReply || commentMediaInfo?.type === 'webpage' ? (
+ {
+ if (e.button === 0 && isReply) {
+ e.preventDefault();
+ toggleExpanded && toggleExpanded();
+ }
+ }}
+ >
+ {mediaComponent}
+
+ ) : (
+ {mediaComponent}
+ )}
);
diff --git a/src/components/reply/reply.module.css b/src/components/reply/reply.module.css
index 236ccf31..a77d031c 100644
--- a/src/components/reply/reply.module.css
+++ b/src/components/reply/reply.module.css
@@ -130,6 +130,11 @@
line-height: 20px;
}
+.removedOrDeletedContent {
+ margin-bottom: 0 !important;
+ margin-top: 0 !important;
+}
+
.md p {
margin-bottom: 5px;
}
@@ -165,6 +170,18 @@
.removedContent, .deletedContent {
text-transform: lowercase;
+ background-color: var(--removed-reply-backgrouhd-color);
+ display: inline-block;
+ padding: 5px;
+}
+
+.removedUsername {
+ color: var(--text-info);
+ text-transform: lowercase;
+}
+
+.hiddenMidcol {
+ visibility: hidden !important;
}
.usertext a {
@@ -189,11 +206,13 @@
}
.collapsedEntry .author,
+.collapsedEntry .removedUsername,
.collapsedEntry .moderator,
.collapsedEntry .admin,
.collapsedEntry .owner,
.collapsedEntry .moderatorBrackets,
.collapsedEntry .time,
+.collapsedEntry .score,
.collapsedEntry .children {
color: var(--text-info) !important;
font-style: italic !important;
diff --git a/src/components/reply/reply.tsx b/src/components/reply/reply.tsx
index 7c82bd77..2a78e8e1 100644
--- a/src/components/reply/reply.tsx
+++ b/src/components/reply/reply.tsx
@@ -19,7 +19,7 @@ import ReplyForm from '../reply-form';
import useDownvote from '../../hooks/use-downvote';
import useStateString from '../../hooks/use-state-string';
import useUpvote from '../../hooks/use-upvote';
-import { isInboxView, isPostContextView } from '../../lib/utils/view-utils';
+import { isInboxView, isPostContextView, isPostView } from '../../lib/utils/view-utils';
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
import Markdown from '../markdown';
import { getHostname } from '../../lib/utils/url-utils';
@@ -28,13 +28,16 @@ interface ReplyAuthorProps {
address: string;
authorRole: string;
cid: string;
+ deleted: boolean;
displayName: string;
imageUrl: string | undefined;
isAvatarDefined: boolean;
+ removed: boolean;
shortAuthorAddress: string | undefined;
}
-const ReplyAuthor = ({ address, authorRole, cid, displayName, imageUrl, isAvatarDefined, shortAuthorAddress }: ReplyAuthorProps) => {
+const ReplyAuthor = ({ address, authorRole, cid, deleted, displayName, imageUrl, isAvatarDefined, removed, shortAuthorAddress }: ReplyAuthorProps) => {
+ const { t } = useTranslation();
const isAuthorAdmin = authorRole === 'admin';
const isAuthorOwner = authorRole === 'owner';
const isAuthorModerator = authorRole === 'moderator';
@@ -44,28 +47,34 @@ const ReplyAuthor = ({ address, authorRole, cid, displayName, imageUrl, isAvatar
return (
<>
- {isAvatarDefined && (
-
-
-
- )}
- {displayName && (
-
- {shortDisplayName}{' '}
-
- )}
-
- {displayName ? `u/${shortAuthorAddress}` : shortAuthorAddress}
-
- {authorRole && (
-
- {' '}
- [
-
- {authorRoleInitial}
-
- ]
-
+ {removed || deleted ? (
+ [{removed ? t('removed') : deleted ? t('deleted') : ''}]
+ ) : (
+ <>
+ {isAvatarDefined && (
+
+
+
+ )}
+ {displayName && (
+
+ {shortDisplayName}{' '}
+
+ )}
+
+ {displayName ? `u/${shortAuthorAddress}` : shortAuthorAddress}
+
+ {authorRole && (
+
+ {' '}
+ [
+
+ {authorRoleInitial}
+
+ ]
+
+ )}
+ >
)}
>
);
@@ -265,19 +274,14 @@ const Reply = ({ cidOfReplyWithContext, depth = 0, isSingleComment, isSingleRepl
const [showSpoiler, setShowSpoiler] = useState(false);
- const { blocked, unblock } = useBlock({ cid });
- const [collapsed, setCollapsed] = useState(blocked);
- useEffect(() => {
- if (blocked) {
- setCollapsed(true);
- }
- }, [blocked]);
- const handleCollapseButton = () => {
- if (blocked) {
- unblock();
- }
- setCollapsed(!collapsed);
- };
+ const pendingReply = useAccountComment({ commentIndex: reply?.index });
+ const parentOfPendingReply = useComment({ commentCid: pendingReply?.parentCid });
+
+ const location = useLocation();
+ const params = useParams();
+ const isInInboxView = isInboxView(location.pathname);
+ const isInPostContextView = isPostContextView(location.pathname, params, location.search);
+ const isInPostView = isPostView(location.pathname, params);
const authorRole = subplebbit?.roles?.[author?.address]?.role;
const { shortAuthorAddress } = useAuthorAddress({ comment: reply });
@@ -331,8 +335,6 @@ const Reply = ({ cidOfReplyWithContext, depth = 0, isSingleComment, isSingleRepl
{cid === undefined && state !== 'failed' && }
{editState === 'failed' && }
{editState === 'pending' && }
- {deleted && }
- {removed && }
{spoiler && }
);
@@ -341,13 +343,19 @@ const Reply = ({ cidOfReplyWithContext, depth = 0, isSingleComment, isSingleRepl
const childrenCount = unnestedReplies.length;
const childrenString = childrenCount === 1 ? t('child', { childrenCount }) : t('children', { childrenCount });
- const pendingReply = useAccountComment({ commentIndex: reply?.index });
- const parentOfPendingReply = useComment({ commentCid: pendingReply?.parentCid });
-
- const location = useLocation();
- const params = useParams();
- const isInInboxView = isInboxView(location.pathname);
- const isInPostContextView = isPostContextView(location.pathname, params, location.search);
+ const { blocked, unblock } = useBlock({ cid });
+ const [collapsed, setCollapsed] = useState(blocked);
+ useEffect(() => {
+ if (blocked || (isInPostView && (deleted || removed) && childrenCount === 0)) {
+ setCollapsed(true);
+ }
+ }, [blocked, isInPostView, deleted, removed, childrenCount]);
+ const handleCollapseButton = () => {
+ if (blocked) {
+ unblock();
+ }
+ setCollapsed(!collapsed);
+ };
return (
@@ -355,7 +363,7 @@ const Reply = ({ cidOfReplyWithContext, depth = 0, isSingleComment, isSingleRepl
{isInInboxView &&
}
0 && styles.nested}`}>
{!collapsed && (
-
+
cid && upvote()} />
cid && downvote()} />
@@ -371,9 +379,11 @@ const Reply = ({ cidOfReplyWithContext, depth = 0, isSingleComment, isSingleRepl
address={author?.address}
authorRole={authorRole}
cid={cid}
+ deleted={deleted}
displayName={author?.displayName}
imageUrl={imageUrl}
isAvatarDefined={!!author?.avatar}
+ removed={removed}
shortAuthorAddress={shortAuthorAddress}
/>
{scoreString}{' '}
@@ -430,13 +440,17 @@ const Reply = ({ cidOfReplyWithContext, depth = 0, isSingleComment, isSingleRepl
{isEditing ? (
) : (
-
+
{spoiler && !showSpoiler &&
{t('view_spoiler')}
}
{content &&
(removed ? (
-
[{t('removed')}]
+
[{t('removed')}]
) : deleted ? (
-
[{t('deleted')}]
+
[{t('deleted')}]
) : (
))}
@@ -460,12 +474,14 @@ const Reply = ({ cidOfReplyWithContext, depth = 0, isSingleComment, isSingleRepl
30 * day) {
export const timeFilterNames = ['1h', '24h', '1w', '1m', '1y', 'all', lastVisitTimeFilterName];
+function convertTimeStringToSeconds(timeString: string): number {
+ const match = timeString.match(/^(\d+)([hdwmy])$/);
+ if (!match) {
+ throw new Error(`Invalid time filter format: ${timeString}`);
+ }
+
+ const [, value, unit] = match;
+ const numValue = parseInt(value, 10);
+
+ switch (unit) {
+ case 'h':
+ return numValue * 60 * 60;
+ case 'd':
+ return numValue * 24 * 60 * 60;
+ case 'w':
+ return numValue * 7 * 24 * 60 * 60;
+ case 'm':
+ return numValue * 30 * 24 * 60 * 60;
+ case 'y':
+ return numValue * 365 * 24 * 60 * 60;
+ default:
+ throw new Error(`Invalid time unit: ${unit}`);
+ }
+}
+
const useTimeFilter = () => {
const params = useParams();
const location = useLocation();
@@ -59,25 +84,11 @@ const useTimeFilter = () => {
} else if (timeFilterName && timeFilterName in timeFilterNamesToSeconds) {
timeFilterSeconds = timeFilterNamesToSeconds[timeFilterName as keyof typeof timeFilterNamesToSeconds];
} else if (timeFilterName) {
- // Handle dynamic time filters (e.g., "3d", "2w")
- const match = timeFilterName.match(/^(\d+)([dwmy])$/);
- if (match) {
- const [, value, unit] = match;
- const numValue = parseInt(value, 10);
- switch (unit) {
- case 'd':
- timeFilterSeconds = numValue * 24 * 60 * 60;
- break;
- case 'w':
- timeFilterSeconds = numValue * 7 * 24 * 60 * 60;
- break;
- case 'm':
- timeFilterSeconds = numValue * 30 * 24 * 60 * 60;
- break;
- case 'y':
- timeFilterSeconds = numValue * 365 * 24 * 60 * 60;
- break;
- }
+ try {
+ timeFilterSeconds = convertTimeStringToSeconds(timeFilterName);
+ } catch (e) {
+ console.error(`Invalid time filter format: ${timeFilterName}`);
+ timeFilterSeconds = undefined;
}
}
diff --git a/src/hooks/use-validate-route-params.ts b/src/hooks/use-validate-route-params.ts
index 6db52cd2..e58d9933 100644
--- a/src/hooks/use-validate-route-params.ts
+++ b/src/hooks/use-validate-route-params.ts
@@ -18,7 +18,7 @@ const useValidateRouteParams = () => {
accountComments?.length > 0 &&
parseInt(accountCommentIndex) < accountComments.length);
- const isDynamicTimeFilter = (filter: string) => /^\d+[dwmy]$/.test(filter);
+ const isDynamicTimeFilter = (filter: string) => /^\d+[hdwmy]$/.test(filter);
const isTimeFilterNameValid =
!timeFilterName || timeFilterNames.includes(timeFilterName as any) || timeFilterName === lastVisitTimeFilterName || isDynamicTimeFilter(timeFilterName);
diff --git a/src/lib/init-translations.ts b/src/lib/init-translations.ts
index 05333dd5..dacc105b 100644
--- a/src/lib/init-translations.ts
+++ b/src/lib/init-translations.ts
@@ -12,99 +12,41 @@ i18next
.init({
fallbackLng: 'en',
supportedLngs: [
- 'af',
- 'am',
'ar',
- 'az',
- 'be',
- 'bg',
'bn',
- 'bs',
- 'ca',
- 'ckb',
'cs',
- 'cy',
'da',
'de',
'el',
'en',
- 'eo',
'es',
- 'et',
- 'eu',
'fa',
'fi',
+ 'fil',
'fr',
- 'fy',
- 'ga',
- 'gd',
- 'gl',
- 'gu',
- 'ha',
'he',
'hi',
- 'hr',
- 'ht',
'hu',
- 'hy',
'id',
- 'ig',
- 'is',
'it',
'ja',
- 'ka',
- 'kk',
- 'km',
- 'kn',
'ko',
- 'ku',
- 'ky',
- 'lb',
- 'lo',
- 'lt',
- 'lv',
- 'mg',
- 'mk',
- 'ml',
- 'mn',
'mr',
- 'ms',
- 'mt',
- 'my',
- 'ne',
'nl',
'no',
- 'or',
- 'pa',
'pl',
- 'ps',
'pt',
'ro',
'ru',
- 'rw',
- 'si',
- 'sk',
- 'sl',
- 'sn',
- 'so',
'sq',
- 'sr',
'sv',
- 'sw',
- 'ta',
'te',
'th',
- 'tl',
'tr',
- 'ug',
'uk',
'ur',
- 'uz',
'vi',
- 'yi',
- 'yo',
'zh',
- 'zu',
],
ns: ['default'],
diff --git a/src/themes.css b/src/themes.css
index 32afd1ff..701c6e72 100644
--- a/src/themes.css
+++ b/src/themes.css
@@ -54,6 +54,7 @@
--play-button: url("/public/assets/buttons/play-button-dark.png");
--play-button-hover: url("/public/assets/buttons/play-button-hover.png");
--red: rgb(200, 0, 0);
+ --removed-reply-backgrouhd-color: rgb(27, 30, 32);
--text: #bfbfbf;
--text-button: url("/public/assets/buttons/text-button-dark.png");
--text-button-hover: url("/public/assets/buttons/text-button-hover.png");
@@ -125,6 +126,7 @@
--play-button: url("/public/assets/buttons/play-button.png");
--play-button-hover: url("/public/assets/buttons/play-button-hover.png");
--red: red;
+ --removed-reply-backgrouhd-color: #f0f0f0;
--text: black;
--text-button: url("/public/assets/buttons/text-button.png");
--text-button-hover: url("/public/assets/buttons/text-button-hover.png");