diff --git a/.github/workflows/build-render.yml b/.github/workflows/build-render.yml new file mode 100644 index 0000000000..4540694821 --- /dev/null +++ b/.github/workflows/build-render.yml @@ -0,0 +1,75 @@ +name: Build Electron Render + +on: + push: + tags: + - "v*" + +env: + VITE_WEB_URL: ${{ vars.VITE_WEB_URL }} + VITE_API_URL: ${{ vars.VITE_API_URL }} + VITE_IMGPROXY_URL: ${{ vars.VITE_IMGPROXY_URL }} + VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }} + VITE_OPENPANEL_CLIENT_ID: ${{ vars.VITE_OPENPANEL_CLIENT_ID }} + VITE_OPENPANEL_API_URL: ${{ vars.VITE_OPENPANEL_API_URL }} + VITE_FIREBASE_CONFIG: ${{ vars.VITE_FIREBASE_CONFIG }} + NODE_OPTIONS: --max-old-space-size=8192 + +jobs: + build-render: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + + permissions: + id-token: write + contents: write + attestations: write + + steps: + - name: Check out Git repository + uses: actions/checkout@v4 + with: + lfs: true + + - name: Cache pnpm modules + uses: actions/cache@v4 + with: + path: ~/.pnpm-store + key: ${{ matrix.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ matrix.os }}- + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: "pnpm" + + - name: Install dependencies + run: pnpm i + - name: Build + run: pnpm build:render + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + + - name: Setup Version + id: version + uses: ./.github/actions/setup-version + + - name: Create Release Draft + uses: softprops/action-gh-release@v2 + with: + name: v${{ steps.version.outputs.APP_VERSION }} + draft: false + prerelease: true + tag_name: v${{ steps.version.outputs.APP_VERSION }} + files: | + dist/manifest.yml + dist/*.tar.gz diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 882e633196..a5121fe827 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -103,7 +103,8 @@ jobs: - name: Install dependencies run: pnpm i - + - name: Update main hash + run: pnpm update:main-hash - name: Build if: matrix.os != 'macos-latest' run: npm exec turbo run //#build diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index daf79a6348..bf713316f8 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -72,25 +72,20 @@ jobs: CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 PP_PATH=$RUNNER_TEMP/build_pp.provisionprofile KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db - # import certificate and provisioning profile from secrets echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH - # create temporary keychain security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH security set-keychain-settings -lut 21600 $KEYCHAIN_PATH security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - # import certificate to keychain security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH security list-keychain -d user -s $KEYCHAIN_PATH - # apply provisioning profile mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles - - name: Install dependencies run: pnpm i @@ -115,6 +110,9 @@ jobs: fi echo "Updated version to $NIGHTLY_VERSION" + - name: Update main hash + run: pnpm update:main-hash + - name: Build if: matrix.os != 'macos-latest' run: pnpm build @@ -130,6 +128,10 @@ jobs: KEYCHAIN_PATH: ${{ runner.temp }}/app-signing.keychain-db run: pnpm build:macos + - name: Build (Render) + run: pnpm build:render + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - name: Upload artifacts uses: actions/upload-artifact@v4 with: @@ -140,6 +142,9 @@ jobs: out/make/**/*.AppImage out/make/**/*.yml out/make/**/*.dmg + dist/manifest.yml + dist/*.tar.gz + retention-days: 7 - name: Generate artifact attestation @@ -152,6 +157,8 @@ jobs: out/make/**/*.exe out/make/**/*.AppImage out/make/**/*.yml + dist/manifest.yml + dist/*.tar.gz - name: Create Nightly Release uses: softprops/action-gh-release@v2 @@ -159,16 +166,19 @@ jobs: name: Nightly ${{ env.NIGHTLY_VERSION }} draft: false prerelease: true - tag_name: nightly-${{ env.NIGHTLY_VERSION }} + tag_name: ${{ env.NIGHTLY_VERSION }} files: | out/make/**/*.dmg out/make/**/*.zip out/make/**/*.exe out/make/**/*.AppImage out/make/**/*.yml + dist/manifest.yml + dist/*.tar.gz + body: | This is an automated nightly release for testing purposes. - Version: 0.0.0-nightly.${{ env.NIGHTLY_VERSION }} + Version: ${{ env.NIGHTLY_VERSION }} **Warning:** This build may be unstable and is not recommended for production use. env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a0b929acc..645a3c9228 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # CHANGELOG -## [0.2.3-beta.0](https://github.com/RSSNext/follow/compare/v0.2.0-beta.2...v0.2.3-beta.0) (2024-11-21) +## [0.2.5-beta.0](https://github.com/RSSNext/follow/compare/v0.2.0-beta.2...v0.2.5-beta.0) (2024-11-28) ### Bug Fixes @@ -8,10 +8,14 @@ * determining app is updated logic ([f61dc56](https://github.com/RSSNext/follow/commit/f61dc5664635a5d01be66da27682774f2c73b494)) * `defineSettingPageData` remove func wrapper ([d0d4f05](https://github.com/RSSNext/follow/commit/d0d4f0564715c1786e71b0f009e86aa3a9dc4b7c)) * action does not take effect after saving ([c27665d](https://github.com/RSSNext/follow/commit/c27665df92cf007600f710a108cfa2edaef9b5c2)), closes [#1600](https://github.com/RSSNext/follow/issues/1600) +* active state on mobile ([ad3a4cb](https://github.com/RSSNext/follow/commit/ad3a4cb556908d4cdff3a1e46150c5d73849fef3)) * add a workaround type check for tipUsers to ensure it's an array ([#1531](https://github.com/RSSNext/follow/issues/1531)) ([5fed7c8](https://github.com/RSSNext/follow/commit/5fed7c862982ca3423b08e83462e04d2487f5bbb)) * add changelog ([4de6355](https://github.com/RSSNext/follow/commit/4de6355bb77ae7b00416be6827a5b71a3a724dfa)) * add cleaner cron impl ([faae74b](https://github.com/RSSNext/follow/commit/faae74b8ba465a240bcdd40de33e446bc0799c41)) +* add deps for integration cmds ([#1696](https://github.com/RSSNext/follow/issues/1696)) ([1353d4b](https://github.com/RSSNext/follow/commit/1353d4bfc552c20fdcfc9723a4cafb9ef47c769d)) +* add feed and feed list overlap ([6395d22](https://github.com/RSSNext/follow/commit/6395d223750d06d6a73e8bef4f5081cdf8d7c46a)) * add manifest to purge cache ([2f8dc0b](https://github.com/RSSNext/follow/commit/2f8dc0bc8561ebf9bb40160de8658eac799b31b0)) +* add minify ([555fc4c](https://github.com/RSSNext/follow/commit/555fc4cde9c8cd9c7fc911f5a27a1ce8c84f269c)) * add missing event bus ([33b3ac4](https://github.com/RSSNext/follow/commit/33b3ac4c53faa617080cbcf3039cbee7fdeb1931)) * add rsshub discover error boundary ([665c009](https://github.com/RSSNext/follow/commit/665c009bf8aec87ead601428410f56ba6756a8db)) * add Supsense wrapper for app upgrade provider ([58f1791](https://github.com/RSSNext/follow/commit/58f1791526620f470aa11c776fd7b245d54c45ab)) @@ -25,9 +29,12 @@ * allow selecting when meta key is pressed ([6b8572c](https://github.com/RSSNext/follow/commit/6b8572c37aa287aa544e6ddb2847693ba597b0be)) * always exclude routeParams ([1466886](https://github.com/RSSNext/follow/commit/1466886c426095aa59bbc06f93a44245bfa3b480)) * app notification font size ([1b5f944](https://github.com/RSSNext/follow/commit/1b5f9448062bde224770dfe5129304d7a812585d)) +* app update logic ([9680351](https://github.com/RSSNext/follow/commit/9680351f2c5f6c6ca6542396a722719a69fa5064)) * app upgrade toast open link should in new window ([fc81546](https://github.com/RSSNext/follow/commit/fc81546bed6b07626215a57543a735001728f520)) * app upgraded toast href error ([#1444](https://github.com/RSSNext/follow/issues/1444)) ([c875d69](https://github.com/RSSNext/follow/commit/c875d697b2a1cd9e49f3c91397531a82928e1bc8)) * assets rewrite ([#1204](https://github.com/RSSNext/follow/issues/1204)) ([488e8ef](https://github.com/RSSNext/follow/commit/488e8effc6f00769df7dbf68d4e6d1e4e950b8e7)) +* audio cover style ([21a93c7](https://github.com/RSSNext/follow/commit/21a93c757dfbc161d67f28500f0129bde1959662)) +* audio player on mobile ([1e936e2](https://github.com/RSSNext/follow/commit/1e936e2c593adafa92d54ddc542ff30688c0e3bc)) * auto archived list flash ([#1269](https://github.com/RSSNext/follow/issues/1269)) ([8d2478c](https://github.com/RSSNext/follow/commit/8d2478c6a93aa9f5a99fddc55bbb8e532e1a0fe1)) * auto load achieved entries when show achieved button is available ([074a60d](https://github.com/RSSNext/follow/commit/074a60d3cf93bcede88ea4d70b07cf7480be4229)) * **boost:** card text align center ([632d5d4](https://github.com/RSSNext/follow/commit/632d5d42d67999523e602fb18f59ca72543b7d7a)) @@ -36,7 +43,10 @@ * bump config ([117bd1b](https://github.com/RSSNext/follow/commit/117bd1ba08e4c287dc071669614567197e470d12)) * bump hook ([67e234d](https://github.com/RSSNext/follow/commit/67e234de8b6e229187be8b4318926f31c25e18c1)) * can not exit entry by close button ([e654c3a](https://github.com/RSSNext/follow/commit/e654c3ac165e5eb276ba51dc4ae0ab2da230a9d1)), closes [#1311](https://github.com/RSSNext/follow/issues/1311) +* can not keep text selection, fixed [#1895](https://github.com/RSSNext/follow/issues/1895) ([4330a69](https://github.com/RSSNext/follow/commit/4330a693968abe16eeb0908943e643890a7b26a5)) * can not play bilibili video ([11ff813](https://github.com/RSSNext/follow/commit/11ff813c450fb519292b187a15236abe7d900abb)), closes [#1376](https://github.com/RSSNext/follow/issues/1376) +* center dialog ([4a1ae06](https://github.com/RSSNext/follow/commit/4a1ae0668855b24513cf85b2a6527edf20c2da47)) +* changelog ([f84aa0b](https://github.com/RSSNext/follow/commit/f84aa0b0cfdc350f26e79ce68c75788cc090321d)) * **ci:** concurrency group ([fb0152e](https://github.com/RSSNext/follow/commit/fb0152eaec76b28ed9fc26381917ca7ec2aecb0b)) * **ci:** forge build error ([#1500](https://github.com/RSSNext/follow/issues/1500)) ([a5e3e02](https://github.com/RSSNext/follow/commit/a5e3e0228c7088e78061801466267ebdbd288e2c)) * **ci:** turbo not found ([c812bd7](https://github.com/RSSNext/follow/commit/c812bd71aabc63b5c9ebbe032f25aba5ac51bd64)) @@ -48,10 +58,12 @@ * **cmdk:** change default cmdk search type to feed ([4a8fdea](https://github.com/RSSNext/follow/commit/4a8fdea84a4df90313f8c577d77d3944a34aa149)) * condition key ([ffb3b94](https://github.com/RSSNext/follow/commit/ffb3b943b0c502cf940d7f3c0d4d506489b65d37)) * context menu for multi select ([d68de79](https://github.com/RSSNext/follow/commit/d68de7933235351d64df68e3e2c49623184f4284)) +* corner player style on safari ([e740d4d](https://github.com/RSSNext/follow/commit/e740d4da78032547c48014a31a25968fe1ad192f)) * correct z-index in toast ([6507744](https://github.com/RSSNext/follow/commit/6507744847b6ceee093adb17076292993d525313)) * csrf token singleton ([b4e935f](https://github.com/RSSNext/follow/commit/b4e935fba0b2f874a5c24006d5afdd54f50f3e76)) * csrf token singleton ([#1463](https://github.com/RSSNext/follow/issues/1463)) ([16b5349](https://github.com/RSSNext/follow/commit/16b5349c3d4461356ef124c10df8c117fd3bac81)) * css editor lazy and input composition handler ([5c6004a](https://github.com/RSSNext/follow/commit/5c6004a0905c6207c6119de0d37e9a5baf744dab)) +* **css-editor:** Compatible with browsers that do not support plaintext-only ([5824921](https://github.com/RSSNext/follow/commit/58249210567c00a6e56b129cebe7cb319555347a)) * **css-editor:** handle copy and paste get the plain text ([febc678](https://github.com/RSSNext/follow/commit/febc678834d2e0a520e4c223dfb7f1feb8c49a77)) * ctx sub menu z-index higher than root ([8daec12](https://github.com/RSSNext/follow/commit/8daec12d2c1e61260b8917ee43066b752d4c9f06)) * debug html ([e9afc79](https://github.com/RSSNext/follow/commit/e9afc7944f4dc42be2ea530b4852e9c2f6207ac8)) @@ -60,6 +72,7 @@ * delete condition item ([8b9750d](https://github.com/RSSNext/follow/commit/8b9750da1219f3d046b78c70c1966e284b086348)) * delete last condition ([72d894a](https://github.com/RSSNext/follow/commit/72d894a754ba3a0c3b6c4e030b3b274dc98de6b6)) * detecting windows11 ([#1170](https://github.com/RSSNext/follow/issues/1170)) ([2813f1d](https://github.com/RSSNext/follow/commit/2813f1d6540fcb215287dd32cbc0f8cf87cb1cb0)) +* **devtool:** fix devtool font in windows ([2b64659](https://github.com/RSSNext/follow/commit/2b6465921c23d2357413423c27da89fe7bd06568)) * dialog did not close after confirmation ([#1628](https://github.com/RSSNext/follow/issues/1628)) ([6a5ec1b](https://github.com/RSSNext/follow/commit/6a5ec1b044f35e7ffa537436d0a7caba2f8d488e)) * disable `pointerDownOutside` trigger `onDismiss` ([#1215](https://github.com/RSSNext/follow/issues/1215)) ([2bf5245](https://github.com/RSSNext/follow/commit/2bf5245126bc9ef890c06afe5a825d1e1ab4059c)) * disable auto load archive for inbox and list ([7825e16](https://github.com/RSSNext/follow/commit/7825e16b6d1230ba2c13e5ef00474fc05cc510e5)) @@ -72,6 +85,7 @@ * do not exclude required path params ([f6cbace](https://github.com/RSSNext/follow/commit/f6cbacec853dd8aa901c93c8c86820bbbf1f8232)), closes [#1623](https://github.com/RSSNext/follow/issues/1623) * do not update action setting for archived query ([4adcded](https://github.com/RSSNext/follow/commit/4adcded02179b8d42f1a906b75b10fd403438f8d)) * don't cache user session ([e8d7e6c](https://github.com/RSSNext/follow/commit/e8d7e6ce64d09bb1f2b8f3e26f7aba250c3cca84)) +* downgrade radix scrollview ([199e0bc](https://github.com/RSSNext/follow/commit/199e0bc7c514822233c42f048691926f4f7cb190)) * **dx:** merge script and fix host rewrite ([3fa3bd5](https://github.com/RSSNext/follow/commit/3fa3bd5fc5b730421f6abc4dd2938beea67322dc)) * dynamic import dexie-export-import ([48aea7b](https://github.com/RSSNext/follow/commit/48aea7b88e4d72d20e52ce7a5037ef239d80611d)) * enhance tray icon visual effect in Windows platform ([64c891f](https://github.com/RSSNext/follow/commit/64c891f30c27ff1c868a4ba8fb88adf2acda7dfd)) @@ -83,6 +97,7 @@ * escape for seo meta tags ([9564257](https://github.com/RSSNext/follow/commit/9564257564e417e18bdeedded9397d469e6daf14)), closes [#1232](https://github.com/RSSNext/follow/issues/1232) * explicitly assign a value to `cancelId` for ‘Clear All Data' dialog ([#1624](https://github.com/RSSNext/follow/issues/1624)) ([fdb87cf](https://github.com/RSSNext/follow/commit/fdb87cf5cdf9623f16eafa7e9ae25f4fc7950e05)) * **external:** add global 404 page ([5e49b81](https://github.com/RSSNext/follow/commit/5e49b8114e0446b734a54eea3291721d8f9656b3)) +* **external:** continue in browser should reload page ([a9569cc](https://github.com/RSSNext/follow/commit/a9569cc2ec41f54d0a2ab07a2b99e0e2cde373cb)) * **external:** header logo href ([b867af9](https://github.com/RSSNext/follow/commit/b867af9b45de0124e04ee95ec4771b9f35903951)) * **external:** remove duplicate border bottom ([32f68ee](https://github.com/RSSNext/follow/commit/32f68ee1987182271295292bff92f482c151b78b)) * **external:** set auth config first ([ef46c70](https://github.com/RSSNext/follow/commit/ef46c70a9849c315cc8d827f4ad8a3c004003bac)) @@ -90,6 +105,9 @@ * fallback for code can not render to html ([08acded](https://github.com/RSSNext/follow/commit/08acded2d549bc80d2ed8f180a6d89cc3a3c861a)), closes [#1142](https://github.com/RSSNext/follow/issues/1142) * feed action in selection ([ef9db5c](https://github.com/RSSNext/follow/commit/ef9db5c8b39c8229b1e907c6a0e6e74431f677de)) * feed boost modal title wrap ([a3eb287](https://github.com/RSSNext/follow/commit/a3eb2875e9456e8ec30ed96294b2bad82ccd5a4d)) +* feed icon fallback delay ms ([89239d8](https://github.com/RSSNext/follow/commit/89239d89aa52429eef348e500c7ed7d6170c766a)) +* feed icon margin right ([0f32364](https://github.com/RSSNext/follow/commit/0f3236406dc354adc9830c51e73f9fb4790f159a)) +* feedIcon margin ([03ce13d](https://github.com/RSSNext/follow/commit/03ce13d45bd89908bd9bcb06484a07873b6388f5)) * fill height when zooming ([75e0da2](https://github.com/RSSNext/follow/commit/75e0da2a866a7789606bd40d484fc29973fd9e0a)) * float sidebar missing background ([b9982e0](https://github.com/RSSNext/follow/commit/b9982e0a6b44f476621daf30f93f8e740a544a02)) * follow external server fetch ua ([072dec0](https://github.com/RSSNext/follow/commit/072dec02b787bcdd8af7f5c0bf25398e2da8c15e)) @@ -99,18 +117,24 @@ * handling default port in proxy configuration ([0bd76a2](https://github.com/RSSNext/follow/commit/0bd76a24a240951782d44b7e38040c305bb2b9b9)) * hide cache control in web ([961bd0f](https://github.com/RSSNext/follow/commit/961bd0fccd63e7e9a21c9bea62cd426bac9a8423)) * hide to tray on close window ([392135b](https://github.com/RSSNext/follow/commit/392135b8c4b4ec00aa49e66b28bcb0e77390683a)) +* hide viewSourceContent on mobile ([#1711](https://github.com/RSSNext/follow/issues/1711)) ([86caeb6](https://github.com/RSSNext/follow/commit/86caeb6df42451a8bf8dbb9092d7e0aa8fdd1535)) * hot key switch view ([45d0fd9](https://github.com/RSSNext/follow/commit/45d0fd914f374bb7217e9a48c48bc1df4a5a8c6e)) * html table to markdown error ([#1288](https://github.com/RSSNext/follow/issues/1288)) ([8d1ca24](https://github.com/RSSNext/follow/commit/8d1ca24f18859f5651559c77cef6fc30ce228f00)) +* **i18n:** improve translations and modal titles for feeds ([#1892](https://github.com/RSSNext/follow/issues/1892)) ([4186c89](https://github.com/RSSNext/follow/commit/4186c898c690e241e361000841a278e65b2f925f)) * **i18n:** remove `external` ns in renderer ([229b3e1](https://github.com/RSSNext/follow/commit/229b3e18f50c81c916f877a2a65325e3dcaa783a)) +* **i18n:** update Japanese translations for better clarity ([#1842](https://github.com/RSSNext/follow/issues/1842)) ([bf9fb22](https://github.com/RSSNext/follow/commit/bf9fb2289ae7d6c553ec10ba28cab9f04e9f1bcd)) * **i18n:** update label for notification badge settings in zh-CN locale and others ([#1455](https://github.com/RSSNext/follow/issues/1455)) ([ec82f03](https://github.com/RSSNext/follow/commit/ec82f036b44a3d0d463c48c1d793fc517def1d07)) * ignore `file` and editor protocols ([2badb9b](https://github.com/RSSNext/follow/commit/2badb9b1c0bf6628d2a2fc177063fd0c7c7fb08b)) * immer object extensible ([6ad35ad](https://github.com/RSSNext/follow/commit/6ad35ad6712a68db9e231bf5733a2956ccecda90)) * improve multi select behavior ([24017df](https://github.com/RSSNext/follow/commit/24017dfb3190c91969ec68d3d88544e051880469)) +* improve perform for feed column ([#1708](https://github.com/RSSNext/follow/issues/1708)) ([1f349ed](https://github.com/RSSNext/follow/commit/1f349ed73e912825fb34877f5db9df13c6c6f879)) * initializeDayjs in server ([d408581](https://github.com/RSSNext/follow/commit/d408581a13ad78d5a871d98f3ff0ff9bb5e45ae5)), closes [#1198](https://github.com/RSSNext/follow/issues/1198) * inline image style ([1d0eca1](https://github.com/RSSNext/follow/commit/1d0eca1ca16621634d0d0957b1d4d6c5f037498c)), closes [#1318](https://github.com/RSSNext/follow/issues/1318) * input and toast font style use user custom font ([#1322](https://github.com/RSSNext/follow/issues/1322)) ([c40becf](https://github.com/RSSNext/follow/commit/c40becff782e129c42984b354f2004b82c7f2615)) +* lint ([dafaf0a](https://github.com/RSSNext/follow/commit/dafaf0a97d40a6df659a7869d7c11f5cf529dfe7)) * load instagram image fail ([f8fd58f](https://github.com/RSSNext/follow/commit/f8fd58f78ec11bc221eb9190ac7b6a66dd858f9c)), closes [#1539](https://github.com/RSSNext/follow/issues/1539) * localeCompare is undefined ([ab28156](https://github.com/RSSNext/follow/commit/ab28156cb8bd998ec1bc6b43b9a44bd2ecd17137)) +* mark feed unread dirty, refetch unread next time, fixed [#1830](https://github.com/RSSNext/follow/issues/1830) ([58d0e9c](https://github.com/RSSNext/follow/commit/58d0e9c1902157c2e9fb1444bc1c0db0b775485b)) * mark single feed as all read ([a4acd2f](https://github.com/RSSNext/follow/commit/a4acd2fb65335f228875ed5b38f03a9c71b76e1b)) * md lint ([c8e556a](https://github.com/RSSNext/follow/commit/c8e556a3ac861cab4a4a7aff1aa2fa51aaca1f42)) * missing site url in feed selector ([ea677ac](https://github.com/RSSNext/follow/commit/ea677ac6a3a43489a9508a32ecb22fa38ecf4f27)) @@ -121,19 +145,28 @@ * **modal:** fixed close modal button position ([78e9424](https://github.com/RSSNext/follow/commit/78e942473fff309e372708b56a59a9009d5bb0e9)) * **modal:** modal dismiss action ([b05ff50](https://github.com/RSSNext/follow/commit/b05ff50f14660f20ed881f6aeabb732d80c9539c)) * **modal:** repeat user profile dialog ([#1155](https://github.com/RSSNext/follow/issues/1155)) ([ad86372](https://github.com/RSSNext/follow/commit/ad86372bfb345b6f921da783c401be30cdbe4b89)) +* motion button motion style in mobile ([611d2cf](https://github.com/RSSNext/follow/commit/611d2cf0349be360aa86601ed4eb0522118bad49)) +* multi select state ([f476483](https://github.com/RSSNext/follow/commit/f476483fe3b944371c0327495d53ec3d42006dda)) * not showing feed certification icon on the sidebar ([#1442](https://github.com/RSSNext/follow/issues/1442)) ([b395891](https://github.com/RSSNext/follow/commit/b395891b66a581254f24aae75d0d61869fb6fb33)) * omit cacheDimensions from img attributes in MediaImpl ([ec12b35](https://github.com/RSSNext/follow/commit/ec12b3538f42c8596c4e700ae2284a02b4d7791e)) * only show ComboboxOptions when there are suggestions ([136aa20](https://github.com/RSSNext/follow/commit/136aa205794b6193826d1839350e65228d2b4f20)) +* only use discussion for feed error ([e30783b](https://github.com/RSSNext/follow/commit/e30783bfaaa9e40815493d28b49ba7c41ac86414)) * optimize print mode ([6f1eb26](https://github.com/RSSNext/follow/commit/6f1eb26eb110b8ad061473e05c451debc9c69074)) * optimize some styles ([9b99bf4](https://github.com/RSSNext/follow/commit/9b99bf4428cf6f1ca3218a3074a944af4d014f57)) * optimize the animation of the Follow button on the User Profile Modal ([#1156](https://github.com/RSSNext/follow/issues/1156)) ([87717d6](https://github.com/RSSNext/follow/commit/87717d642607cca4254f11d673a8a910fc6ab076)) +* package export path for tsconfig ([#1690](https://github.com/RSSNext/follow/issues/1690)) ([00826ae](https://github.com/RSSNext/follow/commit/00826aecccac8dfcedba571ac02af1e3e1911c31)) * pass a component for Trans ([7d38cb6](https://github.com/RSSNext/follow/commit/7d38cb6d2f814d3b6012b66ba6024c05b6edcf2d)) +* player cover height ([5486edd](https://github.com/RSSNext/follow/commit/5486eddd640bd5b72a174946ff804531e6e4b3d3)) +* player margin safe ([1182497](https://github.com/RSSNext/follow/commit/1182497f6becb14a2954b079d9b47440ed071ef1)) +* player show state ([7d034e0](https://github.com/RSSNext/follow/commit/7d034e0d355a02c14c368bb0f8a4737b5219cfc7)) * pnpm lockfile ([fdc547a](https://github.com/RSSNext/follow/commit/fdc547a5aaeb11aa13d6321ea60f1d3a4e01a8b4)) * polling unread and add ua for electron api client ([549f10d](https://github.com/RSSNext/follow/commit/549f10d7ebb9e0eca9faa4d9cb27b7e448a8af0f)) * power icon align ([11059f6](https://github.com/RSSNext/follow/commit/11059f639ee955a0f10cbd97cb6eed85e335fed7)) * preferred title not working in entry column ([#1268](https://github.com/RSSNext/follow/issues/1268)) ([4bdf28e](https://github.com/RSSNext/follow/commit/4bdf28ea766242c74af051e36a9ef205f02eb874)) +* prevent default click behavior in Media ([#1905](https://github.com/RSSNext/follow/issues/1905)) ([97dee6e](https://github.com/RSSNext/follow/commit/97dee6eda40e63dec2e66aa20da31a5d1ceed875)) * prevent default for cmd+k ([81d49f0](https://github.com/RSSNext/follow/commit/81d49f0893100de85a22d114765f76e7c9dea36c)) * prevent default scrolling behavior while using arrow keys to switch between entries ([#1447](https://github.com/RSSNext/follow/issues/1447)) ([ed5ee50](https://github.com/RSSNext/follow/commit/ed5ee50fe2676309e5451d73531baf59bbf0d746)) +* prevent overscroll bounce in some scene ([11803c8](https://github.com/RSSNext/follow/commit/11803c84b38a219d40cc0c97a0e6ec2c826dbd81)) * prevent right cilck on content menu ([#1525](https://github.com/RSSNext/follow/issues/1525)) ([ca6428f](https://github.com/RSSNext/follow/commit/ca6428f21926524d08c8aae121e9c705a147540e)) * prevent withdrawal of zero amount to avoid unnecessary fees ([#1422](https://github.com/RSSNext/follow/issues/1422)) ([6584526](https://github.com/RSSNext/follow/commit/65845268f9940247d3b645b1588d1564ec0b4cff)) * **profile-menu:** limit max-char ([8a72a97](https://github.com/RSSNext/follow/commit/8a72a97c0ce145a65583fafe792ae99a634c52c3)) @@ -144,16 +177,22 @@ * remove `useSingleton` ([d1d3fcf](https://github.com/RSSNext/follow/commit/d1d3fcf493bf26ce58cd33fedd7320cfcfe8ef5f)) * remove cache-control header ([#1202](https://github.com/RSSNext/follow/issues/1202)) ([c683bda](https://github.com/RSSNext/follow/commit/c683bdaef63716659ca78875022ff38350e50b82)) * remove callbackUrl in electron ([10a217e](https://github.com/RSSNext/follow/commit/10a217e8c1262a7ab695f65e21087d96169930bb)) +* remove corner player in entry column ([fdb466b](https://github.com/RSSNext/follow/commit/fdb466bf4ba6bc5a2af9a917e855cacca4f93657)) * remove gcore icon ([#1220](https://github.com/RSSNext/follow/issues/1220)) ([486fa20](https://github.com/RSSNext/follow/commit/486fa202ee9488981a733f235f95c90a073e7166)) * remove hardcode minfest ([0432618](https://github.com/RSSNext/follow/commit/04326186b71dc840e1454e6a1573257c97db32c8)) * remove immer set to avoid object extensible ([7e5a791](https://github.com/RSSNext/follow/commit/7e5a79155a083a8242579dee18d019049b7ad6f5)) +* remove prev entries ids ref ([996629c](https://github.com/RSSNext/follow/commit/996629c14c752f76b65d423ebc23d60b315dee77)) * remove skeleton when app load but in 404 ([54d4e52](https://github.com/RSSNext/follow/commit/54d4e529c796e6cc22c36b2a9a1e949b1dad5b37)) * remove vercel header rewrite ([#1463](https://github.com/RSSNext/follow/issues/1463)) ([#1480](https://github.com/RSSNext/follow/issues/1480)) ([94971da](https://github.com/RSSNext/follow/commit/94971dad4e1d828aa4c90d8f02445cc04a8500c9)) * remove vercel header rewrite for assets ([79e6f48](https://github.com/RSSNext/follow/commit/79e6f483265216773e0ccf5ac0690d06300d3e80)) * remove wdyr in prod ([70e3b72](https://github.com/RSSNext/follow/commit/70e3b72b0ae88a722e2ff9e7d6027003f88829e2)) +* replace Avatar component with UserAvatar in ranking and transaction sections ([#1759](https://github.com/RSSNext/follow/issues/1759)) ([52c3695](https://github.com/RSSNext/follow/commit/52c3695355ed3962d9ad53e61a612d88c54ab650)) * response error toast style ([7748588](https://github.com/RSSNext/follow/commit/77485886089e9faea54cf80a96a48a0bedd2a39e)) +* revalidate when switch from subscription startupScreen ([e63c12c](https://github.com/RSSNext/follow/commit/e63c12c27227341a2477eb06fa039e2b0a2a00e5)) +* revert merge chunk ([4a9679b](https://github.com/RSSNext/follow/commit/4a9679bad109a786b80939ca9ead32f1505be186)) * scroll bar z index ([5057999](https://github.com/RSSNext/follow/commit/50579995367b871e84768c5983b3302f75f8e077)), closes [#1233](https://github.com/RSSNext/follow/issues/1233) * selecting state ([146fe8f](https://github.com/RSSNext/follow/commit/146fe8fa5ccf4c6211bb5d72be9efbb0714de878)) +* server styles ([ff6b0f7](https://github.com/RSSNext/follow/commit/ff6b0f780d80d43bb974736f8579f70a45e1be61)) * **server:** complie esmodule ([c438119](https://github.com/RSSNext/follow/commit/c438119ea8208f4da14da2294a381a6c34a14e25)) * **server:** serialize data ([2410b84](https://github.com/RSSNext/follow/commit/2410b843af3d16f77e88482d9c73f25c1dd3431a)) * **server:** use `xss` to serialize data ([93f58ec](https://github.com/RSSNext/follow/commit/93f58ecfd8fadfa67f56ebfe629e73f44bb94a49)) @@ -170,36 +209,51 @@ * shouldLoadArchivedEntries condition ([94b097f](https://github.com/RSSNext/follow/commit/94b097fd25533b5d1437bf1054dbe44af7828099)) * show action limit message ([2ac52eb](https://github.com/RSSNext/follow/commit/2ac52ebda3b4cb06e00ce2759f2cac7cd5d86f3d)), closes [#1360](https://github.com/RSSNext/follow/issues/1360) * show correct available actions ([4d2f88b](https://github.com/RSSNext/follow/commit/4d2f88b156770ffb1348e2f216b04c9b1a0cc09a)) +* show duration on mobile ([59709f0](https://github.com/RSSNext/follow/commit/59709f0200396b98411ee97905e53745c90b8fdb)) * show scrollbar when list table overflows in settings menu ([#1266](https://github.com/RSSNext/follow/issues/1266)) ([47b200f](https://github.com/RSSNext/follow/commit/47b200f7f3340b5b262674c71e876328ec758192)) * showArchivedButton in PictureMasonry ([07b075a](https://github.com/RSSNext/follow/commit/07b075ad4587919729790ca5747ba32d5e833cc1)), closes [#1583](https://github.com/RSSNext/follow/issues/1583) * sidebar copy feed url, fixed [#1361](https://github.com/RSSNext/follow/issues/1361) ([4e84196](https://github.com/RSSNext/follow/commit/4e841964e6573fa20ae214538b04dd0dc18a6f5e)) * smooth step ([f32cda6](https://github.com/RSSNext/follow/commit/f32cda6240d474a51bbaaf0efc1baf2fe5f36d03)) +* **ssr-server:** fix hydrate data ([185bbe1](https://github.com/RSSNext/follow/commit/185bbe1dd9dd34335b3021766559338539b344a7)) +* **ssr-server:** minify code result should await ([1c5cd04](https://github.com/RSSNext/follow/commit/1c5cd0484636f1415c99c17feb39af2584486ee8)) +* **ssr-server:** try to hydrate date charset ([f21f3ee](https://github.com/RSSNext/follow/commit/f21f3ee33b82404ac6ffbecb2ef6542430ebbc3d)) * **store:** fix selector fallback value ([0ca5abc](https://github.com/RSSNext/follow/commit/0ca5abc3bee1c15d176d02352ed4f675857311a7)) * **store:** immer draft should to raw ([3a3992d](https://github.com/RSSNext/follow/commit/3a3992d63f0f62cd37aa5accb4910ad79e3190e8)) * **store:** singleton store guard ([5a8725b](https://github.com/RSSNext/follow/commit/5a8725b50767bb6faddca6e0af6cef2cfb28da29)) * **stream:** prevent request if no ids ([a92ef49](https://github.com/RSSNext/follow/commit/a92ef49dedf480f519525cf8f25a0e5383b27059)) +* style for audio item ([7e4653f](https://github.com/RSSNext/follow/commit/7e4653f01cc987614843db06871b2d3bd30221f5)) +* sync active view after close sheet ([6f751a4](https://github.com/RSSNext/follow/commit/6f751a4a21796652d8cfc38b6209d7bfb4016ade)) +* sync unread when fetch unread ([196b717](https://github.com/RSSNext/follow/commit/196b717a7fbdd362d9c2128223272c6616363bc5)) +* text wrap for feed title ([f123e5c](https://github.com/RSSNext/follow/commit/f123e5c0555976b496a5e79f3d5c7937984ebdeb)) +* text wrap not always ([3cb6364](https://github.com/RSSNext/follow/commit/3cb6364462a3c738af3a7b86eae17513ccb6705c)) * the volume bar can click and adjust style. ([#1474](https://github.com/RSSNext/follow/issues/1474)) ([c409a5c](https://github.com/RSSNext/follow/commit/c409a5c7945eb178fca5274dd6ac64f82025821d)) * theme cannot be switched ([#1157](https://github.com/RSSNext/follow/issues/1157)) ([d5be028](https://github.com/RSSNext/follow/commit/d5be0287ad03e47635be057222ca7155a033ae80)) * **toc:** fix text truncate in title line ([f9b1a61](https://github.com/RSSNext/follow/commit/f9b1a61c0185b449184b34df5f3a3869a4e722a4)) * **toc:** should focus when toc item clicked ([f51dfdd](https://github.com/RSSNext/follow/commit/f51dfddc9ef40f4a7f226f32f9357e03a04f6ee0)) * translate form style ([6f3bb61](https://github.com/RSSNext/follow/commit/6f3bb61023dde1d58164425d8649c90bb4f680c6)), closes [#1184](https://github.com/RSSNext/follow/issues/1184) +* **translations:** update zh-CN locale files for accuracy ([#1739](https://github.com/RSSNext/follow/issues/1739)) ([83c9f71](https://github.com/RSSNext/follow/commit/83c9f717dd7092dc4a4b1106961d746988066fd9)) * trial limit ([eee2bf8](https://github.com/RSSNext/follow/commit/eee2bf8771d2d30bba1f534984e3ca8d5e26cf04)) * try fix dayjs init ([093f80d](https://github.com/RSSNext/follow/commit/093f80d9da223c567125285cc0f312f419bb4042)) +* try fix float bar inset bottom ([3110f95](https://github.com/RSSNext/follow/commit/3110f953da896302a90bb65bd44de034fbb87ec4)) * try remove skeleton ([e0d9502](https://github.com/RSSNext/follow/commit/e0d9502b160928fa4d7b3fa870b948f17aa56eef)) * try stable sort result for feed list ([677b2ea](https://github.com/RSSNext/follow/commit/677b2ea214cc116b748879628a8745ca17609043)) * ts import ([14311dd](https://github.com/RSSNext/follow/commit/14311dde0a9cb14e797e1a0f654a11f8502e58f8)) * turbo build cmd ([0b9c279](https://github.com/RSSNext/follow/commit/0b9c279fccd25231b90810359125571f817b7f7c)) * tweak boost progress calculation ([#1594](https://github.com/RSSNext/follow/issues/1594)) ([bd2d9d1](https://github.com/RSSNext/follow/commit/bd2d9d1175e953b199d8d686ac83d124caffbfef)) +* type error ([4d7365f](https://github.com/RSSNext/follow/commit/4d7365facf51c427e5005af7fdc0e4d6d7354e84)) * types ([8241b28](https://github.com/RSSNext/follow/commit/8241b280c9b0fd4fbace59cebf220cdb562c593a)) * typo ([f9a6772](https://github.com/RSSNext/follow/commit/f9a6772af805929338bd92e10605b855d6566d13)) * **ui:** resolve conflict between video progress bar and swiper drag … ([#1337](https://github.com/RSSNext/follow/issues/1337)) ([aa9206f](https://github.com/RSSNext/follow/commit/aa9206f5b1e3ea0c008f912ef22cdaa406546251)) * **ui:** use safe inset top to fix windows overlays, fixed [#1184](https://github.com/RSSNext/follow/issues/1184) ([dc85570](https://github.com/RSSNext/follow/commit/dc855703289ac98db76f4a57760fcd66047b8dd1)) * **ui:** user profile follow button style ([d629215](https://github.com/RSSNext/follow/commit/d629215305a0ec9323dea0db929582e9609a82f6)) +* update class names for responsive design and modify zh-HK ([#1782](https://github.com/RSSNext/follow/issues/1782)) ([70b46f5](https://github.com/RSSNext/follow/commit/70b46f57bf476df15216efd440899798e0e1fa0b)) * update daily claim amount reference in tooltip ([#1487](https://github.com/RSSNext/follow/issues/1487)) ([3792c2d](https://github.com/RSSNext/follow/commit/3792c2d850d9fc3f96ea4591784b61c631ee7e86)) * update empty category correctly ([c7efc67](https://github.com/RSSNext/follow/commit/c7efc67636b4d8b39ec50e26eb2d4839d0cd8750)) * update tray icon path for windows and improve tray behavior ([#1511](https://github.com/RSSNext/follow/issues/1511)) ([3706874](https://github.com/RSSNext/follow/commit/3706874e42c215a84df71eae1c135a114be8985a)) * update version toast ([25e7cea](https://github.com/RSSNext/follow/commit/25e7cea3c139335960c3de2922cf0ffa9c9c6c39)), closes [#1450](https://github.com/RSSNext/follow/issues/1450) * **upgrade:** changelog container should w-full ([1551bf6](https://github.com/RSSNext/follow/commit/1551bf648c511cbf615f515b2e2d93077b0e42b4)) +* use external link for login button ([d1975c2](https://github.com/RSSNext/follow/commit/d1975c20217290b2b7dc39866826c99f803dbd57)) +* use turbo run concurrently task ([742f915](https://github.com/RSSNext/follow/commit/742f91517d87d136bc8dc6ee65008e9b6880d435)) * vercel do not support `__` prefix for api route ([45a66e0](https://github.com/RSSNext/follow/commit/45a66e0f972d18831795d92b1d3c1f3893f396dc)) * vercel rewrite config ([#1203](https://github.com/RSSNext/follow/issues/1203)) ([c954d61](https://github.com/RSSNext/follow/commit/c954d61f074e282a9a86cbf1d1546748871c20e7)) * video media can not play in video view ([4cbf8a5](https://github.com/RSSNext/follow/commit/4cbf8a54fcf11fb71ff935baee52c79a83f5035c)), closes [#1645](https://github.com/RSSNext/follow/issues/1645) @@ -222,6 +276,7 @@ * add image preview in picture gallery images ([59728a8](https://github.com/RSSNext/follow/commit/59728a89295c2a9c6130651d06699eef027fef5e)) * add old browser check ([a4796ce](https://github.com/RSSNext/follow/commit/a4796ce4c9a0d1558b9467b80f1f0806dfaba321)) * add random color for user fallback bg ([dcbae89](https://github.com/RSSNext/follow/commit/dcbae89020e88947be693c100c644ee603519e96)) +* add reveal log file ([adbdf74](https://github.com/RSSNext/follow/commit/adbdf7472b6789a365489b2c5b09dd503ec648e9)) * add sentry trace id ([40843ac](https://github.com/RSSNext/follow/commit/40843ac79f390d0db81490522475a3557f57b2e4)) * add server announcement toast ([7b34357](https://github.com/RSSNext/follow/commit/7b3435749b8516466eae388172694f22d6b26a83)) * add updatelog modal ([84228bc](https://github.com/RSSNext/follow/commit/84228bc5c8755c4b957bc901e6e552ef176ed00c)) @@ -230,10 +285,12 @@ * and or condition for action ([#1616](https://github.com/RSSNext/follow/issues/1616)) ([4193a6b](https://github.com/RSSNext/follow/commit/4193a6b14d284a5e399fe94d9f305146179a75b4)) * **app:** support cache limit and clean cache ([dee294d](https://github.com/RSSNext/follow/commit/dee294dda589ca9e2eaee8a7e55197635b6323c7)) * bring rehypeUrlToAnchor back ([6f0cc4d](https://github.com/RSSNext/follow/commit/6f0cc4d566c9f2f552f0fedf1a8e570a817e0f56)), closes [#1373](https://github.com/RSSNext/follow/issues/1373) +* customizable columns for masonry view, closed [#1749](https://github.com/RSSNext/follow/issues/1749) ([0e0ce84](https://github.com/RSSNext/follow/commit/0e0ce843235f01f33f4c5b9708aa67dac5901b46)) * discover rsshub card background use single color ([7eeea5e](https://github.com/RSSNext/follow/commit/7eeea5e694c142803a37564ef8886d4fc4d2dab4)) * **discover:** enhance RSSHub recommendations with filters ([#1481](https://github.com/RSSNext/follow/issues/1481)) ([eb70126](https://github.com/RSSNext/follow/commit/eb70126b8283b6e0b246f86751e588a37cb34902)) * **discover:** improve trending style and discover no image placeholer ([86be1a2](https://github.com/RSSNext/follow/commit/86be1a2e46d3298157cece0731e687a462f833d5)) * dnd ([#1471](https://github.com/RSSNext/follow/issues/1471)) ([c9333d5](https://github.com/RSSNext/follow/commit/c9333d5004c170955ab96b9f7f5a2825e1042271)) +* dynamic auth providers ([b031ce5](https://github.com/RSSNext/follow/commit/b031ce57b8d7835c15fd9eecc6f99eaf50a368d1)) * entry image gallery modal ([e0d3e17](https://github.com/RSSNext/follow/commit/e0d3e17da4ee17217d7b78871b546f43af87d893)) * export database ([85b4502](https://github.com/RSSNext/follow/commit/85b4502f9c113b8de73ccf4a167aa514a3c149ea)) * **external:** move `login` and `redirect` route to external ([7916803](https://github.com/RSSNext/follow/commit/791680332d5e1c2c52eda792dd7ff69281f25adb)) @@ -245,35 +302,44 @@ * **i18n:** discover categories and mark all read undo button ([#1506](https://github.com/RSSNext/follow/issues/1506)) ([06bdf6c](https://github.com/RSSNext/follow/commit/06bdf6cfb90b03e1624189ebc05a389495e8bedc)) * **icon:** use gradient fallback background ([e827002](https://github.com/RSSNext/follow/commit/e8270025e469d9a0463d267d3da591a2991951bd)) * image zoom ([1e47ba2](https://github.com/RSSNext/follow/commit/1e47ba25671000408e69fc3bae2c4626d0bd664e)), closes [#1183](https://github.com/RSSNext/follow/issues/1183) +* **infra:** electron app can hot update renderer layer ([#1209](https://github.com/RSSNext/follow/issues/1209)) ([ca4751a](https://github.com/RSSNext/follow/commit/ca4751acd275579614a477d133ed643fca3fbf1a)) * **integration:** add outline integration ([#1229](https://github.com/RSSNext/follow/issues/1229)) ([0d0266b](https://github.com/RSSNext/follow/commit/0d0266b25189a74efdc63ce0062f3a379d4a0729)) * List delete add secondary confirmation ([#1254](https://github.com/RSSNext/follow/issues/1254)) ([14f2bac](https://github.com/RSSNext/follow/commit/14f2bac8f3ba7e8f2dbd3958a4394c5b11ea2537)) * load archived entries automatically ([5fe9e0c](https://github.com/RSSNext/follow/commit/5fe9e0c0e24460ca0fe435db03b753e9dcd3df17)) +* manual action ([#1867](https://github.com/RSSNext/follow/issues/1867)) ([0eedbba](https://github.com/RSSNext/follow/commit/0eedbbadc263b474e2d4bfd5b6c498ac39c16c34)) * **mark-all-button:** add countdown to auto-confirm message ([#1414](https://github.com/RSSNext/follow/issues/1414)) ([e1a5fc6](https://github.com/RSSNext/follow/commit/e1a5fc63f20c941140071789c9b67685da19ea5c)) * merge redirect page into login page ([2fa16b7](https://github.com/RSSNext/follow/commit/2fa16b75d376fe04472e6c91675c67e725da8633)) +* mobile design ([#1568](https://github.com/RSSNext/follow/issues/1568)) ([edd4f9e](https://github.com/RSSNext/follow/commit/edd4f9e5a6dc041e9eae2cee5ddf4eb624527ef5)), closes [#1575](https://github.com/RSSNext/follow/issues/1575) * move hideExtraBadge ([2f14c30](https://github.com/RSSNext/follow/commit/2f14c30307a895bc866ac57989df1dc624eab4e6)) * multi select with shift ([2158b1c](https://github.com/RSSNext/follow/commit/2158b1cc255baf3ff5e4e1a5806d85b7df64bd55)), closes [#1256](https://github.com/RSSNext/follow/issues/1256) +* new user guide in mobile ([9eca05e](https://github.com/RSSNext/follow/commit/9eca05ef6f3612658397719a0af00dfc30196ca3)) * **obsidian:** use readability content when available ([b4a3197](https://github.com/RSSNext/follow/commit/b4a3197e6ebd39bb259a6bc67f408569354a205e)) * prefer origin addresses for content images ([d4d4345](https://github.com/RSSNext/follow/commit/d4d43451dec839b64239e1835b2ac1a1aa2478be)) * **reader:** support custom css, fixed [#256](https://github.com/RSSNext/follow/issues/256) ([b251fa9](https://github.com/RSSNext/follow/commit/b251fa9421417c75d71707c8f08850f2cc902e1a)) * remove DISABLE_PERSONAL_DAILY_POWER ([064f8f5](https://github.com/RSSNext/follow/commit/064f8f524398a451d46e88b2d8b604f807d4ea96)) +* remove entries total and remaining to reduce big queries ([df343b1](https://github.com/RSSNext/follow/commit/df343b10d0745a239e3b15fe11c1d3b2cd9ac3bb)) * remove entryCount in feed claim list ([723cc43](https://github.com/RSSNext/follow/commit/723cc43891e3fe7e3ce8c09a422df9fdd42577be)) +* remove firebase in renderer ([177c265](https://github.com/RSSNext/follow/commit/177c2655a4b0cf1ebc4d83a8c74cca35a084d089)) * **renderer:** prevent currently executing async entry action from being executed again ([#1348](https://github.com/RSSNext/follow/issues/1348)) ([be82fe2](https://github.com/RSSNext/follow/commit/be82fe2a8ce37bac7205a1a43abeca396fdadba6)) * replace twMacro with unplugin-ast ([#1462](https://github.com/RSSNext/follow/issues/1462)) ([05da9ca](https://github.com/RSSNext/follow/commit/05da9ca7d3d1f66bd22250599df8b66ea0fd3a43)) * reset feed ([#1419](https://github.com/RSSNext/follow/issues/1419)) ([9066758](https://github.com/RSSNext/follow/commit/9066758c322b8b31c1a9a137be017283fa92bea8)) * separate packaging for macOS x64 and arm64 architectures ([#1389](https://github.com/RSSNext/follow/issues/1389)) ([3e8de30](https://github.com/RSSNext/follow/commit/3e8de308a3b3a07c58ab3fd7d26aad5aa2328c99)) * set default unreadOnly to true ([8c8c765](https://github.com/RSSNext/follow/commit/8c8c765ff518b6ccc6457959fdf3fe8a4ddeee22)) * show progress in searching ([e592e97](https://github.com/RSSNext/follow/commit/e592e97c9e0e6da8451c09b9b5f27612e6dbb971)), closes [#1457](https://github.com/RSSNext/follow/issues/1457) +* support alway on top, fixed [#1740](https://github.com/RSSNext/follow/issues/1740) ([09df663](https://github.com/RSSNext/follow/commit/09df663a1c0b9a5ec5d30f2609a36b09c3e66e56)) * support markdown for announcement content ([539ec03](https://github.com/RSSNext/follow/commit/539ec03293fc6e3a11dbec69bbf78fe1c5dd2e8c)) * support zen mode sidebar entry timeline selector ([4ab132c](https://github.com/RSSNext/follow/commit/4ab132c593a7acce0167d02f6025030775a9fac5)) * unified feed title ([b86afe5](https://github.com/RSSNext/follow/commit/b86afe5a9789ad13c5a026adf553d2c6bc333bff)) * uniq macos entry column position ([4b63023](https://github.com/RSSNext/follow/commit/4b63023389e125353de343b679840e5fbca1a4d3)) * update og image ([c58df35](https://github.com/RSSNext/follow/commit/c58df35d1f48e7374973a393c9b7efea38d12494)) * update og image ([142f9f5](https://github.com/RSSNext/follow/commit/142f9f57ce2697ab82fd5e1dd2cee7d0f7500721)) +* update server hono ([5beaca3](https://github.com/RSSNext/follow/commit/5beaca3c772d7bc097fd53779c581eb6b41c9eaa)) * update text ([5aae827](https://github.com/RSSNext/follow/commit/5aae8273dcd999bc11904bc8c9d7b72e769fa200)) * update text ([89bef43](https://github.com/RSSNext/follow/commit/89bef43267199f60de376c3998d3cfff26faad23)) * upload arm64 dmg separately ([1d77c09](https://github.com/RSSNext/follow/commit/1d77c092ce18fbabe6cfbbd21d9a96e3445795da)) * use FeedTitle ([73d2c30](https://github.com/RSSNext/follow/commit/73d2c30416ad3f2da626db003996c0fb036025ac)) * **ux:** support native space key to page down, fixed [#1121](https://github.com/RSSNext/follow/issues/1121) ([baa66fe](https://github.com/RSSNext/follow/commit/baa66feaf19416d2b9e5efe6b79f6af1b3966404)) +* web push ([cd32115](https://github.com/RSSNext/follow/commit/cd3211587109d0bf3e762df128356708fcea24b1)) * zen mode and hide extra badge ([7ba07ad](https://github.com/RSSNext/follow/commit/7ba07adff1e7cd3292cc308f3e70803fe11fb8ce)) * zoom on long image ([930e328](https://github.com/RSSNext/follow/commit/930e328eddb84cde7d664a75e24222d8644af50a)) @@ -285,6 +351,7 @@ * lazy show action, reduce lcp time ([fcf775c](https://github.com/RSSNext/follow/commit/fcf775c976591258f83dea6e34ca3161b539b15e)) * modify some animations to CSS animations ([9ec3fe5](https://github.com/RSSNext/follow/commit/9ec3fe562bf3527ad4279a9a57c0b0bb5934dba1)) * optmize masonry perfermance ([d1748c6](https://github.com/RSSNext/follow/commit/d1748c6ce93f7075270d8cc6e88cbd944d165d50)) +* reduce mobile entry header re-render ([44646d8](https://github.com/RSSNext/follow/commit/44646d8a86fde1bbf72831477b61774c75414e24)) * reduce re-render zooming ([9b35074](https://github.com/RSSNext/follow/commit/9b350741d16cfd81770e82b866e92d732498ccf3)) * reduce the number of items rendered on the first render ([6704d61](https://github.com/RSSNext/follow/commit/6704d61baac7f54368cedc0438845aaacfb98960)) * resolve feed item select area performance problem ([f87c0ff](https://github.com/RSSNext/follow/commit/f87c0ff2abfc430719894765796ca457c3f8e7f0)) diff --git a/README.md b/README.md index f4049ccd6c..bd04086dd2 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,9 @@ Whether for users or professional developers, Follow will be your open informati Feel free to try it using the following methods: -| [![](https://img.shields.io/badge/Try%20Web%20App-Online-55b467?labelColor=black&logo=vercel&style=flat-square)](https://app.follow.is) | No installation necessary! Visit our web app to experience it firsthand. | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------------- | -| [![](https://img.shields.io/github/v/release/RSSNext/Follow?color=369eff&labelColor=black&logo=github&style=flat-square&label=Releases)](https://github.com/RSSNext/Follow/releases/latest) | Download and install the desktop client for a smoother experience and enhanced features. | +| [![](https://img.shields.io/badge/Try%20Web%20App-Online-55b467?labelColor=black&logo=vercel&style=flat-square)](https://app.follow.is) | No installation necessary! Visit our web app to experience it firsthand. | +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------- | +| [![](https://img.shields.io/github/v/release/RSSNext/Follow?color=369eff&labelColor=black&logo=github&style=flat-square&label=Releases)](https://github.com/RSSNext/Follow/releases/latest) [![](https://img.shields.io/github/downloads/RSSNext/Follow/total?color=369eff&labelColor=black&logo=github&style=flat-square&label=Downloads)](https://github.com/RSSNext/Follow/releases/latest) | Download and install the desktop client for a smoother experience and enhanced features. | You can also install using the following methods maintained by our community: diff --git a/apps/main/global.d.ts b/apps/main/global.d.ts index 2f2956a52b..fb3018c3d6 100644 --- a/apps/main/global.d.ts +++ b/apps/main/global.d.ts @@ -1,2 +1,7 @@ import "../../types/vite" import "../../types/authjs" + +declare global { + const GIT_COMMIT_HASH: string +} +export {} diff --git a/apps/main/package.json b/apps/main/package.json index 83d904c271..beec8b1f0f 100644 --- a/apps/main/package.json +++ b/apps/main/package.json @@ -28,26 +28,32 @@ "@eneris/push-receiver": "4.3.0", "@follow/shared": "workspace:*", "@mozilla/readability": "^0.5.0", + "@openpanel/web": "1.0.1", "@sentry/electron": "5.7.0", "builder-util-runtime": "9.2.10", "electron-context-menu": "4.0.4", - "electron-log": "5.2.0", + "electron-log": "5.2.3", "electron-squirrel-startup": "1.0.1", "electron-updater": "^6.3.9", + "es-toolkit": "1.27.0", "fast-folder-size": "2.3.0", "font-list": "1.5.1", - "i18next": "^23.16.4", + "i18next": "^24.0.2", + "js-yaml": "4.1.0", "linkedom": "^0.18.5", "lowdb": "7.0.1", "msedge-tts": "1.3.4", + "node-machine-id": "1.1.12", "ofetch": "1.4.1", "semver": "7.6.3", + "tar": "7.4.3", "vscode-languagedetection": "npm:@vscode/vscode-languagedetection@^1.0.22" }, "devDependencies": { - "@types/node": "^22.8.7", - "electron": "33.0.2", + "@types/js-yaml": "4.0.9", + "@types/node": "^22.10.0", + "electron": "33.2.0", "electron-devtools-installer": "3.2.0", - "hono": "4.6.8" + "hono": "4.6.11" } } diff --git a/apps/main/src/constants/app.ts b/apps/main/src/constants/app.ts index 9f9441bdc8..26dcaa5b46 100644 --- a/apps/main/src/constants/app.ts +++ b/apps/main/src/constants/app.ts @@ -1,5 +1,13 @@ -// 5min +import path from "node:path" + +import { app } from "electron" + export const UNREAD_BACKGROUND_POLLING_INTERVAL = 1000 * 60 * 5 +export const HOTUPDATE_RENDER_ENTRY_DIR = path.resolve(app.getPath("userData"), "render") + +export const GITHUB_OWNER = process.env.GITHUB_OWNER || "RSSNext" +export const GITHUB_REPO = process.env.GITHUB_REPO || "follow" + // https://github.com/electron/electron/issues/25081 export const START_IN_TRAY_ARGS = "--start-in-tray" diff --git a/apps/main/src/constants/system.ts b/apps/main/src/constants/system.ts new file mode 100644 index 0000000000..5cb751c988 --- /dev/null +++ b/apps/main/src/constants/system.ts @@ -0,0 +1,3 @@ +import { machineIdSync } from "node-machine-id" + +export const DEVICE_ID = machineIdSync() diff --git a/apps/main/src/index.ts b/apps/main/src/index.ts index 3c79df140c..3cc590da52 100644 --- a/apps/main/src/index.ts +++ b/apps/main/src/index.ts @@ -4,8 +4,10 @@ import { APP_PROTOCOL } from "@follow/shared/constants" import { env } from "@follow/shared/env" import { imageRefererMatches, selfRefererMatches } from "@follow/shared/image" import { app, BrowserWindow, session } from "electron" +import type { Cookie } from "electron/main" import squirrelStartup from "electron-squirrel-startup" +import { DEVICE_ID } from "./constants/system" import { isDev, isMacOS } from "./env" import { initializeAppStage0, initializeAppStage1 } from "./init" import { updateProxy } from "./lib/proxy" @@ -14,6 +16,7 @@ import { store } from "./lib/store" import { registerAppTray } from "./lib/tray" import { setAuthSessionToken, updateNotificationsToken } from "./lib/user" import { registerUpdater } from "./updater" +import { cleanupOldRender } from "./updater/hot-updater" import { createMainWindow, getMainWindow, @@ -23,6 +26,9 @@ import { if (isDev) console.info("[main] env loaded:", env) +const apiURL = process.env["VITE_API_URL"] || import.meta.env.VITE_API_URL + +console.info("[main] device id:", DEVICE_ID) if (squirrelStartup) { app.quit() } @@ -56,7 +62,7 @@ function bootstrap() { // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. - app.whenReady().then(() => { + app.whenReady().then(async () => { // Default open or close DevTools by F12 in development // and ignore CommandOrControl + R in production. // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils @@ -69,6 +75,28 @@ function bootstrap() { mainWindow = createMainWindow() + // restore cookies + const cookies = store.get("cookies") as Cookie[] + if (cookies) { + Promise.all( + cookies.map((cookie) => { + const setCookieDetails: Electron.CookiesSetDetails = { + url: apiURL, + name: cookie.name, + value: cookie.value, + domain: cookie.domain, + path: cookie.path, + secure: cookie.secure, + httpOnly: cookie.httpOnly, + expirationDate: cookie.expirationDate, + sameSite: cookie.sameSite as "unspecified" | "no_restriction" | "lax" | "strict", + } + + return mainWindow.webContents.session.cookies.set(setCookieDetails) + }), + ) + } + updateProxy() registerUpdater() registerAppTray() @@ -135,7 +163,7 @@ function bootstrap() { } }) - app.on("before-quit", () => { + app.on("before-quit", async () => { // store window pos when before app quit const window = getMainWindow() if (!window || window.isDestroyed()) return @@ -147,6 +175,12 @@ function bootstrap() { x: bounds.x, y: bounds.y, }) + await session.defaultSession.cookies.flushStore() + + const cookies = await session.defaultSession.cookies.get({}) + store.set("cookies", cookies) + + await cleanupOldRender() }) const handleOpen = async (url: string) => { @@ -158,7 +192,6 @@ function bootstrap() { const token = urlObj.searchParams.get("token") const userId = urlObj.searchParams.get("userId") - const apiURL = process.env["VITE_API_URL"] || import.meta.env.VITE_API_URL if (token && apiURL) { setAuthSessionToken(token) mainWindow.webContents.session.cookies.set({ diff --git a/apps/main/src/init.ts b/apps/main/src/init.ts index 3c3bff55cf..2358554fad 100644 --- a/apps/main/src/init.ts +++ b/apps/main/src/init.ts @@ -58,7 +58,6 @@ export const initializeAppStage1 = () => { // code. You can also put them in separate files and require them here. registerMenuAndContextMenu() - registerPushNotifications() clearCacheCronJob() checkAndCleanCodeCache() diff --git a/apps/main/src/lib/op.ts b/apps/main/src/lib/op.ts new file mode 100644 index 0000000000..f068f6cf39 --- /dev/null +++ b/apps/main/src/lib/op.ts @@ -0,0 +1,20 @@ +import { env } from "@follow/shared/env" +import { OpenPanel } from "@openpanel/web" +import { app } from "electron" + +import { DEVICE_ID } from "~/constants/system" + +export const op = new OpenPanel({ + clientId: env.VITE_OPENPANEL_CLIENT_ID ?? "", + trackScreenViews: false, + trackOutgoingLinks: false, + trackAttributes: false, + trackHashChanges: false, + apiUrl: env.VITE_OPENPANEL_API_URL, +}) + +op.setGlobalProperties({ + device_id: DEVICE_ID, + app_version: app.getVersion(), + build: "electron", +}) diff --git a/apps/main/src/lib/utils.ts b/apps/main/src/lib/utils.ts index ae79457346..12b3d133d0 100644 --- a/apps/main/src/lib/utils.ts +++ b/apps/main/src/lib/utils.ts @@ -1 +1,13 @@ +import type { BrowserWindow } from "electron" + export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) +// To solve the vibrancy losing issue when leaving full screen mode +// @see https://github.com/toeverything/AFFiNE/blob/280e24934a27557529479a70ab38c4f5fc65cb00/packages/frontend/electron/src/main/windows-manager/main-window.ts:L157 +export function refreshBound(window: BrowserWindow, timeout = 0) { + setTimeout(() => { + // FIXME: workaround for theme bug in full screen mode + const size = window?.getSize() + window?.setSize(size[0] + 1, size[1] + 1) + window?.setSize(size[0], size[1]) + }, timeout) +} diff --git a/apps/main/src/menu.ts b/apps/main/src/menu.ts index 1a73f46fe5..6c40ad9e40 100644 --- a/apps/main/src/menu.ts +++ b/apps/main/src/menu.ts @@ -8,7 +8,7 @@ import { isDev, isMacOS } from "./env" import { clearAllDataAndConfirm } from "./lib/cleaner" import { t } from "./lib/i18n" import { revealLogFile } from "./logger" -import { checkForUpdates, quitAndInstall } from "./updater" +import { checkForAppUpdates, quitAndInstall } from "./updater" import { createSettingWindow, createWindow, getMainWindow } from "./window" export const registerAppMenu = () => { @@ -168,6 +168,17 @@ export const registerAppMenu = () => { role: "front", label: t("menu.front"), }, + { + label: "Always on top", + type: "checkbox", + checked: getMainWindow()?.isAlwaysOnTop(), + click: () => { + const mainWindow = getMainWindow() + if (!mainWindow) return + mainWindow.setAlwaysOnTop(!mainWindow.isAlwaysOnTop()) + registerAppMenu() + }, + }, ], }, { @@ -184,7 +195,7 @@ export const registerAppMenu = () => { label: t("menu.checkForUpdates"), click: async () => { getMainWindow()?.show() - await checkForUpdates() + await checkForAppUpdates() }, }, ], diff --git a/apps/main/src/sentry.ts b/apps/main/src/sentry.ts index 22000ee6ed..5337d02070 100644 --- a/apps/main/src/sentry.ts +++ b/apps/main/src/sentry.ts @@ -1,6 +1,9 @@ import * as Sentry from "@sentry/electron/main" +import { app } from "electron" import { FetchError } from "ofetch" +import { DEVICE_ID } from "./constants/system" + export const initializeSentry = () => { Sentry.init({ dsn: process.env.VITE_SENTRY_DSN, @@ -32,4 +35,7 @@ export const initializeSentry = () => { return event }, }) + Sentry.setTag("device_id", DEVICE_ID) + Sentry.setTag("app_version", app.getVersion()) + Sentry.setTag("build", "electron") } diff --git a/apps/main/src/tipc/app.ts b/apps/main/src/tipc/app.ts index e064653a80..1dd75869b3 100644 --- a/apps/main/src/tipc/app.ts +++ b/apps/main/src/tipc/app.ts @@ -1,19 +1,21 @@ +/* eslint-disable @typescript-eslint/require-await */ import fs from "node:fs" import fsp from "node:fs/promises" import path from "node:path" +import { fileURLToPath } from "node:url" import { getRendererHandlers } from "@egoist/tipc/main" import { callWindowExpose } from "@follow/shared/bridge" -import type { BrowserWindow } from "electron" -import { app, clipboard, dialog, screen, shell } from "electron" +import { app, BrowserWindow, clipboard, dialog, screen, shell } from "electron" import { registerMenuAndContextMenu } from "~/init" import { clearAllData, getCacheSize } from "~/lib/cleaner" import { store, StoreKey } from "~/lib/store" import { registerAppTray } from "~/lib/tray" -import { logger } from "~/logger" +import { logger, revealLogFile } from "~/logger" +import { cleanupOldRender, loadDynamicRenderEntry } from "~/updater/hot-updater" -import { isWindows11 } from "../env" +import { isDev, isWindows11 } from "../env" import { downloadFile } from "../lib/download" import { i18n } from "../lib/i18n" import { cleanAuthSessionToken, cleanUser } from "../lib/user" @@ -272,6 +274,33 @@ ${content} } }), + getAppVersion: t.procedure.action(async () => { + return app.getVersion() + }), + rendererUpdateReload: t.procedure.action(async () => { + const __dirname = fileURLToPath(new URL(".", import.meta.url)) + const allWindows = BrowserWindow.getAllWindows() + const dynamicRenderEntry = loadDynamicRenderEntry() + + const appLoadEntry = dynamicRenderEntry || path.resolve(__dirname, "../../renderer/index.html") + logger.info("appLoadEntry", appLoadEntry) + const mainWindow = getMainWindow() + + for (const window of allWindows) { + if (window === mainWindow) { + if (isDev) { + logger.verbose("[rendererUpdateReload]: skip reload in dev") + break + } + window.loadFile(appLoadEntry) + } else window.destroy() + } + + setTimeout(() => { + cleanupOldRender() + }, 1000) + }), + getCacheSize: t.procedure.action(async () => { return getCacheSize() }), @@ -313,6 +342,10 @@ ${content} store.set(StoreKey.CacheSizeLimit, input) } }), + + revealLogFile: t.procedure.action(async () => { + return revealLogFile() + }), } interface Sender extends Electron.WebContents { diff --git a/apps/main/src/tracker/index.ts b/apps/main/src/tracker/index.ts new file mode 100644 index 0000000000..0f2522b1b7 --- /dev/null +++ b/apps/main/src/tracker/index.ts @@ -0,0 +1,16 @@ +import { op } from "~/lib/op" + +const PREFIX = "app:" +export const hotUpdateDownloadTrack = (version: string) => { + op.track(`${PREFIX}hot-update-download`, { version }) +} +export const hotUpdateAppNotSupportTriggerTrack = (data: { + appVersion: string + manifestVersion: string +}) => { + op.track(`${PREFIX}hot-update-app-not-support-trigger`, data) +} + +export const hotUpdateRenderSuccessTrack = (version: string) => { + op.track(`${PREFIX}hot-update-render-success`, { version }) +} diff --git a/apps/main/src/updater/configs.ts b/apps/main/src/updater/configs.ts new file mode 100644 index 0000000000..2d216fa318 --- /dev/null +++ b/apps/main/src/updater/configs.ts @@ -0,0 +1,19 @@ +import { version as appVersion } from "@pkg" + +import { isDev } from "~/env" + +const isNightlyBuild = appVersion.includes("nightly") + +export const appUpdaterConfig = { + // Disable renderer hot update will trigger app update when available + enableRenderHotUpdate: !isDev && isNightlyBuild, + // Disable app update will also disable renderer hot update + // enableAppUpdate: true, + enableAppUpdate: !isDev, + + app: { + autoCheckUpdate: true, + autoDownloadUpdate: true, + checkUpdateInterval: 15 * 60 * 1000, + }, +} diff --git a/apps/main/src/updater/hot-updater.ts b/apps/main/src/updater/hot-updater.ts new file mode 100644 index 0000000000..68b7e6d797 --- /dev/null +++ b/apps/main/src/updater/hot-updater.ts @@ -0,0 +1,245 @@ +/** + * @description This file handles hot updates for the electron renderer layer + */ +import { createHash } from "node:crypto" +import { existsSync, readFileSync } from "node:fs" +import { mkdir, readdir, rename, rm, stat, writeFile } from "node:fs/promises" +import os from "node:os" +import path from "node:path" + +import { callWindowExpose } from "@follow/shared/bridge" +import { mainHash, version as appVersion } from "@pkg" +import log from "electron-log" +import { load } from "js-yaml" +import { x } from "tar" + +import { GITHUB_OWNER, GITHUB_REPO, HOTUPDATE_RENDER_ENTRY_DIR } from "~/constants/app" +import { hotUpdateDownloadTrack, hotUpdateRenderSuccessTrack } from "~/tracker" +import { getMainWindow } from "~/window" + +import { appUpdaterConfig } from "./configs" +import type { GitHubReleasesItem } from "./types" + +const logger = log.scope("hot-updater") + +const isNightlyBuild = appVersion.includes("nightly") + +const url = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}` +const releasesUrl = `${url}/releases` +const releaseApiUrl = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases` + +const getLatestReleaseTag = async () => { + if (!isNightlyBuild) { + const res = await fetch(`${releaseApiUrl}/latest`) + const json = await res.json() + + return json.tag_name + } else { + const res = await fetch(releaseApiUrl) + const json = (await res.json()) as GitHubReleasesItem[] + + // Search the top nightly release + const nightlyRelease = json.find((item) => item.prerelease) + if (!nightlyRelease) return json[0].tag_name + return nightlyRelease.tag_name + } +} + +const getFileDownloadUrl = async (filename: string) => { + const tag = await getLatestReleaseTag() + + return `${releasesUrl}/download/${tag}/${filename}` +} + +type Manifest = { + /** Render version */ + version: string + hash: string + commit: string + filename: string + /** Only electron main hash equal to this value, renderer will can be updated */ + mainHash: string +} +const getLatestReleaseManifest = async () => { + const url = await getFileDownloadUrl("manifest.yml") + logger.info(`Fetching manifest from ${url}`) + const res = await fetch(url) + const text = await res.text() + const manifest = load(text) as Manifest + if (typeof manifest !== "object") { + logger.error("Invalid manifest", text) + return null + } + return manifest +} +const downloadTempDir = path.resolve(os.tmpdir(), "follow-render-update") + +export enum CanUpdateRenderState { + // If version is equal, no need to update + NO_NEEDED, + // Can be only update render layer, not fully upgrade app + NEEDED, + // App not support, should trigger app force update + APP_NOT_SUPPORT, + // Network error, can fetch manifest + NETWORK_ERROR, +} +export const canUpdateRender = async (): Promise<[CanUpdateRenderState, Manifest | null]> => { + const manifest = await getLatestReleaseManifest() + + logger.info("fetched manifest", manifest) + + if (!manifest) return [CanUpdateRenderState.NETWORK_ERROR, null] + + // const isAppShouldUpdate = shouldUpdateApp(appVersion, manifest.version) + // if (isAppShouldUpdate) { + // logger.info( + // "app should update, skip render update, app version: ", + // appVersion, + // ", the manifest version: ", + // manifest.version, + // ) + // return false + // } + + const appSupport = mainHash === manifest.mainHash + if (!appSupport) { + logger.info("app not support, should trigger app force update, app version: ", appVersion) + // hotUpdateAppNotSupportTriggerTrack({ + // appVersion, + // manifestVersion: manifest.version, + // }) + // // Trigger app force update + // checkForAppUpdates().then(() => { + // downloadAppUpdate() + // }) + return [CanUpdateRenderState.APP_NOT_SUPPORT, null] + } + + const isVersionEqual = appVersion === manifest.version + if (isVersionEqual) { + logger.info("version is equal, skip update") + return [CanUpdateRenderState.NO_NEEDED, null] + } + const isCommitEqual = GIT_COMMIT_HASH === manifest.commit + if (isCommitEqual) { + logger.info("commit is equal, skip update") + return [CanUpdateRenderState.NO_NEEDED, null] + } + + const manifestFilePath = path.resolve(HOTUPDATE_RENDER_ENTRY_DIR, "manifest.yml") + const manifestExist = existsSync(manifestFilePath) + + const oldManifest: Manifest | null = manifestExist + ? (load(readFileSync(manifestFilePath, "utf-8")) as Manifest) + : null + + if (oldManifest) { + if (oldManifest.version === manifest.version) { + logger.info("manifest version is equal, skip update") + return [CanUpdateRenderState.NO_NEEDED, null] + } + if (oldManifest.commit === manifest.commit) { + logger.info("manifest commit is equal, skip update") + return [CanUpdateRenderState.NO_NEEDED, null] + } + } + return [CanUpdateRenderState.NEEDED, manifest] +} +const downloadRenderAsset = async (manifest: Manifest) => { + hotUpdateDownloadTrack(manifest.version) + const { filename } = manifest + const url = await getFileDownloadUrl(filename) + + logger.info(`Downloading ${url}`) + const res = await fetch(url) + const arrayBuffer = await res.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) + const filePath = path.resolve(downloadTempDir, filename) + await mkdir(downloadTempDir, { recursive: true }) + await writeFile(filePath, buffer) + + const sha256 = createHash("sha256") + sha256.update(buffer) + const hash = sha256.digest("hex") + if (hash !== manifest.hash) { + logger.error("Hash mismatch", hash, manifest.hash) + return false + } + return filePath +} +export const hotUpdateRender = async (manifest: Manifest) => { + if (!appUpdaterConfig.enableRenderHotUpdate) return false + + if (!manifest) return false + + const filePath = await downloadRenderAsset(manifest) + if (!filePath) return false + + // Extract the tar.gz file + await mkdir(HOTUPDATE_RENDER_ENTRY_DIR, { recursive: true }) + await x({ + f: filePath, + cwd: HOTUPDATE_RENDER_ENTRY_DIR, + }) + + // Rename `renderer` folder to `manifest.version` + await rename( + path.resolve(HOTUPDATE_RENDER_ENTRY_DIR, "renderer"), + path.resolve(HOTUPDATE_RENDER_ENTRY_DIR, manifest.version), + ) + + await writeFile( + path.resolve(HOTUPDATE_RENDER_ENTRY_DIR, "manifest.yml"), + JSON.stringify(manifest), + ) + logger.info(`Hot update render success, update to ${manifest.version}`) + hotUpdateRenderSuccessTrack(manifest.version) + const mainWindow = getMainWindow() + if (!mainWindow) return false + const caller = callWindowExpose(mainWindow) + caller.readyToUpdate() + return true +} + +export const getCurrentRenderManifest = () => { + const manifestFilePath = path.resolve(HOTUPDATE_RENDER_ENTRY_DIR, "manifest.yml") + const manifestExist = existsSync(manifestFilePath) + if (!manifestExist) return null + return load(readFileSync(manifestFilePath, "utf-8")) as Manifest +} +export const cleanupOldRender = async () => { + const manifest = getCurrentRenderManifest() + if (!manifest) { + // Empty the directory + await rm(HOTUPDATE_RENDER_ENTRY_DIR, { recursive: true, force: true }) + return + } + + const currentRenderVersion = manifest.version + // Clean all not current version + const dirs = await readdir(HOTUPDATE_RENDER_ENTRY_DIR) + for (const dir of dirs) { + const isDir = (await stat(path.resolve(HOTUPDATE_RENDER_ENTRY_DIR, dir))).isDirectory() + if (!isDir) continue + if (dir === currentRenderVersion) continue + await rm(path.resolve(HOTUPDATE_RENDER_ENTRY_DIR, dir), { recursive: true, force: true }) + } +} + +export const loadDynamicRenderEntry = () => { + if (!appUpdaterConfig.enableRenderHotUpdate) return + const manifest = getCurrentRenderManifest() + if (!manifest) return + // check main hash is equal to manifest.mainHash + const appSupport = mainHash === manifest.mainHash + if (!appSupport) return + + const currentRenderVersion = manifest.version + const dir = path.resolve(HOTUPDATE_RENDER_ENTRY_DIR, currentRenderVersion) + const entryFile = path.resolve(dir, "index.html") + const entryFileExists = existsSync(entryFile) + + if (!entryFileExists) return + return entryFile +} diff --git a/apps/main/src/updater/index.ts b/apps/main/src/updater/index.ts index 9f74934079..65d93f0a0a 100644 --- a/apps/main/src/updater/index.ts +++ b/apps/main/src/updater/index.ts @@ -1,15 +1,20 @@ import { getRendererHandlers } from "@egoist/tipc/main" import { autoUpdater as defaultAutoUpdater } from "electron-updater" +import { GITHUB_OWNER, GITHUB_REPO } from "~/constants/app" +import { canUpdateRender, CanUpdateRenderState, hotUpdateRender } from "~/updater/hot-updater" + import { channel, isDev, isWindows } from "../env" import { logger } from "../logger" import type { RendererHandlers } from "../renderer-handlers" import { destroyMainWindow, getMainWindow } from "../window" +import { appUpdaterConfig } from "./configs" import { CustomGitHubProvider } from "./custom-github-provider" import { WindowsUpdater } from "./windows-updater" // skip auto update in dev mode -const disabled = isDev +// const disabled = isDev +const disabled = !appUpdaterConfig.enableAppUpdate const autoUpdater = isWindows ? new WindowsUpdater() : defaultAutoUpdater @@ -28,32 +33,39 @@ export const quitAndInstall = () => { let downloading = false let checkingUpdate = false -export type UpdaterConfig = { - autoCheckUpdate: boolean - autoDownloadUpdate: boolean - checkUpdateInterval: number -} - -const config: UpdaterConfig = { - autoCheckUpdate: true, - autoDownloadUpdate: true, - checkUpdateInterval: 15 * 60 * 1000, +const upgradeRenderIfNeeded = async () => { + const [state, manifest] = await canUpdateRender() + if (state === CanUpdateRenderState.NO_NEEDED) { + return true + } + if (state === CanUpdateRenderState.NEEDED && manifest) { + await hotUpdateRender(manifest) + return true + } + return false } - -export const checkForUpdates = async () => { +export const checkForAppUpdates = async () => { if (disabled || checkingUpdate) { return } + checkingUpdate = true try { - const info = await autoUpdater.checkForUpdates() - return info + if (appUpdaterConfig.enableRenderHotUpdate) { + const isRenderUpgraded = await upgradeRenderIfNeeded() + if (isRenderUpgraded) { + return + } + } + return autoUpdater.checkForUpdates() + } catch (e) { + logger.error("Error checking for updates", e) } finally { checkingUpdate = false } } -export const downloadUpdate = async () => { +export const downloadAppUpdate = async () => { if (disabled || downloading) { return } @@ -71,8 +83,7 @@ export const registerUpdater = async () => { return } - const allowAutoUpdate = true - + // Disable there, control this in event autoUpdater.autoDownload = false autoUpdater.allowPrerelease = channel !== "stable" autoUpdater.autoInstallOnAppQuit = true @@ -82,8 +93,8 @@ export const registerUpdater = async () => { channel, // hack for custom provider provider: "custom" as "github", - repo: "follow", - owner: "RSSNext", + repo: GITHUB_REPO, + owner: GITHUB_OWNER, releaseType: channel === "stable" ? "release" : "prerelease", // @ts-expect-error hack for custom provider updateProvider: CustomGitHubProvider, @@ -100,10 +111,21 @@ export const registerUpdater = async () => { autoUpdater.on("checking-for-update", () => { logger.info("Checking for update") }) - autoUpdater.on("update-available", (info) => { + autoUpdater.on("update-available", async (info) => { logger.info("Update available", info) - if (config.autoDownloadUpdate && allowAutoUpdate) { - downloadUpdate().catch((err) => { + + // The app hotfix strategy is as follows: + // Determine whether the app should be updated in full or only the renderer layer based on the version number. + // https://www.notion.so/rss3/Follow-Hotfix-Electron-Renderer-layer-RFC-fe2444b9ac194c2cb38f9fa0bb1ef3c1?pvs=4#12e35ea049b480f1b268f1e605d86a62 + if (appUpdaterConfig.enableRenderHotUpdate) { + const isRenderUpgraded = await upgradeRenderIfNeeded() + if (isRenderUpgraded) { + return + } + } + + if (appUpdaterConfig.app.autoDownloadUpdate) { + downloadAppUpdate().catch((err) => { logger.error(err) }) } @@ -130,14 +152,14 @@ export const registerUpdater = async () => { autoUpdater.forceDevUpdateConfig = isDev setInterval(() => { - if (config.autoCheckUpdate) { - checkForUpdates().catch((err) => { + if (appUpdaterConfig.app.autoCheckUpdate) { + checkForAppUpdates().catch((err) => { logger.error("Error checking for updates", err) }) } - }, config.checkUpdateInterval) - if (config.autoCheckUpdate) { - checkForUpdates().catch((err) => { + }, appUpdaterConfig.app.checkUpdateInterval) + if (appUpdaterConfig.app.autoCheckUpdate) { + checkForAppUpdates().catch((err) => { logger.error("Error checking for updates", err) }) } diff --git a/apps/main/src/updater/types.ts b/apps/main/src/updater/types.ts new file mode 100644 index 0000000000..7a9cb54d74 --- /dev/null +++ b/apps/main/src/updater/types.ts @@ -0,0 +1,91 @@ +export interface GitHubReleasesItem { + url: string + assets_url: string + upload_url: string + html_url: string + id: number + author: Author + node_id: string + tag_name: string + target_commitish: string + name: string + draft: boolean + prerelease: boolean + created_at: string + published_at: string + assets: AssetsItem[] + tarball_url: string + zipball_url: string + body: string + reactions?: Reactions + mentions_count?: number +} +interface Author { + login: string + id: number + node_id: string + avatar_url: string + gravatar_id: string + url: string + html_url: string + followers_url: string + following_url: string + gists_url: string + starred_url: string + subscriptions_url: string + organizations_url: string + repos_url: string + events_url: string + received_events_url: string + type: string + user_view_type: string + site_admin: boolean +} +interface AssetsItem { + url: string + id: number + node_id: string + name: string + label: string | null + uploader: Uploader + content_type: string + state: string + size: number + download_count: number + created_at: string + updated_at: string + browser_download_url: string +} +interface Uploader { + login: string + id: number + node_id: string + avatar_url: string + gravatar_id: string + url: string + html_url: string + followers_url: string + following_url: string + gists_url: string + starred_url: string + subscriptions_url: string + organizations_url: string + repos_url: string + events_url: string + received_events_url: string + type: string + user_view_type: string + site_admin: boolean +} +interface Reactions { + url: string + total_count: number + "+1": number + "-1": number + laugh: number + hooray: number + confused: number + heart: number + rocket: number + eyes: number +} diff --git a/apps/main/src/updater/utils.ts b/apps/main/src/updater/utils.ts index 88d0b0c7f8..99dc8caf74 100644 --- a/apps/main/src/updater/utils.ts +++ b/apps/main/src/updater/utils.ts @@ -2,6 +2,7 @@ import fs from "node:fs" import path from "node:path" import { app } from "electron" +import { major, minor } from "semver" let _isSquirrelBuild: boolean | null = null export function isSquirrelBuild() { @@ -16,3 +17,39 @@ export function isSquirrelBuild() { return _isSquirrelBuild } + +// The following scenario only requires updating the renderer, so the app update is skipped: +// In x.y.z, the update of z will only trigger renderer hotfix, while the update of y requires updating the entire app. +// The hotfix version of x.y.z-beta.0 adds a suffix number. It triggers renderer update. If the main code is modified and the entire app update needs to be triggered, the hotfix version adds a suffix like x.y.z-beta.0.app. +// For subsequent minor versions that require updating the main code, the suffix .app needs to be added. + +export const shouldUpdateApp = (currentVersion: string, nextVersion: string) => { + if (nextVersion.includes("app")) { + return true + } + // x.y.z 's y or x not equal, need update app + const [x1, x2] = [safeMajor(currentVersion), safeMajor(nextVersion)] + const [y1, y2] = [safeMinor(currentVersion), safeMinor(nextVersion)] + + // Here, it is not determined whether it is a problem of version number downgrade; the updater will handle it automatically. + if (x1 !== x2 || y1 !== y2) { + return true + } + + return false +} + +const safeMajor = (version: string) => { + try { + return major(version) + } catch { + return "0.0.0" + } +} +const safeMinor = (version: string) => { + try { + return minor(version) + } catch { + return "0.0.0" + } +} diff --git a/apps/main/src/window.ts b/apps/main/src/window.ts index ce551da7e9..573b96aba0 100644 --- a/apps/main/src/window.ts +++ b/apps/main/src/window.ts @@ -14,8 +14,10 @@ import { getIconPath } from "./helper" import { t } from "./lib/i18n" import { store } from "./lib/store" import { getTrayConfig } from "./lib/tray" +import { refreshBound } from "./lib/utils" import { logger } from "./logger" import { cancelPollingUpdateUnreadCount, pollingUpdateUnreadCount } from "./tipc/dock" +import { loadDynamicRenderEntry } from "./updater/hot-updater" const windows = { settingWindow: null as BrowserWindow | null, @@ -87,18 +89,10 @@ export function createWindow( }) window.on("leave-html-full-screen", () => { - function refreshBound(timeout = 0) { - setTimeout(() => { - // FIXME: workaround for theme bug in full screen mode - const size = window?.getSize() - window?.setSize(size[0] + 1, size[1] + 1) - window?.setSize(size[0], size[1]) - }, timeout) - } // To solve the vibrancy losing issue when leaving full screen mode // @see https://github.com/toeverything/AFFiNE/blob/280e24934a27557529479a70ab38c4f5fc65cb00/packages/frontend/electron/src/main/windows-manager/main-window.ts:L157 - refreshBound() - refreshBound(1000) + refreshBound(window) + refreshBound(window, 1000) }) window.on("ready-to-show", () => { @@ -149,11 +143,15 @@ export function createWindow( logger.log(process.env["ELECTRON_RENDERER_URL"] + (options?.extraPath || "")) } else { - const openPath = path.resolve(__dirname, "../renderer/index.html") - window.loadFile(openPath, { + // Production entry + const dynamicRenderEntry = loadDynamicRenderEntry() + logger.info("load dynamic render entry", dynamicRenderEntry) + const appLoadEntry = dynamicRenderEntry || path.resolve(__dirname, "../renderer/index.html") + + window.loadFile(appLoadEntry, { hash: options?.extraPath, }) - logger.log(openPath, { + logger.log(appLoadEntry, { hash: options?.extraPath, }) } @@ -165,19 +163,37 @@ export function createWindow( window.webContents.on("devtools-opened", () => { // source-code-font: For code such as Elements panel // monospace-font: For sidebar such as Event Listener Panel - const css = ` - :root { - --source-code-font-family: consolas; - --source-code-font-size: 13px; - --monospace-font-family: consolas; - --monospace-font-size: 13px; - }` + const css = `:root {--devtool-font-family: consolas, operator mono, Cascadia Code, OperatorMonoSSmLig Nerd Font,"Agave Nerd Font","Cascadia Code PL", monospace;--source-code-font-family:var(--devtool-font-family);--source-code-font-size: 13px;--monospace-font-family: var(--devtool-font-family);--monospace-font-size: 13px;}` window.webContents.devToolsWebContents?.executeJavaScript(` const overriddenStyle = document.createElement('style'); overriddenStyle.innerHTML = '${css.replaceAll("\n", " ")}'; document.body.append(overriddenStyle); - document.body.classList.remove('platform-windows'); - `) + document.querySelectorAll('.platform-windows').forEach(el => el.classList.remove('platform-windows')); + addStyleToAutoComplete(); + const observer = new MutationObserver((mutationList, observer) => { + for (const mutation of mutationList) { + if (mutation.type === 'childList') { + for (let i = 0; i < mutation.addedNodes.length; i++) { + const item = mutation.addedNodes[i]; + if (item.classList.contains('editor-tooltip-host')) { + addStyleToAutoComplete(); + } + } + } + } + }); + observer.observe(document.body, {childList: true}); + function addStyleToAutoComplete() { + document.querySelectorAll('.editor-tooltip-host').forEach(element => { + if (element.shadowRoot.querySelectorAll('[data-key="overridden-dev-tools-font"]').length === 0) { + const overriddenStyle = document.createElement('style'); + overriddenStyle.setAttribute('data-key', 'overridden-dev-tools-font'); + overriddenStyle.innerHTML = '.cm-tooltip-autocomplete ul[role=listbox] {font-family: consolas !important;}'; + element.shadowRoot.append(overriddenStyle); + } + }); + } + `) }) } @@ -282,7 +298,7 @@ export const createMainWindow = () => { const caller = callWindowExpose(window) const settings = await caller.getUISettings() - if (settings.showDockBadge) { + if (settings?.showDockBadge) { pollingUpdateUnreadCount() } }) diff --git a/apps/renderer/package.json b/apps/renderer/package.json index 9c947d5aa6..c5668a6ac2 100644 --- a/apps/renderer/package.json +++ b/apps/renderer/package.json @@ -11,16 +11,16 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@dnd-kit/core": "^6.1.0", + "@dnd-kit/core": "^6.2.0", "@egoist/tipc": "0.3.2", "@electron-toolkit/preload": "^3.0.1", "@follow/electron-main": "workspace:*", "@follow/shared": "workspace:*", "@fontsource/sn-pro": "5.1.0", "@headlessui/react": "2.2.0", - "@hono/auth-js": "1.0.13", + "@hono/auth-js": "1.0.15", "@hookform/resolvers": "3.9.1", - "@lottiefiles/dotlottie-react": "0.9.3", + "@lottiefiles/dotlottie-react": "0.10.0", "@microflash/remark-callout-directives": "4.3.2", "@openpanel/web": "1.0.1", "@radix-ui/react-avatar": "1.1.1", @@ -32,63 +32,63 @@ "@radix-ui/react-popover": "1.1.2", "@radix-ui/react-slider": "^1.2.1", "@radix-ui/react-slot": "1.1.0", - "@sentry/react": "8.36.0", - "@shikijs/transformers": "1.22.2", - "@tanstack/query-sync-storage-persister": "5.59.17", - "@tanstack/react-query": "5.59.19", - "@tanstack/react-query-devtools": "5.59.19", - "@tanstack/react-query-persist-client": "5.59.19", + "@sentry/react": "8.40.0", + "@shikijs/transformers": "1.23.1", + "@tanstack/query-sync-storage-persister": "5.61.4", + "@tanstack/react-query": "5.61.4", + "@tanstack/react-query-devtools": "5.61.4", + "@tanstack/react-query-persist-client": "5.61.4", "@use-gesture/react": "10.3.1", "@welldone-software/why-did-you-render": "8.0.3", "@yornaath/batshit": "0.10.1", "camelcase-keys": "9.1.3", - "click-to-react-component": "1.1.0", + "click-to-react-component": "1.1.2", "clsx": "2.1.1", - "cmdk": "1.0.3", + "cmdk": "1.0.4", "dayjs": "1.11.13", - "dexie": "4.0.9", - "dexie-export-import": "^4.1.2", + "dexie": "4.0.10", + "dexie-export-import": "^4.1.4", "dnum": "^2.14.0", - "electron-log": "5.2.0", - "embla-carousel-react": "8.3.1", + "electron-log": "5.2.3", + "embla-carousel-react": "8.5.1", "embla-carousel-wheel-gestures": "8.0.1", "es-toolkit": "catalog:", - "firebase": "10.14.1", - "foxact": "0.2.41", - "framer-motion": "11.11.11", + "firebase": "11.0.2", + "foxact": "0.2.42", + "framer-motion": "11.11.17", "franc-min": "6.2.0", "fuse.js": "7.0.0", "hast-util-to-jsx-runtime": "2.3.2", - "hast-util-to-mdast": "^10.1.0", + "hast-util-to-mdast": "^10.1.1", "hast-util-to-text": "4.0.2", - "i18next": "^23.16.4", + "i18next": "^24.0.2", "i18next-browser-languagedetector": "8.0.0", "idb-keyval": "6.2.1", "immer": "10.1.1", - "jotai": "2.10.1", + "jotai": "2.10.3", "katex": "0.16.11", "lethargy": "1.0.9", "masonic": "4.0.1", "mdast-util-gfm-table": "^2.0.0", - "mdast-util-to-markdown": "^2.1.1", - "nanoid": "5.0.8", + "mdast-util-to-markdown": "^2.1.2", + "nanoid": "5.0.9", "ofetch": "1.4.1", "path-to-regexp": "8.2.0", "plain-shiki": "0.0.12", - "re-resizable": "6.10.0", + "re-resizable": "6.10.1", "react-blurhash": "^0.3.0", "react-error-boundary": "4.1.2", "react-fast-marquee": "1.6.5", - "react-hook-form": "7.53.1", + "react-hook-form": "7.53.2", "react-hotkeys-hook": "4.6.1", - "react-i18next": "^15.1.0", + "react-i18next": "^15.1.2", "react-intersection-observer": "9.13.1", "react-ios-pwa-prompt": "^2.0.6", "react-resizable-layout": "npm:@innei/react-resizable-layout@0.7.3-fork.1", - "react-router-dom": "6.27.0", + "react-router": "7.0.1", "react-selecto": "^1.26.3", "react-shadow": "20.5.0", - "react-virtuoso": "4.12.0", + "react-virtuoso": "4.12.2", "react-zoom-pan-pinch": "^3.6.1", "rehype-infer-description-meta": "2.0.0", "rehype-parse": "9.0.1", @@ -99,9 +99,9 @@ "remark-gh-alerts": "0.0.3", "remark-parse": "11.0.0", "remark-rehype": "11.1.1", - "shiki": "1.22.2", - "sonner": "1.5.0", - "tldts": "6.1.58", + "shiki": "1.23.1", + "sonner": "1.7.0", + "tldts": "6.1.64", "unified": "11.0.5", "unist-util-visit-parents": "^6.0.1", "use-context-selector": "2.0.0", @@ -109,6 +109,7 @@ "use-sync-external-store": "1.2.2", "usehooks-ts": "3.1.0", "vfile": "6.0.3", + "web-push": "3.6.7", "zod": "3.23.8", "zustand": "5.0.1" }, @@ -123,15 +124,16 @@ "@follow/shared": "workspace:*", "@follow/types": "workspace:*", "@follow/utils": "workspace:*", - "@hono/node-server": "1.13.4", + "@hono/node-server": "1.13.7", "@types/katex": "0.16.7", - "@types/node": "^22.8.7", + "@types/node": "^22.10.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vite-pwa/assets-generator": "^0.2.6", "fake-indexeddb": "6.0.0", - "happy-dom": "15.8.0", + "happy-dom": "15.11.6", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-scan": "0.0.33" } } diff --git a/apps/renderer/src/App.tsx b/apps/renderer/src/App.tsx index abfc9e6d57..c0de16b525 100644 --- a/apps/renderer/src/App.tsx +++ b/apps/renderer/src/App.tsx @@ -2,7 +2,7 @@ import { isMobile } from "@follow/components/hooks/useMobile.js" import { IN_ELECTRON } from "@follow/shared/constants" import { cn, getOS } from "@follow/utils/utils" import { useEffect } from "react" -import { Outlet } from "react-router-dom" +import { Outlet } from "react-router" import { queryClient } from "~/lib/query-client" @@ -13,7 +13,6 @@ import { applyAfterReadyCallbacks } from "./initialize/queue" import { removeAppSkeleton } from "./lib/app" import { appLog } from "./lib/log" import { Titlebar } from "./modules/app/Titlebar" -import { useRegisterFollowCommands } from "./modules/command/use-register-follow-commands" import { RootProviders } from "./providers/root-providers" import { handlers } from "./tipc" @@ -55,7 +54,6 @@ function App() { const AppLayer = () => { const appIsReady = useAppIsReady() - useRegisterFollowCommands() useEffect(() => { removeAppSkeleton() diff --git a/apps/renderer/src/atoms/ai-summary.ts b/apps/renderer/src/atoms/ai-summary.ts new file mode 100644 index 0000000000..a1a6f485ae --- /dev/null +++ b/apps/renderer/src/atoms/ai-summary.ts @@ -0,0 +1,11 @@ +import { atom } from "jotai" + +import { createAtomHooks } from "~/lib/jotai" + +export const [, , useShowAISummary, , getShowAISummary, setShowAISummary] = createAtomHooks( + atom(false), +) + +export const toggleShowAISummary = () => setShowAISummary(!getShowAISummary()) +export const enableShowAISummary = () => setShowAISummary(true) +export const disableShowAISummary = () => setShowAISummary(false) diff --git a/apps/renderer/src/atoms/ai-translation.ts b/apps/renderer/src/atoms/ai-translation.ts new file mode 100644 index 0000000000..c08e5a8502 --- /dev/null +++ b/apps/renderer/src/atoms/ai-translation.ts @@ -0,0 +1,10 @@ +import { atom } from "jotai" + +import { createAtomHooks } from "~/lib/jotai" + +export const [, , useShowAITranslation, , getShowAITranslation, setShowAITranslation] = + createAtomHooks(atom(false)) + +export const toggleShowAITranslation = () => setShowAITranslation(!getShowAITranslation()) +export const enableShowAITranslation = () => setShowAITranslation(true) +export const disableShowAITranslation = () => setShowAITranslation(false) diff --git a/apps/renderer/src/atoms/feed.ts b/apps/renderer/src/atoms/feed.ts new file mode 100644 index 0000000000..d5a5d9e87e --- /dev/null +++ b/apps/renderer/src/atoms/feed.ts @@ -0,0 +1,69 @@ +import { jotaiStore } from "@follow/utils/jotai" +import { isBizId } from "@follow/utils/utils" +import { atom, useAtomValue } from "jotai" +import { selectAtom } from "jotai/utils" +import { useMemo } from "react" + +import { + FEED_COLLECTION_LIST, + INBOX_PREFIX_ID, + ROUTE_FEED_IN_LIST, + ROUTE_FEED_PENDING, +} from "~/constants" + +const feedUnreadDirtySetAtom = atom(new Set()) + +// 1. feedId may be feedId, or `inbox-id` or `feedId, feedId,` or `list-id`, or `all`, or `collections` +export const useFeedUnreadIsDirty = (feedId: string) => { + return useAtomValue( + useMemo( + () => + selectAtom(feedUnreadDirtySetAtom, (set) => { + const isRealFeedId = isBizId(feedId) + if (isRealFeedId) return set.has(feedId) + + if (feedId.startsWith(ROUTE_FEED_IN_LIST) || feedId.startsWith(INBOX_PREFIX_ID)) { + // List/Inbox is not supported unread + return false + } + + if (feedId === ROUTE_FEED_PENDING) { + return set.size > 0 + } + + if (feedId === FEED_COLLECTION_LIST) { + // Entry in collections has not unread status + return false + } + + const splitted = feedId.split(",") + let isDirty = false + for (const feedId of splitted) { + if (isBizId(feedId)) { + isDirty = isDirty || set.has(feedId) + + if (isDirty) break + } + } + return isDirty + }), + [feedId], + ), + ) +} + +export const setFeedUnreadDirty = (feedId: string) => { + jotaiStore.set(feedUnreadDirtySetAtom, (prev) => { + const newSet = new Set(prev) + newSet.add(feedId) + return newSet + }) +} + +export const clearFeedUnreadDirty = (feedId: string) => { + jotaiStore.set(feedUnreadDirtySetAtom, (prev) => { + const newSet = new Set(prev) + newSet.delete(feedId) + return newSet + }) +} diff --git a/apps/renderer/src/atoms/settings/general.ts b/apps/renderer/src/atoms/settings/general.ts index c6021f948e..66323ee14f 100644 --- a/apps/renderer/src/atoms/settings/general.ts +++ b/apps/renderer/src/atoms/settings/general.ts @@ -7,6 +7,8 @@ const createDefaultSettings = (): GeneralSettings => ({ // App appLaunchOnStartup: false, language: "en", + translationLanguage: "zh-CN", + // mobile app startupScreen: "timeline", // Data control diff --git a/apps/renderer/src/atoms/updater.ts b/apps/renderer/src/atoms/updater.ts index 548d3cfa1c..70c566f2c7 100644 --- a/apps/renderer/src/atoms/updater.ts +++ b/apps/renderer/src/atoms/updater.ts @@ -1,10 +1,12 @@ -import { getStorageNS } from "@follow/utils/ns" -import { atomWithStorage } from "jotai/utils" +import { atom } from "jotai" import { createAtomHooks } from "~/lib/jotai" -export const [, , useUpdaterStatus, , , setUpdaterStatus] = createAtomHooks( - atomWithStorage(getStorageNS("updater"), false, undefined, { - getOnInit: true, - }), +export type UpdaterStatus = "ready" +export type UpdaterStatusAtom = { + type: "app" | "renderer" + status: UpdaterStatus +} | null +export const [, , useUpdaterStatus, , getUpdaterStatus, setUpdaterStatus] = createAtomHooks( + atom(null as UpdaterStatusAtom), ) diff --git a/apps/renderer/src/components/common/ErrorElement.tsx b/apps/renderer/src/components/common/ErrorElement.tsx index 710bfe7132..30ec8b5b01 100644 --- a/apps/renderer/src/components/common/ErrorElement.tsx +++ b/apps/renderer/src/components/common/ErrorElement.tsx @@ -1,7 +1,7 @@ import { Button } from "@follow/components/ui/button/index.js" import { captureException } from "@sentry/react" import { useEffect, useRef } from "react" -import { isRouteErrorResponse, useNavigate, useRouteError } from "react-router-dom" +import { isRouteErrorResponse, useNavigate, useRouteError } from "react-router" import { toast } from "sonner" import { removeAppSkeleton } from "~/lib/app" diff --git a/apps/renderer/src/components/common/NotFound.tsx b/apps/renderer/src/components/common/NotFound.tsx index 0f1c24ed89..03792e7ab8 100644 --- a/apps/renderer/src/components/common/NotFound.tsx +++ b/apps/renderer/src/components/common/NotFound.tsx @@ -2,8 +2,8 @@ import { Logo } from "@follow/components/icons/logo.jsx" import { Button } from "@follow/components/ui/button/index.js" import { captureException } from "@sentry/react" import { useEffect } from "react" -import type { Location } from "react-router-dom" -import { useLocation, useNavigate } from "react-router-dom" +import type { Location } from "react-router" +import { useLocation, useNavigate } from "react-router" import { isElectronBuild } from "~/constants" import { removeAppSkeleton } from "~/lib/app" diff --git a/apps/renderer/src/components/common/PoweredByFooter.tsx b/apps/renderer/src/components/common/PoweredByFooter.tsx index 91291e13a3..7ef3a34363 100644 --- a/apps/renderer/src/components/common/PoweredByFooter.tsx +++ b/apps/renderer/src/components/common/PoweredByFooter.tsx @@ -4,7 +4,8 @@ import pkg from "@pkg" export const PoweredByFooter: Component = ({ className }) => (