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 ( - + ) ); }; 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' && ); @@ -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");