mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(publishing): abandon challenge close and clean failed local posts
This commit is contained in:
@@ -18,6 +18,7 @@ const useParentAddress = (parentCid?: string) => {
|
||||
interface ChallengeProps {
|
||||
challenge: ChallengeType;
|
||||
closeModal: () => void;
|
||||
abandonModal: () => void;
|
||||
}
|
||||
|
||||
const TextChallenge = ({ challenge }: { challenge: string }) => <div className={styles.challengeMedia}>{challenge}</div>;
|
||||
@@ -29,12 +30,12 @@ interface IframeChallengeProps {
|
||||
shortSubplebbitAddress?: string;
|
||||
subplebbitAddress?: string;
|
||||
readableUrl: string;
|
||||
closeModal: () => void;
|
||||
onCancel: () => void;
|
||||
onDone: () => void;
|
||||
publicationDetails: React.ReactNode;
|
||||
}
|
||||
|
||||
const IframeChallenge = ({ challenge, shortSubplebbitAddress, subplebbitAddress, readableUrl, closeModal, onDone, publicationDetails }: IframeChallengeProps) => {
|
||||
const IframeChallenge = ({ challenge, shortSubplebbitAddress, subplebbitAddress, readableUrl, onCancel, onDone, publicationDetails }: IframeChallengeProps) => {
|
||||
const account = useAccount();
|
||||
const [theme] = useTheme();
|
||||
const [showIframeConfirmation, setShowIframeConfirmation] = useState(true);
|
||||
@@ -71,9 +72,9 @@ const IframeChallenge = ({ challenge, shortSubplebbitAddress, subplebbitAddress,
|
||||
} catch (error) {
|
||||
console.error('Invalid iframe challenge URL', { error });
|
||||
alert('Error: Invalid URL for authentication challenge');
|
||||
closeModal();
|
||||
onCancel();
|
||||
}
|
||||
}, [account, challenge, closeModal, theme]);
|
||||
}, [account, challenge, onCancel, theme]);
|
||||
|
||||
const sendThemeToIframe = useCallback(() => {
|
||||
if (!iframeRef.current || !iframeOrigin) return;
|
||||
@@ -106,7 +107,7 @@ const IframeChallenge = ({ challenge, shortSubplebbitAddress, subplebbitAddress,
|
||||
<div className={`${styles.challengeFooter} ${styles.iframeFooter}`}>
|
||||
<span className={styles.buttons}>
|
||||
<button onClick={handleLoadIframe}>Open</button>
|
||||
<button onClick={closeModal}>Cancel</button>
|
||||
<button onClick={onCancel}>Cancel</button>
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
@@ -134,7 +135,7 @@ const IframeChallenge = ({ challenge, shortSubplebbitAddress, subplebbitAddress,
|
||||
);
|
||||
};
|
||||
|
||||
const Challenge = ({ challenge, closeModal }: ChallengeProps) => {
|
||||
const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const challenges = challenge?.[0]?.challenges;
|
||||
@@ -207,7 +208,7 @@ const Challenge = ({ challenge, closeModal }: ChallengeProps) => {
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const onIframeClose = useCallback(() => {
|
||||
const onIframeDone = useCallback(() => {
|
||||
if (!publication) return;
|
||||
publication.publishChallengeAnswers(['']);
|
||||
closeModal();
|
||||
@@ -226,16 +227,12 @@ const Challenge = ({ challenge, closeModal }: ChallengeProps) => {
|
||||
useEffect(() => {
|
||||
const onEscapeKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (isIframeChallenge) {
|
||||
onIframeClose();
|
||||
} else {
|
||||
closeModal();
|
||||
}
|
||||
abandonModal();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', onEscapeKey);
|
||||
return () => document.removeEventListener('keydown', onEscapeKey);
|
||||
}, [closeModal, isIframeChallenge, onIframeClose]);
|
||||
}, [abandonModal]);
|
||||
|
||||
const getChallengeUrl = useCallback(() => {
|
||||
try {
|
||||
@@ -314,7 +311,7 @@ const Challenge = ({ challenge, closeModal }: ChallengeProps) => {
|
||||
>
|
||||
<div className={`challengeHandle ${styles.title}`} {...(!isMobile ? bind() : {})}>
|
||||
Challenge for {publicationType}
|
||||
<button className={styles.closeIcon} onClick={closeModal} title='close' />
|
||||
<button className={styles.closeIcon} onClick={abandonModal} title='close' />
|
||||
</div>
|
||||
<div className={styles.publication}>
|
||||
{isIframeChallenge ? (
|
||||
@@ -324,8 +321,8 @@ const Challenge = ({ challenge, closeModal }: ChallengeProps) => {
|
||||
shortSubplebbitAddress={shortSubplebbitAddress}
|
||||
subplebbitAddress={subplebbitAddress}
|
||||
readableUrl={readableUrl}
|
||||
closeModal={closeModal}
|
||||
onDone={onIframeClose}
|
||||
onCancel={abandonModal}
|
||||
onDone={onIframeDone}
|
||||
publicationDetails={publicationDetails}
|
||||
/>
|
||||
) : (
|
||||
@@ -357,7 +354,7 @@ const Challenge = ({ challenge, closeModal }: ChallengeProps) => {
|
||||
{t('submit')}
|
||||
</button>
|
||||
)}
|
||||
<button onClick={closeModal}>Cancel</button>
|
||||
<button onClick={abandonModal}>Cancel</button>
|
||||
{challenges?.length > 1 && (
|
||||
<button disabled={!challenges?.[currentChallengeIndex - 1]} onClick={() => setCurrentChallengeIndex((prev) => prev - 1)}>
|
||||
{t('previous')}
|
||||
@@ -374,14 +371,17 @@ const Challenge = ({ challenge, closeModal }: ChallengeProps) => {
|
||||
};
|
||||
|
||||
const ChallengeModal = () => {
|
||||
const { challenges, removeChallenge } = useChallengesStore();
|
||||
const { challenges, removeChallenge, abandonCurrentChallenge } = useChallengesStore();
|
||||
const isOpen = !!challenges.length;
|
||||
const closeModal = () => removeChallenge();
|
||||
const abandonModal = () => {
|
||||
void abandonCurrentChallenge();
|
||||
};
|
||||
const current = challenges[0];
|
||||
const challenge = current?.challenge;
|
||||
const challengeId = current?.id ?? 0;
|
||||
|
||||
return isOpen && challenge ? <Challenge key={challengeId} challenge={challenge} closeModal={closeModal} /> : null;
|
||||
return isOpen && challenge ? <Challenge key={challengeId} challenge={challenge} closeModal={closeModal} abandonModal={abandonModal} /> : null;
|
||||
};
|
||||
|
||||
export default ChallengeModal;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom';
|
||||
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
||||
import { Comment, useEditedComment, useReplies, useAccount, useAccountComment } from '@bitsocialhq/bitsocial-react-hooks';
|
||||
import { Comment, deleteComment, useEditedComment, useReplies, useAccount, useAccountComment } from '@bitsocialhq/bitsocial-react-hooks';
|
||||
import getShortAddress from '../../lib/get-short-address';
|
||||
import styles from '../../views/post/post.module.css';
|
||||
import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils';
|
||||
@@ -98,6 +98,7 @@ const PostInfo = ({
|
||||
const displayName = author?.displayName?.trim();
|
||||
const authorRole = roles?.[address]?.role?.replace('moderator', 'mod');
|
||||
const hasFailedState = state === 'failed';
|
||||
const canDeleteFailedPost = hasFailedState && typeof post?.index === 'number';
|
||||
const isReply = parentCid;
|
||||
const { showOmittedReplies } = useShowOmittedReplies();
|
||||
const directories = useDirectories();
|
||||
@@ -161,6 +162,7 @@ const PostInfo = ({
|
||||
});
|
||||
|
||||
const [initiatedPendingAction, setInitiatedPendingAction] = useState<'approve' | 'reject' | null>(null);
|
||||
const [isDeletingFailedPost, setIsDeletingFailedPost] = useState(false);
|
||||
|
||||
const handlePendingApprove = useCallback(async () => {
|
||||
const confirm = window.confirm(t('double_confirm'));
|
||||
@@ -244,6 +246,23 @@ const PostInfo = ({
|
||||
: openReplyModal && openReplyModal(cid, post?.number, postCid, threadNumber, subplebbitAddress);
|
||||
};
|
||||
|
||||
const onDeleteFailedPost = () => {
|
||||
if (isDeletingFailedPost || !canDeleteFailedPost) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDeletingFailedPost(true);
|
||||
deleteComment(post?.cid || post.index)
|
||||
.then(() => {
|
||||
setIsDeletingFailedPost(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to delete failed post:', error);
|
||||
alert(`Failed to delete post: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
setIsDeletingFailedPost(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.postInfo}>
|
||||
{isHidden ? parentCid && <span className={styles.hiddenReplyEditMenuSpacer} /> : <EditMenu post={post} />}
|
||||
@@ -452,6 +471,15 @@ const PostInfo = ({
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{canDeleteFailedPost && (
|
||||
<span className={styles.failedPublishNotice}>
|
||||
this post failed to publish, it's not visible to other users [{' '}
|
||||
<button type='button' className={styles.failedDeletePostButton} disabled={isDeletingFailedPost} onClick={onDeleteFailedPost}>
|
||||
Delete Post
|
||||
</button>{' '}
|
||||
]
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{!(removed || deleted) && !isModQueue && <PostMenuDesktop postMenu={postMenuProps} />}
|
||||
{cid && parentCid && <ReplyBacklinks post={post} quotedByMap={quotedByMap} directRepliesByParentCid={directRepliesByParentCid} />}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom';
|
||||
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
||||
import { Comment, useEditedComment, useReplies, useAccount, usePublishCommentModeration, useAccountComment } from '@bitsocialhq/bitsocial-react-hooks';
|
||||
import { Comment, deleteComment, useEditedComment, useReplies, useAccount, usePublishCommentModeration, useAccountComment } from '@bitsocialhq/bitsocial-react-hooks';
|
||||
import getShortAddress from '../../lib/get-short-address';
|
||||
import styles from '../../views/post/post.module.css';
|
||||
import { shouldShowSnow } from '../../lib/snow';
|
||||
@@ -188,6 +188,7 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
||||
const isOverThreshold = isAwaitingApproval && timeWaiting > alertThresholdSeconds;
|
||||
|
||||
const hasFailedState = state === 'failed';
|
||||
const canDeleteFailedPost = hasFailedState && typeof post?.index === 'number';
|
||||
const postMenuProps = selectPostMenuProps(post);
|
||||
|
||||
const pseudonymityMode = useSubplebbitField(subplebbitAddress, (sub) => sub?.features?.pseudonymityMode);
|
||||
@@ -223,6 +224,24 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
||||
: openReplyModal && openReplyModal(cid, post?.number, postCid, threadNumber, subplebbitAddress);
|
||||
};
|
||||
|
||||
const [isDeletingFailedPost, setIsDeletingFailedPost] = useState(false);
|
||||
const onDeleteFailedPost = () => {
|
||||
if (isDeletingFailedPost || !canDeleteFailedPost) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDeletingFailedPost(true);
|
||||
deleteComment(post?.cid || post.index)
|
||||
.then(() => {
|
||||
setIsDeletingFailedPost(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to delete failed post:', error);
|
||||
alert(`Failed to delete post: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
setIsDeletingFailedPost(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.postInfo}>
|
||||
@@ -393,6 +412,15 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
{canDeleteFailedPost && (
|
||||
<span className={styles.failedPublishNotice}>
|
||||
this post failed to publish, it's not visible to other users [{' '}
|
||||
<button type='button' className={styles.failedDeletePostButton} disabled={isDeletingFailedPost} onClick={onDeleteFailedPost}>
|
||||
Delete Post
|
||||
</button>{' '}
|
||||
]
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{(hasThumbnail || link) && !(deleted || removed) && <PostMediaContent key={cid} post={post} link={link} />}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { Comment, usePublishComment } from '@bitsocialhq/bitsocial-react-hooks';
|
||||
import usePublishPostStore from '../stores/use-publish-post-store';
|
||||
import useChallengesStore from '../stores/use-challenges-store';
|
||||
|
||||
const usePublishPost = ({ subplebbitAddress }: { subplebbitAddress?: string }) => {
|
||||
const { author, title, content, link, spoiler, publishCommentOptions } = usePublishPostStore((state) => ({
|
||||
@@ -14,6 +15,8 @@ const usePublishPost = ({ subplebbitAddress }: { subplebbitAddress?: string }) =
|
||||
|
||||
const setPublishPostStore = usePublishPostStore((state) => state.setPublishPostStore);
|
||||
const resetPublishPostStore = usePublishPostStore((state) => state.resetPublishPostStore);
|
||||
const addChallenge = useChallengesStore((state) => state.addChallenge);
|
||||
const abandonPublishRef = useRef<(() => Promise<void>) | undefined>();
|
||||
|
||||
const createBaseOptions = useCallback(() => {
|
||||
const baseOptions: Comment = {
|
||||
@@ -51,7 +54,20 @@ const usePublishPost = ({ subplebbitAddress }: { subplebbitAddress?: string }) =
|
||||
|
||||
const resetPublishPostOptions = useCallback(() => resetPublishPostStore(), [resetPublishPostStore]);
|
||||
|
||||
const { index, publishComment } = usePublishComment(publishCommentOptions);
|
||||
const publishOptionsWithAbandon = useMemo(
|
||||
() => ({
|
||||
...publishCommentOptions,
|
||||
onChallenge: async (...args: any[]) => {
|
||||
addChallenge(args, async () => {
|
||||
await abandonPublishRef.current?.();
|
||||
});
|
||||
},
|
||||
}),
|
||||
[addChallenge, publishCommentOptions],
|
||||
);
|
||||
|
||||
const { index, publishComment, abandonPublish } = usePublishComment(publishOptionsWithAbandon);
|
||||
abandonPublishRef.current = abandonPublish;
|
||||
|
||||
return {
|
||||
setPublishPostOptions,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { Comment, usePublishComment } from '@bitsocialhq/bitsocial-react-hooks';
|
||||
import usePublishReplyStore from '../stores/use-publish-reply-store';
|
||||
import usePostNumberStore from '../stores/use-post-number-store';
|
||||
import { getQuotedCidsFromContent, mergeQuotedCids } from '../lib/utils/reply-quote-utils';
|
||||
import useChallengesStore from '../stores/use-challenges-store';
|
||||
|
||||
const usePublishReply = ({ cid, subplebbitAddress, postCid }: { cid: string; subplebbitAddress: string; postCid?: string }) => {
|
||||
const parentCid = cid;
|
||||
@@ -17,6 +18,8 @@ const usePublishReply = ({ cid, subplebbitAddress, postCid }: { cid: string; sub
|
||||
|
||||
const setPublishReplyStore = usePublishReplyStore((state) => state.setPublishReplyStore);
|
||||
const resetPublishReplyStore = usePublishReplyStore((state) => state.resetPublishReplyStore);
|
||||
const addChallenge = useChallengesStore((state) => state.addChallenge);
|
||||
const abandonPublishRef = useRef<(() => Promise<void>) | undefined>();
|
||||
|
||||
const createBaseOptions = useCallback(() => {
|
||||
const baseOptions: Comment = {
|
||||
@@ -58,9 +61,21 @@ const usePublishReply = ({ cid, subplebbitAddress, postCid }: { cid: string; sub
|
||||
const scopedNumberToCid = usePostNumberStore((state) => (subplebbitAddress ? state.numberToCid[subplebbitAddress] : undefined));
|
||||
const quotedCids = useMemo(() => getQuotedCidsFromContent(content, scopedNumberToCid), [content, scopedNumberToCid]);
|
||||
|
||||
const publishOptions = useMemo(() => mergeQuotedCids(publishCommentOptions, quotedCids), [publishCommentOptions, quotedCids]);
|
||||
const mergedPublishOptions = useMemo(() => mergeQuotedCids(publishCommentOptions, quotedCids), [publishCommentOptions, quotedCids]);
|
||||
const publishOptionsWithAbandon = useMemo(
|
||||
() => ({
|
||||
...mergedPublishOptions,
|
||||
onChallenge: async (...args: any[]) => {
|
||||
addChallenge(args, async () => {
|
||||
await abandonPublishRef.current?.();
|
||||
});
|
||||
},
|
||||
}),
|
||||
[addChallenge, mergedPublishOptions],
|
||||
);
|
||||
|
||||
const { index, publishComment } = usePublishComment(publishOptions);
|
||||
const { index, publishComment, abandonPublish } = usePublishComment(publishOptionsWithAbandon);
|
||||
abandonPublishRef.current = abandonPublish;
|
||||
|
||||
return {
|
||||
setPublishReplyOptions,
|
||||
|
||||
@@ -4,16 +4,17 @@ import { Challenge } from '@bitsocialhq/bitsocial-react-hooks';
|
||||
let nextChallengeId = 0;
|
||||
|
||||
interface State {
|
||||
challenges: Array<{ challenge: Challenge; id: number }>;
|
||||
addChallenge: (challenge: Challenge) => void;
|
||||
challenges: Array<{ challenge: Challenge; id: number; onAbandon?: () => Promise<void> | void }>;
|
||||
addChallenge: (challenge: Challenge, onAbandon?: () => Promise<void> | void) => void;
|
||||
removeChallenge: () => void;
|
||||
abandonCurrentChallenge: () => Promise<void>;
|
||||
}
|
||||
|
||||
const useChallengesStore = create<State>((set) => ({
|
||||
const useChallengesStore = create<State>((set, get) => ({
|
||||
challenges: [],
|
||||
addChallenge: (challenge: Challenge) => {
|
||||
addChallenge: (challenge: Challenge, onAbandon?: () => Promise<void> | void) => {
|
||||
set((state) => ({
|
||||
challenges: [...state.challenges, { challenge, id: nextChallengeId++ }],
|
||||
challenges: [...state.challenges, { challenge, id: nextChallengeId++, onAbandon }],
|
||||
}));
|
||||
},
|
||||
removeChallenge: () => {
|
||||
@@ -23,6 +24,16 @@ const useChallengesStore = create<State>((set) => ({
|
||||
return { challenges };
|
||||
});
|
||||
},
|
||||
abandonCurrentChallenge: async () => {
|
||||
const currentChallenge = get().challenges[0];
|
||||
try {
|
||||
await currentChallenge?.onAbandon?.();
|
||||
} catch (error) {
|
||||
console.error('Failed to abandon challenge publication:', error);
|
||||
} finally {
|
||||
get().removeChallenge();
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export default useChallengesStore;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ChallengeVerification, Comment, PublishCommentOptions } from '@bitsocialhq/bitsocial-react-hooks';
|
||||
import { create } from 'zustand';
|
||||
import { alertChallengeVerificationFailed } from '../lib/utils/challenge-utils';
|
||||
import useChallengesStore from './use-challenges-store';
|
||||
|
||||
type SubmitState = {
|
||||
author?: any | undefined;
|
||||
@@ -16,8 +15,6 @@ type SubmitState = {
|
||||
resetPublishPostStore: () => void;
|
||||
};
|
||||
|
||||
const { addChallenge } = useChallengesStore.getState();
|
||||
|
||||
const usePublishPostStore = create<SubmitState>((set) => ({
|
||||
author: undefined,
|
||||
displayName: undefined,
|
||||
@@ -44,7 +41,6 @@ const usePublishPostStore = create<SubmitState>((set) => ({
|
||||
content,
|
||||
link,
|
||||
spoiler,
|
||||
onChallenge: (...args: any) => addChallenge(args),
|
||||
onChallengeVerification: (challengeVerification: ChallengeVerification, comment: Comment) => {
|
||||
alertChallengeVerificationFailed(challengeVerification, comment);
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ChallengeVerification, Comment, PublishCommentOptions } from '@bitsocialhq/bitsocial-react-hooks';
|
||||
import { create } from 'zustand';
|
||||
import { alertChallengeVerificationFailed } from '../lib/utils/challenge-utils';
|
||||
import useChallengesStore from './use-challenges-store';
|
||||
|
||||
type ReplyState = {
|
||||
author: { [parentCid: string]: any | undefined };
|
||||
@@ -14,8 +13,6 @@ type ReplyState = {
|
||||
resetPublishReplyStore: (parentCid: string) => void;
|
||||
};
|
||||
|
||||
const { addChallenge } = useChallengesStore.getState();
|
||||
|
||||
const usePublishReplyStore = create<ReplyState>((set) => ({
|
||||
author: {},
|
||||
displayName: {},
|
||||
@@ -42,7 +39,6 @@ const usePublishReplyStore = create<ReplyState>((set) => ({
|
||||
content,
|
||||
link,
|
||||
spoiler,
|
||||
onChallenge: (...args: any) => addChallenge(args),
|
||||
onChallengeVerification: (challengeVerification: ChallengeVerification, comment: Comment) => {
|
||||
alertChallengeVerificationFailed(challengeVerification, comment);
|
||||
},
|
||||
|
||||
@@ -582,6 +582,32 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.failedPublishNotice {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: red;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.failedDeletePostButton {
|
||||
all: unset;
|
||||
color: red;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
text-decoration: underline;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.failedDeletePostButton:hover:not(:disabled) {
|
||||
color: #b30000;
|
||||
}
|
||||
|
||||
.failedDeletePostButton:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.7;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.grayEditMessage {
|
||||
color: var(--post-mobile-abbr-text-color);
|
||||
}
|
||||
@@ -709,4 +735,4 @@
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user