Skip to content

Commit

Permalink
Merge pull request #416 from plebbit/development
Browse files Browse the repository at this point in the history
Development
  • Loading branch information
plebeius-eth authored Nov 10, 2024
2 parents 7217ad3 + 5e1902f commit a91c97e
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 112 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"dependencies": {
"@capacitor/app": "6.0.1",
"@floating-ui/react": "0.26.1",
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#de80441257496942063bf032d48e955b7f812937",
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#0d4219ad95c3322eac78db23091eb57239be8776",
"@testing-library/jest-dom": "5.14.1",
"@testing-library/react": "13.0.0",
"@testing-library/user-event": "13.2.1",
Expand Down
32 changes: 6 additions & 26 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { useEffect } from 'react';
import { Outlet, Route, Routes, useLocation, useNavigate, useParams } from 'react-router-dom';
import { useAccountComments } from '@plebbit/plebbit-react-hooks';
import { Outlet, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import useTheme from './hooks/use-theme';
import useTimeFilter from './hooks/use-time-filter';
import useValidateRouteParams from './hooks/use-validate-route-params';
import styles from './app.module.css';
import About from './views/about';
import All from './views/all';
Expand All @@ -24,32 +23,13 @@ import ChallengeModal from './components/challenge-modal';
import Header from './components/header';
import StickyHeader from './components/sticky-header';
import TopBar from './components/topbar';
import { isHomeAboutView } from './lib/utils/view-utils';

export const sortTypes = ['hot', 'new', 'active', 'controversialAll', 'topAll'];

const ValidateRouteParams = () => {
const { accountCommentIndex, sortType, timeFilterName } = useParams();
const { timeFilterNames, lastVisitTimeFilterName } = useTimeFilter();
const { accountComments } = useAccountComments();
const isSortTypeValid = !sortType || sortTypes.includes(sortType);
const ValidatedRoute = () => {
const isValid = useValidateRouteParams();

const isValidAccountCommentIndex =
!accountCommentIndex ||
(!isNaN(parseInt(accountCommentIndex)) &&
parseInt(accountCommentIndex) >= 0 &&
accountComments?.length > 0 &&
parseInt(accountCommentIndex) < accountComments.length);

const isDynamicTimeFilter = (filter: string) => /^\d+[dwmy]$/.test(filter);
const isTimeFilterNameValid =
!timeFilterName || timeFilterNames.includes(timeFilterName as any) || timeFilterName === lastVisitTimeFilterName || isDynamicTimeFilter(timeFilterName);

const isAccountCommentIndexValid = !accountCommentIndex || !isNaN(parseInt(accountCommentIndex));
const location = useLocation();
const isInHomeAboutView = isHomeAboutView(location.pathname);

if (!isValidAccountCommentIndex || (!isSortTypeValid && !isInHomeAboutView) || !isTimeFilterNameValid || !isAccountCommentIndexValid) {
if (!isValid) {
return <NotFound />;
}

Expand Down Expand Up @@ -144,7 +124,7 @@ const App = () => {
<Route path='*' element={<NotFound />} />
</Route>
<Route element={feedLayout}>
<Route element={<ValidateRouteParams />}>
<Route element={<ValidatedRoute />}>
<Route path='/:sortType?/:timeFilterName?' element={<Home />} />

<Route path='/p/all/:sortType?/:timeFilterName?' element={<All />} />
Expand Down
60 changes: 38 additions & 22 deletions src/components/post/comment-tools/mod-menu/mod-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { autoUpdate, flip, FloatingFocusManager, offset, shift, useClick, useDismiss, useFloating, useId, useInteractions, useRole } from '@floating-ui/react';
import { Trans, useTranslation } from 'react-i18next';
import { PublishCommentEditOptions, useComment, useEditedComment, usePublishCommentEdit } from '@plebbit/plebbit-react-hooks';
import { PublishCommentModerationOptions, useComment, useEditedComment, usePublishCommentModeration } from '@plebbit/plebbit-react-hooks';
import styles from './mod-menu.module.css';
import { alertChallengeVerificationFailed } from '../../../../lib/utils/challenge-utils';
import challengesStore from '../../../../stores/use-challenges-store';
Expand All @@ -15,7 +15,6 @@ type ModMenuProps = {

const ModMenu = ({ cid, isCommentAuthorMod }: ModMenuProps) => {
const { t } = useTranslation();

let post: any;
const comment = useComment({ commentCid: cid });
const { editedComment } = useEditedComment({ comment });
Expand All @@ -27,14 +26,16 @@ const ModMenu = ({ cid, isCommentAuthorMod }: ModMenuProps) => {
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 },
const defaultPublishOptions: PublishCommentModerationOptions = {
commentCid: post?.cid,
subplebbitAddress: post?.subplebbitAddress,
commentModeration: {
removed: post?.removed ?? false,
locked: post?.locked ?? false,
spoiler: post?.spoiler ?? false,
pinned: post?.pinned ?? false,
banExpiresAt: post?.banExpiresAt,
},
onChallenge: (...args: any) => addChallenge([...args, post]),
onChallengeVerification: alertChallengeVerificationFailed,
onError: (error: Error) => {
Expand All @@ -43,8 +44,8 @@ const ModMenu = ({ cid, isCommentAuthorMod }: ModMenuProps) => {
},
};

const [publishCommentEditOptions, setPublishCommentEditOptions] = useState(defaultPublishOptions);
const { publishCommentEdit } = usePublishCommentEdit(publishCommentEditOptions);
const [publishCommentModerationOptions, setPublishCommentModerationOptions] = useState(defaultPublishOptions);
const { publishCommentModeration } = usePublishCommentModeration(publishCommentModerationOptions);

const [banDuration, setBanDuration] = useState(1);

Expand All @@ -57,26 +58,41 @@ const ModMenu = ({ cid, isCommentAuthorMod }: ModMenuProps) => {
const onCheckbox = (e: React.ChangeEvent<HTMLInputElement>) => {
const { id, checked } = e.target;
if (id === 'banUser') {
setPublishCommentEditOptions((state) => ({
setPublishCommentModerationOptions((state) => ({
...state,
commentAuthor: { ...state.commentAuthor, banExpiresAt: checked ? daysToTimestampInSeconds(banDuration) : undefined },
commentModeration: {
...state.commentModeration,
banExpiresAt: checked ? daysToTimestampInSeconds(banDuration) : undefined,
},
}));
} else {
setPublishCommentEditOptions((state) => ({ ...state, [id]: checked }));
setPublishCommentModerationOptions((state) => ({
...state,
commentModeration: { ...state.commentModeration, [id]: checked },
}));
}
};

const onBanDurationChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const days = parseInt(e.target.value, 10) || 1;
setBanDuration(days);
setPublishCommentEditOptions((state) => ({
setPublishCommentModerationOptions((state) => ({
...state,
commentAuthor: { ...state.commentAuthor, banExpiresAt: daysToTimestampInSeconds(days) },
commentModeration: {
...state.commentModeration,
banExpiresAt: daysToTimestampInSeconds(days),
},
}));
};

const onReason = (e: React.ChangeEvent<HTMLInputElement>) =>
setPublishCommentEditOptions((state) => ({ ...state, reason: e.target.value ? e.target.value : undefined }));
setPublishCommentModerationOptions((state) => ({
...state,
commentModeration: {
...state.commentModeration,
reason: e.target.value ? e.target.value : undefined,
},
}));

const { refs, floatingStyles, context } = useFloating({
placement: 'bottom-start',
Expand All @@ -95,7 +111,7 @@ const ModMenu = ({ cid, isCommentAuthorMod }: ModMenuProps) => {
const headingId = useId();

const handleSaveClick = async () => {
await publishCommentEdit();
await publishCommentModeration();
setIsModMenuOpen(false);
};

Expand All @@ -110,34 +126,34 @@ const ModMenu = ({ cid, isCommentAuthorMod }: ModMenuProps) => {
<div className={styles.ModMenu}>
<div className={styles.menuItem}>
<label>
<input onChange={onCheckbox} checked={publishCommentEditOptions.removed} type='checkbox' id='removed' />
<input onChange={onCheckbox} checked={publishCommentModerationOptions.commentModeration.removed} type='checkbox' id='removed' />
{t('removed')}
</label>
</div>
{!isReply && (
<div className={styles.menuItem}>
<label>
<input onChange={onCheckbox} checked={publishCommentEditOptions.locked} type='checkbox' id='locked' />
<input onChange={onCheckbox} checked={publishCommentModerationOptions.commentModeration.locked} type='checkbox' id='locked' />
{t('locked')}
</label>
</div>
)}
<div className={styles.menuItem}>
<label>
<input onChange={onCheckbox} checked={publishCommentEditOptions.spoiler} type='checkbox' id='spoiler' />
<input onChange={onCheckbox} checked={publishCommentModerationOptions.commentModeration.spoiler} type='checkbox' id='spoiler' />
{t('spoiler')}
</label>
</div>
<div className={styles.menuItem}>
<label>
<input onChange={onCheckbox} checked={publishCommentEditOptions.pinned} type='checkbox' id='pinned' />
<input onChange={onCheckbox} checked={publishCommentModerationOptions.commentModeration.pinned} type='checkbox' id='pinned' />
{isReply ? t('stickied_comment') : t('announcement')}
</label>
</div>
{!isCommentAuthorMod && (
<div className={styles.menuItem}>
<label>
<input onChange={onCheckbox} checked={!!publishCommentEditOptions.commentAuthor?.banExpiresAt} type='checkbox' id='banUser' />
<input onChange={onCheckbox} checked={!!publishCommentModerationOptions.commentModeration.banExpiresAt} type='checkbox' id='banUser' />
<Trans
i18nKey='ban_user_for'
shouldUnescape={true}
Expand Down
47 changes: 24 additions & 23 deletions src/components/post/post.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import styles from './post.module.css';
import { Link, useLocation, useParams } from 'react-router-dom';
import { Comment, useAuthorAddress, useBlock, useComment, useEditedComment, useSubplebbit, useSubscribe } from '@plebbit/plebbit-react-hooks';
import { useTranslation } from 'react-i18next';
import { isAllView, isPostView, isProfileHiddenView, isSubplebbitView } from '../../lib/utils/view-utils';
import { fetchWebpageThumbnailIfNeeded, getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
import { CommentMediaInfo, fetchWebpageThumbnailIfNeeded, getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
import { getPostScore } from '../../lib/utils/post-utils';
import { getHostname } from '../../lib/utils/url-utils';
import { getFormattedTimeAgo, formatLocalizedUTCTimestamp } from '../../lib/utils/time-utils';
import CommentEditForm from '../comment-edit-form';
Expand Down Expand Up @@ -120,17 +121,25 @@ const Post = ({ index, post = {} }: PostProps) => {
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 initialCommentMediaInfo = useMemo(() => getCommentMediaInfo(post), [post]);
const [commentMediaInfo, setCommentMediaInfo] = useState(initialCommentMediaInfo);
const fetchThumbnail = useCallback(async () => {
if (initialCommentMediaInfo?.type === 'webpage' && !initialCommentMediaInfo.thumbnail) {
const newMediaInfo = await fetchWebpageThumbnailIfNeeded(initialCommentMediaInfo);
setCommentMediaInfo(newMediaInfo);
}
}, [initialCommentMediaInfo]);
const [commentMediaInfo, setCommentMediaInfo] = useState<CommentMediaInfo | undefined>();

useEffect(() => {
fetchThumbnail();
}, [fetchThumbnail]);
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 [isExpanded, setIsExpanded] = useState(isInPostView);
const toggleExpanded = () => setIsExpanded(!isExpanded);
Expand All @@ -141,15 +150,7 @@ const Post = ({ index, post = {} }: PostProps) => {

const [upvoted, upvote] = useUpvote(post);
const [downvoted, downvote] = useDownvote(post);
const getPostScore = () => {
if ((upvoteCount === 0 && downvoteCount === 0) || state === 'pending' || state === 'failed') {
return '•';
} else if (upvoteCount === undefined || downvoteCount === undefined) {
return '?';
}
return upvoteCount - downvoteCount;
};

const postScore = getPostScore(upvoteCount, downvoteCount, state);
const postTitle = (title?.length > 300 ? title?.slice(0, 300) + '...' : title) || (content?.length > 300 ? content?.slice(0, 300) + '...' : content);

const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
Expand Down Expand Up @@ -189,7 +190,7 @@ const Post = ({ index, post = {} }: PostProps) => {
<div className={styles.arrowWrapper}>
<div className={`${styles.arrowCommon} ${upvoted ? styles.upvoted : styles.arrowUp}`} onClick={() => cid && upvote()} />
</div>
<div className={styles.score}>{getPostScore()}</div>
<div className={styles.score}>{postScore}</div>
<div className={styles.arrowWrapper}>
<div className={`${styles.arrowCommon} ${downvoted ? styles.downvoted : styles.arrowDown}`} onClick={() => cid && downvote()} />
</div>
Expand Down Expand Up @@ -236,7 +237,7 @@ const Post = ({ index, post = {} }: PostProps) => {
</span>
)}
</p>
{!isInPostView && (
{!isInPostView && commentMediaInfo?.type !== 'webpage' && (
<ExpandButton
commentMediaInfo={commentMediaInfo}
content={content}
Expand Down
32 changes: 10 additions & 22 deletions src/components/sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { useTranslation } from 'react-i18next';
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
import { Comment, useAccount, useBlock, Role, Subplebbit, useSubplebbitStats, useAccountComment } from '@plebbit/plebbit-react-hooks';
import styles from './sidebar.module.css';
import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
import useIsMobile from '../../hooks/use-is-mobile';
import { getPostScore } from '../../lib/utils/post-utils';
import { getFormattedDate, getFormattedTimeDuration, getFormattedTimeAgo } from '../../lib/utils/time-utils';
import { findSubplebbitCreator } from '../../lib/utils/user-utils';
import {
Expand All @@ -20,8 +23,6 @@ import SearchBar from '../search-bar';
import SubscribeButton from '../subscribe-button';
import packageJson from '../../../package.json';
import LoadingEllipsis from '../loading-ellipsis';
import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
import useIsMobile from '../../hooks/use-is-mobile';

const { version } = packageJson;
const commitRef = process.env.REACT_APP_COMMIT_REF;
Expand Down Expand Up @@ -59,23 +60,11 @@ const ModeratorsList = ({ roles }: { roles: Record<string, Role> }) => {
);
};

const PostInfo = ({
address,
cid,
downvoteCount = 0,
timestamp = 0,
upvoteCount = 0,
}: {
address?: string;
cid?: string;
downvoteCount?: number;
timestamp?: number;
updatedAt?: number;
upvoteCount?: number;
}) => {
const PostInfo = ({ comment }: { comment: Comment | undefined }) => {
const { t, i18n } = useTranslation();
const { language } = i18n;
const postScore = upvoteCount - downvoteCount;
const { upvoteCount, downvoteCount, timestamp, state, subplebbitAddress, cid } = comment || {};
const postScore = getPostScore(upvoteCount, downvoteCount, state);
const totalVotes = upvoteCount + downvoteCount;
const upvotePercentage = totalVotes > 0 ? Math.round((upvoteCount / totalVotes) * 100) : 0;
const postDate = getFormattedDate(timestamp, language);
Expand All @@ -87,10 +76,11 @@ const PostInfo = ({
</div>
<div className={styles.postScore}>
<span className={styles.postScoreNumber}>{postScore} </span>
<span className={styles.postScoreWord}>{postScore === 1 ? t('point') : t('points')}</span> ({upvotePercentage}% {t('upvoted')})
<span className={styles.postScoreWord}>{postScore === 1 ? t('point') : t('points')}</span>{' '}
{`(${postScore === '?' ? '?' : `${upvotePercentage}`}% ${t('upvoted')})`}
</div>
<div className={styles.shareLink}>
{t('share_link')}: <input type='text' value={`https://pleb.bz/p/${address}/c/${cid}`} readOnly={true} />
{t('share_link')}: <input type='text' value={`https://pleb.bz/p/${subplebbitAddress}/c/${cid}`} readOnly={true} />
</div>
</div>
);
Expand Down Expand Up @@ -216,8 +206,6 @@ export const Footer = () => {
const Sidebar = ({ comment, isSubCreatedButNotYetPublished, settings, subplebbit }: sidebarProps) => {
const { t } = useTranslation();
const { address, createdAt, description, roles, rules, title, updatedAt } = subplebbit || {};
const { cid, downvoteCount, timestamp, upvoteCount } = comment || {};

const { allActiveUserCount, hourActiveUserCount } = useSubplebbitStats({ subplebbitAddress: address });
const { isOffline, offlineTitle } = useIsSubplebbitOffline(subplebbit || {});
const onlineNotice = t('users_online', { count: hourActiveUserCount });
Expand Down Expand Up @@ -283,7 +271,7 @@ const Sidebar = ({ comment, isSubCreatedButNotYetPublished, settings, subplebbit
return (
<div className={`${isMobile ? styles.mobileSidebar : styles.sidebar}`}>
<SearchBar />
{isInPostView && <PostInfo address={address} cid={cid} downvoteCount={downvoteCount} timestamp={timestamp} upvoteCount={upvoteCount} />}
{isInPostView && <PostInfo comment={comment} />}
<Link to={submitRoute}>
{/* TODO: add .largeButtonDisabled and disabledButtonDescription classnames for subs that don't accept posts */}
<div className={styles.largeButton}>
Expand Down
Loading

0 comments on commit a91c97e

Please sign in to comment.