mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
Merge branch 'codex/fix/spamblocker-confirm'
This commit is contained in:
@@ -31,6 +31,9 @@ vi.mock('react-i18next', () => ({
|
||||
if (key === 'challenge_counter') {
|
||||
return `${options?.index}/${options?.total}`;
|
||||
}
|
||||
if (key === 'iframe_challenge_confirm') {
|
||||
return `${options?.board} wants to open ${options?.site}.\n\nFor ${options?.publicationType}: ${options?.excerpt}`;
|
||||
}
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
@@ -88,8 +91,10 @@ vi.mock('@use-gesture/react', () => ({
|
||||
}));
|
||||
|
||||
let alertSpy: ReturnType<typeof vi.spyOn>;
|
||||
let confirmSpy: ReturnType<typeof vi.spyOn>;
|
||||
let container: HTMLDivElement;
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||
let nextStoredChallengeId = 1;
|
||||
let postMessageMock: ReturnType<typeof vi.fn>;
|
||||
let root: Root;
|
||||
|
||||
@@ -106,13 +111,16 @@ const createPublication = (): Record<string, any> => ({
|
||||
|
||||
const createStoredChallenge = (challenge: any, publication = createPublication(), publicationTarget?: Record<string, unknown>) => ({
|
||||
challenge: [{ challenges: Array.isArray(challenge) ? challenge : [challenge] }, publication, publicationTarget],
|
||||
id: 1,
|
||||
id: nextStoredChallengeId++,
|
||||
});
|
||||
|
||||
const renderModal = async () => {
|
||||
await act(async () => {
|
||||
root.render(createElement(ChallengeModal));
|
||||
});
|
||||
await act(async () => {
|
||||
await new Promise<void>((resolve) => queueMicrotask(resolve));
|
||||
});
|
||||
};
|
||||
|
||||
const dispatchInput = async (element: HTMLInputElement, value: string) => {
|
||||
@@ -155,6 +163,7 @@ describe('ChallengeModal', () => {
|
||||
testState.theme = 'dark';
|
||||
testState.votePreview = 'upvote';
|
||||
alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => undefined);
|
||||
confirmSpy = vi.spyOn(window, 'confirm').mockImplementation(() => true);
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
postMessageMock = vi.fn();
|
||||
Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', {
|
||||
@@ -172,6 +181,7 @@ describe('ChallengeModal', () => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
alertSpy.mockRestore();
|
||||
confirmSpy.mockRestore();
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -264,9 +274,16 @@ describe('ChallengeModal', () => {
|
||||
];
|
||||
|
||||
await renderModal();
|
||||
expect(container.textContent).toContain('mu wants to open mintpass.org');
|
||||
|
||||
await clickButton('Open');
|
||||
expect(confirmSpy).toHaveBeenCalledWith('mu wants to open mintpass.org.\n\nFor post: Subject');
|
||||
expect(container.textContent).toContain('mintpass.org');
|
||||
expect(container.textContent).toContain('Done');
|
||||
expect(container.textContent).not.toContain('Challenge for post');
|
||||
expect(container.textContent).not.toContain('Open');
|
||||
expect(container.textContent).not.toContain('Cancel');
|
||||
expect(container.textContent).not.toContain('Alice');
|
||||
expect(container.textContent).not.toContain('Subject');
|
||||
expect(container.textContent).not.toContain('Publication content');
|
||||
expect(container.textContent).not.toContain('mu wants to open');
|
||||
|
||||
const iframe = container.querySelector('iframe');
|
||||
expect(iframe).not.toBeNull();
|
||||
@@ -304,9 +321,8 @@ describe('ChallengeModal', () => {
|
||||
];
|
||||
|
||||
await renderModal();
|
||||
expect(container.textContent).toContain('mu wants to open localhost:3000');
|
||||
|
||||
await clickButton('Open');
|
||||
expect(confirmSpy).toHaveBeenCalledWith('mu wants to open localhost:3000.\n\nFor post: Subject');
|
||||
|
||||
const iframe = container.querySelector('iframe');
|
||||
expect(iframe).not.toBeNull();
|
||||
@@ -339,7 +355,6 @@ describe('ChallengeModal', () => {
|
||||
];
|
||||
|
||||
await renderModal();
|
||||
await clickButton('Open');
|
||||
|
||||
await act(async () => {
|
||||
window.dispatchEvent(
|
||||
@@ -371,7 +386,6 @@ describe('ChallengeModal', () => {
|
||||
];
|
||||
|
||||
await renderModal();
|
||||
await clickButton('Open');
|
||||
|
||||
await act(async () => {
|
||||
window.dispatchEvent(
|
||||
@@ -408,8 +422,47 @@ describe('ChallengeModal', () => {
|
||||
];
|
||||
|
||||
await renderModal();
|
||||
expect(container.textContent).toContain(`${getShortAddress(longCommunityAddress)} wants to open localhost:3000`);
|
||||
expect(container.textContent).not.toContain(`${longCommunityAddress} wants to open localhost:3000`);
|
||||
|
||||
expect(confirmSpy).toHaveBeenCalledWith(`${getShortAddress(longCommunityAddress)} wants to open localhost:3000.\n\nFor post: Subject`);
|
||||
expect(confirmSpy.mock.calls[0]?.[0]).not.toContain(longCommunityAddress);
|
||||
});
|
||||
|
||||
it('uses reply content in the iframe confirmation excerpt', async () => {
|
||||
const publication = {
|
||||
...createPublication(),
|
||||
content: '>>17\nA reply body with enough context',
|
||||
title: '',
|
||||
};
|
||||
testState.publicationType = 'reply';
|
||||
testState.challenges = [
|
||||
createStoredChallenge(
|
||||
{
|
||||
challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123',
|
||||
type: 'url/iframe',
|
||||
},
|
||||
publication,
|
||||
),
|
||||
];
|
||||
|
||||
await renderModal();
|
||||
|
||||
expect(confirmSpy).toHaveBeenCalledWith('mu wants to open spamblocker.bitsocial.net.\n\nFor reply: >>17 A reply body with enough context');
|
||||
});
|
||||
|
||||
it('abandons iframe challenges when the external confirmation is canceled', async () => {
|
||||
confirmSpy.mockReturnValueOnce(false);
|
||||
testState.challenges = [
|
||||
createStoredChallenge({
|
||||
challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123',
|
||||
type: 'url/iframe',
|
||||
}),
|
||||
];
|
||||
|
||||
await renderModal();
|
||||
|
||||
expect(confirmSpy).toHaveBeenCalledWith('mu wants to open spamblocker.bitsocial.net.\n\nFor post: Subject');
|
||||
expect(testState.abandonCurrentChallengeMock).toHaveBeenCalledOnce();
|
||||
expect(container.querySelector('iframe')).toBeNull();
|
||||
});
|
||||
|
||||
it('alerts when iframe challenges need a signer address and the account is missing one', async () => {
|
||||
@@ -422,9 +475,10 @@ describe('ChallengeModal', () => {
|
||||
];
|
||||
|
||||
await renderModal();
|
||||
await clickButton('Open');
|
||||
|
||||
expect(alertSpy).toHaveBeenCalledWith('Error: Unable to load challenge without your address. Please sign in and try again.');
|
||||
expect(confirmSpy).not.toHaveBeenCalled();
|
||||
expect(testState.abandonCurrentChallengeMock).toHaveBeenCalledOnce();
|
||||
expect(container.querySelector('iframe')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -437,9 +491,9 @@ describe('ChallengeModal', () => {
|
||||
];
|
||||
|
||||
await renderModal();
|
||||
await clickButton('Open');
|
||||
|
||||
expect(alertSpy).toHaveBeenCalledWith('Error: Only HTTPS iframe challenges or localhost HTTP challenges are supported');
|
||||
expect(confirmSpy).not.toHaveBeenCalled();
|
||||
expect(testState.abandonCurrentChallengeMock).toHaveBeenCalledOnce();
|
||||
|
||||
await act(async () => {
|
||||
|
||||
@@ -106,13 +106,6 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.iframeChallengeWarning {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.iframeWrapper {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useRef, useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Challenge as ChallengeType, useAccount, useComment } from '@bitsocial/bitsocial-react-hooks';
|
||||
import { getPublicationPreview, getPublicationType, getVotePreview } from '../../lib/utils/challenge-utils';
|
||||
import { getPublicationPreview, getPublicationType, getVotePreview, type ChallengePublication } from '../../lib/utils/challenge-utils';
|
||||
import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import useChallengesStore from '../../stores/use-challenges-store';
|
||||
import useTheme from '../../hooks/use-theme';
|
||||
@@ -18,10 +18,33 @@ const useParentAddress = (parentCid?: string) => {
|
||||
|
||||
interface ChallengeProps {
|
||||
challenge: ChallengeType;
|
||||
challengeId: number;
|
||||
closeModal: () => void;
|
||||
abandonModal: () => void;
|
||||
}
|
||||
|
||||
const MAX_IFRAME_CONFIRM_EXCERPT_LENGTH = 80;
|
||||
|
||||
const getTrimmedExcerpt = (value: unknown) => (typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '');
|
||||
|
||||
const shortenConfirmExcerpt = (excerpt: string) =>
|
||||
excerpt.length > MAX_IFRAME_CONFIRM_EXCERPT_LENGTH ? `${excerpt.slice(0, MAX_IFRAME_CONFIRM_EXCERPT_LENGTH).trimEnd()}...` : excerpt;
|
||||
|
||||
const getIframeConfirmExcerpt = (publication: ChallengePublication | undefined, publicationTarget: ChallengePublication | undefined, publicationType?: string) => {
|
||||
const title = getTrimmedExcerpt(publication?.title);
|
||||
const content = getTrimmedExcerpt(publication?.content);
|
||||
const link = getTrimmedExcerpt(publication?.link);
|
||||
const excerpt =
|
||||
publicationType === 'vote' ? getTrimmedExcerpt(getPublicationPreview(publicationTarget)) : publicationType === 'reply' ? content || link : title || content || link;
|
||||
|
||||
return shortenConfirmExcerpt(excerpt || getTrimmedExcerpt(getPublicationPreview(publication)));
|
||||
};
|
||||
|
||||
const getDisplayCommunityAddress = (shortCommunityAddress?: string, communityAddress?: string) =>
|
||||
shortCommunityAddress || (communityAddress ? getShortAddress(communityAddress) : '') || communityAddress || '';
|
||||
|
||||
const iframeChallengeConfirmDecisions = new Map<string, 'accepted' | 'rejected'>();
|
||||
|
||||
const TextChallenge = ({ challenge }: { challenge: string }) => <div className={styles.challengeMedia}>{challenge}</div>;
|
||||
|
||||
const MAX_IMAGE_CHALLENGE_BASE64_LENGTH = 2_000_000;
|
||||
@@ -36,6 +59,36 @@ const ImageChallenge = ({ challenge }: { challenge: string }) =>
|
||||
|
||||
const isLocalIframeHostname = (hostname: string) => hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]' || hostname.endsWith('.localhost');
|
||||
|
||||
type ValidatedIframeUrl = { status: 'valid'; finalUrl: string; origin: string } | { status: 'invalid'; error: unknown } | { status: 'unsupported' };
|
||||
|
||||
const validateIframeChallengeUrl = (iframeUrl: string, theme: string): ValidatedIframeUrl => {
|
||||
let validatedUrl: URL;
|
||||
try {
|
||||
validatedUrl = new URL(iframeUrl);
|
||||
} catch (error) {
|
||||
return { status: 'invalid', error };
|
||||
}
|
||||
|
||||
const isHttps = validatedUrl.protocol === 'https:';
|
||||
const isLocalHttp = validatedUrl.protocol === 'http:' && isLocalIframeHostname(validatedUrl.hostname);
|
||||
if (!isHttps && !isLocalHttp) {
|
||||
return { status: 'unsupported' };
|
||||
}
|
||||
|
||||
validatedUrl.pathname = validatedUrl.pathname.replace(/\/{2,}/g, '/');
|
||||
validatedUrl.searchParams.set('theme', theme);
|
||||
return { status: 'valid', finalUrl: validatedUrl.toString(), origin: validatedUrl.origin };
|
||||
};
|
||||
|
||||
const postThemeToIframe = (iframe: HTMLIFrameElement | null, iframeOrigin: string, theme: string) => {
|
||||
if (!iframe || !iframeOrigin) return;
|
||||
try {
|
||||
iframe.contentWindow?.postMessage({ type: 'plebbit-theme', theme, source: 'plebbit-5chan' }, iframeOrigin);
|
||||
} catch (error) {
|
||||
console.warn('Could not send theme to iframe:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getReadableIframeUrl = (challengeUrl: string) => {
|
||||
try {
|
||||
const url = new URL(challengeUrl);
|
||||
@@ -57,35 +110,44 @@ const getIframeSessionId = (challengeUrl: string) => {
|
||||
|
||||
interface IframeChallengeProps {
|
||||
challenge: string;
|
||||
shortCommunityAddress?: string;
|
||||
communityAddress?: string;
|
||||
readableUrl: string;
|
||||
confirmKey: string;
|
||||
confirmMessage: string;
|
||||
onCancel: () => void;
|
||||
onDone: () => void;
|
||||
onAutoComplete: (challengeAnswers: string[]) => void;
|
||||
publicationDetails: React.ReactNode;
|
||||
onReady: () => void;
|
||||
}
|
||||
|
||||
const IframeChallenge = ({
|
||||
challenge,
|
||||
shortCommunityAddress,
|
||||
communityAddress,
|
||||
readableUrl,
|
||||
onCancel,
|
||||
onDone,
|
||||
onAutoComplete,
|
||||
publicationDetails,
|
||||
}: IframeChallengeProps) => {
|
||||
const IframeChallenge = ({ challenge, confirmKey, confirmMessage, onCancel, onDone, onAutoComplete, onReady }: IframeChallengeProps) => {
|
||||
const account = useAccount();
|
||||
const [theme] = useTheme();
|
||||
const [showIframeConfirmation, setShowIframeConfirmation] = useState(true);
|
||||
const [iframeUrlState, setIframeUrl] = useState('');
|
||||
const [iframeOrigin, setIframeOrigin] = useState('');
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const attemptedLoadRef = useRef(false);
|
||||
const mountedRef = useRef(false);
|
||||
const handledAutoCompleteRef = useRef(false);
|
||||
const displayCommunityAddress = shortCommunityAddress || (communityAddress ? getShortAddress(communityAddress) : '') || communityAddress || 'unknown board';
|
||||
const expectedSessionId = getIframeSessionId(challenge);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const openValidatedIframe = useCallback(
|
||||
(validatedUrl: { finalUrl: string; origin: string }) => {
|
||||
queueMicrotask(() => {
|
||||
if (!mountedRef.current) return;
|
||||
setIframeUrl(validatedUrl.finalUrl);
|
||||
setIframeOrigin(validatedUrl.origin);
|
||||
onReady();
|
||||
});
|
||||
},
|
||||
[onReady],
|
||||
);
|
||||
|
||||
const handleLoadIframe = useCallback(() => {
|
||||
const iframeUrl = challenge;
|
||||
if (!iframeUrl) return;
|
||||
@@ -95,41 +157,52 @@ const IframeChallenge = ({
|
||||
|
||||
if (requiresUserAddress && !rawUserAddress) {
|
||||
alert('Error: Unable to load challenge without your address. Please sign in and try again.');
|
||||
iframeChallengeConfirmDecisions.set(confirmKey, 'rejected');
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
const encodedAddress = rawUserAddress ? encodeURIComponent(rawUserAddress) : undefined;
|
||||
const replacedUrl = requiresUserAddress && encodedAddress ? iframeUrl.replace(/\{userAddress\}/g, encodedAddress) : iframeUrl;
|
||||
|
||||
try {
|
||||
const validatedUrl = new URL(replacedUrl);
|
||||
const isHttps = validatedUrl.protocol === 'https:';
|
||||
const isLocalHttp = validatedUrl.protocol === 'http:' && isLocalIframeHostname(validatedUrl.hostname);
|
||||
if (!isHttps && !isLocalHttp) {
|
||||
alert('Error: Only HTTPS iframe challenges or localhost HTTP challenges are supported');
|
||||
const validatedUrl = validateIframeChallengeUrl(replacedUrl, theme);
|
||||
if (validatedUrl.status === 'unsupported') {
|
||||
alert('Error: Only HTTPS iframe challenges or localhost HTTP challenges are supported');
|
||||
iframeChallengeConfirmDecisions.set(confirmKey, 'rejected');
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
if (validatedUrl.status === 'invalid') {
|
||||
console.error('Invalid iframe challenge URL', { error: validatedUrl.error });
|
||||
alert('Error: Invalid URL for authentication challenge');
|
||||
iframeChallengeConfirmDecisions.set(confirmKey, 'rejected');
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
const decision = iframeChallengeConfirmDecisions.get(confirmKey);
|
||||
if (decision === 'rejected') {
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
if (decision !== 'accepted') {
|
||||
if (!window.confirm(confirmMessage)) {
|
||||
iframeChallengeConfirmDecisions.set(confirmKey, 'rejected');
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
validatedUrl.pathname = validatedUrl.pathname.replace(/\/{2,}/g, '/');
|
||||
validatedUrl.searchParams.set('theme', theme);
|
||||
const finalUrl = validatedUrl.toString();
|
||||
setIframeUrl(finalUrl);
|
||||
setIframeOrigin(validatedUrl.origin);
|
||||
setShowIframeConfirmation(false);
|
||||
} catch (error) {
|
||||
console.error('Invalid iframe challenge URL', { error });
|
||||
alert('Error: Invalid URL for authentication challenge');
|
||||
onCancel();
|
||||
iframeChallengeConfirmDecisions.set(confirmKey, 'accepted');
|
||||
}
|
||||
}, [account, challenge, onCancel, theme]);
|
||||
openValidatedIframe(validatedUrl);
|
||||
}, [account, challenge, confirmKey, confirmMessage, onCancel, openValidatedIframe, theme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (attemptedLoadRef.current) return;
|
||||
attemptedLoadRef.current = true;
|
||||
handleLoadIframe();
|
||||
}, [handleLoadIframe]);
|
||||
|
||||
const sendThemeToIframe = useCallback(() => {
|
||||
if (!iframeRef.current || !iframeOrigin) return;
|
||||
try {
|
||||
iframeRef.current.contentWindow?.postMessage({ type: 'plebbit-theme', theme, source: 'plebbit-5chan' }, iframeOrigin);
|
||||
} catch (error) {
|
||||
console.warn('Could not send theme to iframe:', error);
|
||||
}
|
||||
postThemeToIframe(iframeRef.current, iframeOrigin, theme);
|
||||
}, [iframeOrigin, theme]);
|
||||
|
||||
const handleIframeLoad = () => {
|
||||
@@ -137,13 +210,13 @@ const IframeChallenge = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (iframeRef.current && iframeUrlState && iframeOrigin && !showIframeConfirmation) {
|
||||
if (iframeRef.current && iframeUrlState && iframeOrigin) {
|
||||
sendThemeToIframe();
|
||||
}
|
||||
}, [iframeOrigin, iframeUrlState, sendThemeToIframe, showIframeConfirmation]);
|
||||
}, [iframeOrigin, iframeUrlState, sendThemeToIframe]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showIframeConfirmation || !iframeOrigin || !expectedSessionId) {
|
||||
if (!iframeOrigin || !expectedSessionId) {
|
||||
handledAutoCompleteRef.current = false;
|
||||
return;
|
||||
}
|
||||
@@ -164,25 +237,10 @@ const IframeChallenge = ({
|
||||
|
||||
window.addEventListener('message', handleMessage);
|
||||
return () => window.removeEventListener('message', handleMessage);
|
||||
}, [expectedSessionId, iframeOrigin, onAutoComplete, showIframeConfirmation]);
|
||||
}, [expectedSessionId, iframeOrigin, onAutoComplete]);
|
||||
|
||||
if (showIframeConfirmation) {
|
||||
return (
|
||||
<>
|
||||
{publicationDetails}
|
||||
<div className={styles.challengeMediaWrapper}>
|
||||
<div className={`${styles.challengeMedia} ${styles.iframeChallengeWarning}`}>
|
||||
{displayCommunityAddress} wants to open {readableUrl || 'an external site'}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${styles.challengeFooter} ${styles.iframeFooter}`}>
|
||||
<span className={styles.buttons}>
|
||||
<button onClick={handleLoadIframe}>Open</button>
|
||||
<button onClick={onCancel}>Cancel</button>
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
if (!iframeUrlState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -206,7 +264,7 @@ const IframeChallenge = ({
|
||||
);
|
||||
};
|
||||
|
||||
const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
|
||||
const Challenge = ({ challenge, challengeId, closeModal, abandonModal }: ChallengeProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const challenges = challenge?.[0]?.challenges;
|
||||
@@ -224,6 +282,7 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
|
||||
|
||||
const [currentChallengeIndex, setCurrentChallengeIndex] = useState(0);
|
||||
const [answers, setAnswers] = useState<string[]>([]);
|
||||
const [readyIframeChallengeKey, setReadyIframeChallengeKey] = useState('');
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const nodeRef = useRef<HTMLDivElement>(null);
|
||||
@@ -235,9 +294,11 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
|
||||
}));
|
||||
|
||||
const currentChallenge = challenges?.[currentChallengeIndex];
|
||||
const iframeChallengeKey = `${challengeId}:${currentChallengeIndex}:${currentChallenge?.challenge ?? ''}`;
|
||||
const isTextChallenge = currentChallenge?.type === 'text/plain';
|
||||
const isImageChallenge = currentChallenge?.type === 'image/png';
|
||||
const isIframeChallenge = currentChallenge?.type === 'url/iframe';
|
||||
const isIframePending = isIframeChallenge && readyIframeChallengeKey !== iframeChallengeKey;
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
@@ -294,6 +355,10 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
|
||||
[closeModal, publication],
|
||||
);
|
||||
|
||||
const onIframeReady = useCallback(() => {
|
||||
setReadyIframeChallengeKey(iframeChallengeKey);
|
||||
}, [iframeChallengeKey]);
|
||||
|
||||
const onEnterKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key !== 'Enter') return;
|
||||
if (!isValidAnswer(currentChallengeIndex)) return;
|
||||
@@ -340,6 +405,15 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
|
||||
|
||||
const mobileX = isIframeVisible ? 5 : window.innerWidth / 2 - 150;
|
||||
const mobileY = isIframeVisible ? Math.max(10, (window.innerHeight - 600) / 2) : window.innerHeight / 2 - 200;
|
||||
const displayCommunityAddress = getDisplayCommunityAddress(shortCommunityAddress, communityAddress) || t('board');
|
||||
const iframeConfirmMessage = t('iframe_challenge_confirm', {
|
||||
board: displayCommunityAddress,
|
||||
excerpt: getIframeConfirmExcerpt(publication, publicationTarget, publicationType) || t('none'),
|
||||
interpolation: { escapeValue: false },
|
||||
publicationType: publicationType ? t(publicationType) : t('post'),
|
||||
site: readableUrl || t('webpage'),
|
||||
});
|
||||
const challengeTitle = isIframeChallenge ? readableUrl || t('iframe') : `Challenge for ${publicationType}`;
|
||||
|
||||
const publicationDetails = (
|
||||
<>
|
||||
@@ -374,11 +448,12 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
|
||||
style={{
|
||||
x: isMobile ? mobileX : x.to((value) => Math.round(value)),
|
||||
y: isMobile ? mobileY : y.to((value) => Math.round(value)),
|
||||
display: isIframePending ? 'none' : undefined,
|
||||
touchAction: 'none',
|
||||
}}
|
||||
>
|
||||
<div id='challenge-modal-title' className={`challengeHandle ${styles.title}`} {...(!isMobile ? bind() : {})}>
|
||||
Challenge for {publicationType}
|
||||
{challengeTitle}
|
||||
<button type='button' className={styles.closeIcon} onClick={abandonModal} title='close' aria-label={t('close')} />
|
||||
</div>
|
||||
<div className={styles.publication}>
|
||||
@@ -386,13 +461,12 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
|
||||
<IframeChallenge
|
||||
key={currentChallengeIndex}
|
||||
challenge={currentChallenge?.challenge ?? ''}
|
||||
shortCommunityAddress={shortCommunityAddress}
|
||||
communityAddress={communityAddress}
|
||||
readableUrl={readableUrl}
|
||||
confirmKey={iframeChallengeKey}
|
||||
confirmMessage={iframeConfirmMessage}
|
||||
onCancel={abandonModal}
|
||||
onDone={onIframeDone}
|
||||
onAutoComplete={onIframeAutoComplete}
|
||||
publicationDetails={publicationDetails}
|
||||
onReady={onIframeReady}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
@@ -450,7 +524,7 @@ const ChallengeModal = () => {
|
||||
const challenge = current?.challenge;
|
||||
const challengeId = current?.id ?? 0;
|
||||
|
||||
return isOpen && challenge ? <Challenge key={challengeId} challenge={challenge} closeModal={closeModal} abandonModal={abandonModal} /> : null;
|
||||
return isOpen && challenge ? <Challenge key={challengeId} challenge={challenge} challengeId={challengeId} closeModal={closeModal} abandonModal={abandonModal} /> : null;
|
||||
};
|
||||
|
||||
export default ChallengeModal;
|
||||
|
||||
Reference in New Issue
Block a user