From f3dd2926b78906ba20d72b3a4599c6d5180c0bf7 Mon Sep 17 00:00:00 2001 From: plebeius Date: Fri, 11 Jul 2025 22:55:54 +0200 Subject: [PATCH 01/12] fix: improve PWA update detection and HTML caching strategy Use NetworkFirst for HTML caching and window.location.reload() for more reliable service worker updates --- src/index.tsx | 10 +++++++++- vite.config.js | 7 ++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 5d3e3206..bd5cdfef 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -20,7 +20,15 @@ const reloadSW = registerSW({ immediate: true, onNeedRefresh() { // Reload the page to load the new version - reloadSW(true); + // Use window.location.reload() as it's more reliable than reloadSW(true) + if (!sessionStorage.getItem('sw-update-reload')) { + sessionStorage.setItem('sw-update-reload', 'true'); + window.location.reload(); + } + }, + onOfflineReady() { + // Clear the reload flag when offline-ready (prevents loops) + sessionStorage.removeItem('sw-update-reload'); }, }); diff --git a/vite.config.js b/vite.config.js index e8a0d011..800a24f0 100644 --- a/vite.config.js +++ b/vite.config.js @@ -86,12 +86,13 @@ export default defineConfig({ navigateFallbackDenylist: [/^\/api/, /^\/_(.*)/], runtimeCaching: [ - // Fix index.html not refreshing on new versions + // Always get fresh HTML from network first { urlPattern: ({ url }) => url.pathname === '/' || url.pathname === '/index.html', - handler: 'StaleWhileRevalidate', + handler: 'NetworkFirst', options: { - cacheName: 'html-cache' + cacheName: 'html-cache', + networkTimeoutSeconds: 3 } }, // PNG caching From a60142f49330f08b313cbedfe4e9d9b71ee211c8 Mon Sep 17 00:00:00 2001 From: plebeius Date: Fri, 11 Jul 2025 23:12:33 +0200 Subject: [PATCH 02/12] fix: react-scan was included in production builds --- .github/workflows/release.yml | 8 ++++---- package.json | 8 ++++---- vite.config.js | 4 +++- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c6426568..f259023a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,7 @@ jobs: with: node-version: 22 - name: Build React app (with Node v22) - run: CI='' yarn build + run: CI='' NODE_ENV=production yarn build - name: Build Electron app for Linux (with Node v22) run: yarn electron:build:linux - name: List dist directory @@ -82,7 +82,7 @@ jobs: with: node-version: 22 - name: Build React app (with Node v22) - run: CI='' yarn build + run: CI='' NODE_ENV=production yarn build - name: Build Electron app for Mac (with Node v22) run: yarn electron:build:mac - name: List dist directory @@ -116,7 +116,7 @@ jobs: - name: Install dependencies (with Node v22) # --network-timeout and --network-concurrency are yarn v1 flags. run: yarn install --frozen-lockfile --network-timeout 100000 --network-concurrency 1 - name: Build React app (with Node v22) - run: yarn build + run: NODE_ENV=production yarn build - name: Build Electron app for Windows (with Node v22) run: yarn electron:build:windows - name: List dist directory @@ -158,7 +158,7 @@ jobs: - name: Install dependencies (with Node v20) run: yarn install --frozen-lockfile --ignore-engines # build react app - - run: CI='' yarn build + - run: CI='' NODE_ENV=production yarn build # set android versionCode and versionName - run: sed -i "s/versionCode 1/versionCode $(git tag | wc -l)/" ./android/app/build.gradle - run: sed -i "s/versionName \"1.0\"/versionName \"$(node -e "console.log(require('./package.json').version)")\"/" ./android/app/build.gradle diff --git a/package.json b/package.json index 9144ffdc..f69a021d 100755 --- a/package.json +++ b/package.json @@ -59,12 +59,12 @@ }, "scripts": { "start": "vite", - "build": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=false vite build", - "build:preload": "vite build --config electron/vite.preload.config.js", - "build-netlify": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" PUBLIC_URL=./ GENERATE_SOURCEMAP=true VITE_COMMIT_REF=$COMMIT_REF CI='' vite build", + "build": "cross-env NODE_ENV=production PUBLIC_URL=./ GENERATE_SOURCEMAP=false vite build", + "build:preload": "cross-env NODE_ENV=production vite build --config electron/vite.preload.config.js", + "build-netlify": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" NODE_ENV=production PUBLIC_URL=./ GENERATE_SOURCEMAP=true VITE_COMMIT_REF=$COMMIT_REF CI='' vite build", "test": "vitest", "preview": "vite preview", - "analyze-bundle": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=true vite build && npx source-map-explorer 'build/assets/*.js'", + "analyze-bundle": "cross-env NODE_ENV=production PUBLIC_URL=./ GENERATE_SOURCEMAP=true vite build && npx source-map-explorer 'build/assets/*.js'", "electron": "yarn build:preload && cross-env ELECTRON_IS_DEV=1 yarn electron:before && cross-env ELECTRON_IS_DEV=1 electron .", "electron:no-delete-data": "yarn electron:before:download-ipfs && electron .", "electron:start": "concurrently \"cross-env BROWSER=none yarn start\" \"wait-on http://localhost:3000 && yarn electron\"", diff --git a/vite.config.js b/vite.config.js index 800a24f0..34a31314 100644 --- a/vite.config.js +++ b/vite.config.js @@ -7,6 +7,7 @@ import { VitePWA } from 'vite-plugin-pwa'; import reactScan from '@react-scan/vite-plugin-react-scan'; const isProduction = process.env.NODE_ENV === 'production'; +const isDevelopment = process.env.NODE_ENV === 'development'; export default defineConfig({ plugins: [ @@ -19,7 +20,8 @@ export default defineConfig({ ] } }), - !isProduction && reactScan({ + // Only include React Scan in development mode - never in production builds + (isDevelopment || (!isProduction && process.env.NODE_ENV !== 'production')) && reactScan({ showToolbar: true, playSound: true, }), From 5d5cdcb3d7a3b9cc5d10d7716cdb87b97eca6a8d Mon Sep 17 00:00:00 2001 From: plebeius Date: Sat, 12 Jul 2025 17:25:57 +0200 Subject: [PATCH 03/12] Update index.tsx --- src/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.tsx b/src/index.tsx index bd5cdfef..6d11c457 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -16,7 +16,7 @@ if (window.location.hostname.startsWith('p2p.')) { }; } -const reloadSW = registerSW({ +registerSW({ immediate: true, onNeedRefresh() { // Reload the page to load the new version From 71b36d22edca2b1189dfa47b7d6b8b58169144a6 Mon Sep 17 00:00:00 2001 From: plebeius Date: Sat, 12 Jul 2025 17:33:37 +0200 Subject: [PATCH 04/12] Update release.yml --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f259023a..db2f7e23 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -116,7 +116,7 @@ jobs: - name: Install dependencies (with Node v22) # --network-timeout and --network-concurrency are yarn v1 flags. run: yarn install --frozen-lockfile --network-timeout 100000 --network-concurrency 1 - name: Build React app (with Node v22) - run: NODE_ENV=production yarn build + run: npx cross-env NODE_ENV=production yarn build - name: Build Electron app for Windows (with Node v22) run: yarn electron:build:windows - name: List dist directory From 8e1ec8ac3d9bcb260ed1405cfbfc6b8e5f773d4d Mon Sep 17 00:00:00 2001 From: plebeius Date: Sat, 12 Jul 2025 19:24:49 +0200 Subject: [PATCH 05/12] feat(subplebbit settings): enable editing via pubsub --- src/lib/utils/user-utils.ts | 6 ++++++ .../subplebbit-settings.tsx | 19 ++++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/lib/utils/user-utils.ts b/src/lib/utils/user-utils.ts index 1a79d471..7cee8f50 100644 --- a/src/lib/utils/user-utils.ts +++ b/src/lib/utils/user-utils.ts @@ -7,6 +7,12 @@ export const isUserOwner = (roles: Roles | undefined, userAddress: string | unde return roles[userAddress]?.role === 'owner'; }; +export const isUserOwnerOrAdmin = (roles: Roles | undefined, userAddress: string | undefined): boolean => { + if (!roles || !userAddress) return false; + const userRole = roles[userAddress]?.role; + return userRole === 'owner' || userRole === 'admin'; +}; + export const findSubplebbitCreator = (roles: Roles | undefined): string => { if (!roles) { return 'anonymous'; diff --git a/src/views/subplebbit-settings/subplebbit-settings.tsx b/src/views/subplebbit-settings/subplebbit-settings.tsx index 4d2df8fe..22834fa2 100644 --- a/src/views/subplebbit-settings/subplebbit-settings.tsx +++ b/src/views/subplebbit-settings/subplebbit-settings.tsx @@ -11,7 +11,7 @@ import { useSubplebbit, useSubscribe, } from '@plebbit/plebbit-react-hooks'; -import { isUserOwner, Roles } from '../../lib/utils/user-utils'; +import { isUserOwner, isUserOwnerOrAdmin, Roles } from '../../lib/utils/user-utils'; import { isValidURL } from '../../lib/utils/url-utils'; import { isCreateSubplebbitView, isSubplebbitSettingsView } from '../../lib/utils/view-utils'; import useSubplebbitSettingsStore from '../../stores/use-subplebbit-settings-store'; @@ -374,7 +374,14 @@ const SubplebbitSettings = () => { navigate('/', { replace: true }); } - const isReadOnly = (!settings && isInSubplebbitSettingsView) || (!isConnectedToRpc && isInCreateSubplebbitView); + const userAddress = account?.author?.address; + const userIsOwnerOrAdmin = isUserOwnerOrAdmin(roles, userAddress); + + // General fields can be edited by owners/admins even without RPC connection + const isReadOnly = (!settings && isInSubplebbitSettingsView && !userIsOwnerOrAdmin) || (!isConnectedToRpc && isInCreateSubplebbitView && !userIsOwnerOrAdmin); + + // Challenges are always read-only when not connected to RPC + const isChallengesReadOnly = !isConnectedToRpc; const { publishSubplebbitEditOptions, resetSubplebbitSettingsStore, setSubplebbitSettingsStore, title: storeTitle } = useSubplebbitSettingsStore(); const { error: publishSubplebbitEditError, publishSubplebbitEdit } = usePublishSubplebbitEdit(publishSubplebbitEditOptions); @@ -551,7 +558,6 @@ const SubplebbitSettings = () => { window.scrollTo(0, 0); }, []); - const userAddress = account?.author?.address; const userIsOwner = isUserOwner(roles, userAddress); const loadingStateString = useStateString(subplebbit); @@ -577,14 +583,17 @@ const SubplebbitSettings = () => { )} - {isReadOnly && !userIsOwner &&
{t('owner_settings_notice')}
} + {isReadOnly && !userIsOwnerOrAdmin &&
{t('owner_settings_notice')}
} + {!isReadOnly && userIsOwnerOrAdmin && !isConnectedToRpc && ( +
editing anti-spam challenges requires running a full node (or connecting via RPC)
+ )} <Description isReadOnly={isReadOnly} /> {!isInCreateSubplebbitView && <Address isReadOnly={isReadOnly} />} <Logo isReadOnly={isReadOnly} /> <Rules isReadOnly={isReadOnly} /> <Moderators isReadOnly={isReadOnly} /> - <Challenges isReadOnly={isReadOnly} readOnlyChallenges={subplebbit?.challenges} challengeNames={challengeNames} challengesSettings={rpcChallenges} /> + <Challenges isReadOnly={isChallengesReadOnly} readOnlyChallenges={subplebbit?.challenges} challengeNames={challengeNames} challengesSettings={rpcChallenges} /> {!isInCreateSubplebbitView && <JSONSettings isReadOnly={isReadOnly} />} <div className={styles.saveOptions}> {!isInCreateSubplebbitView && !isReadOnly && ( From 614fb427bf21392245943e76b962711edf68a56b Mon Sep 17 00:00:00 2001 From: plebeius <tom@plebbit.com> Date: Sun, 13 Jul 2025 16:20:19 +0200 Subject: [PATCH 06/12] fix: resolve Electron performance issues by ensuring Node.js version consistency in CI --- .github/workflows/release.yml | 42 +++++++++++++++++------------------ 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index db2f7e23..9c2b6bad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,20 +20,17 @@ jobs: with: # needed for git commit history changelog fetch-depth: 0 - - name: Setup Node.js v20 - uses: actions/setup-node@v2 - with: - node-version: 20 - - name: Install dependencies (with Node v20) - run: yarn install --frozen-lockfile --ignore-engines - # make sure the ipfs executable is executable - - name: Download IPFS and set permissions (with Node v20) - run: node electron/download-ipfs && sudo chmod +x bin/linux/ipfs - - - name: Setup Node.js v22 for Electron build + - name: Setup Node.js v22 uses: actions/setup-node@v2 with: node-version: 22 + - name: Verify Node.js version consistency + run: yarn verify-node + - name: Install dependencies (with Node v22) + run: yarn install --frozen-lockfile --ignore-engines + # make sure the ipfs executable is executable + - name: Download IPFS and set permissions (with Node v22) + run: node electron/download-ipfs && sudo chmod +x bin/linux/ipfs - name: Build React app (with Node v22) run: CI='' NODE_ENV=production yarn build - name: Build Electron app for Linux (with Node v22) @@ -62,25 +59,22 @@ jobs: with: # needed for git commit history changelog fetch-depth: 0 - - name: Setup Node.js v20 + - name: Setup Node.js v22 uses: actions/setup-node@v2 with: - node-version: 20 + node-version: 22 # install missing dep for sqlite - run: python3 -m ensurepip - run: pip install setuptools - - name: Install dependencies (with Node v20) + - name: Verify Node.js version consistency + run: yarn verify-node + - name: Install dependencies (with Node v22) run: yarn install --frozen-lockfile --ignore-engines # make sure the ipfs executable is executable - - name: Download IPFS and set permissions (with Node v20) + - name: Download IPFS and set permissions (with Node v22) run: node electron/download-ipfs && sudo chmod +x bin/mac/ipfs - - - name: Setup Node.js v22 for Electron build - uses: actions/setup-node@v2 - with: - node-version: 22 - name: Build React app (with Node v22) run: CI='' NODE_ENV=production yarn build - name: Build Electron app for Mac (with Node v22) @@ -113,6 +107,8 @@ jobs: uses: actions/setup-node@v2 with: node-version: 22 + - name: Verify Node.js version consistency + run: yarn verify-node - name: Install dependencies (with Node v22) # --network-timeout and --network-concurrency are yarn v1 flags. run: yarn install --frozen-lockfile --network-timeout 100000 --network-concurrency 1 - name: Build React app (with Node v22) @@ -151,11 +147,13 @@ jobs: gradle-version: 8.9 - uses: actions/setup-node@v2 with: - node-version: 20 + node-version: 22 + - name: Verify Node.js version consistency + run: yarn verify-node - run: sudo apt install -y apksigner zipalign # install all dependencies (including devDependencies needed for React build) - - name: Install dependencies (with Node v20) + - name: Install dependencies (with Node v22) run: yarn install --frozen-lockfile --ignore-engines # build react app - run: CI='' NODE_ENV=production yarn build From eae61d691c217c7be4ae83f7bf8fd94164c7dc16 Mon Sep 17 00:00:00 2001 From: plebeius <tom@plebbit.com> Date: Tue, 15 Jul 2025 12:16:41 +0200 Subject: [PATCH 07/12] add reset button for debugging --- src/views/mod/mod.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/views/mod/mod.tsx b/src/views/mod/mod.tsx index 48c01af3..4d150ae5 100644 --- a/src/views/mod/mod.tsx +++ b/src/views/mod/mod.tsx @@ -214,6 +214,11 @@ const Mod = () => { </div> ) : ( <div className={styles.feed}> + {process.env.NODE_ENV !== 'production' && ( + <button className={styles.debugButton} onClick={reset}> + Reset Feed + </button> + )} <Virtuoso increaseViewportBy={{ bottom: 1200, top: 600 }} totalCount={feed?.length || 0} From 503dc228319f14ea0d2dab063da784cefb568e37 Mon Sep 17 00:00:00 2001 From: plebeius <tom@plebbit.com> Date: Tue, 15 Jul 2025 22:40:06 +0200 Subject: [PATCH 08/12] fix eslint --- src/views/subplebbit-settings/subplebbit-settings.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/views/subplebbit-settings/subplebbit-settings.tsx b/src/views/subplebbit-settings/subplebbit-settings.tsx index 22834fa2..76f390af 100644 --- a/src/views/subplebbit-settings/subplebbit-settings.tsx +++ b/src/views/subplebbit-settings/subplebbit-settings.tsx @@ -11,7 +11,7 @@ import { useSubplebbit, useSubscribe, } from '@plebbit/plebbit-react-hooks'; -import { isUserOwner, isUserOwnerOrAdmin, Roles } from '../../lib/utils/user-utils'; +import { isUserOwnerOrAdmin, Roles } from '../../lib/utils/user-utils'; import { isValidURL } from '../../lib/utils/url-utils'; import { isCreateSubplebbitView, isSubplebbitSettingsView } from '../../lib/utils/view-utils'; import useSubplebbitSettingsStore from '../../stores/use-subplebbit-settings-store'; @@ -558,7 +558,6 @@ const SubplebbitSettings = () => { window.scrollTo(0, 0); }, []); - const userIsOwner = isUserOwner(roles, userAddress); const loadingStateString = useStateString(subplebbit); if (!hasLoaded && !isInCreateSubplebbitView) { From d21b5ddb5b68bce9612b01f49c059e8f8c6b0a23 Mon Sep 17 00:00:00 2001 From: plebeius <tom@plebbit.com> Date: Tue, 15 Jul 2025 22:41:00 +0200 Subject: [PATCH 09/12] feat: add info tooltips --- src/components/info-tooltip/index.ts | 1 + .../info-tooltip/info-tooltip.module.css | 43 ++++++++++ src/components/info-tooltip/info-tooltip.tsx | 78 +++++++++++++++++++ src/themes.css | 4 + 4 files changed, 126 insertions(+) create mode 100644 src/components/info-tooltip/index.ts create mode 100644 src/components/info-tooltip/info-tooltip.module.css create mode 100644 src/components/info-tooltip/info-tooltip.tsx diff --git a/src/components/info-tooltip/index.ts b/src/components/info-tooltip/index.ts new file mode 100644 index 00000000..87014d15 --- /dev/null +++ b/src/components/info-tooltip/index.ts @@ -0,0 +1 @@ +export { default } from './info-tooltip'; diff --git a/src/components/info-tooltip/info-tooltip.module.css b/src/components/info-tooltip/info-tooltip.module.css new file mode 100644 index 00000000..e0d58f66 --- /dev/null +++ b/src/components/info-tooltip/info-tooltip.module.css @@ -0,0 +1,43 @@ +.tooltipIcon { + cursor: help; +} + +.tooltip { + background: var(--background); + color: var(--info-tooltip-text); + border: 1px solid var(--info-tooltip-border-color); + padding: 3px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25); + z-index: 100; + transition: opacity 0.15s ease-in-out; + /* Default triangle position as fallback */ + --triangle-left-border: 8px; + --triangle-left-fill: 9px; +} + +.tooltipContent { + white-space: pre-wrap; + font: 10px verdana, arial, helvetica, sans-serif; + color: var(--spoiler-tooltip-text); + margin: .5em; +} + +.tooltip::before { + content: ''; + position: absolute; + top: -20px; + left: var(--triangle-left-border); + border: 10px solid transparent; + border-bottom-color: var(--info-tooltip-border-color); + z-index: 1; +} + +.tooltip::after { + content: ''; + position: absolute; + top: -18px; + left: var(--triangle-left-fill); + border: 9px solid transparent; + border-bottom-color: var(--background); + z-index: 2; +} \ No newline at end of file diff --git a/src/components/info-tooltip/info-tooltip.tsx b/src/components/info-tooltip/info-tooltip.tsx new file mode 100644 index 00000000..d0f7aae2 --- /dev/null +++ b/src/components/info-tooltip/info-tooltip.tsx @@ -0,0 +1,78 @@ +import { useState, useLayoutEffect } from 'react'; +import { useFloating, autoUpdate, offset, shift, size, useHover, useFocus, useDismiss, useRole, useInteractions, FloatingPortal, safePolygon } from '@floating-ui/react'; +import styles from './info-tooltip.module.css'; + +interface InfoTooltipProps { + content: string; + showTooltip?: boolean; +} + +const InfoTooltip = ({ content, showTooltip = true }: InfoTooltipProps) => { + const [isOpen, setIsOpen] = useState(false); + + const { refs, floatingStyles, context } = useFloating({ + open: isOpen, + onOpenChange: setIsOpen, + placement: 'bottom-start', + whileElementsMounted: autoUpdate, + middleware: [ + offset({ mainAxis: 12 }), // 5px lower (7+5=12), 10px to the left (-10) + shift(), + size({ + apply({ availableWidth, elements }) { + Object.assign(elements.floating.style, { + maxWidth: `${Math.min(availableWidth, 460)}px`, // 35em ≈ 560px with 20px right padding + }); + }, + }), + ], + }); + + // Calculate triangle position dynamically + useLayoutEffect(() => { + if (isOpen && refs.reference.current && refs.floating.current) { + const referenceRect = refs.reference.current.getBoundingClientRect(); + const floatingRect = refs.floating.current.getBoundingClientRect(); + + // Calculate the horizontal center of the reference element relative to the floating element + const referenceCenterX = referenceRect.left + referenceRect.width / 2; + const floatingLeftX = floatingRect.left; + const triangleLeft = referenceCenterX - floatingLeftX; + + // Set CSS custom properties for triangle positioning + // Keep the 1px offset between border and fill triangles for proper border effect + refs.floating.current.style.setProperty('--triangle-left-border', `${Math.max(0, triangleLeft - 2)}px`); + refs.floating.current.style.setProperty('--triangle-left-fill', `${Math.max(1, triangleLeft - 1)}px`); + } + }, [isOpen, refs.reference, refs.floating, floatingStyles]); + + const hover = useHover(context, { + move: false, + delay: { open: 200, close: 0 }, + handleClose: safePolygon(), + }); + const focus = useFocus(context); + const dismiss = useDismiss(context); + const role = useRole(context, { role: 'tooltip' }); + + const { getReferenceProps, getFloatingProps } = useInteractions([hover, focus, dismiss, role]); + + return ( + <> + <sup className={styles.tooltipIcon} ref={refs.setReference} {...getReferenceProps()}> + [?] + </sup> + {showTooltip && ( + <FloatingPortal> + {isOpen && ( + <div className={styles.tooltip} ref={refs.setFloating} style={floatingStyles} {...getFloatingProps()}> + <p className={styles.tooltipContent}>{content}</p> + </div> + )} + </FloatingPortal> + )} + </> + ); +}; + +export default InfoTooltip; diff --git a/src/themes.css b/src/themes.css index dcaaf596..652d44eb 100644 --- a/src/themes.css +++ b/src/themes.css @@ -51,6 +51,8 @@ --green: #228822; --green-bright: rgb(76, 207, 92); --icon: #c6c6c6; + --info-tooltip-text: #bfbfbf; + --info-tooltip-border-color: #3e3e3e; --link: #bfbfbf; --link-primary: rgb(125, 175, 216); --link-visited: #757575; @@ -151,6 +153,8 @@ --green: #228822; --green-bright: #3bc54c; --icon: #c6c6c6; + --info-tooltip-text: #333; + --info-tooltip-border-color: gray; --link: #0000ff; --link-primary: #369; --link-visited: #551a8b; From 704c2fe7799602929d8be65ef5f90da63af22c40 Mon Sep 17 00:00:00 2001 From: plebeius <tom@plebbit.com> Date: Tue, 15 Jul 2025 23:19:44 +0200 Subject: [PATCH 10/12] add smooth fade and slide animations to info tooltip --- .../info-tooltip/info-tooltip.module.css | 53 +++++++++++++++---- src/components/info-tooltip/info-tooltip.tsx | 51 +++++++++++++++--- 2 files changed, 89 insertions(+), 15 deletions(-) diff --git a/src/components/info-tooltip/info-tooltip.module.css b/src/components/info-tooltip/info-tooltip.module.css index e0d58f66..ddd21998 100644 --- a/src/components/info-tooltip/info-tooltip.module.css +++ b/src/components/info-tooltip/info-tooltip.module.css @@ -1,28 +1,63 @@ .tooltipIcon { cursor: help; + margin-left: 0.5em; } .tooltip { - background: var(--background); - color: var(--info-tooltip-text); - border: 1px solid var(--info-tooltip-border-color); - padding: 3px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25); + /* Just handles positioning from floating-ui */ z-index: 100; - transition: opacity 0.15s ease-in-out; /* Default triangle position as fallback */ --triangle-left-border: 8px; --triangle-left-fill: 9px; } +.tooltipInner { + /* Enhanced animation with both opacity and transform */ + animation: tooltipFadeIn 0.1s ease-out forwards; + /* Move all visual styles here so they animate together */ + background: var(--background); + color: var(--info-tooltip-text); + border: 1px solid var(--info-tooltip-border-color); + padding: 3px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25); +} + +.tooltip.exiting .tooltipInner { + animation: tooltipFadeOut 0.1s ease-in forwards; +} + +/* Fade in animation: slides down from above while fading in */ +@keyframes tooltipFadeIn { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Fade out animation: slides up while fading out */ +@keyframes tooltipFadeOut { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(-6px); + } +} + .tooltipContent { white-space: pre-wrap; font: 10px verdana, arial, helvetica, sans-serif; - color: var(--spoiler-tooltip-text); + color: var(--info-tooltip-text); margin: .5em; } -.tooltip::before { +.tooltipInner::before { content: ''; position: absolute; top: -20px; @@ -32,7 +67,7 @@ z-index: 1; } -.tooltip::after { +.tooltipInner::after { content: ''; position: absolute; top: -18px; diff --git a/src/components/info-tooltip/info-tooltip.tsx b/src/components/info-tooltip/info-tooltip.tsx index d0f7aae2..88410266 100644 --- a/src/components/info-tooltip/info-tooltip.tsx +++ b/src/components/info-tooltip/info-tooltip.tsx @@ -1,4 +1,4 @@ -import { useState, useLayoutEffect } from 'react'; +import { useState, useLayoutEffect, useRef } from 'react'; import { useFloating, autoUpdate, offset, shift, size, useHover, useFocus, useDismiss, useRole, useInteractions, FloatingPortal, safePolygon } from '@floating-ui/react'; import styles from './info-tooltip.module.css'; @@ -9,10 +9,38 @@ interface InfoTooltipProps { const InfoTooltip = ({ content, showTooltip = true }: InfoTooltipProps) => { const [isOpen, setIsOpen] = useState(false); + const [isVisible, setIsVisible] = useState(false); + const [isExiting, setIsExiting] = useState(false); + const exitTimeoutRef = useRef<NodeJS.Timeout>(); + + // Handle opening and closing with animations + const handleOpenChange = (open: boolean) => { + if (open) { + // Opening: show immediately and set open state + setIsVisible(true); + setIsOpen(true); + setIsExiting(false); + // Clear any pending exit timeout + if (exitTimeoutRef.current) { + clearTimeout(exitTimeoutRef.current); + exitTimeoutRef.current = undefined; + } + } else { + // Closing: start exit animation, then hide after animation completes + setIsOpen(false); + setIsExiting(true); + + // Remove from DOM after exit animation completes (200ms) + exitTimeoutRef.current = setTimeout(() => { + setIsVisible(false); + setIsExiting(false); + }, 200); + } + }; const { refs, floatingStyles, context } = useFloating({ open: isOpen, - onOpenChange: setIsOpen, + onOpenChange: handleOpenChange, placement: 'bottom-start', whileElementsMounted: autoUpdate, middleware: [ @@ -48,7 +76,7 @@ const InfoTooltip = ({ content, showTooltip = true }: InfoTooltipProps) => { const hover = useHover(context, { move: false, - delay: { open: 200, close: 0 }, + delay: { open: 200, close: 1000 }, handleClose: safePolygon(), }); const focus = useFocus(context); @@ -57,6 +85,15 @@ const InfoTooltip = ({ content, showTooltip = true }: InfoTooltipProps) => { const { getReferenceProps, getFloatingProps } = useInteractions([hover, focus, dismiss, role]); + // Clean up timeout on unmount + useLayoutEffect(() => { + return () => { + if (exitTimeoutRef.current) { + clearTimeout(exitTimeoutRef.current); + } + }; + }, []); + return ( <> <sup className={styles.tooltipIcon} ref={refs.setReference} {...getReferenceProps()}> @@ -64,9 +101,11 @@ const InfoTooltip = ({ content, showTooltip = true }: InfoTooltipProps) => { </sup> {showTooltip && ( <FloatingPortal> - {isOpen && ( - <div className={styles.tooltip} ref={refs.setFloating} style={floatingStyles} {...getFloatingProps()}> - <p className={styles.tooltipContent}>{content}</p> + {isVisible && ( + <div className={`${styles.tooltip} ${isExiting ? styles.exiting : ''}`} ref={refs.setFloating} style={floatingStyles} {...getFloatingProps()}> + <div className={styles.tooltipInner}> + <p className={styles.tooltipContent}>{content}</p> + </div> </div> )} </FloatingPortal> From 3a60a61570aa98a8ffddfee164380a97a96d2266 Mon Sep 17 00:00:00 2001 From: plebeius <tom@plebbit.com> Date: Wed, 16 Jul 2025 22:09:34 +0200 Subject: [PATCH 11/12] add wip alert --- src/components/sidebar/sidebar.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/sidebar/sidebar.tsx b/src/components/sidebar/sidebar.tsx index b8a631ee..fb0d80aa 100644 --- a/src/components/sidebar/sidebar.tsx +++ b/src/components/sidebar/sidebar.tsx @@ -54,7 +54,9 @@ const ModeratorsList = ({ roles }: { roles: Record<string, Role> }) => { <div className={styles.listTitle}>{t('moderators')}</div> <ul className={`${styles.listContent} ${styles.modsList}`}> {rolesList.map(({ address }, index) => ( - <li key={index}>u/{Plebbit.getShortAddress(address)}</li> + <li key={index} onClick={() => window.alert('Direct profile links are not supported yet.')}> + u/{Plebbit.getShortAddress(address)} + </li> ))} {/* TODO: https://github.com/plebbit/seedit/issues/274 <li className={styles.listMore}>{t('about_moderation')} »</li> */} From 1ce2dc7935de86984c4cfaa73fe2424c731f32be Mon Sep 17 00:00:00 2001 From: plebeius <tom@plebbit.com> Date: Wed, 16 Jul 2025 22:51:47 +0200 Subject: [PATCH 12/12] add info tooltips --- src/views/settings/wallet-settings/wallet-settings.tsx | 4 ++++ src/views/submit-page/submit-page.tsx | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/views/settings/wallet-settings/wallet-settings.tsx b/src/views/settings/wallet-settings/wallet-settings.tsx index 2b2de629..59500c77 100644 --- a/src/views/settings/wallet-settings/wallet-settings.tsx +++ b/src/views/settings/wallet-settings/wallet-settings.tsx @@ -2,6 +2,7 @@ import { Fragment, useState } from 'react'; import { Account, setAccount, useAccount } from '@plebbit/plebbit-react-hooks'; import styles from './wallet-settings.module.css'; import { Trans, useTranslation } from 'react-i18next'; +import InfoTooltip from '../../../components/info-tooltip'; interface Wallet { chainTicker: string; @@ -184,6 +185,9 @@ const CryptoWalletsForm = ({ account }: { account: Account | undefined }) => { i18nKey='add_wallet' components={{ 1: <button key={`addWalletButton-${walletsArray.length}`} onClick={() => setWalletsArray([...walletsArray, defaultWalletObject])} /> }} /> + <InfoTooltip + content={`Link crypto wallets to your account by proving ownership through signed messages. Communities can verify your token/NFT holdings to grant posting privileges or special access.`} + /> </div> {walletsInputs} </> diff --git a/src/views/submit-page/submit-page.tsx b/src/views/submit-page/submit-page.tsx index f0122a07..9be71940 100644 --- a/src/views/submit-page/submit-page.tsx +++ b/src/views/submit-page/submit-page.tsx @@ -16,6 +16,7 @@ import Markdown from '../../components/markdown'; import Embed from '../../components/post/embed'; import { FormattingHelpTable } from '../../components/reply-form/reply-form'; import styles from './submit-page.module.css'; +import InfoTooltip from '../../components/info-tooltip'; const isAndroid = Capacitor.getPlatform() === 'android'; const isElectron = window.electronApi?.isElectron === true; @@ -75,7 +76,12 @@ const UrlField = () => { {url && isValidURL(url) ? ( <div className={styles.mediaPreview}>{mediaError ? <span className={styles.mediaError}>{t('no_media_found')}</span> : mediaComponent}</div> ) : ( - <div className={styles.description}>{t('submit_url_description')}</div> + <div className={styles.description}> + {t('submit_url_description')} + <InfoTooltip + content={`Seedit also supports links from the following sites: YouTube, Twitter/X, Reddit, Twitch, TikTok, Instagram, Odysee, Bitchute, Streamable, Spotify, and SoundCloud.`} + /> + </div> )} </div> </div>