refactor(utils): optimize checkCurrentView with switch, move all utils to their own file category in utils folder

This commit is contained in:
plebeius.eth
2023-11-09 16:33:15 +01:00
parent 3a5aa52160
commit 105ae9ef24
15 changed files with 220 additions and 228 deletions

View File

@@ -1,35 +1,40 @@
import { useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { Link, useLocation, useParams } from 'react-router-dom';
import styles from './header.module.css';
import useTheme from '../../hooks/use-theme';
import AccountBar from './account-bar';
import { useSubplebbit } from '@plebbit/plebbit-react-hooks';
import { useTranslation } from 'react-i18next';
import useCurrentView from '../../hooks/use-current-view';
import { checkCurrentView } from '../../lib/utils/view-utils';
const sortTypes = ['/hot', '/new', '/active', '/controversialAll', '/topAll'];
const Header = () => {
const [theme] = useTheme();
const { t } = useTranslation();
const { sortType, subplebbitAddress, commentCid } = useParams();
const [selectedSortType, setSelectedSortType] = useState(sortType || '/hot');
const location = useLocation();
const params = useParams();
const [selectedSortType, setSelectedSortType] = useState(params.sortType || '/hot');
const sortLabels = [t('header_hot'), t('header_new'), t('header_active'), t('header_controversial'), t('header_top')];
const subplebbit = useSubplebbit({ subplebbitAddress });
const subplebbit = useSubplebbit({ subplebbitAddress: params.subplebbitAddress });
const { title, shortAddress } = subplebbit || {};
const { isHomeView, isSubplebbitView, isPostView, isSubmitView, isSubplebbitSubmitView } = useCurrentView();
const isHomeView = checkCurrentView('home', location.pathname, params)
const isPostView = checkCurrentView('post', location.pathname, params)
const isSubplebbitView = checkCurrentView('subplebbit', location.pathname, params)
const isSubmitView = checkCurrentView('submit', location.pathname, params)
const isSubplebbitSubmitView = checkCurrentView('subplebbit/submit', location.pathname, params)
const handleSelect = (choice: string) => {
setSelectedSortType(choice);
};
useEffect(() => {
if (sortType) {
setSelectedSortType('/' + sortType);
if (params.sortType) {
setSelectedSortType('/' + params.sortType);
} else {
setSelectedSortType('/hot');
}
}, [sortType]);
}, [params.sortType]);
const sortItems = sortTypes.map((choice, index) => (
<li key={choice}>
@@ -41,7 +46,7 @@ const Header = () => {
const commentsButton = (
<li>
<Link to={`/p/${subplebbitAddress}/c/${commentCid}`} className={styles.selected}>
<Link to={`/p/${params.subplebbitAddress}/c/${params.commentCid}`} className={styles.selected}>
{t('header_comments')}
</Link>
</li>
@@ -56,7 +61,7 @@ const Header = () => {
const subplebbitTitle = (
<Link
to={`/p/${subplebbitAddress}`}
to={`/p/${params.subplebbitAddress}`}
onClick={(e) => {
e.preventDefault();
}}

View File

@@ -1,5 +1,5 @@
import styles from './expand-button.module.css';
import { CommentMediaInfo } from '../../../lib/utils';
import { CommentMediaInfo } from '../../../lib/utils/media-utils';
interface ExpandButtonProps {
commentMediaInfo?: CommentMediaInfo;

View File

@@ -1,7 +1,7 @@
import { Link } from 'react-router-dom';
import styles from './expando.module.css';
import Embed from '../embed';
import { CommentMediaInfo } from '../../../lib/utils';
import { CommentMediaInfo } from '../../../lib/utils/media-utils';
interface ExpandoProps {
commentMediaInfo?: CommentMediaInfo;

View File

@@ -1,15 +1,17 @@
import { useState } from 'react';
import styles from './post.module.css';
import { Link } from 'react-router-dom';
import utils from '../../lib/utils';
import { Link, useLocation, useParams } from 'react-router-dom';
import { useAccount, Comment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
import { useTranslation } from 'react-i18next';
import { checkCurrentView } from '../../lib/utils/view-utils';
import { getCommentMediaInfoMemoized, getHasThumbnail } from '../../lib/utils/media-utils';
import { getHostname } from '../../lib/utils/url-utils';
import { getFormattedTime } from '../../lib/utils/time-utils';
import ExpandButton from './expand-button';
import Expando from './expando';
import Flair from './flair';
import PostTools from './post-tools';
import Thumbnail from './thumbnail';
import useCurrentView from '../../hooks/use-current-view';
interface PostProps {
index?: number;
@@ -21,15 +23,18 @@ const Post = ({ post, index }: PostProps) => {
const account = useAccount();
const subplebbit = useSubplebbit({ subplebbitAddress });
const { t } = useTranslation();
const params = useParams();
const location = useLocation();
const { isPendingView, isPostView } = useCurrentView();
const isPostView = checkCurrentView('post', location.pathname, params);
const isPendingView = checkCurrentView('pending', location.pathname, params);
const isInPostView = isPostView || isPendingView;
const [isExpanded, setIsExpanded] = useState(isInPostView);
const toggleExpanded = () => setIsExpanded(!isExpanded);
const commentMediaInfo = utils.getCommentMediaInfoMemoized(post);
const hasThumbnail = utils.hasThumbnail(commentMediaInfo, link);
const linkUrl = utils.getHostname(link);
const commentMediaInfo = getCommentMediaInfoMemoized(post);
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
const linkUrl = getHostname(link);
const postAuthor = isPendingView ? account?.author?.shortAddress : author?.shortAddress;
const postScore = upvoteCount === 0 && downvoteCount === 0 ? '•' : upvoteCount - downvoteCount || '•';
@@ -99,7 +104,7 @@ const Post = ({ post, index }: PostProps) => {
/>
)}
<p className={styles.tagline}>
{t('post_submitted')} {utils.getFormattedTime(timestamp)} {t('post_by')}{' '}
{t('post_submitted')} {getFormattedTime(timestamp)} {t('post_by')}{' '}
<Link className={styles.author} to={`u/${postAuthor}`} onClick={(e) => e.preventDefault()}>
u/{postAuthor}
</Link>

View File

@@ -1,6 +1,6 @@
import styles from './thumbnail.module.css';
import { Link } from 'react-router-dom';
import { CommentMediaInfo } from '../../../lib/utils';
import { CommentMediaInfo } from '../../../lib/utils/media-utils';
interface ThumbnailProps {
cid?: string;

View File

@@ -4,7 +4,8 @@ import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import styles from './reply.module.css';
import useReplies from '../../hooks/use-replies';
import utils from '../../lib/utils';
import { getCommentMediaInfoMemoized, getHasThumbnail } from '../../lib/utils/media-utils';
import { getFormattedTime } from '../../lib/utils/time-utils';
import Expando from '../post/expando/';
import ExpandButton from '../post/expand-button/';
import Thumbnail from '../post/thumbnail/';
@@ -32,8 +33,8 @@ const Reply = ({ reply }: ReplyProps) => {
const replies = useReplies(reply);
const [expanded, setExpanded] = useState(false);
const toggleExpanded = () => setExpanded(!expanded);
const commentMediaInfo = utils.getCommentMediaInfoMemoized(reply);
const hasThumbnail = utils.hasThumbnail(commentMediaInfo, link);
const commentMediaInfo = getCommentMediaInfoMemoized(reply);
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
const { t } = useTranslation();
let score = upvoteCount - downvoteCount;
if ((upvoteCount === 0 && downvoteCount === 0) || (upvoteCount === 1 && downvoteCount === 0)) {
@@ -61,7 +62,7 @@ const Reply = ({ reply }: ReplyProps) => {
>
{shortAddress}
</Link>
<span className={styles.score}>{scoreTranslation}</span> <span className={styles.time}>{utils.getFormattedTime(timestamp)}</span>
<span className={styles.score}>{scoreTranslation}</span> <span className={styles.time}>{getFormattedTime(timestamp)}</span>
{flair && (
<>
{' '}

View File

@@ -1,35 +0,0 @@
import { useLocation, useParams } from 'react-router-dom';
type CurrentView = {
isHomeView: boolean;
isPendingView: boolean;
isPostView: boolean;
isSubmitView: boolean;
isSubplebbitView: boolean;
isSubplebbitSubmitView: boolean;
};
type ParamsType = {
accountCommentIndex?: string;
commentCid?: string;
subplebbitAddress?: string;
};
const sortTypes = ['/hot', '/new', '/active', '/controversialAll', '/topAll'];
const useCurrentView = (): CurrentView => {
const location = useLocation();
const { accountCommentIndex, commentCid, subplebbitAddress } = useParams<ParamsType>();
const pathname = location.pathname;
const isHomeView = pathname === `/` || sortTypes.includes(pathname);
const isPendingView = pathname === `/profile/${accountCommentIndex}`;
const isPostView = subplebbitAddress && commentCid ? pathname.startsWith(`/p/${subplebbitAddress}/c/${commentCid}`) : false;
const isSubmitView = pathname === `/submit`;
const isSubplebbitView = subplebbitAddress ? pathname.startsWith(`/p/${subplebbitAddress}`) : false;
const isSubplebbitSubmitView = subplebbitAddress ? pathname === `/p/${subplebbitAddress}/submit` : false;
return { isHomeView, isSubplebbitView, isPendingView, isPostView, isSubmitView, isSubplebbitSubmitView };
};
export default useCurrentView;

View File

@@ -1,163 +0,0 @@
import i18next from 'i18next';
import memoize from 'memoizee';
import extName from 'ext-name';
import { ChallengeVerification, Comment } from '@plebbit/plebbit-react-hooks';
import { canEmbed } from '../components/post/embed';
export interface CommentMediaInfo {
url: string;
type: string;
thumbnail?: string;
patternThumbnailUrl?: string;
}
export const alertChallengeVerificationFailed = (challengeVerification: ChallengeVerification, publication: any) => {
if (challengeVerification?.challengeSuccess === false) {
console.warn(challengeVerification, publication);
alert(`p/${publication?.subplebbitAddress} challenge error: ${[...(challengeVerification?.challengeErrors || []), challengeVerification?.reason].join(' ')}`);
} else {
console.log(challengeVerification, publication);
}
};
const getCommentMediaInfo = (comment: Comment) => {
if (!comment?.thumbnailUrl && !comment?.link) {
return;
}
if (comment?.link) {
let mime: string | undefined;
try {
mime = extName(new URL(comment?.link).pathname.toLowerCase().replace('/', ''))[0]?.mime;
} catch (e) {
return;
}
const url = new URL(comment.link);
const host = url.hostname;
let patternThumbnailUrl;
if (['youtube.com', 'www.youtube.com', 'youtu.be'].includes(host)) {
const videoId = host === 'youtu.be' ? url.pathname.slice(1) : url.searchParams.get('v');
patternThumbnailUrl = `https://img.youtube.com/vi/${videoId}/sddefault.jpg`;
} else if (host.includes('streamable.com')) {
const videoId = url.pathname.split('/')[1];
patternThumbnailUrl = `https://cdn-cf-east.streamable.com/image/${videoId}.jpg`;
}
if (canEmbed(url)) {
return {
url: comment.link,
type: 'iframe',
thumbnail: comment.thumbnailUrl,
patternThumbnailUrl,
};
}
if (mime?.startsWith('image')) {
return { url: comment.link, type: 'image' };
}
if (mime?.startsWith('video')) {
return { url: comment.link, type: 'video', thumbnail: comment.thumbnailUrl };
}
if (mime?.startsWith('audio')) {
return { url: comment.link, type: 'audio' };
}
if (comment?.thumbnailUrl && comment?.thumbnailUrl !== comment?.link) {
return { url: comment.link, type: 'webpage', thumbnail: comment.thumbnailUrl };
}
if (comment?.link) {
return { url: comment.link, type: 'webpage' };
}
}
};
const getCommentMediaInfoMemoized = memoize(getCommentMediaInfo, { max: 1000 });
const getFormattedTime = (unixTimestamp: number): string => {
const currentTime = Date.now() / 1000;
const timeDifference = currentTime - unixTimestamp;
const t = i18next.t;
if (timeDifference < 60) {
return t('time_1_minute_ago');
}
if (timeDifference < 3600) {
return t('time_x_minutes_ago', { count: Math.floor(timeDifference / 60) });
}
if (timeDifference < 7200) {
return t('time_1_hour_ago');
}
if (timeDifference < 86400) {
return t('time_x_hours_ago', { count: Math.floor(timeDifference / 3600) });
}
if (timeDifference < 172800) {
return t('time_1_day_ago');
}
if (timeDifference < 2592000) {
return t('time_x_days_ago', { count: Math.floor(timeDifference / 86400) });
}
if (timeDifference < 5184000) {
return t('time_1_month_ago');
}
if (timeDifference < 31104000) {
return t('time_x_months_ago', { count: Math.floor(timeDifference / 2592000) });
}
if (timeDifference < 62208000) {
return t('time_1_year_ago');
}
return t('time_x_years_ago', { count: Math.floor(timeDifference / 31104000) });
};
const getHostname = (url: string) => {
try {
return new URL(url).hostname.replace(/^www\./, '');
} catch (e) {
return '';
}
};
const hasThumbnail = (commentMediaInfo: CommentMediaInfo | undefined, link: string | undefined): boolean => {
const iframeThumbnail = commentMediaInfo?.patternThumbnailUrl || commentMediaInfo?.thumbnail;
return link &&
commentMediaInfo &&
(commentMediaInfo.type === 'image' ||
commentMediaInfo.type === 'video' ||
(commentMediaInfo.type === 'webpage' && commentMediaInfo.thumbnail) ||
(commentMediaInfo.type === 'iframe' && iframeThumbnail))
? true
: false;
};
export const isValidENS = (address: string) => {
return address.endsWith('.eth');
};
export const isValidIPFS = (address: string) => {
const IPFS_REGEX = /^12D3KooW[A-Za-z0-9]+$/;
return IPFS_REGEX.test(address) && address.length === 52;
};
export const isValidURL = (url: string) => {
try {
new URL(url);
return true;
} catch {
return false;
}
};
const utils = {
alertChallengeVerificationFailed,
getCommentMediaInfoMemoized,
getFormattedTime,
getHostname,
hasThumbnail,
isValidENS,
isValidIPFS,
isValidURL,
};
export default utils;

View File

@@ -0,0 +1,10 @@
import { ChallengeVerification } from '@plebbit/plebbit-react-hooks';
export const alertChallengeVerificationFailed = (challengeVerification: ChallengeVerification, publication: any) => {
if (challengeVerification?.challengeSuccess === false) {
console.warn(challengeVerification, publication);
alert(`p/${publication?.subplebbitAddress} challenge error: ${[...(challengeVerification?.challengeErrors || []), challengeVerification?.reason].join(' ')}`);
} else {
console.log(challengeVerification, publication);
}
};

View File

@@ -0,0 +1,79 @@
import { Comment } from "@plebbit/plebbit-react-hooks";
import extName from 'ext-name';
import { canEmbed } from "../../components/post/embed";
import memoize from 'memoizee';
export interface CommentMediaInfo {
url: string;
type: string;
thumbnail?: string;
patternThumbnailUrl?: string;
}
export const getHasThumbnail = (commentMediaInfo: CommentMediaInfo | undefined, link: string | undefined): boolean => {
const iframeThumbnail = commentMediaInfo?.patternThumbnailUrl || commentMediaInfo?.thumbnail;
return link &&
commentMediaInfo &&
(commentMediaInfo.type === 'image' ||
commentMediaInfo.type === 'video' ||
(commentMediaInfo.type === 'webpage' && commentMediaInfo.thumbnail) ||
(commentMediaInfo.type === 'iframe' && iframeThumbnail))
? true
: false;
};
const getCommentMediaInfo = (comment: Comment) => {
if (!comment?.thumbnailUrl && !comment?.link) {
return;
}
if (comment?.link) {
let mime: string | undefined;
try {
mime = extName(new URL(comment?.link).pathname.toLowerCase().replace('/', ''))[0]?.mime;
} catch (e) {
return;
}
const url = new URL(comment.link);
const host = url.hostname;
let patternThumbnailUrl;
if (['youtube.com', 'www.youtube.com', 'youtu.be'].includes(host)) {
const videoId = host === 'youtu.be' ? url.pathname.slice(1) : url.searchParams.get('v');
patternThumbnailUrl = `https://img.youtube.com/vi/${videoId}/sddefault.jpg`;
} else if (host.includes('streamable.com')) {
const videoId = url.pathname.split('/')[1];
patternThumbnailUrl = `https://cdn-cf-east.streamable.com/image/${videoId}.jpg`;
}
if (canEmbed(url)) {
return {
url: comment.link,
type: 'iframe',
thumbnail: comment.thumbnailUrl,
patternThumbnailUrl,
};
}
if (mime?.startsWith('image')) {
return { url: comment.link, type: 'image' };
}
if (mime?.startsWith('video')) {
return { url: comment.link, type: 'video', thumbnail: comment.thumbnailUrl };
}
if (mime?.startsWith('audio')) {
return { url: comment.link, type: 'audio' };
}
if (comment?.thumbnailUrl && comment?.thumbnailUrl !== comment?.link) {
return { url: comment.link, type: 'webpage', thumbnail: comment.thumbnailUrl };
}
if (comment?.link) {
return { url: comment.link, type: 'webpage' };
}
}
};
export const getCommentMediaInfoMemoized = memoize(getCommentMediaInfo, { max: 1000 });

View File

@@ -0,0 +1,36 @@
import i18next from "i18next";
export const getFormattedTime = (unixTimestamp: number): string => {
const currentTime = Date.now() / 1000;
const timeDifference = currentTime - unixTimestamp;
const t = i18next.t;
if (timeDifference < 60) {
return t('time_1_minute_ago');
}
if (timeDifference < 3600) {
return t('time_x_minutes_ago', { count: Math.floor(timeDifference / 60) });
}
if (timeDifference < 7200) {
return t('time_1_hour_ago');
}
if (timeDifference < 86400) {
return t('time_x_hours_ago', { count: Math.floor(timeDifference / 3600) });
}
if (timeDifference < 172800) {
return t('time_1_day_ago');
}
if (timeDifference < 2592000) {
return t('time_x_days_ago', { count: Math.floor(timeDifference / 86400) });
}
if (timeDifference < 5184000) {
return t('time_1_month_ago');
}
if (timeDifference < 31104000) {
return t('time_x_months_ago', { count: Math.floor(timeDifference / 2592000) });
}
if (timeDifference < 62208000) {
return t('time_1_year_ago');
}
return t('time_x_years_ago', { count: Math.floor(timeDifference / 31104000) });
};

View File

@@ -0,0 +1,7 @@
export const getHostname = (url: string) => {
try {
return new URL(url).hostname.replace(/^www\./, '');
} catch (e) {
return '';
}
};

View File

@@ -0,0 +1,17 @@
export const isValidENS = (address: string) => {
return address.endsWith('.eth');
};
export const isValidIPFS = (address: string) => {
const IPFS_REGEX = /^12D3KooW[A-Za-z0-9]+$/;
return IPFS_REGEX.test(address) && address.length === 52;
};
export const isValidURL = (url: string) => {
try {
new URL(url);
return true;
} catch {
return false;
}
};

View File

@@ -0,0 +1,28 @@
export type ParamsType = {
accountCommentIndex?: string;
commentCid?: string;
subplebbitAddress?: string;
};
export type ViewType = 'home' | 'pending' | 'post' | 'submit' | 'subplebbit' | 'subplebbit/submit';
const sortTypes = ['/hot', '/new', '/active', '/controversialAll', '/topAll'];
export const checkCurrentView = (view: ViewType, pathname: string, params: ParamsType): boolean => {
switch (view) {
case 'home':
return pathname === '/' || sortTypes.includes(pathname);
case 'pending':
return pathname === `/profile/${params.accountCommentIndex}`;
case 'post':
return params.subplebbitAddress && params.commentCid ? pathname.startsWith(`/p/${params.subplebbitAddress}/c/${params.commentCid}`) : false;
case 'submit':
return pathname === '/submit';
case 'subplebbit':
return params.subplebbitAddress ? pathname.startsWith(`/p/${params.subplebbitAddress}`) : false;
case 'subplebbit/submit':
return params.subplebbitAddress ? pathname === `/p/${params.subplebbitAddress}/submit` : false;
default:
return false;
}
};

View File

@@ -1,12 +1,13 @@
import { useEffect, useRef, useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
import { usePublishComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
import { useTranslation } from 'react-i18next';
import { create } from 'zustand';
import { checkCurrentView } from '../../lib/utils/view-utils';
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
import { isValidENS, isValidIPFS, isValidURL } from '../../lib/utils/validation-utils';
import styles from './submit.module.css';
import useCurrentView from '../../hooks/use-current-view';
import challengesStore from '../../hooks/use-challenges';
import { alertChallengeVerificationFailed, isValidENS, isValidIPFS, isValidURL } from '../../lib/utils';
type SubmitStoreState = {
subplebbitAddress: string | undefined;
@@ -50,8 +51,9 @@ const useSubmitStore = create<SubmitStoreState>((set) => ({
const Submit = () => {
const { t } = useTranslation();
const { isSubplebbitSubmitView } = useCurrentView();
const location = useLocation();
const params = useParams();
const isSubplebbitSubmitView = checkCurrentView('subplebbit/submit', location.pathname, params);
const paramsSubplebbitAddress = params.subplebbitAddress;
const subplebbit = useSubplebbit({ subplebbitAddress: paramsSubplebbitAddress });
const navigate = useNavigate();