diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c6426568..9c2b6bad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,22 +20,19 @@ 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='' 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 @@ -62,27 +59,24 @@ 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='' 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 @@ -113,10 +107,12 @@ 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) - run: 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 @@ -151,14 +147,16 @@ 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='' 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/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..ddd21998 --- /dev/null +++ b/src/components/info-tooltip/info-tooltip.module.css @@ -0,0 +1,78 @@ +.tooltipIcon { + cursor: help; + margin-left: 0.5em; +} + +.tooltip { + /* Just handles positioning from floating-ui */ + z-index: 100; + /* 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(--info-tooltip-text); + margin: .5em; +} + +.tooltipInner::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; +} + +.tooltipInner::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..88410266 --- /dev/null +++ b/src/components/info-tooltip/info-tooltip.tsx @@ -0,0 +1,117 @@ +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'; + +interface InfoTooltipProps { + content: string; + showTooltip?: boolean; +} + +const InfoTooltip = ({ content, showTooltip = true }: InfoTooltipProps) => { + const [isOpen, setIsOpen] = useState(false); + const [isVisible, setIsVisible] = useState(false); + const [isExiting, setIsExiting] = useState(false); + const exitTimeoutRef = useRef(); + + // 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: handleOpenChange, + 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: 1000 }, + handleClose: safePolygon(), + }); + const focus = useFocus(context); + const dismiss = useDismiss(context); + const role = useRole(context, { role: 'tooltip' }); + + const { getReferenceProps, getFloatingProps } = useInteractions([hover, focus, dismiss, role]); + + // Clean up timeout on unmount + useLayoutEffect(() => { + return () => { + if (exitTimeoutRef.current) { + clearTimeout(exitTimeoutRef.current); + } + }; + }, []); + + return ( + <> + + [?] + + {showTooltip && ( + + {isVisible && ( +
+
+

{content}

+
+
+ )} +
+ )} + + ); +}; + +export default InfoTooltip; 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 }) => {
{t('moderators')}