From b53736f40dacb1bb72f3cd5e96d3b60213570d29 Mon Sep 17 00:00:00 2001 From: plebeius Date: Sat, 27 Sep 2025 12:34:22 +0200 Subject: [PATCH 01/20] docs(release): add Windows SmartScreen guidance to release notes --- scripts/release-body.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/release-body.js b/scripts/release-body.js index 5167d5c3..bf577ae6 100644 --- a/scripts/release-body.js +++ b/scripts/release-body.js @@ -85,6 +85,7 @@ const macSection = section('macOS', [ const winSection = section('Windows', [ winSetupX64 && `- Installer (x64): [Download EXE](${linkTo(winSetupX64)})`, winPortableX64 && `- Portable (x64): [Download EXE](${linkTo(winPortableX64)})`, + (win.length > 0) && `- If Windows shows "Windows protected your PC" (SmartScreen), click "More info" then "Run anyway". To permanently allow, right-click the .exe → Properties → check "Unblock", or run in PowerShell: \`Unblock-File -Path .\\path\\to\\file.exe\`.`, ]); const linuxSection = section('Linux', [ From 6b5e002d780a7b038a65b1ce40cd7ebada355483 Mon Sep 17 00:00:00 2001 From: plebeius Date: Sat, 27 Sep 2025 13:05:24 +0200 Subject: [PATCH 02/20] feat(header,account-bar): remove unnecessary effects and derive values from route/visibility --- src/components/account-bar/account-bar.tsx | 10 +++------- src/components/header/header.tsx | 18 +++--------------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/src/components/account-bar/account-bar.tsx b/src/components/account-bar/account-bar.tsx index 93166853..9c962174 100644 --- a/src/components/account-bar/account-bar.tsx +++ b/src/components/account-bar/account-bar.tsx @@ -54,12 +54,8 @@ const AccountBar = () => { [searchBarRef, accountSelectButtonRef, accountDropdownRef, accountdropdownItemsRef], ); - const [isFocused, setIsFocused] = useState(false); - useEffect(() => { - if (searchVisible) { - setIsFocused(true); - } - }, [searchVisible]); + // Derive focus intent from visibility to avoid effect; SearchBar will handle actual focusing + const shouldFocusSearch = searchVisible; useEffect(() => { document.addEventListener('mousedown', handleClickOutside); @@ -120,7 +116,7 @@ const AccountBar = () => { {searchVisible && (
- +
)} diff --git a/src/components/header/header.tsx b/src/components/header/header.tsx index c15524fd..1c1df71a 100644 --- a/src/components/header/header.tsx +++ b/src/components/header/header.tsx @@ -1,4 +1,3 @@ -import { useEffect, useState } from 'react'; import { Link, useLocation, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useAccount, useAccountComment, useSubplebbit } from '@plebbit/plebbit-react-hooks'; @@ -95,19 +94,10 @@ const SortItems = () => { const isInModView = isModView(location.pathname); const isInDomainView = isDomainView(location.pathname); const isInSubplebbitView = isSubplebbitView(location.pathname, params); - const [selectedSortType, setSelectedSortType] = useState(params.sortType || '/hot'); + // Derive selection directly from route instead of syncing via an effect + const selectedSortType = isInHomeAboutView || isInSubplebbitAboutView || isInPostPageAboutView ? '' : params.sortType || 'hot'; const timeFilterName = params.timeFilterName; - useEffect(() => { - if (isInHomeAboutView || isInSubplebbitAboutView || isInPostPageAboutView) { - setSelectedSortType(''); - } else if (params.sortType) { - setSelectedSortType(params.sortType); - } else { - setSelectedSortType('hot'); - } - }, [params.sortType, isInHomeAboutView, isInSubplebbitAboutView, isInPostPageAboutView]); - return sortTypes.map((sortType, index) => { let sortLink = isInSubplebbitView ? `/p/${params.subplebbitAddress}/${sortType}` @@ -123,9 +113,7 @@ const SortItems = () => { } return (
  • - setSelectedSortType(sortType)}> - {t(sortLabels[index])} - + {t(sortLabels[index])}
  • ); }); From 70bc289c0b810d6135a18b4faa06fdcd1e57ffa6 Mon Sep 17 00:00:00 2001 From: plebeius Date: Sat, 27 Sep 2025 13:26:55 +0200 Subject: [PATCH 03/20] perf(post-page,subplebbit,submit): remove effects that sync derived booleans, replace a mount-only randomizer with useMemo --- src/views/post-page/post-page.tsx | 11 ++--------- src/views/submit-page/submit-page.tsx | 17 +++++++---------- src/views/subplebbit/subplebbit.tsx | 11 ++--------- 3 files changed, 11 insertions(+), 28 deletions(-) diff --git a/src/views/post-page/post-page.tsx b/src/views/post-page/post-page.tsx index 7b523fd9..1a3b3296 100644 --- a/src/views/post-page/post-page.tsx +++ b/src/views/post-page/post-page.tsx @@ -301,15 +301,8 @@ const PostPage = () => { window.scrollTo(0, 0); }, [commentCid, subplebbitAddress, accountCommentIndex]); - // probably not necessary to show the error to the user if the post loaded successfully - const [shouldShowErrorToUser, setShouldShowErrorToUser] = useState(false); - useEffect(() => { - if (post?.error && ((post?.replyCount > 0 && post?.replies?.length === 0) || (post?.state === 'failed' && post?.error))) { - setShouldShowErrorToUser(true); - } else if (post?.replyCount > 0 && post?.replies?.length > 0) { - setShouldShowErrorToUser(false); - } - }, [post]); + // Derive whether to show error directly from current post state + const shouldShowErrorToUser = Boolean(post?.error && ((post?.replyCount > 0 && post?.replies?.length === 0) || post?.state === 'failed')); return isBroadlyNsfwSubplebbit && !hasAcceptedWarning ? ( diff --git a/src/views/submit-page/submit-page.tsx b/src/views/submit-page/submit-page.tsx index 9be71940..5e4dc38d 100644 --- a/src/views/submit-page/submit-page.tsx +++ b/src/views/submit-page/submit-page.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useDropzone } from 'react-dropzone'; import { Trans, useTranslation } from 'react-i18next'; import { Link, useNavigate, useParams } from 'react-router-dom'; @@ -314,12 +314,8 @@ const SubplebbitAddressField = () => { const [isInputAddressFocused, setIsInputAddressFocused] = useState(false); const [activeDropdownIndex, setActiveDropdownIndex] = useState(-1); - // show list of random subplebbits only once when the component mounts - const [randomSubplebbitSuggestions, setRandomSubplebbitSuggestions] = useState([]); - useEffect(() => { - const generatedSubplebbits = getRandomSubplebbits(defaultSubplebbitAddresses, 10); - setRandomSubplebbitSuggestions(generatedSubplebbits); - }, [defaultSubplebbitAddresses]); + // Generate random suggestions derived from defaults without an effect + const randomSubplebbitSuggestions = useMemo(() => getRandomSubplebbits(defaultSubplebbitAddresses, 10), [defaultSubplebbitAddresses]); const listSource = subscriptions?.length > 5 ? subscriptions : randomSubplebbitSuggestions; const handleKeyDown = useCallback( @@ -353,10 +349,11 @@ const SubplebbitAddressField = () => { setActiveDropdownIndex(-1); }; - const getRandomSubplebbits = (addresses: string[], count: number) => { - let shuffled = addresses.sort(() => 0.5 - Math.random()); + function getRandomSubplebbits(addresses: string[], count: number) { + // Non-mutating shuffle (copy first), avoids side effects + const shuffled = [...addresses].sort(() => 0.5 - Math.random()); return shuffled.slice(0, count); - }; + } const defaultSubplebbitsDropdown = (
      diff --git a/src/views/subplebbit/subplebbit.tsx b/src/views/subplebbit/subplebbit.tsx index c629feb3..bc83405c 100644 --- a/src/views/subplebbit/subplebbit.tsx +++ b/src/views/subplebbit/subplebbit.tsx @@ -368,15 +368,8 @@ const Subplebbit = () => { document.title = title ? title : shortAddress || subplebbitAddress; }, [title, shortAddress, subplebbitAddress]); - // probably not necessary to show the error to the user if the feed loaded successfully - const [shouldShowErrorToUser, setShouldShowErrorToUser] = useState(false); - useEffect(() => { - if (error?.message && feed.length === 0) { - setShouldShowErrorToUser(true); - } else if (feed.length > 0) { - setShouldShowErrorToUser(false); - } - }, [error, feed]); + // Derive whether to show error directly from current feed state + const shouldShowErrorToUser = Boolean(error?.message && feed.length === 0); return isBroadlyNsfwSubplebbit && !hasUnhiddenAnyNsfwCommunity ? ( From afb4be7ff55b5a4634c91e4e3e3066c2f99e60d7 Mon Sep 17 00:00:00 2001 From: plebeius Date: Sat, 27 Sep 2025 13:31:21 +0200 Subject: [PATCH 04/20] fix eslint --- scripts/release-body.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/release-body.js b/scripts/release-body.js index bf577ae6..bb8a7513 100644 --- a/scripts/release-body.js +++ b/scripts/release-body.js @@ -65,9 +65,7 @@ const linuxX64 = linux.find(isX64); const macArm = mac.find(isArm); const macX64 = mac.find(isX64); -const winSetupArm = win.find(f => has(f, 'setup') && isArm(f)); const winSetupX64 = win.find(f => has(f, 'setup') && isX64(f)); -const winPortableArm = win.find(f => has(f, 'portable') && isArm(f)); const winPortableX64 = win.find(f => has(f, 'portable') && isX64(f)); // small section builder without push() From 610ff8ee35248e6e17b00702caeb6f07fc414d63 Mon Sep 17 00:00:00 2001 From: plebeius Date: Tue, 30 Sep 2025 18:10:45 +0200 Subject: [PATCH 05/20] feat(challenges): add iframe modal for external authentication challenges implements mintpass challenge --- src/app.tsx | 2 + .../iframe-challenge-modal.tsx | 125 ++++++++++++++++++ .../iframe-challenge-modal/index.ts | 1 + src/lib/utils/challenge-utils.ts | 54 +++++++- src/stores/use-challenges-store.ts | 67 ++++++++++ 5 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 src/components/iframe-challenge-modal/iframe-challenge-modal.tsx create mode 100644 src/components/iframe-challenge-modal/index.ts diff --git a/src/app.tsx b/src/app.tsx index 6c59b10c..2f713cf6 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -22,6 +22,7 @@ import SubplebbitSettings from './views/subplebbit-settings'; import Subplebbits from './views/subplebbits'; import AccountBar from './components/account-bar/'; import ChallengeModal from './components/challenge-modal'; +import IframeChallengeModal from './components/iframe-challenge-modal'; import Header from './components/header'; import NotificationHandler from './components/notification-handler'; import StickyHeader from './components/sticky-header'; @@ -36,6 +37,7 @@ const App = () => { const globalLayout = ( <> + diff --git a/src/components/iframe-challenge-modal/iframe-challenge-modal.tsx b/src/components/iframe-challenge-modal/iframe-challenge-modal.tsx new file mode 100644 index 00000000..3adb3f99 --- /dev/null +++ b/src/components/iframe-challenge-modal/iframe-challenge-modal.tsx @@ -0,0 +1,125 @@ +import { useEffect, useState } from 'react'; +import { FloatingFocusManager, useClick, useDismiss, useFloating, useId, useInteractions, useRole } from '@floating-ui/react'; +import { useAccount } from '@plebbit/plebbit-react-hooks'; +import { useTranslation } from 'react-i18next'; +import useChallengesStore from '../../stores/use-challenges-store'; +import styles from '../challenge-modal/challenge-modal.module.css'; +import { getPublicationPreview, getPublicationType, getVotePreview } from '../../lib/utils/challenge-utils'; + +interface IframeChallengeProps { + url: string; + publication: any; + closeModal: () => void; +} + +const IframeChallenge = ({ url, publication, closeModal }: IframeChallengeProps) => { + const { t } = useTranslation(); + const account = useAccount(); + const [showConfirmation, setShowConfirmation] = useState(true); + const [iframeUrl, setIframeUrl] = useState(''); + + const publicationType = getPublicationType(publication); + const publicationContent = getPublicationPreview(publication); + const votePreview = getVotePreview(publication); + const { shortSubplebbitAddress, subplebbitAddress, parentCid } = publication || {}; + + useEffect(() => { + const onEscapeKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + closeModal(); + } + }; + document.addEventListener('keydown', onEscapeKey); + return () => document.removeEventListener('keydown', onEscapeKey); + }, [closeModal]); + + const handleLoadIframe = () => { + // Replace {userAddress} placeholder with actual user address + const userAddress = account?.author?.address || ''; + const processedUrl = url.replace(/\{userAddress\}/g, userAddress); + setIframeUrl(processedUrl); + setShowConfirmation(false); + }; + + return ( +
      +
      {t('challenge_from', { subplebbit: shortSubplebbitAddress || subplebbitAddress })}
      +
      + {publicationType === 'vote' && votePreview + ' '} + {parentCid + ? t('challenge_for_reply', { parentAddress: '', publicationContent, interpolation: { escapeValue: false } }) + : t('challenge_for_post', { publicationContent, interpolation: { escapeValue: false } })} +
      + + {showConfirmation ? ( + <> +
      +
      + {t('iframe_challenge_warning', { + defaultValue: 'This challenge requires loading an external website. Loading it will reveal your IP address to that website. Do you want to continue?', + })} +
      +
      +
      + + + + +
      + + ) : ( + <> +
      +