mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
@@ -45,7 +45,9 @@ const BoardHeader = () => {
|
||||
)}
|
||||
<div className={styles.boardTitle}>
|
||||
{title || `p/${shortAddress || subplebbitAddress}`}
|
||||
{(isOffline || isOnlineStatusLoading) && <span className={`${styles.offlineIcon} ${offlineIconClass}`} title={offlineTitle} />}
|
||||
{(isOffline || isOnlineStatusLoading) && !isInAllView && !isInSubscriptionsView && (
|
||||
<span className={`${styles.offlineIcon} ${offlineIconClass}`} title={offlineTitle} />
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.boardSubtitle}>{subtitle}</div>
|
||||
<hr />
|
||||
|
||||
@@ -8,10 +8,6 @@
|
||||
text-decoration: var(--post-content-link-text-decoration-hover);
|
||||
}
|
||||
|
||||
.markdown li {
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
.hrWrapper {
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
@@ -102,9 +102,9 @@ const PostInfo = ({ openReplyModal, post, postReplyCount = 0, roles, isHidden }:
|
||||
{!(isDescription || isRules) && (
|
||||
<>
|
||||
{isReply && author?.avatar && (
|
||||
<>
|
||||
<span className={styles.authorAvatar} style={{ backgroundImage: `url(${avatarImageUrl})` }} />{' '}
|
||||
</>
|
||||
<span className={styles.authorAvatar}>
|
||||
<img src={avatarImageUrl} alt='' />
|
||||
</span>
|
||||
)}
|
||||
(u/
|
||||
<Tooltip
|
||||
@@ -256,8 +256,8 @@ const PostMessage = ({ post }: PostProps) => {
|
||||
|
||||
const stateString = useStateString(post);
|
||||
|
||||
const loadingString = stateString && (
|
||||
<div className={`${styles.stateString} ${styles.ellipsis}`}>{stateString !== 'Failed' ? <LoadingEllipsis string={stateString} /> : stateString}</div>
|
||||
const loadingString = (
|
||||
<div className={`${styles.stateString} ${styles.ellipsis}`}>{stateString !== 'Failed' ? <LoadingEllipsis string={stateString || t('loading')} /> : stateString}</div>
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -341,7 +341,7 @@ const Reply = ({ openReplyModal, postReplyCount, reply, roles }: PostProps) => {
|
||||
post = editedComment;
|
||||
}
|
||||
|
||||
const { author, cid, content, link, postCid, subplebbitAddress } = post || {};
|
||||
const { author, cid, link, postCid, subplebbitAddress } = post || {};
|
||||
const isRouteLinkToReply = useLocation().pathname.startsWith(`/p/${subplebbitAddress}/c/${cid}`);
|
||||
const { hidden } = useHide({ cid });
|
||||
|
||||
@@ -356,7 +356,7 @@ const Reply = ({ openReplyModal, postReplyCount, reply, roles }: PostProps) => {
|
||||
>
|
||||
<PostInfo openReplyModal={openReplyModal} post={post} postReplyCount={postReplyCount} roles={roles} />
|
||||
{link && !hidden && isValidURL(link) && <PostMedia post={post} />}
|
||||
{content && !hidden && <PostMessage post={post} />}
|
||||
{!hidden && <PostMessage post={post} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -434,7 +434,7 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{post?.replyCount === undefined && (
|
||||
{post?.replyCount === undefined && !isInPendingPostView && (
|
||||
<span className={styles.loadingString}>
|
||||
<LoadingEllipsis string={t('loading_comments')} />
|
||||
</span>
|
||||
@@ -452,14 +452,15 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{stateString && stateString !== 'Failed' ? (
|
||||
<div className={styles.stateString}>
|
||||
<br />
|
||||
<LoadingEllipsis string={stateString} />
|
||||
</div>
|
||||
) : (
|
||||
state === 'failed' && <span className={styles.error}>{t('failed')}</span>
|
||||
)}
|
||||
{!isInPendingPostView &&
|
||||
(stateString && stateString !== 'Failed' ? (
|
||||
<div className={styles.stateString}>
|
||||
<br />
|
||||
<LoadingEllipsis string={stateString} />
|
||||
</div>
|
||||
) : (
|
||||
state === 'failed' && <span className={styles.error}>{t('failed')}</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -75,9 +75,9 @@ const PostInfoAndMedia = ({ openReplyModal, post, postReplyCount = 0, roles }: P
|
||||
{!(isDescription || isRules) && (
|
||||
<>
|
||||
{isReply && author?.avatar && (
|
||||
<>
|
||||
<span className={styles.authorAvatar} style={{ backgroundImage: `url(${avatarImageUrl})` }} />{' '}
|
||||
</>
|
||||
<span className={styles.authorAvatar}>
|
||||
<img src={avatarImageUrl} alt='' />
|
||||
</span>
|
||||
)}
|
||||
(u/
|
||||
<Tooltip
|
||||
@@ -180,77 +180,75 @@ const PostMessageMobile = ({ post }: PostProps) => {
|
||||
|
||||
const stateString = useStateString(post);
|
||||
|
||||
const loadingString = stateString && (
|
||||
<div className={`${styles.stateString} ${styles.ellipsis}`}>{stateString !== 'Failed' ? <LoadingEllipsis string={stateString} /> : stateString}</div>
|
||||
const loadingString = (
|
||||
<div className={`${styles.stateString} ${styles.ellipsis}`}>{stateString !== 'Failed' ? <LoadingEllipsis string={stateString || t('loading')} /> : stateString}</div>
|
||||
);
|
||||
|
||||
return (
|
||||
content && (
|
||||
<blockquote className={`${styles.postMessage} ${!isReply && styles.clampLines}`}>
|
||||
{isReply && !(removed || deleted) && state !== 'failed' && isReplyingToReply && <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotelinkReply} />}
|
||||
{removed ? (
|
||||
<blockquote className={`${styles.postMessage} ${!isReply && styles.clampLines}`}>
|
||||
{isReply && !(removed || deleted) && state !== 'failed' && isReplyingToReply && <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotelinkReply} />}
|
||||
{removed ? (
|
||||
<Tooltip
|
||||
children={<span className={styles.removedContent}>({t('this_post_was_removed')})</span>}
|
||||
content={`${_.capitalize(t('reason'))}: "${reason}"`}
|
||||
showTooltip={!!reason}
|
||||
/>
|
||||
) : deleted ? (
|
||||
<Tooltip children={<span className={styles.deletedContent}>{t('user_deleted_this_post')}</span>} content={reason && `${t('reason')}: ${reason}`} />
|
||||
) : (
|
||||
<>
|
||||
{!showOriginal && <Markdown content={displayContent} />}
|
||||
{edit && original?.content !== post?.content && (
|
||||
<span className={styles.editedInfo}>
|
||||
{showOriginal && <Markdown content={original?.content} />}
|
||||
<br />
|
||||
{t('comment_edited_at_timestamp', { timestamp: getFormattedDate(edit?.timestamp), interpolation: { escapeValue: false } })}{' '}
|
||||
{reason && <>{t('reason_reason', { reason: reason, interpolation: { escapeValue: false } })} </>}
|
||||
{showOriginal ? (
|
||||
<Trans
|
||||
i18nKey={'click_here_to_hide_original'}
|
||||
shouldUnescape={true}
|
||||
components={{ 1: <span className={styles.showOriginal} onClick={() => setShowOriginal(!showOriginal)} /> }}
|
||||
/>
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey={'click_here_to_show_original'}
|
||||
shouldUnescape={true}
|
||||
components={{ 1: <span className={styles.showOriginal} onClick={() => setShowOriginal(!showOriginal)} /> }}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* TODO: commentAuthor is not available outside of editedComment, update when available */}
|
||||
{/* {banned && (
|
||||
<span className={styles.removedContent}>
|
||||
<br />
|
||||
<br />
|
||||
<Tooltip
|
||||
children={<span className={styles.removedContent}>({t('this_post_was_removed')})</span>}
|
||||
content={`${_.capitalize(t('reason'))}: "${reason}"`}
|
||||
showTooltip={!!reason}
|
||||
children={`(${t('user_banned')})`}
|
||||
content={`${t('ban_expires_at', {
|
||||
address: subplebbitAddress && Plebbit.getShortAddress(subplebbitAddress),
|
||||
timestamp: getFormattedDate(commentAuthor?.banExpiresAt),
|
||||
interpolation: { escapeValue: false },
|
||||
})}${reason ? `. ${_.capitalize(t('reason'))}: "${reason}"` : ''}`}
|
||||
/>
|
||||
) : deleted ? (
|
||||
<Tooltip children={<span className={styles.deletedContent}>{t('user_deleted_this_post')}</span>} content={reason && `${t('reason')}: ${reason}`} />
|
||||
) : (
|
||||
<>
|
||||
{!showOriginal && <Markdown content={displayContent} />}
|
||||
{edit && original?.content !== post?.content && (
|
||||
<span className={styles.editedInfo}>
|
||||
{showOriginal && <Markdown content={original?.content} />}
|
||||
<br />
|
||||
{t('comment_edited_at_timestamp', { timestamp: getFormattedDate(edit?.timestamp), interpolation: { escapeValue: false } })}{' '}
|
||||
{reason && <>{t('reason_reason', { reason: reason, interpolation: { escapeValue: false } })} </>}
|
||||
{showOriginal ? (
|
||||
<Trans
|
||||
i18nKey={'click_here_to_hide_original'}
|
||||
shouldUnescape={true}
|
||||
components={{ 1: <span className={styles.showOriginal} onClick={() => setShowOriginal(!showOriginal)} /> }}
|
||||
/>
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey={'click_here_to_show_original'}
|
||||
shouldUnescape={true}
|
||||
components={{ 1: <span className={styles.showOriginal} onClick={() => setShowOriginal(!showOriginal)} /> }}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* TODO: commentAuthor is not available outside of editedComment, update when available */}
|
||||
{/* {banned && (
|
||||
<span className={styles.removedContent}>
|
||||
<br />
|
||||
<br />
|
||||
<Tooltip
|
||||
children={`(${t('user_banned')})`}
|
||||
content={`${t('ban_expires_at', {
|
||||
address: subplebbitAddress && Plebbit.getShortAddress(subplebbitAddress),
|
||||
timestamp: getFormattedDate(commentAuthor?.banExpiresAt),
|
||||
interpolation: { escapeValue: false },
|
||||
})}${reason ? `. ${_.capitalize(t('reason'))}: "${reason}"` : ''}`}
|
||||
/>
|
||||
</span>
|
||||
)} */}
|
||||
{!isReply && content.length > 1000 && !isInPostView && (
|
||||
<span className={styles.abbr}>
|
||||
<br />
|
||||
<Trans i18nKey={'comment_too_long'} shouldUnescape={true} components={{ 1: <Link to={`/p/${subplebbitAddress}/c/${cid}`} /> }} />
|
||||
</span>
|
||||
)}
|
||||
{!cid && state === 'pending' && stateString !== 'Failed' && (
|
||||
<>
|
||||
<br />
|
||||
{loadingString}
|
||||
</>
|
||||
)}
|
||||
</blockquote>
|
||||
)
|
||||
</span>
|
||||
)} */}
|
||||
{!isReply && content.length > 1000 && !isInPostView && (
|
||||
<span className={styles.abbr}>
|
||||
<br />
|
||||
<Trans i18nKey={'comment_too_long'} shouldUnescape={true} components={{ 1: <Link to={`/p/${subplebbitAddress}/c/${cid}`} /> }} />
|
||||
</span>
|
||||
)}
|
||||
{!cid && state === 'pending' && stateString !== 'Failed' && (
|
||||
<>
|
||||
<br />
|
||||
{loadingString}
|
||||
</>
|
||||
)}
|
||||
</blockquote>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -261,7 +259,7 @@ const Reply = ({ openReplyModal, postReplyCount, reply, roles }: PostProps) => {
|
||||
if (editedComment) {
|
||||
post = editedComment;
|
||||
}
|
||||
const { author, cid, content, postCid, subplebbitAddress } = post || {};
|
||||
const { author, cid, postCid, subplebbitAddress } = post || {};
|
||||
const isRouteLinkToReply = useLocation().pathname.startsWith(`/p/${subplebbitAddress}/c/${cid}`);
|
||||
const { hidden } = useHide({ cid });
|
||||
|
||||
@@ -275,7 +273,7 @@ const Reply = ({ openReplyModal, postReplyCount, reply, roles }: PostProps) => {
|
||||
data-post-cid={postCid}
|
||||
>
|
||||
<PostInfoAndMedia openReplyModal={openReplyModal} post={post} postReplyCount={postReplyCount} roles={roles} />
|
||||
{content && !hidden && <PostMessageMobile post={post} />}
|
||||
{!hidden && <PostMessageMobile post={post} />}
|
||||
<ReplyBacklinks post={reply} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -348,7 +346,7 @@ const PostMobile = ({ openReplyModal, post, roles, showAllReplies, showReplies =
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{post?.replyCount === undefined && (
|
||||
{post?.replyCount === undefined && !isInPendingPostView && (
|
||||
<span className={styles.loadingString}>
|
||||
<LoadingEllipsis string={t('loading_comments')} />
|
||||
</span>
|
||||
@@ -365,13 +363,14 @@ const PostMobile = ({ openReplyModal, post, roles, showAllReplies, showReplies =
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{stateString && stateString !== 'Failed' ? (
|
||||
<div className={styles.stateString}>
|
||||
<LoadingEllipsis string={stateString} />
|
||||
</div>
|
||||
) : (
|
||||
state === 'failed' && <span className={styles.error}>{t('failed')}</span>
|
||||
)}
|
||||
{!isInPendingPostView &&
|
||||
(stateString && stateString !== 'Failed' ? (
|
||||
<div className={styles.stateString}>
|
||||
<LoadingEllipsis string={stateString} />
|
||||
</div>
|
||||
) : (
|
||||
state === 'failed' && <span className={styles.error}>{t('failed')}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -86,11 +86,16 @@ const ReplyModal = ({ closeModal, parentCid, scrollY }: ReplyModalProps) => {
|
||||
if (ref) {
|
||||
textRef.current = ref;
|
||||
!isMobile && ref.focus();
|
||||
const len = ref.value.length;
|
||||
ref.setSelectionRange(len, len);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (textRef.current) {
|
||||
const len = textRef.current.value.length;
|
||||
textRef.current.setSelectionRange(len, len);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const contentPrefix = `c/${parentCid && Plebbit.getShortCid(parentCid)}\n`;
|
||||
|
||||
const handleContentInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
border: 1px solid #aaa;
|
||||
background-color: var(--avatar-background-color, white);
|
||||
margin-left: 10px;
|
||||
border-radius: 32px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@ import { useEffect } from 'react';
|
||||
import { Subplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { getFormattedTimeAgo } from '../lib/utils/time-utils';
|
||||
import useSubplebbitOfflineStore from '../stores/use-subplebbit-offline-store';
|
||||
import useSubplebbitsLoadingStartTimestamps from './use-subplebbits-loading-start-timestamps-store';
|
||||
|
||||
const useIsSubplebbitOffline = (subplebbit: Subplebbit) => {
|
||||
const { t } = useTranslation();
|
||||
const { address, state, updatedAt, updatingState } = subplebbit || {};
|
||||
const { subplebbitOfflineState, setSubplebbitOfflineState, initializesubplebbitOfflineState } = useSubplebbitOfflineStore();
|
||||
const subplebbitsLoadingStartTimestamps = useSubplebbitsLoadingStartTimestamps([address]);
|
||||
|
||||
useEffect(() => {
|
||||
if (address && !subplebbitOfflineState[address]) {
|
||||
@@ -22,15 +24,13 @@ const useIsSubplebbitOffline = (subplebbit: Subplebbit) => {
|
||||
}, [address, state, updatedAt, updatingState, setSubplebbitOfflineState]);
|
||||
|
||||
const subplebbitOfflineStore = subplebbitOfflineState[address] || { initialLoad: true };
|
||||
const loadingStartTimestamp = subplebbitsLoadingStartTimestamps[0] || 0;
|
||||
|
||||
const isLoading =
|
||||
updatingState === 'resolving-address' ||
|
||||
(updatingState === 'stopped' && state === 'stopped' && !updatedAt) ||
|
||||
(subplebbitOfflineStore.initialLoad && updatingState === 'fetching-ipns' && !updatedAt);
|
||||
const isLoading = subplebbitOfflineStore.initialLoad && (!updatedAt || Date.now() / 1000 - updatedAt >= 60 * 60) && Date.now() / 1000 - loadingStartTimestamp < 30;
|
||||
|
||||
const isOffline =
|
||||
!isLoading && ((updatedAt && updatedAt < Date.now() / 1000 - 60 * 60) || updatingState === 'failed' || (updatingState === 'fetching-ipns' && !updatedAt));
|
||||
const isOffline = !isLoading && ((updatedAt && updatedAt < Date.now() / 1000 - 60 * 60) || (!updatedAt && Date.now() / 1000 - loadingStartTimestamp >= 30));
|
||||
|
||||
const isOnline = updatedAt && Date.now() / 1000 - updatedAt < 60 * 60;
|
||||
const offlineIconClass = isLoading ? 'yellowOfflineIcon' : isOffline ? 'redOfflineIcon' : '';
|
||||
|
||||
const offlineTitle = isLoading
|
||||
@@ -39,7 +39,7 @@ const useIsSubplebbitOffline = (subplebbit: Subplebbit) => {
|
||||
? isOffline && t('posts_last_synced_info', { time: getFormattedTimeAgo(updatedAt), interpolation: { escapeValue: false } })
|
||||
: t('subplebbit_offline_info');
|
||||
|
||||
return { isOffline, isOnlineStatusLoading: isLoading, offlineIconClass, offlineTitle };
|
||||
return { isOffline: !isOnline && isOffline, isOnlineStatusLoading: !isOnline && isLoading, offlineIconClass, offlineTitle };
|
||||
};
|
||||
|
||||
export default useIsSubplebbitOffline;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface SubplebbitsLoadingStartTimestampsState {
|
||||
timestamps: Record<string, number>;
|
||||
addSubplebbits: (subplebbitAddresses: string[]) => void;
|
||||
}
|
||||
|
||||
const useSubplebbitsLoadingStartTimestampsStore = create<SubplebbitsLoadingStartTimestampsState>((set, get) => ({
|
||||
timestamps: {},
|
||||
addSubplebbits: (subplebbitAddresses) => {
|
||||
const { timestamps } = get();
|
||||
const newTimestamps: Record<string, number> = {};
|
||||
subplebbitAddresses.forEach((subplebbitAddress) => {
|
||||
if (!timestamps[subplebbitAddress]) {
|
||||
newTimestamps[subplebbitAddress] = Math.round(Date.now() / 1000);
|
||||
}
|
||||
});
|
||||
if (Object.keys(newTimestamps).length) {
|
||||
set((state) => ({ timestamps: { ...state.timestamps, ...newTimestamps } }));
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const useSubplebbitsLoadingStartTimestamps = (subplebbitAddresses?: string[]) => {
|
||||
const timestampsStore = useSubplebbitsLoadingStartTimestampsStore((state) => state.timestamps);
|
||||
const addSubplebbits = useSubplebbitsLoadingStartTimestampsStore((state) => state.addSubplebbits);
|
||||
|
||||
useEffect(() => {
|
||||
if (subplebbitAddresses) {
|
||||
addSubplebbits(subplebbitAddresses);
|
||||
}
|
||||
}, [subplebbitAddresses, addSubplebbits]);
|
||||
|
||||
const subplebbitsLoadingStartTimestamps = useMemo(() => {
|
||||
return subplebbitAddresses?.map((subplebbitAddress) => timestampsStore[subplebbitAddress]) || [];
|
||||
}, [timestampsStore, subplebbitAddresses]);
|
||||
|
||||
return subplebbitsLoadingStartTimestamps;
|
||||
};
|
||||
|
||||
export default useSubplebbitsLoadingStartTimestamps;
|
||||
@@ -505,22 +505,23 @@
|
||||
}
|
||||
|
||||
.authorAvatar {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background-size: 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
width: 24px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
position: relative;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.authorAvatar img {
|
||||
width: 24px;
|
||||
max-height: 24px;
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
}
|
||||
|
||||
.stateString {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: calc(100vw - 28px);
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@@ -542,11 +543,13 @@
|
||||
}
|
||||
|
||||
.authorAvatar {
|
||||
vertical-align: middle;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.stateString {
|
||||
padding-left: 5px;
|
||||
.authorAvatar img {
|
||||
width: 20px;
|
||||
max-height: 20px;
|
||||
bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,8 +571,4 @@
|
||||
.replyMobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stateString {
|
||||
padding-top: 10px;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user