diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml
index 9aadbcfb6..499a160c8 100644
--- a/.github/dependabot.yaml
+++ b/.github/dependabot.yaml
@@ -43,6 +43,11 @@ updates:
versions: [">=35.0.0"]
- dependency-name: react-native-sheet-transitions
versions: [">0.1.2"]
+
+ # filter not work
+ - dependency-name: unplugin-ast
+ versions: ["0.14.5"]
+
open-pull-requests-limit: 100
groups:
minor-and-patch:
diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml
index 459306b47..17fc59494 100644
--- a/.github/workflows/build-android.yml
+++ b/.github/workflows/build-android.yml
@@ -2,12 +2,8 @@ name: Build Android
on:
push:
- branches: [dev]
- paths:
- - "apps/mobile/**"
- - "pnpm-lock.yaml"
- pull_request:
- branches: [dev]
+ branches:
+ - "**"
paths:
- "apps/mobile/**"
- "pnpm-lock.yaml"
@@ -22,7 +18,7 @@ on:
description: "Build profile"
concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
+ group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.profile }}
cancel-in-progress: true
jobs:
@@ -81,3 +77,8 @@ jobs:
name: aab-android
path: ${{ github.workspace }}/build.aab
retention-days: 90
+
+ - name: Submit to Google Play
+ if: github.event.inputs.profile == 'production'
+ working-directory: apps/mobile
+ run: eas submit --platform android --path ${{ github.workspace }}/build.aab --non-interactive
diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml
index e6a95a054..1f00fa78e 100644
--- a/.github/workflows/build-desktop.yml
+++ b/.github/workflows/build-desktop.yml
@@ -4,8 +4,9 @@ on:
push:
branches:
- "**"
- tags:
- - "v*"
+ paths:
+ - "apps/desktop/**"
+ - "pnpm-lock.yaml"
workflow_dispatch:
inputs:
tag_version:
@@ -124,20 +125,17 @@ jobs:
- name: Install dependencies
run: pnpm i
- name: Update main hash
- run: |
- cd apps/desktop
- pnpm update:main-hash
+ working-directory: apps/desktop
+ run: pnpm update:main-hash
- name: Build - Vite
- run: |
- cd apps/desktop
- pnpm build:electron-vite ${{ env.PROD == 'false' && '--mode staging' || '' }}
+ working-directory: apps/desktop
+ run: pnpm build:electron-vite ${{ env.PROD == 'false' && '--mode staging' || '' }}
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: Build - Windows and Linux
if: runner.os != 'macOS'
- run: |
- cd apps/desktop
- pnpm build:electron-forge ${{ env.PROD == 'false' && '--mode=staging' || '' }}
+ working-directory: apps/desktop
+ run: pnpm build:electron-forge ${{ env.PROD == 'false' && '--mode=staging' || '' }}
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@@ -166,15 +164,13 @@ jobs:
OSX_SIGN_IDENTITY: ${{ secrets.OSX_SIGN_IDENTITY_MAS }}
OSX_SIGN_PROVISIONING_PROFILE_PATH: ${{ runner.temp }}/build_pp.provisionprofile
BUILD_VERSION: ${{ github.event.inputs.build_version }}
- run: |
- cd apps/desktop
- pnpm build:electron-forge:mas
+ working-directory: apps/desktop
+ run: pnpm build:electron-forge:mas
- name: Build - Renderer
if: runner.os == 'Linux'
- run: |
- cd apps/desktop
- pnpm build:render
+ working-directory: apps/desktop
+ run: pnpm build:render
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
diff --git a/.github/workflows/build-ios-development.yml b/.github/workflows/build-ios-development.yml
index d921bc01a..50b07e47d 100644
--- a/.github/workflows/build-ios-development.yml
+++ b/.github/workflows/build-ios-development.yml
@@ -2,14 +2,8 @@ name: Build iOS for development
on:
push:
- branches: [dev]
- paths:
- - "apps/mobile/web-app/**"
- - "apps/mobile/native/**"
- - "apps/mobile/package.json"
- - "apps/mobile/app.config.ts"
- pull_request:
- branches: [dev]
+ branches:
+ - "**"
paths:
- "apps/mobile/web-app/**"
- "apps/mobile/native/**"
@@ -69,9 +63,8 @@ jobs:
run: pnpm install
- name: 🔨 Build iOS IPA
- run: |
- cd apps/mobile
- eas build --platform ios --profile development --non-interactive --local --output=./build.ipa
+ working-directory: apps/mobile
+ run: eas build --platform ios --profile development --non-interactive --local --output=./build.ipa
env:
CI: true
@@ -116,9 +109,8 @@ jobs:
run: pnpm install
- name: 🔨 Build iOS IPA
- run: |
- cd apps/mobile
- eas build --platform ios --profile ios-simulator --non-interactive --local --output=./build-simulator.ipa
+ working-directory: apps/mobile
+ run: eas build --platform ios --profile ios-simulator --non-interactive --local --output=./build-simulator.ipa
env:
CI: true
diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml
index 61793111b..a0c4be32f 100644
--- a/.github/workflows/build-ios.yml
+++ b/.github/workflows/build-ios.yml
@@ -2,12 +2,8 @@ name: Build iOS
on:
push:
- branches: [dev]
- paths:
- - "apps/mobile/**"
- - "pnpm-lock.yaml"
- pull_request:
- branches: [dev]
+ branches:
+ - "**"
paths:
- "apps/mobile/**"
- "pnpm-lock.yaml"
@@ -22,7 +18,7 @@ on:
description: "Build profile"
concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
+ group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.profile }}
cancel-in-progress: true
jobs:
@@ -74,9 +70,8 @@ jobs:
run: pnpm install
- name: 🔨 Build iOS IPA
- run: |
- cd apps/mobile
- eas build --platform ios --profile ${{ github.event.inputs.profile || 'preview' }} --non-interactive --local --output=./build.ipa
+ working-directory: apps/mobile
+ run: eas build --platform ios --profile ${{ github.event.inputs.profile || 'preview' }} --non-interactive --local --output=./build.ipa
env:
CI: true
@@ -95,6 +90,5 @@ jobs:
- name: Submit to App Store
if: github.event.inputs.profile == 'production'
- run: |
- cd apps/mobile
- eas submit --platform ios --path build.ipa --non-interactive
+ working-directory: apps/mobile
+ run: eas submit --platform ios --path build.ipa --non-interactive
diff --git a/.github/workflows/sync.yaml b/.github/workflows/sync.yaml
new file mode 100644
index 000000000..79ad3bc69
--- /dev/null
+++ b/.github/workflows/sync.yaml
@@ -0,0 +1,42 @@
+name: Branch Synchronization
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ sync-branches:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+
+ - name: Set up Git
+ run: |
+ git config --global user.name 'GitHub Actions'
+ git config --global user.email 'actions@github.com'
+
+ - name: Prepare sync branch
+ id: branch
+ run: |
+ echo "SYNC_BRANCH_MAIN_DEV=sync/main-to-dev-$(date +'%Y%m%d')" >> $GITHUB_ENV
+
+ - name: Sync main to dev
+ if: github.ref == 'refs/heads/main'
+ run: |
+ # Sync main to dev
+ git checkout main
+ SYNC_BRANCH_DEV=${{ env.SYNC_BRANCH_MAIN_DEV }}
+ git checkout -B $SYNC_BRANCH_DEV
+ DIFF=$(git diff origin/dev...)
+ if [ -z "$DIFF" ]; then
+ echo "No changes to sync"
+ exit 0
+ fi
+ git push origin $SYNC_BRANCH_DEV -f
+ gh pr create --base dev --head $SYNC_BRANCH_DEV --title "Sync main branch to dev branch" --body "Automatic sync" || exit 0
+ env:
+ GH_TOKEN: ${{ github.token }}
diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml
index f824fa7d9..58f7c5aa1 100644
--- a/.github/workflows/tag.yml
+++ b/.github/workflows/tag.yml
@@ -45,5 +45,5 @@ jobs:
curl -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github+json" \
- https://api.github.com/repos/${{ github.repository }}/actions/workflows/build.yml/dispatches \
+ https://api.github.com/repos/${{ github.repository }}/actions/workflows/build-desktop.yml/dispatches \
-d '{"ref":"main", "inputs": {"tag_version": "true"}}'
diff --git a/README.md b/README.md
index 3ce3e0213..d11ef6629 100644
--- a/README.md
+++ b/README.md
@@ -54,7 +54,8 @@ You can also install using the following methods maintained by our community:
- If you are using Arch Linux, you can install package [follow-appimage](https://aur.archlinux.org/packages/follow-appimage) that maintained by [timochan](https://github.com/ttimochan).
- If you are using Nix, you can install package [follow](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/fo/follow/package.nix) that maintained by [iosmanthus](https://github.com/iosmanthus).
-- If you are using macOS with [Homebrew](https://brew.sh), you can install cask [follow](https://formulae.brew.sh/cask/follow) (also [@alpha](https://formulae.brew.sh/cask/follow@alpha) and [@nightly](https://formulae.brew.sh/cask/follow@nightly)) that maintained by [realSunyz](https://github.com/realSunyz).
+- If you are using macOS with [Homebrew](https://brew.sh), you can install cask [folo](https://formulae.brew.sh/cask/folo) that maintained by [realSunyz](https://github.com/realSunyz).
+- If you are using Windows with [Scoop](https://scoop.sh), you can install manifest [folo](https://github.com/cscnk52/cetacea/blob/master/bucket/folo.json) that maintained by [cscnk52](https://github.com/cscnk52).
| [](https://discord.gg/followapp) | Join our Discord server to connect with developers, request features, and receive support. |
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------- |
diff --git a/apps/desktop/bump.config.ts b/apps/desktop/bump.config.ts
new file mode 100644
index 000000000..b4d16b9cb
--- /dev/null
+++ b/apps/desktop/bump.config.ts
@@ -0,0 +1,24 @@
+/* eslint-disable no-template-curly-in-string */
+import { defineConfig } from "nbump"
+
+export default defineConfig({
+ leading: [
+ "git pull --rebase",
+ "tsx scripts/apply-changelog.ts ${NEW_VERSION}",
+ "git add changelog",
+ "tsx plugins/vite/generate-main-hash.ts",
+ "pnpm eslint --fix package.json",
+ "pnpm prettier --ignore-unknown --write package.json",
+ "git add package.json",
+ ],
+ trailing: ["git checkout -b release/desktop/${NEW_VERSION}"],
+ finally: [
+ "git push origin release/desktop/${NEW_VERSION}",
+ "gh pr create --title 'release(desktop): Release v${NEW_VERSION}' --body 'v${NEW_VERSION}' --base main --head release/desktop/${NEW_VERSION}",
+ ],
+ push: false,
+ commitMessage: "release(desktop): release v${NEW_VERSION}",
+ tagPrefix: "desktop@",
+ changelog: false,
+ allowedBranches: ["dev"],
+})
diff --git a/apps/desktop/changelog/next.md b/apps/desktop/changelog/next.md
index 62b641fa5..e2c58b405 100644
--- a/apps/desktop/changelog/next.md
+++ b/apps/desktop/changelog/next.md
@@ -2,6 +2,15 @@
## Shiny new things
+- Server side readability with AI summary and translation support (#3498)
+
## Improvements
+- Save AI summary as description option for Cubox integration (#3478)
+- Delete your own RSSHub instance support (#2883)
+
## No longer broken
+
+- Fixed TTS failed to play (#3522)
+- Fixed corner player can not navigate to correct entry (#1401)
+- Fixed unable to save to Readwise (#3497)
diff --git a/apps/desktop/forge.config.cts b/apps/desktop/forge.config.cts
index 9a24e2757..1d1a58028 100644
--- a/apps/desktop/forge.config.cts
+++ b/apps/desktop/forge.config.cts
@@ -96,6 +96,7 @@ const ignorePattern = new RegExp(`^/node_modules/(?!${[...keepModules].join("|")
const config: ForgeConfig = {
packagerConfig: {
+ name: isStaging ? "Folo Staging" : "Folo",
appCategoryType: "public.app-category.news",
buildVersion: process.env.BUILD_VERSION || undefined,
appBundleId: "is.follow",
@@ -196,7 +197,7 @@ const config: ForgeConfig = {
}),
new MakerPKG(
{
- name: "Folo",
+ name: "Folo Staging",
keychain: process.env.KEYCHAIN_PATH,
},
["mas"],
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index f16ca3f50..7e9c54423 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -33,11 +33,12 @@
"update:main-hash": "tsx plugins/vite/generate-main-hash.ts"
},
"dependencies": {
+ "cookie-es": "2.0.0",
"react-google-recaptcha": "3.1.0",
"react-google-recaptcha-v3": "1.10.1"
},
"devDependencies": {
- "@clack/prompts": "0.10.0",
+ "@clack/prompts": "0.10.1",
"@electron-forge/cli": "7.8.0",
"@electron-forge/maker-appx": "7.8.0",
"@electron-forge/maker-dmg": "7.8.0",
@@ -58,50 +59,30 @@
"@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9",
"@types/react-google-recaptcha": "2.1.9",
- "@vitejs/plugin-legacy": "6.0.2",
- "@vitejs/plugin-react": "4.3.4",
+ "@vitejs/plugin-legacy": "6.1.0",
+ "@vitejs/plugin-react": "4.4.0",
"autoprefixer": "10.4.21",
"bufferutil": "4.0.9",
"cssnano": "7.0.6",
- "dotenv": "16.4.7",
- "drizzle-orm": "0.41.0",
- "electron": "34.5.0",
+ "dotenv": "16.5.0",
+ "drizzle-orm": "0.42.0",
+ "electron": "35.1.5",
"electron-devtools-installer": "4.0.0",
"electron-packager-languages": "0.6.0",
"electron-vite": "3.1.0",
"fake-indexeddb": "6.0.0",
"happy-dom": "17.4.4",
- "hono": "4.7.5",
+ "hono": "4.7.6",
"html-minifier-terser": "7.2.0",
"js-yaml": "4.1.0",
- "nbump": "2.0.7",
+ "nbump": "2.1.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"tar": "7.4.3",
"tsup": "8.4.0",
- "unplugin-ast": "0.14.4",
+ "unplugin-ast": "0.14.6",
"utf-8-validate": "6.0.5"
},
"productName": "Folo",
- "mainHash": "274b8ef4d0671d642c1350e1e4463fce6d677f81f77f59a666d6aa83f5866a22",
- "bump": {
- "before": [
- "git pull --rebase",
- "tsx scripts/apply-changelog.ts ${NEW_VERSION}",
- "git add changelog",
- "tsx plugins/vite/generate-main-hash.ts",
- "pnpm eslint --fix package.json",
- "pnpm prettier --ignore-unknown --write package.json",
- "git add package.json"
- ],
- "after": [
- "gh pr create --title 'release(desktop): Release v${NEW_VERSION}' --body 'v${NEW_VERSION}' --base main --head dev"
- ],
- "commit_message": "release(desktop): release v${NEW_VERSION}",
- "tag": false,
- "changelog": false,
- "allowed_branches": [
- "dev"
- ]
- }
+ "mainHash": "274b8ef4d0671d642c1350e1e4463fce6d677f81f77f59a666d6aa83f5866a22"
}
diff --git a/apps/desktop/src/main/package.json b/apps/desktop/src/main/package.json
index 4c329d2bf..60789bd03 100644
--- a/apps/desktop/src/main/package.json
+++ b/apps/desktop/src/main/package.json
@@ -29,7 +29,7 @@
"@follow/shared": "workspace:*",
"@follow/utils": "workspace:*",
"@openpanel/web": "1.0.1",
- "@sentry/electron": "6.4.0",
+ "@sentry/electron": "6.5.0",
"builder-util-runtime": "9.3.1",
"cookie-es": "2.0.0",
"electron-context-menu": "4.0.5",
@@ -37,10 +37,10 @@
"electron-squirrel-startup": "1.0.1",
"electron-store": "10.0.1",
"electron-updater": "6.6.2",
- "es-toolkit": "1.34.1",
+ "es-toolkit": "1.35.0",
"fast-folder-size": "2.4.0",
"font-list": "1.5.1",
- "i18next": "24.2.3",
+ "i18next": "25.0.0",
"js-yaml": "4.1.0",
"linkedom": "0.18.9",
"lowdb": "7.0.1",
@@ -55,9 +55,9 @@
"@follow/models": "workspace:*",
"@follow/types": "workspace:*",
"@types/js-yaml": "4.0.9",
- "@types/node": "22.14.0",
- "electron": "34.5.0",
+ "@types/node": "22.14.1",
+ "electron": "35.1.5",
"electron-devtools-installer": "4.0.0",
- "hono": "4.7.5"
+ "hono": "4.7.6"
}
}
diff --git a/apps/desktop/src/main/src/constants/app.ts b/apps/desktop/src/main/src/constants/app.ts
index 0c5ef5d8d..33c1b5612 100644
--- a/apps/desktop/src/main/src/constants/app.ts
+++ b/apps/desktop/src/main/src/constants/app.ts
@@ -12,4 +12,4 @@ 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"
-export const BETTER_AUTH_COOKIE_NAME_SESSION_TOKEN = "__Secure-better-auth.session_token"
+export const BETTER_AUTH_COOKIE_NAME_SESSION_TOKEN = "better-auth.session_token"
diff --git a/apps/desktop/src/main/src/index.ts b/apps/desktop/src/main/src/index.ts
index 56c258ca6..adcdcd8b9 100644
--- a/apps/desktop/src/main/src/index.ts
+++ b/apps/desktop/src/main/src/index.ts
@@ -1,3 +1,5 @@
+import "./side-effects"
+
import { electronApp, optimizer } from "@electron-toolkit/utils"
import { callWindowExpose } from "@follow/shared/bridge"
import { APP_PROTOCOL, DEV } from "@follow/shared/constants"
@@ -15,7 +17,7 @@ import { updateProxy } from "./lib/proxy"
import { handleUrlRouting } from "./lib/router"
import { store } from "./lib/store"
import { registerAppTray } from "./lib/tray"
-import { setBetterAuthSessionCookie, updateNotificationsToken } from "./lib/user"
+import { updateNotificationsToken } from "./lib/user"
import { logger } from "./logger"
import { registerUpdater } from "./updater"
import { cleanupOldRender } from "./updater/hot-updater"
@@ -39,6 +41,9 @@ const buildSafeHeaders = createBuildSafeHeaders(env.VITE_WEB_URL, [
env.VITE_OPENPANEL_API_URL || "",
IMAGE_PROXY_URL,
env.VITE_API_URL,
+ // Fix unexpected CORS error when modify the origin header to request domain in the preflight request
+ // Learn more https://github.com/RSSNext/Folo/issues/3312
+ "https://readwise.io",
])
function bootstrap() {
@@ -109,67 +114,10 @@ function bootstrap() {
mainWindow = createMainWindow()
- // restore cookies
- const cookies = store.get("cookies")
- 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()
- // handle session cookie when sign in with email in electron
- session.defaultSession.webRequest.onHeadersReceived(
- {
- urls: [
- `${apiURL}/better-auth/sign-in/email`,
- `${apiURL}/better-auth/sign-in/email?*`,
- `${apiURL}/better-auth/two-factor/verify-totp`,
- `${apiURL}/better-auth/two-factor/verify-totp?*`,
- ],
- },
- (detail, callback) => {
- const { responseHeaders } = detail
- if (responseHeaders?.["set-cookie"]) {
- const cookies = responseHeaders["set-cookie"] as string[]
- cookies.forEach((cookie) => {
- const cookieObj = parse(cookie, { decode: (value) => value })
- Object.keys(cookieObj).forEach((name) => {
- const value = cookieObj[name]
- mainWindow.webContents.session.cookies.set({
- url: apiURL,
- name,
- value,
- secure: true,
- httpOnly: true,
- domain: new URL(apiURL).hostname,
- sameSite: "no_restriction",
- })
- })
- })
- }
-
- callback({ cancel: false, responseHeaders })
- },
- )
-
app.on("open-url", (_, url) => {
if (mainWindow && !mainWindow.isDestroyed()) {
if (mainWindow.isMinimized()) mainWindow.restore()
@@ -215,9 +163,6 @@ function bootstrap() {
})
await session.defaultSession.cookies.flushStore()
- const cookies = await session.defaultSession.cookies.get({})
- store.set("cookies", cookies)
-
await cleanupOldRender()
})
@@ -227,29 +172,36 @@ function bootstrap() {
const urlObj = new URL(url)
if (urlObj.hostname === "auth" || urlObj.pathname === "//auth") {
- const ck = urlObj.searchParams.get("ck")
- const userId = urlObj.searchParams.get("userId")
+ const token = urlObj.searchParams.get("token")
- if (ck && apiURL) {
- setBetterAuthSessionCookie(ck)
- const cookie = parse(atob(ck), { decode: (value) => value })
- Object.keys(cookie).forEach((name) => {
- const value = cookie[name]
- mainWindow.webContents.session.cookies.set({
- url: apiURL,
- name,
- value,
- secure: true,
- httpOnly: true,
- domain: new URL(apiURL).hostname,
- sameSite: "no_restriction",
+ if (token) {
+ await callWindowExpose(mainWindow).applyOneTimeToken(token)
+ } else {
+ // compatible with old version of ssr, should be removed in 0.4.4
+ const ck = urlObj.searchParams.get("ck")
+ const userId = urlObj.searchParams.get("userId")
+
+ if (ck && apiURL) {
+ const cookie = parse(atob(ck), { decode: (value) => value })
+ Object.keys(cookie).forEach(async (name) => {
+ const value = cookie[name]
+ await mainWindow.webContents.session.cookies.set({
+ url: apiURL,
+ name,
+ value,
+ secure: true,
+ httpOnly: true,
+ domain: new URL(apiURL).hostname,
+ sameSite: "no_restriction",
+ expirationDate: new Date().setDate(new Date().getDate() + 30),
+ })
})
- })
- userId && (await callWindowExpose(mainWindow).clearIfLoginOtherAccount(userId))
- mainWindow.reload()
+ userId && (await callWindowExpose(mainWindow).clearIfLoginOtherAccount(userId))
+ mainWindow.reload()
- updateNotificationsToken()
+ updateNotificationsToken()
+ }
}
} else {
handleUrlRouting(url)
diff --git a/apps/desktop/src/main/src/init.ts b/apps/desktop/src/main/src/init.ts
index 41bb74d11..cba43e38f 100644
--- a/apps/desktop/src/main/src/init.ts
+++ b/apps/desktop/src/main/src/init.ts
@@ -28,7 +28,6 @@ if (process.argv.length === 3 && process.argv[2]!.startsWith("follow-dev:")) {
* Mandatory and fast initializers for the app
*/
export function initializeAppStage0() {
- if (DEV) app.setPath("appData", path.join(app.getPath("appData"), "Follow (dev)"))
initializeSentry()
}
export const initializeAppStage1 = () => {
@@ -154,8 +153,6 @@ const registerPushNotifications = async () => {
const credentials = store.get(credentialsKey)
const persistentIds = store.get(persistentIdsKey)
- updateNotificationsToken()
-
const instance = new PushReceiver({
debug: true,
firebase: JSON.parse(env.VITE_FIREBASE_CONFIG),
diff --git a/apps/desktop/src/main/src/lib/api-client.ts b/apps/desktop/src/main/src/lib/api-client.ts
index 27e773c9f..035e0624e 100644
--- a/apps/desktop/src/main/src/lib/api-client.ts
+++ b/apps/desktop/src/main/src/lib/api-client.ts
@@ -4,8 +4,10 @@ import PKG from "@pkg"
import { hc } from "hono/client"
import { ofetch } from "ofetch"
+import { BETTER_AUTH_COOKIE_NAME_SESSION_TOKEN } from "~/constants/app"
+import { getMainWindow } from "~/window"
+
import { logger } from "../logger"
-import { getBetterAuthSessionCookie, getUser } from "./user"
const abortController = new AbortController()
export const apiFetch = ofetch.create({
@@ -14,12 +16,6 @@ export const apiFetch = ofetch.create({
signal: abortController.signal,
retry: false,
onRequest({ request }) {
- const betterAuthSessionCookie = getBetterAuthSessionCookie()
- if (!betterAuthSessionCookie) {
- abortController.abort()
- return
- }
-
logger.info(`API Request: ${request.toString()}`)
},
onRequestError(context) {
@@ -31,14 +27,21 @@ export const apiFetch = ofetch.create({
export const apiClient = hc("", {
fetch: async (input, options = {}) => apiFetch(input.toString(), options),
- headers() {
- const betterAuthSessionCookie = getBetterAuthSessionCookie()
- const user = getUser()
+ async headers() {
+ const window = getMainWindow()
+ const cookies = await window?.webContents.session.cookies.get({
+ domain: new URL(env.VITE_API_URL).hostname,
+ })
+ const sessionCookie = cookies?.find((cookie) =>
+ cookie.name.includes(BETTER_AUTH_COOKIE_NAME_SESSION_TOKEN),
+ )
+ const headerCookie = sessionCookie ? `${sessionCookie.name}=${sessionCookie.value}` : ""
+
return {
"X-App-Version": PKG.version,
"X-App-Dev": process.env.NODE_ENV === "development" ? "1" : "0",
- Cookie: betterAuthSessionCookie ? atob(betterAuthSessionCookie) : "",
- "User-Agent": `Folo/${PKG.version}${user?.id ? ` uid: ${user.id}` : ""}`,
+ Cookie: headerCookie,
+ "User-Agent": `Folo/${PKG.version}`,
}
},
})
diff --git a/apps/desktop/src/main/src/lib/dock.ts b/apps/desktop/src/main/src/lib/dock.ts
index a45c60aca..b2fcf8780 100644
--- a/apps/desktop/src/main/src/lib/dock.ts
+++ b/apps/desktop/src/main/src/lib/dock.ts
@@ -11,3 +11,12 @@ export const setDockCount = (input: number) => {
app.setBadgeCount(input)
}
}
+
+export const getDockCount = () => {
+ if (isWindows) return null
+ if (app.dock) {
+ return app.dock.getBadge()
+ } else {
+ return app.getBadgeCount()
+ }
+}
diff --git a/apps/desktop/src/main/src/lib/store.ts b/apps/desktop/src/main/src/lib/store.ts
index edb1abf0c..b8423ad64 100644
--- a/apps/desktop/src/main/src/lib/store.ts
+++ b/apps/desktop/src/main/src/lib/store.ts
@@ -1,5 +1,4 @@
import type { Credentials } from "@eneris/push-receiver/dist/types"
-import type { Cookie } from "electron"
import Store from "electron-store"
// @keep-sorted
@@ -7,9 +6,7 @@ type StoreData = {
"notifications-credentials"?: Credentials | null
"notifications-persistent-ids"?: string[] | null
appearance?: "light" | "dark" | "system" | null
- betterAuthSessionCookie?: string | null
cacheSizeLimit?: number | null
- cookies?: Cookie[] | null
minimizeToTray?: boolean | null
proxy?: string | null
user?: string | null
diff --git a/apps/desktop/src/main/src/lib/tray.ts b/apps/desktop/src/main/src/lib/tray.ts
index c559add8a..a2988ac21 100644
--- a/apps/desktop/src/main/src/lib/tray.ts
+++ b/apps/desktop/src/main/src/lib/tray.ts
@@ -1,12 +1,13 @@
import { name } from "@pkg"
import { app, Menu, nativeImage, Tray } from "electron"
-import { isMacOS } from "~/env"
+import { isMacOS, isWindows } from "~/env"
import { getTrayIconPath } from "~/helper"
import { logger, revealLogFile } from "~/logger"
import { checkForAppUpdates } from "~/updater"
import { getMainWindowOrCreate } from "../window"
+import { getDockCount } from "./dock"
import { t } from "./i18n"
import { store } from "./store"
@@ -14,19 +15,17 @@ import { store } from "./store"
let tray: Tray | null = null
-export const registerAppTray = () => {
- if (!getTrayConfig()) return
- if (tray) {
- destroyAppTray()
- }
-
- const icon = nativeImage.createFromPath(getTrayIconPath())
- // See https://stackoverflow.com/questions/41664208/electron-tray-icon-change-depending-on-dark-theme/41998326#41998326
- const trayIcon = isMacOS ? icon.resize({ width: 16 }) : icon
- trayIcon.setTemplateImage(true)
- tray = new Tray(trayIcon)
-
- const contextMenu = Menu.buildFromTemplate([
+const getTrayContextMenu = () => {
+ const count = getDockCount()
+ return Menu.buildFromTemplate([
+ ...(count
+ ? [
+ {
+ label: `${t("menu.unread")} ${count}`,
+ enabled: false,
+ },
+ ]
+ : []),
{
label: t("menu.open", { name }),
click: showWindow,
@@ -71,9 +70,27 @@ export const registerAppTray = () => {
},
},
])
- tray.setContextMenu(contextMenu)
+}
+export const registerAppTray = () => {
+ if (!getTrayConfig()) return
+ if (tray) {
+ destroyAppTray()
+ }
+
+ const icon = nativeImage.createFromPath(getTrayIconPath())
+ // See https://stackoverflow.com/questions/41664208/electron-tray-icon-change-depending-on-dark-theme/41998326#41998326
+ const trayIcon = isMacOS ? icon.resize({ width: 16 }) : icon
+ trayIcon.setTemplateImage(true)
+ tray = new Tray(trayIcon)
+
+ tray.setContextMenu(getTrayContextMenu())
tray.setToolTip(app.getName())
- tray.on("click", showWindow)
+ tray.on("mouse-enter", () => {
+ tray?.setContextMenu(getTrayContextMenu())
+ })
+ if (isWindows) {
+ tray.on("click", showWindow)
+ }
}
const showWindow = () => {
diff --git a/apps/desktop/src/main/src/lib/user.ts b/apps/desktop/src/main/src/lib/user.ts
index f2445067c..1432f2b91 100644
--- a/apps/desktop/src/main/src/lib/user.ts
+++ b/apps/desktop/src/main/src/lib/user.ts
@@ -1,5 +1,4 @@
import type { Credentials } from "@eneris/push-receiver/dist/types"
-import type { UserModel } from "@follow/models"
import { isLinux, isMacOS, isWindows } from "~/env"
import { logger } from "~/logger"
@@ -7,19 +6,13 @@ import { logger } from "~/logger"
import { apiClient } from "./api-client"
import { store } from "./store"
-const BetterAuthKey = "betterAuthSessionCookie"
-export const setBetterAuthSessionCookie = (cookie: string) => store.set(BetterAuthKey, cookie)
-export const getBetterAuthSessionCookie = () => store.get(BetterAuthKey)
-export const cleanBetterAuthSessionCookie = () => store.set(BetterAuthKey, null)
-
-const UserKey = "user"
-export const setUser = (user: UserModel) => store.set(UserKey, JSON.stringify(user))
-export const getUser = (): UserModel | null => {
- const user = store.get(UserKey)
- return user ? JSON.parse(user) : null
-}
-
-export const cleanUser = () => store.set(UserKey, null)
+const notificationChannel = isMacOS
+ ? "macos"
+ : isWindows
+ ? "windows"
+ : isLinux
+ ? "linux"
+ : "desktop"
export const updateNotificationsToken = async (newCredentials?: Credentials) => {
if (newCredentials) {
@@ -31,7 +24,7 @@ export const updateNotificationsToken = async (newCredentials?: Credentials) =>
await apiClient.messaging.$post({
json: {
token: credentials.fcm.token,
- channel: isMacOS ? "macos" : isWindows ? "windows" : isLinux ? "linux" : "desktop",
+ channel: notificationChannel,
},
})
} catch (error) {
@@ -39,3 +32,11 @@ export const updateNotificationsToken = async (newCredentials?: Credentials) =>
}
}
}
+
+export const deleteNotificationsToken = async () => {
+ await apiClient.messaging.$delete({
+ json: {
+ channel: notificationChannel,
+ },
+ })
+}
diff --git a/apps/desktop/src/main/src/side-effects.ts b/apps/desktop/src/main/src/side-effects.ts
new file mode 100644
index 000000000..dbf90b611
--- /dev/null
+++ b/apps/desktop/src/main/src/side-effects.ts
@@ -0,0 +1,6 @@
+import path from "node:path"
+
+import { DEV } from "@follow/shared"
+import { app } from "electron"
+
+if (DEV) app.setPath("userData", path.join(app.getPath("appData"), "Folo(dev)"))
diff --git a/apps/desktop/src/main/src/tipc/app.ts b/apps/desktop/src/main/src/tipc/app.ts
index c55260526..32567c8e4 100644
--- a/apps/desktop/src/main/src/tipc/app.ts
+++ b/apps/desktop/src/main/src/tipc/app.ts
@@ -6,10 +6,8 @@ import { fileURLToPath } from "node:url"
import { getRendererHandlers } from "@egoist/tipc/main"
import { callWindowExpose } from "@follow/shared/bridge"
import { DEV } from "@follow/shared/constants"
-import { env } from "@follow/shared/env.desktop"
import { app, BrowserWindow, clipboard, dialog, shell } from "electron"
-import { BETTER_AUTH_COOKIE_NAME_SESSION_TOKEN } from "~/constants/app"
import { filePathToAppUrl } from "~/helper"
import { registerMenuAndContextMenu } from "~/init"
import { clearAllData, getCacheSize } from "~/lib/cleaner"
@@ -20,7 +18,6 @@ import { cleanupOldRender, loadDynamicRenderEntry } from "~/updater/hot-updater"
import { downloadFile } from "../lib/download"
import { i18n } from "../lib/i18n"
-import { cleanBetterAuthSessionCookie, cleanUser } from "../lib/user"
import type { RendererHandlers } from "../renderer-handlers"
import { quitAndInstall } from "../updater"
import { getMainWindow } from "../window"
@@ -151,16 +148,6 @@ export const appRoute = {
quitAndInstall()
}),
- cleanBetterAuthSessionCookie: t.procedure.action(async () => {
- cleanBetterAuthSessionCookie()
- cleanUser()
-
- const mainWindow = getMainWindow()
- await mainWindow?.webContents.session.cookies.remove(
- env.VITE_API_URL,
- BETTER_AUTH_COOKIE_NAME_SESSION_TOKEN,
- )
- }),
/// clipboard
readClipboard: t.procedure.action(async () => clipboard.readText()),
diff --git a/apps/desktop/src/main/src/tipc/auth.ts b/apps/desktop/src/main/src/tipc/auth.ts
new file mode 100644
index 000000000..d06a38e9d
--- /dev/null
+++ b/apps/desktop/src/main/src/tipc/auth.ts
@@ -0,0 +1,13 @@
+import { deleteNotificationsToken, updateNotificationsToken } from "~/lib/user"
+
+import { t } from "./_instance"
+
+export const authRoute = {
+ sessionChanged: t.procedure.action(async () => {
+ await updateNotificationsToken()
+ }),
+
+ signOut: t.procedure.action(async () => {
+ await deleteNotificationsToken()
+ }),
+}
diff --git a/apps/desktop/src/main/src/tipc/index.ts b/apps/desktop/src/main/src/tipc/index.ts
index 5918f3139..66a472fef 100644
--- a/apps/desktop/src/main/src/tipc/index.ts
+++ b/apps/desktop/src/main/src/tipc/index.ts
@@ -1,19 +1,19 @@
import { appRoute } from "./app"
+import { authRoute } from "./auth"
import { debugRoute } from "./debug"
import { dockRoute } from "./dock"
import { menuRoute } from "./menu"
import { readerRoute } from "./reader"
import { settingRoute } from "./setting"
-import { trackerRoute } from "./tracker"
export const router = {
...debugRoute,
...menuRoute,
...settingRoute,
...appRoute,
- ...trackerRoute,
...dockRoute,
...readerRoute,
+ ...authRoute,
}
export type Router = typeof router
diff --git a/apps/desktop/src/main/src/tipc/reader.ts b/apps/desktop/src/main/src/tipc/reader.ts
index f1bfe1c99..a20edbaf3 100644
--- a/apps/desktop/src/main/src/tipc/reader.ts
+++ b/apps/desktop/src/main/src/tipc/reader.ts
@@ -41,8 +41,9 @@ export const readerRoute = {
if (!window) return
// It's ok to set voice every time, because it will be cached by msedge-tts
+ // Don't know why webm is not working, using mp3 instead
await tts
- .setMetadata(voice, OUTPUT_FORMAT.WEBM_24KHZ_16BIT_MONO_OPUS)
+ .setMetadata(voice, OUTPUT_FORMAT.AUDIO_24KHZ_96KBITRATE_MONO_MP3)
.catch((error: unknown) => {
console.error("Failed to set voice", error)
if (error instanceof Error) {
@@ -55,12 +56,17 @@ export const readerRoute = {
})
})
- const filePath = path.join(app.getPath("userData"), `${id}.webm`)
- if (fs.existsSync(filePath)) {
+ const dirPath = path.join(app.getPath("userData"), "Cache", "tts", id)
+ const possibleFilePathList = ["mp3", "webm"].map((ext) => {
+ return path.join(dirPath, `audio.${ext}`)
+ })
+ const filePath = possibleFilePathList.find((p) => fs.existsSync(p))
+ if (filePath) {
return filePath
} else {
- await tts.toFile(filePath, text)
- return filePath
+ fs.mkdirSync(dirPath, { recursive: true })
+ const { audioFilePath } = await tts.toFile(dirPath, text)
+ return audioFilePath
}
}),
diff --git a/apps/desktop/src/main/src/tipc/tracker.ts b/apps/desktop/src/main/src/tipc/tracker.ts
deleted file mode 100644
index 63caa1116..000000000
--- a/apps/desktop/src/main/src/tipc/tracker.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { UserModel } from "@follow/models"
-
-import { setUser } from "../lib/user"
-import { t } from "./_instance"
-
-export const trackerRoute = {
- trackerIdentify: t.procedure
- .input<{
- user: UserModel
- }>()
- .action(async ({ input }) => {
- const { user } = input
-
- setUser(user)
- }),
-}
diff --git a/apps/desktop/src/renderer/package.json b/apps/desktop/src/renderer/package.json
index 5e90dc565..5d722af8a 100644
--- a/apps/desktop/src/renderer/package.json
+++ b/apps/desktop/src/renderer/package.json
@@ -20,25 +20,25 @@
"@follow/shared": "workspace:*",
"@follow/tracker": "workspace:*",
"@fontsource/sn-pro": "5.2.5",
- "@headlessui/react": "2.2.1",
+ "@headlessui/react": "2.2.2",
"@hookform/resolvers": "4.1.3",
"@lottiefiles/dotlottie-react": "0.13.3",
"@openpanel/web": "1.0.1",
- "@radix-ui/react-avatar": "1.1.3",
- "@radix-ui/react-context-menu": "2.2.6",
- "@radix-ui/react-dialog": "1.1.6",
- "@radix-ui/react-dropdown-menu": "2.1.6",
- "@radix-ui/react-hover-card": "1.1.6",
- "@radix-ui/react-label": "2.1.2",
- "@radix-ui/react-popover": "1.1.6",
- "@radix-ui/react-slider": "1.2.3",
- "@radix-ui/react-slot": "1.1.2",
- "@sentry/react": "9.11.0",
- "@shikijs/transformers": "3.2.1",
- "@tanstack/query-sync-storage-persister": "5.71.10",
- "@tanstack/react-query": "5.71.10",
- "@tanstack/react-query-devtools": "5.71.10",
- "@tanstack/react-query-persist-client": "5.71.10",
+ "@radix-ui/react-avatar": "1.1.4",
+ "@radix-ui/react-context-menu": "2.2.7",
+ "@radix-ui/react-dialog": "1.1.7",
+ "@radix-ui/react-dropdown-menu": "2.1.7",
+ "@radix-ui/react-hover-card": "1.1.7",
+ "@radix-ui/react-label": "2.1.3",
+ "@radix-ui/react-popover": "1.1.7",
+ "@radix-ui/react-slider": "1.2.4",
+ "@radix-ui/react-slot": "1.2.0",
+ "@sentry/react": "9.13.0",
+ "@shikijs/transformers": "3.2.2",
+ "@tanstack/query-sync-storage-persister": "5.74.4",
+ "@tanstack/react-query": "5.74.4",
+ "@tanstack/react-query-devtools": "5.74.4",
+ "@tanstack/react-query-persist-client": "5.74.4",
"@tanstack/react-virtual": "3.13.6",
"@use-gesture/react": "10.3.1",
"@welldone-software/why-did-you-render": "10.0.1",
@@ -53,23 +53,23 @@
"dnum": "2.14.0",
"embla-carousel-react": "8.6.0",
"embla-carousel-wheel-gestures": "8.0.2",
- "es-toolkit": "1.34.1",
+ "es-toolkit": "1.35.0",
"firebase": "11.6.0",
"foxact": "0.2.45",
- "framer-motion": "12.6.3",
"franc-min": "6.2.0",
"fuse.js": "7.1.0",
"hast-util-to-jsx-runtime": "2.3.6",
"hast-util-to-mdast": "10.1.2",
- "i18next": "24.2.3",
+ "i18next": "25.0.0",
"i18next-browser-languagedetector": "8.0.4",
"idb-keyval": "6.2.1",
"immer": "10.1.1",
- "jotai": "2.12.2",
+ "jotai": "2.12.3",
"lethargy": "1.0.9",
"masonic": "4.0.1",
"mdast-util-gfm-table": "2.0.0",
"mdast-util-to-markdown": "2.1.2",
+ "motion": "12.7.4",
"nanoid": "5.1.5",
"ofetch": "1.4.1",
"plain-shiki": "0.2.0",
@@ -77,23 +77,23 @@
"react-blurhash": "0.3.0",
"react-fast-marquee": "1.6.5",
"react-hook-form": "7.55.0",
- "react-hotkeys-hook": "4.6.1",
+ "react-hotkeys-hook": "5.0.1",
"react-i18next": "15.4.1",
"react-intersection-observer": "9.16.0",
"react-ios-pwa-prompt": "2.0.6",
"react-qr-code": "2.0.15",
"react-resizable-layout": "npm:@innei/react-resizable-layout@0.7.3-fork.1",
- "react-router": "7.5.0",
+ "react-router": "7.5.1",
"react-selecto": "1.26.3",
"react-shadow": "20.6.0",
"react-zoom-pan-pinch": "3.7.0",
- "shiki": "3.2.1",
+ "shiki": "3.2.2",
"sonner": "2.0.3",
- "tldts": "6.1.85",
+ "tldts": "7.0.0",
"use-context-selector": "2.0.0",
"use-sync-external-store": "1.5.0",
"usehooks-ts": "3.1.1",
- "zod": "3.24.2",
+ "zod": "3.24.3",
"zustand": "5.0.3"
},
"devDependencies": {
@@ -105,7 +105,7 @@
"@follow/models": "workspace:*",
"@follow/types": "workspace:*",
"@follow/utils": "workspace:*",
- "@types/node": "22.14.0",
+ "@types/node": "22.14.1",
"@vite-pwa/assets-generator": "1.0.0",
"fake-indexeddb": "6.0.0",
"happy-dom": "17.4.4",
diff --git a/apps/desktop/src/renderer/src/atoms/readability.ts b/apps/desktop/src/renderer/src/atoms/readability.ts
index 961384487..5ebba8eb5 100644
--- a/apps/desktop/src/renderer/src/atoms/readability.ts
+++ b/apps/desktop/src/renderer/src/atoms/readability.ts
@@ -1,4 +1,6 @@
+import { getStorageNS } from "@follow/utils/ns"
import { atom } from "jotai"
+import { atomWithStorage } from "jotai/utils"
import { createAtomHooks } from "~/lib/jotai"
@@ -28,7 +30,11 @@ export const [
getReadabilityContent,
__setReadabilityContent,
useReadabilityContentSelector,
-] = createAtomHooks(atom>({}))
+] = createAtomHooks(
+ atomWithStorage>(getStorageNS("readability-content"), {}, undefined, {
+ getOnInit: true,
+ }),
+)
export const setReadabilityContent = mergeObjectSetter(
__setReadabilityContent,
getReadabilityContent,
@@ -57,6 +63,12 @@ export const useEntryIsInReadability = (entryId?: string) =>
[entryId],
)
+export const useEntryIsInReadabilitySuccess = (entryId?: string) =>
+ useReadabilityStatusSelector(
+ (map) => (entryId ? map[entryId] === ReadabilityStatus.SUCCESS : false),
+ [entryId],
+ )
+
export const useEntryInReadabilityStatus = (entryId?: string) =>
useReadabilityStatusSelector(
(map) => (entryId ? map[entryId] || ReadabilityStatus.INITIAL : ReadabilityStatus.INITIAL),
diff --git a/apps/desktop/src/renderer/src/atoms/settings/general.ts b/apps/desktop/src/renderer/src/atoms/settings/general.ts
index 895679afd..1be78c825 100644
--- a/apps/desktop/src/renderer/src/atoms/settings/general.ts
+++ b/apps/desktop/src/renderer/src/atoms/settings/general.ts
@@ -178,6 +178,13 @@ export function useActionLanguage() {
) as SupportedLanguages
}
+export function getActionLanguage() {
+ const { actionLanguage, language } = getGeneralSettingsInternal()
+ return (
+ actionLanguage === DEFAULT_ACTION_LANGUAGE ? language : actionLanguage
+ ) as SupportedLanguages
+}
+
export const subscribeShouldUseIndexedDB = (callback: (value: boolean) => void) =>
jotaiStore.sub(__generalSettingAtom, () => callback(getGeneralSettingsInternal().dataPersist))
diff --git a/apps/desktop/src/renderer/src/components/common/Motion.tsx b/apps/desktop/src/renderer/src/components/common/Motion.tsx
index f6d092d48..dcec6083c 100644
--- a/apps/desktop/src/renderer/src/components/common/Motion.tsx
+++ b/apps/desktop/src/renderer/src/components/common/Motion.tsx
@@ -1,5 +1,5 @@
-import type { MotionProps, TargetAndTransition } from "framer-motion"
-import { m as M } from "framer-motion"
+import type { MotionProps, TargetAndTransition } from "motion/react"
+import { m as M } from "motion/react"
import { createElement, forwardRef } from "react"
import { useReduceMotion } from "~/hooks/biz/useReduceMotion"
diff --git a/apps/desktop/src/renderer/src/components/ui/auto-completion/AutoCompletion.tsx b/apps/desktop/src/renderer/src/components/ui/auto-completion/AutoCompletion.tsx
index 653b5eafe..6d751dcea 100644
--- a/apps/desktop/src/renderer/src/components/ui/auto-completion/AutoCompletion.tsx
+++ b/apps/desktop/src/renderer/src/components/ui/auto-completion/AutoCompletion.tsx
@@ -3,8 +3,8 @@ import { useCorrectZIndex } from "@follow/components/ui/z-index/ctx.js"
import { stopPropagation } from "@follow/utils/dom"
import { cn } from "@follow/utils/utils"
import { Combobox, ComboboxInput, ComboboxOption, ComboboxOptions } from "@headlessui/react"
-import { AnimatePresence, m } from "framer-motion"
import Fuse from "fuse.js"
+import { AnimatePresence, m } from "motion/react"
import { forwardRef, Fragment, useCallback, useEffect, useState } from "react"
export type Suggestion = {
diff --git a/apps/desktop/src/renderer/src/components/ui/button/CopyButton.tsx b/apps/desktop/src/renderer/src/components/ui/button/CopyButton.tsx
index 6ffbe5a74..7abebc66c 100644
--- a/apps/desktop/src/renderer/src/components/ui/button/CopyButton.tsx
+++ b/apps/desktop/src/renderer/src/components/ui/button/CopyButton.tsx
@@ -7,7 +7,7 @@ import { AnimatedCommandButton } from "./base"
export const CopyButton: Component<{
value: string
style?: React.CSSProperties
- variant?: "solid" | "outline"
+ variant?: "solid" | "outline" | "ghost"
}> = ({ value, className, style, variant = "solid" }) => {
const copiedTimerRef = useRef()
const handleCopy = useCallback(() => {
diff --git a/apps/desktop/src/renderer/src/components/ui/button/base.tsx b/apps/desktop/src/renderer/src/components/ui/button/base.tsx
index a4b8b56d7..9b080cc22 100644
--- a/apps/desktop/src/renderer/src/components/ui/button/base.tsx
+++ b/apps/desktop/src/renderer/src/components/ui/button/base.tsx
@@ -1,8 +1,8 @@
import { MotionButtonBase } from "@follow/components/ui/button/index.js"
import { useTypeScriptHappyCallback } from "@follow/hooks"
import { cn } from "@follow/utils/utils"
-import type { HTMLMotionProps, Variants } from "framer-motion"
-import { AnimatePresence, m } from "framer-motion"
+import type { HTMLMotionProps, Variants } from "motion/react"
+import { AnimatePresence, m } from "motion/react"
import type { FC } from "react"
import * as React from "react"
import { cloneElement, useRef, useState } from "react"
@@ -44,7 +44,7 @@ export const AnimatedCommandButton: FC ,
typescriptreact: ,
@@ -62,15 +25,6 @@ const languageToIconMap = {
css: ,
markdown: ,
}
-export const getLanguageColor = (language?: string) => {
- if (!language) return
-
- const alias = LanguageAlias[language]
- if (alias) {
- return LanguageToColorMap[alias]
- }
- return LanguageToColorMap[language]
-}
export const getLanguageIcon = (language?: string) => {
if (!language) return null
diff --git a/apps/desktop/src/renderer/src/components/ui/code-highlighter/shiki/Shiki.tsx b/apps/desktop/src/renderer/src/components/ui/code-highlighter/shiki/Shiki.tsx
index 752c01eb2..86d17d593 100644
--- a/apps/desktop/src/renderer/src/components/ui/code-highlighter/shiki/Shiki.tsx
+++ b/apps/desktop/src/renderer/src/components/ui/code-highlighter/shiki/Shiki.tsx
@@ -1,4 +1,3 @@
-import { useIsDark } from "@follow/hooks"
import { ELECTRON_BUILD } from "@follow/shared/constants"
import { cn } from "@follow/utils/utils"
import { useIsomorphicLayoutEffect } from "foxact/use-isomorphic-layout-effect"
@@ -11,11 +10,12 @@ import type {
DynamicImportThemeRegistration,
} from "shiki"
-import { useUISettingKey, useUISettingSelector } from "~/atoms/settings/ui"
+import { useUISettingKey } from "~/atoms/settings/ui"
import { tipcClient } from "~/lib/client"
import { CopyButton } from "../../button/CopyButton"
-import { getLanguageColor, getLanguageIcon } from "../constants"
+import { getLanguageIcon } from "../constants"
+import { useShikiDefaultTheme } from "./hooks"
import { shiki, shikiTransformers } from "./shared"
import styles from "./shiki.module.css"
@@ -72,10 +72,7 @@ export const ShikiHighLighter: FC = (props) => {
const [loaded, setLoaded] = useState(false)
- const isDark = useIsDark()
- const codeThemeLight = useUISettingSelector((s) => overrideTheme || s.codeHighlightThemeLight)
- const codeThemeDark = useUISettingSelector((s) => overrideTheme || s.codeHighlightThemeDark)
- const codeTheme = isDark ? codeThemeDark : codeThemeLight
+ const codeTheme = useShikiDefaultTheme(overrideTheme)
useIsomorphicLayoutEffect(() => {
let isMounted = true
@@ -188,27 +185,22 @@ const ShikiCode: FC<
return (
+
+ {language !== "plaintext" && (
+
+ {getLanguageIcon(language)}
+ {language}
+
+ )}
+
+
-
- {language !== "plaintext" && (
-
- {getLanguageIcon(language)}
- {language}
-
- )}
)
}
diff --git a/apps/desktop/src/renderer/src/components/ui/code-highlighter/shiki/hooks.ts b/apps/desktop/src/renderer/src/components/ui/code-highlighter/shiki/hooks.ts
index e85bd7649..9b57d1b07 100644
--- a/apps/desktop/src/renderer/src/components/ui/code-highlighter/shiki/hooks.ts
+++ b/apps/desktop/src/renderer/src/components/ui/code-highlighter/shiki/hooks.ts
@@ -1,7 +1,11 @@
import { useIsDark } from "@follow/hooks"
-export const useShikiDefaultTheme = () => {
- const isDark = useIsDark()
+import { useUISettingSelector } from "~/atoms/settings/ui"
- return isDark ? "github-dark" : "github-light"
+export const useShikiDefaultTheme = (overrideTheme?: string) => {
+ const isDark = useIsDark()
+ const codeThemeLight = useUISettingSelector((s) => overrideTheme || s.codeHighlightThemeLight)
+ const codeThemeDark = useUISettingSelector((s) => overrideTheme || s.codeHighlightThemeDark)
+
+ return isDark ? codeThemeDark : codeThemeLight
}
diff --git a/apps/desktop/src/renderer/src/components/ui/collapse/Collapse.tsx b/apps/desktop/src/renderer/src/components/ui/collapse/Collapse.tsx
index dc243fb90..4efb11a26 100644
--- a/apps/desktop/src/renderer/src/components/ui/collapse/Collapse.tsx
+++ b/apps/desktop/src/renderer/src/components/ui/collapse/Collapse.tsx
@@ -1,8 +1,8 @@
import { jotaiStore } from "@follow/utils/jotai"
import { cn } from "@follow/utils/utils"
-import type { Variants } from "framer-motion"
-import { AnimatePresence, m } from "framer-motion"
import { atom, useAtom, useStore } from "jotai"
+import type { Variants } from "motion/react"
+import { AnimatePresence, m } from "motion/react"
import * as React from "react"
import { useEffect } from "react"
diff --git a/apps/desktop/src/renderer/src/components/ui/constants/spring.ts b/apps/desktop/src/renderer/src/components/ui/constants/spring.ts
index f224a44b2..7f921c604 100644
--- a/apps/desktop/src/renderer/src/components/ui/constants/spring.ts
+++ b/apps/desktop/src/renderer/src/components/ui/constants/spring.ts
@@ -1,4 +1,4 @@
-import type { Spring } from "framer-motion"
+import type { Spring } from "motion/react"
export const reboundPreset: Spring = {
type: "spring",
diff --git a/apps/desktop/src/renderer/src/components/ui/fab/FABContainer.tsx b/apps/desktop/src/renderer/src/components/ui/fab/FABContainer.tsx
index a936f40bc..93cdf3ddc 100644
--- a/apps/desktop/src/renderer/src/components/ui/fab/FABContainer.tsx
+++ b/apps/desktop/src/renderer/src/components/ui/fab/FABContainer.tsx
@@ -2,9 +2,9 @@ import { RootPortal } from "@follow/components/ui/portal/index.jsx"
import { useTypeScriptHappyCallback } from "@follow/hooks"
import { clsx, cn } from "@follow/utils/utils"
import { typescriptHappyForwardRef } from "foxact/typescript-happy-forward-ref"
-import type { HTMLMotionProps } from "framer-motion"
-import { AnimatePresence } from "framer-motion"
import { atom, useAtomValue } from "jotai"
+import type { HTMLMotionProps } from "motion/react"
+import { AnimatePresence } from "motion/react"
import type * as React from "react"
import type { JSX, PropsWithChildren, ReactNode } from "react"
import { useId } from "react"
diff --git a/apps/desktop/src/renderer/src/components/ui/markdown/components/Toc.tsx b/apps/desktop/src/renderer/src/components/ui/markdown/components/Toc.tsx
index 61ff3ed5a..183d8cf66 100644
--- a/apps/desktop/src/renderer/src/components/ui/markdown/components/Toc.tsx
+++ b/apps/desktop/src/renderer/src/components/ui/markdown/components/Toc.tsx
@@ -1,7 +1,7 @@
import { useViewport } from "@follow/components/hooks/useViewport.js"
import { cn } from "@follow/utils/utils"
import * as HoverCard from "@radix-ui/react-hover-card"
-import { AnimatePresence, m } from "framer-motion"
+import { AnimatePresence, m } from "motion/react"
import { memo, useContext, useEffect, useRef, useState } from "react"
import { useRealInWideMode } from "~/atoms/settings/ui"
diff --git a/apps/desktop/src/renderer/src/components/ui/media.tsx b/apps/desktop/src/renderer/src/components/ui/media.tsx
index f8a1b45b9..ee0e44258 100644
--- a/apps/desktop/src/renderer/src/components/ui/media.tsx
+++ b/apps/desktop/src/renderer/src/components/ui/media.tsx
@@ -2,7 +2,7 @@ import { nextFrame } from "@follow/utils/dom"
import { getImageProxyUrl } from "@follow/utils/img-proxy"
import { cn } from "@follow/utils/utils"
import { omit } from "es-toolkit/compat"
-import { useForceUpdate } from "framer-motion"
+import { useForceUpdate } from "motion/react"
import type { FC, ImgHTMLAttributes, VideoHTMLAttributes } from "react"
import { createContext, memo, useContext, useMemo, useState } from "react"
import { Blurhash, BlurhashCanvas } from "react-blurhash"
diff --git a/apps/desktop/src/renderer/src/components/ui/media/VideoPlayer.tsx b/apps/desktop/src/renderer/src/components/ui/media/VideoPlayer.tsx
index 2c3a7cc44..37a8b1b62 100644
--- a/apps/desktop/src/renderer/src/components/ui/media/VideoPlayer.tsx
+++ b/apps/desktop/src/renderer/src/components/ui/media/VideoPlayer.tsx
@@ -5,7 +5,7 @@ import { useRefValue, useVideo } from "@follow/hooks"
import { nextFrame, stopPropagation } from "@follow/utils/dom"
import { clsx, cn } from "@follow/utils/utils"
import * as Slider from "@radix-ui/react-slider"
-import { m, useDragControls, useSpring } from "framer-motion"
+import { m, useDragControls, useSpring } from "motion/react"
import type { PropsWithChildren } from "react"
import {
forwardRef,
diff --git a/apps/desktop/src/renderer/src/components/ui/modal/helper/async-loading.tsx b/apps/desktop/src/renderer/src/components/ui/modal/helper/async-loading.tsx
index 38497947a..2ad058981 100644
--- a/apps/desktop/src/renderer/src/components/ui/modal/helper/async-loading.tsx
+++ b/apps/desktop/src/renderer/src/components/ui/modal/helper/async-loading.tsx
@@ -2,7 +2,7 @@ import { Button } from "@follow/components/ui/button/index.js"
import { LoadingCircle } from "@follow/components/ui/loading/index.jsx"
import { Tooltip, TooltipContent, TooltipTrigger } from "@follow/components/ui/tooltip/index.jsx"
import { stopPropagation } from "@follow/utils/dom"
-import { m } from "framer-motion"
+import { m } from "motion/react"
import * as React from "react"
import { useTranslation } from "react-i18next"
diff --git a/apps/desktop/src/renderer/src/components/ui/modal/stacked/constants.ts b/apps/desktop/src/renderer/src/components/ui/modal/stacked/constants.ts
index 2d4c8ef4f..1d5b90099 100644
--- a/apps/desktop/src/renderer/src/components/ui/modal/stacked/constants.ts
+++ b/apps/desktop/src/renderer/src/components/ui/modal/stacked/constants.ts
@@ -1,4 +1,4 @@
-import type { MotionProps, Target } from "framer-motion"
+import type { MotionProps, Target } from "motion/react"
import { microReboundPreset } from "../../constants/spring"
diff --git a/apps/desktop/src/renderer/src/components/ui/modal/stacked/custom-modal.tsx b/apps/desktop/src/renderer/src/components/ui/modal/stacked/custom-modal.tsx
index c923bd404..a403365bc 100644
--- a/apps/desktop/src/renderer/src/components/ui/modal/stacked/custom-modal.tsx
+++ b/apps/desktop/src/renderer/src/components/ui/modal/stacked/custom-modal.tsx
@@ -1,6 +1,6 @@
import { nextFrame, stopPropagation } from "@follow/utils/dom"
import { cn } from "@follow/utils/utils"
-import { m, useAnimationControls } from "framer-motion"
+import { m, useAnimationControls } from "motion/react"
import type { FC, PropsWithChildren } from "react"
import { useEffect, useState } from "react"
diff --git a/apps/desktop/src/renderer/src/components/ui/modal/stacked/declarative-modal.tsx b/apps/desktop/src/renderer/src/components/ui/modal/stacked/declarative-modal.tsx
index 838f2ec46..47209c442 100644
--- a/apps/desktop/src/renderer/src/components/ui/modal/stacked/declarative-modal.tsx
+++ b/apps/desktop/src/renderer/src/components/ui/modal/stacked/declarative-modal.tsx
@@ -1,5 +1,5 @@
import { cn } from "@follow/utils/utils"
-import { AnimatePresence } from "framer-motion"
+import { AnimatePresence } from "motion/react"
import type { FC, ReactNode } from "react"
import { useId, useMemo } from "react"
diff --git a/apps/desktop/src/renderer/src/components/ui/modal/stacked/hooks.tsx b/apps/desktop/src/renderer/src/components/ui/modal/stacked/hooks.tsx
index 56e707814..efb199485 100644
--- a/apps/desktop/src/renderer/src/components/ui/modal/stacked/hooks.tsx
+++ b/apps/desktop/src/renderer/src/components/ui/modal/stacked/hooks.tsx
@@ -1,7 +1,7 @@
import { Button } from "@follow/components/ui/button/index.js"
import { nextFrame } from "@follow/utils/dom"
-import type { DragControls } from "framer-motion"
import { atom, useAtomValue } from "jotai"
+import type { DragControls } from "motion/react"
import type { ResizeCallback, ResizeStartCallback } from "re-resizable"
import { useContext, useId, useRef, useState } from "react"
import { flushSync } from "react-dom"
diff --git a/apps/desktop/src/renderer/src/components/ui/modal/stacked/internal/use-animate.ts b/apps/desktop/src/renderer/src/components/ui/modal/stacked/internal/use-animate.ts
index 68f77f64c..8f990f015 100644
--- a/apps/desktop/src/renderer/src/components/ui/modal/stacked/internal/use-animate.ts
+++ b/apps/desktop/src/renderer/src/components/ui/modal/stacked/internal/use-animate.ts
@@ -1,5 +1,5 @@
import { nextFrame } from "@follow/utils/dom"
-import { useAnimationControls } from "framer-motion"
+import { useAnimationControls } from "motion/react"
import { useCallback, useEffect, useLayoutEffect } from "react"
import { useEventCallback } from "usehooks-ts"
diff --git a/apps/desktop/src/renderer/src/components/ui/modal/stacked/internal/use-drag.ts b/apps/desktop/src/renderer/src/components/ui/modal/stacked/internal/use-drag.ts
index e34b87752..db89c4a9d 100644
--- a/apps/desktop/src/renderer/src/components/ui/modal/stacked/internal/use-drag.ts
+++ b/apps/desktop/src/renderer/src/components/ui/modal/stacked/internal/use-drag.ts
@@ -1,4 +1,4 @@
-import { useDragControls } from "framer-motion"
+import { useDragControls } from "motion/react"
import type { PointerEventHandler, RefObject } from "react"
import { useCallback } from "react"
diff --git a/apps/desktop/src/renderer/src/components/ui/modal/stacked/modal-stack.electron.tsx b/apps/desktop/src/renderer/src/components/ui/modal/stacked/modal-stack.electron.tsx
index 3ccc950c7..5a13b83b6 100644
--- a/apps/desktop/src/renderer/src/components/ui/modal/stacked/modal-stack.electron.tsx
+++ b/apps/desktop/src/renderer/src/components/ui/modal/stacked/modal-stack.electron.tsx
@@ -1,5 +1,5 @@
-import { AnimatePresence } from "framer-motion"
import { useAtomValue } from "jotai"
+import { AnimatePresence } from "motion/react"
import { modalStackAtom } from "./atom"
import { useModalStack } from "./hooks"
diff --git a/apps/desktop/src/renderer/src/components/ui/modal/stacked/modal-stack.mobile.tsx b/apps/desktop/src/renderer/src/components/ui/modal/stacked/modal-stack.mobile.tsx
index 4045af289..f5edc948c 100644
--- a/apps/desktop/src/renderer/src/components/ui/modal/stacked/modal-stack.mobile.tsx
+++ b/apps/desktop/src/renderer/src/components/ui/modal/stacked/modal-stack.mobile.tsx
@@ -2,9 +2,9 @@ import { sheetStackAtom } from "@follow/components/ui/sheet/context.js"
import type { SheetRef } from "@follow/components/ui/sheet/Sheet.js"
import { PresentSheet } from "@follow/components/ui/sheet/Sheet.js"
import { jotaiStore } from "@follow/utils/jotai"
-import { AnimatePresence } from "framer-motion"
import { produce } from "immer"
import { useAtomValue, useSetAtom } from "jotai"
+import { AnimatePresence } from "motion/react"
import { createElement, useMemo, useRef } from "react"
import { useEventCallback } from "usehooks-ts"
diff --git a/apps/desktop/src/renderer/src/components/ui/modal/stacked/modal.tsx b/apps/desktop/src/renderer/src/components/ui/modal/stacked/modal.tsx
index 5e465db72..14a003360 100644
--- a/apps/desktop/src/renderer/src/components/ui/modal/stacked/modal.tsx
+++ b/apps/desktop/src/renderer/src/components/ui/modal/stacked/modal.tsx
@@ -7,10 +7,10 @@ import { ELECTRON_BUILD } from "@follow/shared/constants"
import { nextFrame, preventDefault, stopPropagation } from "@follow/utils/dom"
import { cn, getOS } from "@follow/utils/utils"
import * as Dialog from "@radix-ui/react-dialog"
-import type { BoundingBox } from "framer-motion"
import { produce } from "immer"
import { useAtomValue, useSetAtom } from "jotai"
import { selectAtom } from "jotai/utils"
+import type { BoundingBox } from "motion/react"
import { Resizable } from "re-resizable"
import type { FC, PropsWithChildren, SyntheticEvent } from "react"
import {
diff --git a/apps/desktop/src/renderer/src/components/ui/modal/stacked/overlay.tsx b/apps/desktop/src/renderer/src/components/ui/modal/stacked/overlay.tsx
index 7914d75cc..83ca2898f 100644
--- a/apps/desktop/src/renderer/src/components/ui/modal/stacked/overlay.tsx
+++ b/apps/desktop/src/renderer/src/components/ui/modal/stacked/overlay.tsx
@@ -1,6 +1,6 @@
import { cn } from "@follow/utils/utils"
import * as Dialog from "@radix-ui/react-dialog"
-import { AnimatePresence } from "framer-motion"
+import { AnimatePresence } from "motion/react"
import type { ForwardedRef } from "react"
import { forwardRef } from "react"
diff --git a/apps/desktop/src/renderer/src/components/ux/transition/icon.tsx b/apps/desktop/src/renderer/src/components/ux/transition/icon.tsx
index 882aef604..573736f44 100644
--- a/apps/desktop/src/renderer/src/components/ux/transition/icon.tsx
+++ b/apps/desktop/src/renderer/src/components/ux/transition/icon.tsx
@@ -1,6 +1,6 @@
import { cn } from "@follow/utils/utils"
-import type { Spring, Target } from "framer-motion"
-import { AnimatePresence, m } from "framer-motion"
+import type { Spring, Target } from "motion/react"
+import { AnimatePresence, m } from "motion/react"
import * as React from "react"
import { cloneElement, useEffect, useState } from "react"
diff --git a/apps/desktop/src/renderer/src/hooks/biz/useEntryActions.tsx b/apps/desktop/src/renderer/src/hooks/biz/useEntryActions.tsx
index eeb3b06bd..5f33139c6 100644
--- a/apps/desktop/src/renderer/src/hooks/biz/useEntryActions.tsx
+++ b/apps/desktop/src/renderer/src/hooks/biz/useEntryActions.tsx
@@ -1,20 +1,22 @@
import { isMobile } from "@follow/components/hooks/useMobile.js"
-import { FeedViewType, UserRole } from "@follow/constants"
+import { FeedViewType, UserRole, views } from "@follow/constants"
import { IN_ELECTRON } from "@follow/shared/constants"
-import { useCallback, useMemo } from "react"
+import { useMemo } from "react"
import { useShowAISummaryAuto, useShowAISummaryOnce } from "~/atoms/ai-summary"
import { useShowAITranslationAuto, useShowAITranslationOnce } from "~/atoms/ai-translation"
import {
+ getReadabilityContent,
getReadabilityStatus,
ReadabilityStatus,
setReadabilityContent,
setReadabilityStatus,
+ useEntryIsInReadability,
} from "~/atoms/readability"
import { useShowSourceContent } from "~/atoms/source-content"
import { useUserRole, whoami } from "~/atoms/user"
import { shortcuts } from "~/constants/shortcuts"
-import { tipcClient } from "~/lib/client"
+import { apiClient } from "~/lib/api-fetch"
import { COMMAND_ID } from "~/modules/command/commands/id"
import { useRunCommandFn } from "~/modules/command/hooks/use-command"
import type { FollowCommandId } from "~/modules/command/types"
@@ -25,41 +27,45 @@ import { useInboxById } from "~/store/inbox"
import { useRouteParamsSelector } from "./useRouteParams"
-export const useEntryReadabilityToggle = ({ id, url }: { id: string; url: string }) =>
- useCallback(async () => {
- const status = getReadabilityStatus()[id]
- const isTurnOn = status !== ReadabilityStatus.INITIAL && !!status
+export const toggleEntryReadability = async ({ id, url }: { id: string; url: string }) => {
+ const status = getReadabilityStatus()[id]
+ const isTurnOn = status !== ReadabilityStatus.INITIAL && !!status
- if (!isTurnOn && url) {
- setReadabilityStatus({
- [id]: ReadabilityStatus.WAITING,
- })
- const result = await tipcClient
- ?.readability({
- url,
- })
- .catch(() => {
- setReadabilityStatus({
- [id]: ReadabilityStatus.FAILURE,
- })
- })
+ if (!isTurnOn && url) {
+ setReadabilityStatus({
+ [id]: ReadabilityStatus.WAITING,
+ })
+ try {
+ let data = getReadabilityContent()[id]
- if (result) {
+ if (!data) {
+ const result = await apiClient.entries.readability.$get({ query: { id } })
+ if (result.data) {
+ data = result.data
+ }
+ }
+
+ if (data) {
const status = getReadabilityStatus()[id]
if (status !== ReadabilityStatus.WAITING) return
setReadabilityStatus({
[id]: ReadabilityStatus.SUCCESS,
})
setReadabilityContent({
- [id]: result,
+ [id]: data,
})
}
- } else {
+ } catch {
setReadabilityStatus({
- [id]: ReadabilityStatus.INITIAL,
+ [id]: ReadabilityStatus.FAILURE,
})
}
- }, [id, url])
+ } else {
+ setReadabilityStatus({
+ [id]: ReadabilityStatus.INITIAL,
+ })
+ }
+}
export type EntryActionItem = {
id: FollowCommandId
@@ -68,10 +74,25 @@ export type EntryActionItem = {
shortcut?: string
active?: boolean
disabled?: boolean
+ notice?: boolean
+ entryId: string
}
-export const useEntryActions = ({ entryId, view }: { entryId: string; view?: FeedViewType }) => {
+function hasHTMLTags(text?: string | null): boolean {
+ return /<[^>]+>/.test(text || "")
+}
+
+export const useEntryActions = ({
+ entryId,
+ view,
+ compact,
+}: {
+ entryId: string
+ view?: FeedViewType
+ compact?: boolean
+}) => {
const entry = useEntry(entryId)
+ const isEntryInReadability = useEntryIsInReadability(entry?.entries.id)
const imageLength = entry?.entries.media?.filter((a) => a.type === "photo").length || 0
const feed = useFeedById(entry?.feedId, (feed) => {
return {
@@ -84,6 +105,7 @@ export const useEntryActions = ({ entryId, view }: { entryId: string; view?: Fee
const inList = !!listId
const inbox = useInboxById(entry?.inboxId)
const isInbox = !!inbox
+ const isContentContainsHTMLTags = hasHTMLTags(entry?.entries.content)
const isShowSourceContent = useShowSourceContent()
const isShowAISummaryAuto = useShowAISummaryAuto(entry)
@@ -208,17 +230,49 @@ export const useEntryActions = ({ entryId, view }: { entryId: string; view?: Fee
active: !!entry?.read,
shortcut: shortcuts.entry.toggleRead.key,
},
+ {
+ id: COMMAND_ID.entry.tts,
+ onClick: runCmdFn(COMMAND_ID.entry.tts, [
+ { entryId, entryContent: entry?.entries.content },
+ ]),
+ hide: !IN_ELECTRON || compact || !entry?.entries.content,
+ shortcut: shortcuts.entry.tts.key,
+ },
+ {
+ id: COMMAND_ID.entry.readability,
+ onClick: runCmdFn(COMMAND_ID.entry.readability, [
+ { entryId, entryUrl: entry?.entries.url },
+ ]),
+ hide:
+ !!entry.settings?.readability ||
+ compact ||
+ (view && views[view]!.wideMode) ||
+ !entry?.entries.url,
+ active: isEntryInReadability,
+ notice: !isContentContainsHTMLTags && !isEntryInReadability,
+ },
{
id: COMMAND_ID.settings.customizeToolbar,
onClick: runCmdFn(COMMAND_ID.settings.customizeToolbar, []),
},
- ].filter((config) => !config.hide)
+ ]
+ .filter((config) => !config.hide)
+ .map((config) => {
+ return {
+ ...config,
+ entryId,
+ }
+ })
}, [
+ compact,
entry?.collections,
+ entry?.entries.content,
entry?.entries.url,
entry?.read,
+ entry?.settings?.readability,
entry?.view,
entryId,
+ isEntryInReadability,
feed?.id,
feed?.ownerUserId,
hasEntry,
@@ -230,6 +284,7 @@ export const useEntryActions = ({ entryId, view }: { entryId: string; view?: Fee
isShowAITranslationAuto,
isShowAITranslationOnce,
isShowSourceContent,
+ isContentContainsHTMLTags,
runCmdFn,
userRole,
view,
@@ -241,11 +296,13 @@ export const useEntryActions = ({ entryId, view }: { entryId: string; view?: Fee
export const useSortedEntryActions = ({
entryId,
view,
+ compact,
}: {
entryId: string
view?: FeedViewType
+ compact?: boolean
}) => {
- const entryActions = useEntryActions({ entryId, view })
+ const entryActions = useEntryActions({ entryId, view, compact })
const orderMap = useToolbarOrderMap()
const mainAction = useMemo(
() =>
diff --git a/apps/desktop/src/renderer/src/hooks/biz/useFeedActions.tsx b/apps/desktop/src/renderer/src/hooks/biz/useFeedActions.tsx
index e29f64a44..1df39c7e7 100644
--- a/apps/desktop/src/renderer/src/hooks/biz/useFeedActions.tsx
+++ b/apps/desktop/src/renderer/src/hooks/biz/useFeedActions.tsx
@@ -359,6 +359,16 @@ export const useFeedActions = ({
navigator.clipboard.writeText(feedId)
},
},
+ {
+ type: "text" as const,
+ label: t("sidebar.feed_actions.copy_feed_badge"),
+ disabled: isEntryList,
+ click: () => {
+ navigator.clipboard.writeText(
+ `https://badge.follow.is/feed/${feedId}?color=FF5C00&labelColor=black&style=flat-square`,
+ )
+ },
+ },
]
return items.filter(
diff --git a/apps/desktop/src/renderer/src/hooks/biz/useReduceMotion.ts b/apps/desktop/src/renderer/src/hooks/biz/useReduceMotion.ts
index 36d023f3b..1edb4d191 100644
--- a/apps/desktop/src/renderer/src/hooks/biz/useReduceMotion.ts
+++ b/apps/desktop/src/renderer/src/hooks/biz/useReduceMotion.ts
@@ -1,4 +1,4 @@
-import { useReducedMotion } from "framer-motion"
+import { useReducedMotion } from "motion/react"
import { useUISettingKey } from "~/atoms/settings/ui"
diff --git a/apps/desktop/src/renderer/src/hooks/biz/useSignOut.ts b/apps/desktop/src/renderer/src/hooks/biz/useSignOut.ts
deleted file mode 100644
index 1655a0095..000000000
--- a/apps/desktop/src/renderer/src/hooks/biz/useSignOut.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { clearStorage } from "@follow/utils/ns"
-import { useCallback } from "react"
-
-import { setWhoami } from "~/atoms/user"
-import { QUERY_PERSIST_KEY } from "~/constants"
-import { signOut } from "~/lib/auth"
-import { tipcClient } from "~/lib/client"
-import { clearLocalPersistStoreData } from "~/store/utils/clear"
-
-export const useSignOut = () =>
- useCallback(async () => {
- if (window.__RN__) {
- window.ReactNativeWebView?.postMessage("sign-out")
- return
- }
-
- // Clear query cache
- localStorage.removeItem(QUERY_PERSIST_KEY)
-
- // setLoginModalShow(true)
- setWhoami(null)
-
- // Clear local storage
- clearStorage()
-
- // clear local store data
- await Promise.allSettled([
- clearLocalPersistStoreData(),
- tipcClient?.cleanBetterAuthSessionCookie(),
- ])
- // Sign out
- await signOut().then(() => {
- window.location.reload()
- })
- }, [])
diff --git a/apps/desktop/src/renderer/src/hooks/common/useSwitchHotkeyScope.ts b/apps/desktop/src/renderer/src/hooks/common/useSwitchHotkeyScope.ts
index 46c0c7477..b24c298a3 100644
--- a/apps/desktop/src/renderer/src/hooks/common/useSwitchHotkeyScope.ts
+++ b/apps/desktop/src/renderer/src/hooks/common/useSwitchHotkeyScope.ts
@@ -26,8 +26,8 @@ export const useSwitchHotKeyScope = () => {
)
}
export const useHotkeyScopeFn = (scope: keyof typeof HotKeyScopeMap) => {
- const { enableScope, disableScope, enabledScopes } = useHotkeysContext()
- const currentScopeRef = useRef(enabledScopes)
+ const { enableScope, disableScope, activeScopes } = useHotkeysContext()
+ const currentScopeRef = useRef(activeScopes)
return useCallback(() => {
const currentScope = currentScopeRef.current
diff --git a/apps/desktop/src/renderer/src/lib/auth.ts b/apps/desktop/src/renderer/src/lib/auth.ts
index 793923b3c..be46cbc0f 100644
--- a/apps/desktop/src/renderer/src/lib/auth.ts
+++ b/apps/desktop/src/renderer/src/lib/auth.ts
@@ -1,52 +1,22 @@
-import { IN_ELECTRON } from "@follow/shared"
+import { Auth } from "@follow/shared/auth"
import { env } from "@follow/shared/env.desktop"
-import type { authPlugins } from "@follow/shared/hono"
-import type { BetterAuthClientPlugin } from "better-auth/client"
-import { inferAdditionalFields, twoFactorClient } from "better-auth/client/plugins"
-import { createAuthClient } from "better-auth/react"
-import { WEB_URL } from "~/constants/env"
-
-type AuthPlugin = (typeof authPlugins)[number]
-const serverPlugins = [
- {
- id: "customGetProviders",
- $InferServerPlugin: {} as Extract,
- },
- {
- id: "customCreateSession",
- $InferServerPlugin: {} as Extract,
- },
- {
- id: "getAccountInfo",
- $InferServerPlugin: {} as Extract,
- },
- inferAdditionalFields({
- user: {
- handle: {
- type: "string",
- required: false,
- },
- },
- }),
-] satisfies BetterAuthClientPlugin[]
-
-const authClient = createAuthClient({
- baseURL: `${env.VITE_API_URL}/better-auth`,
- plugins: [...serverPlugins, twoFactorClient()],
+const auth = new Auth({
+ apiURL: env.VITE_API_URL,
+ webURL: env.VITE_WEB_URL,
})
// @keep-sorted
export const {
changeEmail,
changePassword,
- createSession,
forgetPassword,
getAccountInfo,
getProviders,
getSession,
linkSocial,
listAccounts,
+ oneTimeToken,
resetPassword,
sendVerificationEmail,
signIn,
@@ -55,33 +25,6 @@ export const {
twoFactor,
unlinkAccount,
updateUser,
-} = authClient
+} = auth.authClient
-export type LoginRuntime = "browser" | "app"
-export const loginHandler = async (
- provider: string,
- runtime?: LoginRuntime,
- args?: {
- email?: string
- password?: string
- headers?: Record
- },
-) => {
- const { email, password, headers } = args ?? {}
- if (IN_ELECTRON && provider !== "credential") {
- window.open(`${WEB_URL}/login?provider=${provider}`)
- } else {
- if (provider === "credential") {
- if (!email || !password) {
- window.location.href = "/login"
- return
- }
- return signIn.email({ email, password }, { headers })
- }
-
- signIn.social({
- provider: provider as "google" | "github" | "apple",
- callbackURL: runtime === "app" ? `${WEB_URL}/login` : WEB_URL,
- })
- }
-}
+export const { loginHandler } = auth
diff --git a/apps/desktop/src/renderer/src/lib/load-language.ts b/apps/desktop/src/renderer/src/lib/load-language.ts
index c8b3feb23..138a8a335 100644
--- a/apps/desktop/src/renderer/src/lib/load-language.ts
+++ b/apps/desktop/src/renderer/src/lib/load-language.ts
@@ -40,6 +40,9 @@ export const loadLanguageAndApply = async (lang: string) => {
const loaded = loadedLangs.has(lang)
if (loaded) {
+ if (import.meta.env.DEV) {
+ EventBus.dispatch("I18N_UPDATE", "")
+ }
return
}
diff --git a/apps/desktop/src/renderer/src/lib/translate.ts b/apps/desktop/src/renderer/src/lib/translate.ts
index bcf602b8d..58786b64c 100644
--- a/apps/desktop/src/renderer/src/lib/translate.ts
+++ b/apps/desktop/src/renderer/src/lib/translate.ts
@@ -4,6 +4,7 @@ import type { SupportedActionLanguage } from "@follow/shared"
import { ACTION_LANGUAGE_MAP } from "@follow/shared"
import { franc } from "franc-min"
+import { getReadabilityContent } from "~/atoms/readability"
import type { FlatEntryModel } from "~/store/entry"
import { apiClient } from "./api-fetch"
@@ -57,6 +58,16 @@ export async function translate({
}
fields = fields.filter((field) => {
+ if (language && field === "readabilityContent") {
+ const content = getReadabilityContent()[entry.entries.id]?.content
+ if (!content) return false
+ const isLanguageMatch = checkLanguage({
+ content,
+ language,
+ })
+ return !isLanguageMatch
+ }
+
if (language && entry.entries[field]) {
const isLanguageMatch = checkLanguage({
content: entry.entries[field],
diff --git a/apps/desktop/src/renderer/src/modules/action/target-action-list.tsx b/apps/desktop/src/renderer/src/modules/action/target-action-list.tsx
index 196254619..e6a8d3c67 100644
--- a/apps/desktop/src/renderer/src/modules/action/target-action-list.tsx
+++ b/apps/desktop/src/renderer/src/modules/action/target-action-list.tsx
@@ -23,6 +23,7 @@ import { actionActions, useActionByIndex } from "~/store/action"
type Action = {
title: string
+ icon: React.ReactNode
config?: () => React.ReactNode
configInline?: boolean
enabled: boolean
@@ -73,6 +74,7 @@ export const TargetActionList = ({ index }: { index: number }) => {
() => [
{
title: t("actions.action_card.generate_summary"),
+ icon: ,
enabled: !!summary,
onInit: (data) => {
data.result.summary = true
@@ -83,6 +85,7 @@ export const TargetActionList = ({ index }: { index: number }) => {
},
{
title: t("actions.action_card.translate_into"),
+ icon: ,
enabled: !!translation,
onInit: (data) => {
data.result.translation = true
@@ -93,6 +96,7 @@ export const TargetActionList = ({ index }: { index: number }) => {
},
{
title: t("actions.action_card.enable_readability"),
+ icon: ,
enabled: !!readability,
onInit: (data) => {
data.result.readability = true
@@ -103,6 +107,7 @@ export const TargetActionList = ({ index }: { index: number }) => {
},
{
title: t("actions.action_card.source_content"),
+ icon: ,
enabled: !!sourceContent,
onInit: (data) => {
data.result.sourceContent = true
@@ -113,6 +118,7 @@ export const TargetActionList = ({ index }: { index: number }) => {
},
{
title: t("actions.action_card.new_entry_notification"),
+ icon: ,
enabled: !!newEntryNotification,
onInit: (data) => {
data.result.newEntryNotification = true
@@ -123,6 +129,7 @@ export const TargetActionList = ({ index }: { index: number }) => {
},
{
title: t("actions.action_card.silence"),
+ icon: ,
enabled: !!silence,
onInit: (data) => {
data.result.silence = true
@@ -133,6 +140,7 @@ export const TargetActionList = ({ index }: { index: number }) => {
},
{
title: t("actions.action_card.block"),
+ icon: ,
enabled: !!block,
onInit: (data) => {
data.result.block = true
@@ -143,6 +151,7 @@ export const TargetActionList = ({ index }: { index: number }) => {
},
{
title: t("actions.action_card.rewrite_rules"),
+ icon: ,
enabled: !!rewriteRules,
onInit: (data) => {
data.result.rewriteRules = [
@@ -226,6 +235,7 @@ export const TargetActionList = ({ index }: { index: number }) => {
},
{
title: t("actions.action_card.webhooks"),
+ icon: ,
enabled: !!webhooks,
onInit: (data) => {
data.result.webhooks = [""]
@@ -324,7 +334,10 @@ export const TargetActionList = ({ index }: { index: number }) => {
})
}}
>
- {action.title}
+
+ {action.icon}
+ {action.title}
+
)
})}
@@ -338,7 +351,10 @@ export const TargetActionList = ({ index }: { index: number }) => {
return (
-
{action.title}
+
+ {action.icon}
+ {action.title}
+
{action.configInline && action.config && action.config()}
-
+
{t("ai_daily.title", { title })}
diff --git a/apps/desktop/src/renderer/src/modules/app-layout/entry-content/EntryContentPlaceholder.tsx b/apps/desktop/src/renderer/src/modules/app-layout/entry-content/EntryContentPlaceholder.tsx
index 8ef6cbe31..2479e90d8 100644
--- a/apps/desktop/src/renderer/src/modules/app-layout/entry-content/EntryContentPlaceholder.tsx
+++ b/apps/desktop/src/renderer/src/modules/app-layout/entry-content/EntryContentPlaceholder.tsx
@@ -1,7 +1,7 @@
import { FeedViewType } from "@follow/constants"
import { cn } from "@follow/utils/utils"
-import { AnimatePresence, LayoutGroup, m } from "framer-motion"
import { atom, useAtomValue, useSetAtom } from "jotai"
+import { AnimatePresence, LayoutGroup, m } from "motion/react"
import * as React from "react"
import { CollapseGroup } from "~/components/ui/collapse"
diff --git a/apps/desktop/src/renderer/src/modules/app-layout/entry-content/desktop.tsx b/apps/desktop/src/renderer/src/modules/app-layout/entry-content/desktop.tsx
index 081fc415c..757520080 100644
--- a/apps/desktop/src/renderer/src/modules/app-layout/entry-content/desktop.tsx
+++ b/apps/desktop/src/renderer/src/modules/app-layout/entry-content/desktop.tsx
@@ -1,7 +1,7 @@
import { views } from "@follow/constants"
import { cn } from "@follow/utils/utils"
import { useWheel } from "@use-gesture/react"
-import { easeOut } from "framer-motion"
+import { easeOut } from "motion/react"
import type { FC, PropsWithChildren } from "react"
import { useState } from "react"
import { useHotkeys } from "react-hotkeys-hook"
diff --git a/apps/desktop/src/renderer/src/modules/app-layout/feed-column/float-bar.mobile.tsx b/apps/desktop/src/renderer/src/modules/app-layout/feed-column/float-bar.mobile.tsx
index 6e22dd1c3..b5030e4b1 100644
--- a/apps/desktop/src/renderer/src/modules/app-layout/feed-column/float-bar.mobile.tsx
+++ b/apps/desktop/src/renderer/src/modules/app-layout/feed-column/float-bar.mobile.tsx
@@ -4,7 +4,7 @@ import { DividerVertical } from "@follow/components/ui/divider/Divider.js"
import { views } from "@follow/constants"
import { stopPropagation } from "@follow/utils/dom"
import { clsx } from "@follow/utils/utils"
-import { m, useAnimationControls } from "framer-motion"
+import { m, useAnimationControls } from "motion/react"
import { useCallback, useEffect, useRef, useState } from "react"
import { useAudioPlayerAtomSelector } from "~/atoms/player"
diff --git a/apps/desktop/src/renderer/src/modules/app-layout/subview/index.mobile.tsx b/apps/desktop/src/renderer/src/modules/app-layout/subview/index.mobile.tsx
index 36fe36aa6..c73ab8e5a 100644
--- a/apps/desktop/src/renderer/src/modules/app-layout/subview/index.mobile.tsx
+++ b/apps/desktop/src/renderer/src/modules/app-layout/subview/index.mobile.tsx
@@ -1,5 +1,5 @@
import { cn } from "@follow/utils/utils"
-import { easeOut, m, useScroll, useTransform } from "framer-motion"
+import { easeOut, m, useScroll, useTransform } from "motion/react"
import { Outlet } from "react-router"
import { HeaderTopReturnBackButton } from "~/components/mobile/button"
diff --git a/apps/desktop/src/renderer/src/modules/auth/Form.tsx b/apps/desktop/src/renderer/src/modules/auth/Form.tsx
index af750617a..61232385f 100644
--- a/apps/desktop/src/renderer/src/modules/auth/Form.tsx
+++ b/apps/desktop/src/renderer/src/modules/auth/Form.tsx
@@ -8,6 +8,7 @@ import {
FormMessage,
} from "@follow/components/ui/form/index.js"
import { Input } from "@follow/components/ui/input/Input.js"
+import type { LoginRuntime } from "@follow/shared/auth"
import { env } from "@follow/shared/env.desktop"
import { zodResolver } from "@hookform/resolvers/zod"
import { useRef } from "react"
@@ -18,8 +19,8 @@ import { toast } from "sonner"
import { z } from "zod"
import { useCurrentModal, useModalStack } from "~/components/ui/modal/stacked/hooks"
-import type { LoginRuntime } from "~/lib/auth"
import { loginHandler, signUp, twoFactor } from "~/lib/auth"
+import { handleSessionChanges } from "~/queries/auth"
import { TOTPForm } from "../profile/two-factor"
@@ -71,14 +72,14 @@ export function LoginWithPassword({ runtime }: { runtime: LoginRuntime }) {
}
}}
onSuccess={() => {
- window.location.reload()
+ handleSessionChanges()
}}
/>
)
},
})
} else {
- window.location.reload()
+ handleSessionChanges()
}
}
@@ -188,7 +189,7 @@ function RegisterForm() {
callbackURL: "/",
fetchOptions: {
onSuccess() {
- window.location.reload()
+ handleSessionChanges()
},
onError(context) {
toast.error(context.error.message)
diff --git a/apps/desktop/src/renderer/src/modules/auth/LoginModalContent.tsx b/apps/desktop/src/renderer/src/modules/auth/LoginModalContent.tsx
index 605db979c..25a075f25 100644
--- a/apps/desktop/src/renderer/src/modules/auth/LoginModalContent.tsx
+++ b/apps/desktop/src/renderer/src/modules/auth/LoginModalContent.tsx
@@ -8,12 +8,12 @@ import {
TooltipPortal,
TooltipTrigger,
} from "@follow/components/ui/tooltip/index.js"
+import type { LoginRuntime } from "@follow/shared/auth"
import { clsx } from "@follow/utils/utils"
-import { m } from "framer-motion"
+import { m } from "motion/react"
import { useTranslation } from "react-i18next"
import { useCurrentModal } from "~/components/ui/modal/stacked/hooks"
-import type { LoginRuntime } from "~/lib/auth"
import { loginHandler } from "~/lib/auth"
import { useAuthProviders } from "~/queries/users"
diff --git a/apps/desktop/src/renderer/src/modules/boost/level-benefits.tsx b/apps/desktop/src/renderer/src/modules/boost/level-benefits.tsx
index 69fab6069..757ea0fba 100644
--- a/apps/desktop/src/renderer/src/modules/boost/level-benefits.tsx
+++ b/apps/desktop/src/renderer/src/modules/boost/level-benefits.tsx
@@ -28,7 +28,7 @@ const benefits = [
level: 4,
benefits: [
{ icon: "i-mgc-eye-2-cute-re", text: "More developer attention" },
- { icon: "i-mgc-magic-2-cute-re", text: "AI summary for feed", comingSoon: true },
+ { icon: "i-mgc-ai-cute-re", text: "AI summary for feed", comingSoon: true },
{ icon: "i-mgc-rocket-cute-re", text: "Faster feed refresh time" },
],
},
diff --git a/apps/desktop/src/renderer/src/modules/boost/modal.tsx b/apps/desktop/src/renderer/src/modules/boost/modal.tsx
index a4a328940..df99f5d8a 100644
--- a/apps/desktop/src/renderer/src/modules/boost/modal.tsx
+++ b/apps/desktop/src/renderer/src/modules/boost/modal.tsx
@@ -1,7 +1,7 @@
import { Button } from "@follow/components/ui/button/index.js"
import { LoadingWithIcon } from "@follow/components/ui/loading/index.jsx"
import { from } from "dnum"
-import { AnimatePresence, m } from "framer-motion"
+import { AnimatePresence, m } from "motion/react"
import { useCallback, useState } from "react"
import { softSpringPreset } from "~/components/ui/constants/spring"
diff --git a/apps/desktop/src/renderer/src/modules/boost/radio-cards.tsx b/apps/desktop/src/renderer/src/modules/boost/radio-cards.tsx
index d7fed3050..8c435cbf6 100644
--- a/apps/desktop/src/renderer/src/modules/boost/radio-cards.tsx
+++ b/apps/desktop/src/renderer/src/modules/boost/radio-cards.tsx
@@ -1,7 +1,7 @@
import { RadioCard } from "@follow/components/ui/radio-group/RadioCard.js"
import { RadioGroup } from "@follow/components/ui/radio-group/RadioGroup.js"
import dayjs from "dayjs"
-import { m } from "framer-motion"
+import { m } from "motion/react"
import { softSpringPreset } from "~/components/ui/constants/spring"
diff --git a/apps/desktop/src/renderer/src/modules/command/commands/entry.tsx b/apps/desktop/src/renderer/src/modules/command/commands/entry.tsx
index 4be5033fa..f9e526dac 100644
--- a/apps/desktop/src/renderer/src/modules/command/commands/entry.tsx
+++ b/apps/desktop/src/renderer/src/modules/command/commands/entry.tsx
@@ -1,21 +1,25 @@
import { FeedViewType, UserRole } from "@follow/constants"
import { IN_ELECTRON } from "@follow/shared/constants"
-import { cn, getOS } from "@follow/utils/utils"
+import { cn } from "@follow/utils/utils"
import { useMutation } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { toggleShowAISummaryOnce } from "~/atoms/ai-summary"
import { toggleShowAITranslationOnce } from "~/atoms/ai-translation"
+import { AudioPlayer, getAudioPlayerAtomValue } from "~/atoms/player"
+import { useGeneralSettingKey } from "~/atoms/settings/general"
import {
getShowSourceContent,
toggleShowSourceContent,
useSourceContentModal,
} from "~/atoms/source-content"
import { useUserRole } from "~/atoms/user"
+import { toggleEntryReadability } from "~/hooks/biz/useEntryActions"
import { navigateEntry } from "~/hooks/biz/useNavigateEntry"
import { getRouteParams } from "~/hooks/biz/useRouteParams"
import { tipcClient } from "~/lib/client"
+import { parseHtml } from "~/lib/parse-html"
import { useActivationModal } from "~/modules/activation"
import { useGalleryModal } from "~/modules/entry-content/hooks"
import { useTipModal } from "~/modules/wallet/hooks"
@@ -100,6 +104,8 @@ export const useRegisterEntryCommands = () => {
const role = useUserRole()
const presentActivationModal = useActivationModal()
+ const voice = useGeneralSettingKey("voice")
+
useRegisterFollowCommand([
{
id: COMMAND_ID.entry.tip,
@@ -259,12 +265,7 @@ export const useRegisterEntryCommands = () => {
{
id: COMMAND_ID.entry.share,
label: t("entry_actions.share"),
- icon:
- getOS() === "macOS" ? (
-
- ) : (
-
- ),
+ icon: ,
run: ({ entryId }) => {
const entry = useEntryStore.getState().flatMapEntries[entryId]
if (!entry || !entry.entries.url) {
@@ -313,6 +314,43 @@ export const useRegisterEntryCommands = () => {
openGalleryModal(entryId)
},
},
+ {
+ id: COMMAND_ID.entry.tts,
+ label: t("entry_content.header.play_tts"),
+ icon: ,
+ run: async ({ entryId, entryContent }) => {
+ if (getAudioPlayerAtomValue().entryId === entryId) {
+ AudioPlayer.togglePlayAndPause()
+ } else {
+ const filePath = await tipcClient?.tts({
+ id: entryId,
+ text: parseHtml(entryContent).toText(),
+ voice,
+ })
+ if (filePath) {
+ AudioPlayer.mount({
+ type: "audio",
+ entryId,
+ src: `file://${filePath}`,
+ currentTime: 0,
+ })
+ }
+ }
+ },
+ },
+ {
+ id: COMMAND_ID.entry.readability,
+ label: t("entry_content.header.readability"),
+ icon: (props) => (
+
+ ),
+ run: async ({ entryId, entryUrl }) => {
+ return toggleEntryReadability({
+ id: entryId,
+ url: entryUrl,
+ })
+ },
+ },
])
useRegisterFollowCommand(
@@ -320,7 +358,7 @@ export const useRegisterEntryCommands = () => {
{
id: COMMAND_ID.entry.toggleAISummary,
label: t("entry_actions.toggle_ai_summary"),
- icon: ,
+ icon: ,
run: () => {
if (role === UserRole.Trial) {
presentActivationModal()
@@ -332,7 +370,7 @@ export const useRegisterEntryCommands = () => {
{
id: COMMAND_ID.entry.toggleAITranslation,
label: t("entry_actions.toggle_ai_translation"),
- icon: ,
+ icon: ,
run: () => {
if (role === UserRole.Trial) {
presentActivationModal()
diff --git a/apps/desktop/src/renderer/src/modules/command/commands/id.ts b/apps/desktop/src/renderer/src/modules/command/commands/id.ts
index d2b50314f..9c7213f7b 100644
--- a/apps/desktop/src/renderer/src/modules/command/commands/id.ts
+++ b/apps/desktop/src/renderer/src/modules/command/commands/id.ts
@@ -13,6 +13,8 @@ export const COMMAND_ID = {
toggleAITranslation: "entry:toggle-ai-translation",
exportAsPDF: "entry:export-as-pdf",
imageGallery: "entry:image-gallery",
+ tts: "entry:tts",
+ readability: "entry:ability",
},
integration: {
saveToEagle: "integration:save-to-eagle",
diff --git a/apps/desktop/src/renderer/src/modules/command/commands/integration.tsx b/apps/desktop/src/renderer/src/modules/command/commands/integration.tsx
index 52055fc20..508b97e4b 100644
--- a/apps/desktop/src/renderer/src/modules/command/commands/integration.tsx
+++ b/apps/desktop/src/renderer/src/modules/command/commands/integration.tsx
@@ -16,10 +16,13 @@ import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { getReadabilityContent, getReadabilityStatus, ReadabilityStatus } from "~/atoms/readability"
-import { useIntegrationSettingKey } from "~/atoms/settings/integration"
+import { getActionLanguage } from "~/atoms/settings/general"
+import { getIntegrationSettings, useIntegrationSettingKey } from "~/atoms/settings/integration"
import { useRouteParams } from "~/hooks/biz/useRouteParams"
import { tipcClient } from "~/lib/client"
import { parseHtml } from "~/lib/parse-html"
+import { queryClient } from "~/lib/query-client"
+import { Queries } from "~/queries"
import type { FlatEntryModel } from "~/store/entry"
import { useEntryStore } from "~/store/entry"
@@ -464,29 +467,6 @@ const useRegisterCuboxCommands = () => {
const enableCuboxAutoMemo = useIntegrationSettingKey("enableCuboxAutoMemo")
const cuboxAvailable = enableCubox && !!cuboxToken
- const buildUrlRequestBody = (entry: FlatEntryModel) => {
- return {
- type: "url",
- content: entry.entries.url || "",
- title: entry.entries.title || "",
- description: entry.entries.description || "",
- tags: [],
- folder: "",
- }
- }
-
- const buildMemoRequestBody = (entry: FlatEntryModel, selectedText: string) => {
- return {
- type: "memo",
- content: selectedText,
- title: entry.entries.title || "",
- description: entry.entries.description || "",
- tags: [],
- folder: "",
- source_url: entry.entries.url,
- }
- }
-
useRegisterCommandEffect(
!cuboxAvailable
? []
@@ -539,3 +519,42 @@ const useRegisterCuboxCommands = () => {
},
)
}
+
+const getDescription = (entry: FlatEntryModel) => {
+ const actionLanguage = getActionLanguage()
+ const { saveSummaryAsDescription } = getIntegrationSettings()
+
+ if (!saveSummaryAsDescription) {
+ return entry.entries.description || ""
+ }
+ const summary = queryClient
+ .getQueriesData({
+ queryKey: Queries.ai.summary({ entryId: entry.entries.id, language: actionLanguage }).key,
+ })
+ .at(0)
+ ?.at(1) as string | undefined
+ return summary || entry.entries.description || ""
+}
+
+const buildUrlRequestBody = (entry: FlatEntryModel) => {
+ return {
+ type: "url",
+ content: entry.entries.url || "",
+ title: entry.entries.title || "",
+ description: getDescription(entry),
+ tags: [],
+ folder: "",
+ }
+}
+
+const buildMemoRequestBody = (entry: FlatEntryModel, selectedText: string) => {
+ return {
+ type: "memo",
+ content: selectedText,
+ title: entry.entries.title || "",
+ description: getDescription(entry),
+ tags: [],
+ folder: "",
+ source_url: entry.entries.url,
+ }
+}
diff --git a/apps/desktop/src/renderer/src/modules/command/commands/types.ts b/apps/desktop/src/renderer/src/modules/command/commands/types.ts
index c0613a29a..81bc3cf8d 100644
--- a/apps/desktop/src/renderer/src/modules/command/commands/types.ts
+++ b/apps/desktop/src/renderer/src/modules/command/commands/types.ts
@@ -70,6 +70,16 @@ export type ImageGalleryCommand = Command<{
fn: ({ entryId }) => void
}>
+export type TTSCommand = Command<{
+ id: typeof COMMAND_ID.entry.tts
+ fn: ({ entryId, entryContent }) => void
+}>
+
+export type ReadabilityCommand = Command<{
+ id: typeof COMMAND_ID.entry.readability
+ fn: ({ entryId, entryUrl }) => void
+}>
+
export type EntryCommand =
| TipCommand
| StarCommand
@@ -84,6 +94,8 @@ export type EntryCommand =
| ToggleAISummaryCommand
| ToggleAITranslationCommand
| ImageGalleryCommand
+ | TTSCommand
+ | ReadabilityCommand
// Settings commands
diff --git a/apps/desktop/src/renderer/src/modules/command/hooks/use-register-command.ts b/apps/desktop/src/renderer/src/modules/command/hooks/use-register-command.ts
index 6eeb9f6ec..adf98ed0d 100644
--- a/apps/desktop/src/renderer/src/modules/command/hooks/use-register-command.ts
+++ b/apps/desktop/src/renderer/src/modules/command/hooks/use-register-command.ts
@@ -20,7 +20,8 @@ export const useRegisterCommandEffect = (
const { t } = useTranslation()
useEffect(() => {
if (!Array.isArray(options)) {
- return registerCommand(options)
+ const unsubscribe = registerCommand(options)
+ return () => unsubscribe()
}
const unsubscribes = options.map((option) => registerCommand(option))
diff --git a/apps/desktop/src/renderer/src/modules/customize-toolbar/constant.ts b/apps/desktop/src/renderer/src/modules/customize-toolbar/constant.ts
index 0d32692e9..b6d485f61 100644
--- a/apps/desktop/src/renderer/src/modules/customize-toolbar/constant.ts
+++ b/apps/desktop/src/renderer/src/modules/customize-toolbar/constant.ts
@@ -12,6 +12,7 @@ const entryItemInMore = new Set([
COMMAND_ID.entry.openInBrowser,
COMMAND_ID.entry.exportAsPDF,
COMMAND_ID.entry.read,
+ COMMAND_ID.entry.tts,
])
export const DEFAULT_ACTION_ORDER: ToolbarActionOrder = {
diff --git a/apps/desktop/src/renderer/src/modules/discover/form.tsx b/apps/desktop/src/renderer/src/modules/discover/form.tsx
index 159e82465..1cb82bf51 100644
--- a/apps/desktop/src/renderer/src/modules/discover/form.tsx
+++ b/apps/desktop/src/renderer/src/modules/discover/form.tsx
@@ -16,9 +16,9 @@ import { getBackgroundGradient } from "@follow/utils/color"
import { zodResolver } from "@hookform/resolvers/zod"
import { repository } from "@pkg"
import { useMutation } from "@tanstack/react-query"
-import { m } from "framer-motion"
import { produce } from "immer"
import { atom, useAtomValue, useStore } from "jotai"
+import { m } from "motion/react"
import type { ChangeEvent, FC } from "react"
import { memo, useCallback, useState } from "react"
import { useForm } from "react-hook-form"
diff --git a/apps/desktop/src/renderer/src/modules/entry-column/Items/picture-masonry.tsx b/apps/desktop/src/renderer/src/modules/entry-column/Items/picture-masonry.tsx
index 4c16012cb..9cd791d33 100644
--- a/apps/desktop/src/renderer/src/modules/entry-column/Items/picture-masonry.tsx
+++ b/apps/desktop/src/renderer/src/modules/entry-column/Items/picture-masonry.tsx
@@ -12,7 +12,7 @@ import { useRefValue } from "@follow/hooks"
import { clsx } from "@follow/utils/utils"
import type { RenderComponentProps } from "masonic"
import { useInfiniteLoader } from "masonic"
-import type { FC } from "react"
+import type { FC, ReactNode } from "react"
import {
createContext,
startTransition,
@@ -228,6 +228,15 @@ export const PictureMasonry: FC = (props) => {
onRender={handleRender}
itemKey={itemKey}
/>
+ {props.Footer ? (
+ typeof props.Footer === "function" ? (
+
+ ) : (
+ {props.Footer}
+ )
+ ) : null}
@@ -269,6 +278,7 @@ interface MasonryProps {
data: string[]
endReached: () => any
hasNextPage: boolean
+ Footer?: FC | ReactNode
}
const LoadingSkeletonItem = () => {
diff --git a/apps/desktop/src/renderer/src/modules/entry-column/components/DateItem.tsx b/apps/desktop/src/renderer/src/modules/entry-column/components/DateItem.tsx
index 26d631528..867fcec73 100644
--- a/apps/desktop/src/renderer/src/modules/entry-column/components/DateItem.tsx
+++ b/apps/desktop/src/renderer/src/modules/entry-column/components/DateItem.tsx
@@ -2,7 +2,7 @@ import { ActionButton } from "@follow/components/ui/button/index.js"
import { FeedViewType } from "@follow/constants"
import { stopPropagation } from "@follow/utils/dom"
import { cn } from "@follow/utils/utils"
-import { m } from "framer-motion"
+import { m } from "motion/react"
import type { FC, PropsWithChildren } from "react"
import { cloneElement, memo, useCallback, useId, useMemo, useRef, useState } from "react"
import { Trans } from "react-i18next"
diff --git a/apps/desktop/src/renderer/src/modules/entry-column/grid.tsx b/apps/desktop/src/renderer/src/modules/entry-column/grid.tsx
index 2881e92a2..c708ae033 100644
--- a/apps/desktop/src/renderer/src/modules/entry-column/grid.tsx
+++ b/apps/desktop/src/renderer/src/modules/entry-column/grid.tsx
@@ -35,6 +35,7 @@ export const EntryColumnGrid: FC = (props) => {
hasNextPage={hasNextPage}
endReached={fetchNextPage}
data={entriesIds}
+ Footer={props.Footer}
/>
)
}
diff --git a/apps/desktop/src/renderer/src/modules/entry-column/layouts/EntryListHeader.shared.tsx b/apps/desktop/src/renderer/src/modules/entry-column/layouts/EntryListHeader.shared.tsx
index ead208a7a..fec68038b 100644
--- a/apps/desktop/src/renderer/src/modules/entry-column/layouts/EntryListHeader.shared.tsx
+++ b/apps/desktop/src/renderer/src/modules/entry-column/layouts/EntryListHeader.shared.tsx
@@ -42,7 +42,7 @@ export const DailyReportButton: FC = () => {
}}
tooltip={t("entry_list_header.daily_report")}
>
-
+
)
}
diff --git a/apps/desktop/src/renderer/src/modules/entry-column/list.tsx b/apps/desktop/src/renderer/src/modules/entry-column/list.tsx
index 9ac32524e..46e172e20 100644
--- a/apps/desktop/src/renderer/src/modules/entry-column/list.tsx
+++ b/apps/desktop/src/renderer/src/modules/entry-column/list.tsx
@@ -6,7 +6,7 @@ import { LRUCache } from "@follow/utils/lru-cache"
import { clsx } from "@follow/utils/utils"
import type { Range, VirtualItem, Virtualizer } from "@tanstack/react-virtual"
import { defaultRangeExtractor, useVirtualizer } from "@tanstack/react-virtual"
-import type { HTMLMotionProps } from "framer-motion"
+import type { HTMLMotionProps } from "motion/react"
import type { FC, MutableRefObject, ReactNode } from "react"
import {
forwardRef,
diff --git a/apps/desktop/src/renderer/src/modules/entry-column/templates/list-item-template.tsx b/apps/desktop/src/renderer/src/modules/entry-column/templates/list-item-template.tsx
index 407a9ebef..45101533e 100644
--- a/apps/desktop/src/renderer/src/modules/entry-column/templates/list-item-template.tsx
+++ b/apps/desktop/src/renderer/src/modules/entry-column/templates/list-item-template.tsx
@@ -77,7 +77,7 @@ export function ListItem({
title: envIsSafari ? `line-clamp-[${lineClampTitle}]` : "",
description: envIsSafari ? `line-clamp-[${lineClampDescription}]` : "",
}
- }, [settingWideMode])
+ }, [settingWideMode, simple, translation?.description, translation?.title])
const audioAttachment = useMemo(() => {
return entry?.entries?.attachments?.find((a) => a.mime_type?.startsWith("audio") && a.url)
diff --git a/apps/desktop/src/renderer/src/modules/entry-column/translation.tsx b/apps/desktop/src/renderer/src/modules/entry-column/translation.tsx
index 9a0f4e149..331641f7f 100644
--- a/apps/desktop/src/renderer/src/modules/entry-column/translation.tsx
+++ b/apps/desktop/src/renderer/src/modules/entry-column/translation.tsx
@@ -7,7 +7,8 @@ export const EntryTranslation: Component<{
source?: string | null
target?: string
isHTML?: boolean
-}> = ({ source, target, className, isHTML }) => {
+ inline?: boolean
+}> = ({ source, target, className, isHTML, inline = true }) => {
const nextTarget = useMemo(() => {
if (!target || source === target) {
return ""
@@ -19,35 +20,24 @@ export const EntryTranslation: Component<{
return null
}
+ const SourceTag = inline ? "span" : "p"
+
return (
{isHTML ? (
- <>
- {nextTarget && (
- <>
-
- {nextTarget}
-
-
- >
- )}
-
- {source}
-
- >
+
+ {nextTarget || source}
+
) : (
-
- {nextTarget && (
+
+ {nextTarget && inline && (
<>
{nextTarget}
-
+
>
)}
-
{source}
+
{source}
+ {nextTarget && !inline &&
{nextTarget}
}
)}
diff --git a/apps/desktop/src/renderer/src/modules/entry-content/AISummary.tsx b/apps/desktop/src/renderer/src/modules/entry-content/AISummary.tsx
index 1b271455b..6fc40c3ae 100644
--- a/apps/desktop/src/renderer/src/modules/entry-content/AISummary.tsx
+++ b/apps/desktop/src/renderer/src/modules/entry-content/AISummary.tsx
@@ -3,6 +3,7 @@ import { cn } from "@follow/utils/utils"
import { useTranslation } from "react-i18next"
import { useShowAISummary } from "~/atoms/ai-summary"
+import { useEntryIsInReadabilitySuccess } from "~/atoms/readability"
import { useActionLanguage } from "~/atoms/settings/general"
import { CopyButton } from "~/components/ui/button/CopyButton"
import { useAuthQuery } from "~/hooks/common"
@@ -12,12 +13,14 @@ import { useEntry } from "~/store/entry"
export function AISummary({ entryId }: { entryId: string }) {
const { t } = useTranslation()
const entry = useEntry(entryId)
+ const isInReadabilitySuccess = useEntryIsInReadabilitySuccess(entryId)
const showAISummary = useShowAISummary(entry)
const actionLanguage = useActionLanguage()
const summary = useAuthQuery(
Queries.ai.summary({
entryId,
language: actionLanguage,
+ target: isInReadabilitySuccess ? "readabilityContent" : "content",
}),
{
enabled: showAISummary,
@@ -60,7 +63,7 @@ export function AISummary({ entryId }: { entryId: string }) {
{
- const entryReadabilityStatus = useEntryInReadabilityStatus(entry?.entries.id)
-
- const { t } = useTranslation()
-
- const feed = useFeedById(entry?.feedId)
-
- const populatedEntry = useMemo(() => {
- if (!entry) return null
- if (!feed) return null
- return {
- ...entry,
- feeds: feed!,
- } as CombinedEntryModel
- }, [entry, feed])
-
- const readabilityToggle = useEntryReadabilityToggle({
- id: populatedEntry?.entries.id ?? "",
- url: populatedEntry?.entries.url ?? "",
- })
-
- const [ttsLoading, setTtsLoading] = useState(false)
- const voice = useGeneralSettingKey("voice")
-
- if (!populatedEntry) return null
-
- const items = [
- {
- key: "tts",
- name: t("entry_content.header.play_tts"),
- shortcut: shortcuts.entry.tts.key,
- className: ttsLoading ? "i-mgc-loading-3-cute-re animate-spin" : "i-mgc-voice-cute-re",
-
- disabled: !populatedEntry.entries.content,
- onClick: async () => {
- if (ttsLoading) return
- if (!populatedEntry.entries.content) return
- setTtsLoading(true)
- if (getAudioPlayerAtomValue().entryId === populatedEntry.entries.id) {
- AudioPlayer.togglePlayAndPause()
- } else {
- const filePath = await tipcClient?.tts({
- id: populatedEntry.entries.id,
- text: parseHtml(populatedEntry.entries.content).toText(),
- voice,
- })
- if (filePath) {
- AudioPlayer.mount({
- type: "audio",
- entryId: populatedEntry.entries.id,
- src: `file://${filePath}`,
- currentTime: 0,
- })
- }
- }
- setTtsLoading(false)
- },
- },
- {
- name: t("entry_content.header.readability"),
- className: cn(
- isInReadability(entryReadabilityStatus)
- ? `i-mgc-docment-cute-fi`
- : `i-mgc-docment-cute-re`,
- entryReadabilityStatus === ReadabilityStatus.WAITING ? `animate-pulse` : "",
- ),
- key: "readability",
- hide: views[view]!.wideMode || !populatedEntry.entries.url,
- active: isInReadability(entryReadabilityStatus),
- onClick: readabilityToggle,
- },
- ]
-
- if (items.length === 0) return null
- return (
- <>
- {items
- .filter((item) => !item.hide)
- .map((item) => (
- }
- active={item.active}
- shortcut={item.shortcut}
- onClick={item.onClick}
- tooltip={item.name}
- key={item.name}
- />
- ))}
-
- >
- )
- }
- : noop
diff --git a/apps/desktop/src/renderer/src/modules/entry-content/actions/header-actions.tsx b/apps/desktop/src/renderer/src/modules/entry-content/actions/header-actions.tsx
index 24c774364..fed404c4a 100644
--- a/apps/desktop/src/renderer/src/modules/entry-content/actions/header-actions.tsx
+++ b/apps/desktop/src/renderer/src/modules/entry-content/actions/header-actions.tsx
@@ -8,8 +8,16 @@ import { COMMAND_ID } from "~/modules/command/commands/id"
import { useCommandHotkey } from "~/modules/command/hooks/use-register-hotkey"
import { useEntry } from "~/store/entry/hooks"
-export const EntryHeaderActions = ({ entryId, view }: { entryId: string; view?: FeedViewType }) => {
- const { mainAction: actionConfigs } = useSortedEntryActions({ entryId, view })
+export const EntryHeaderActions = ({
+ entryId,
+ view,
+ compact,
+}: {
+ entryId: string
+ view?: FeedViewType
+ compact?: boolean
+}) => {
+ const { mainAction: actionConfigs } = useSortedEntryActions({ entryId, view, compact })
const entry = useEntry(entryId)
const hasModal = useHasModal()
@@ -31,6 +39,8 @@ export const EntryHeaderActions = ({ entryId, view }: { entryId: string; view?:
onClick={config.onClick}
shortcut={config.shortcut}
clickableDisabled={config.disabled}
+ tooltipDefaultOpen={config.notice}
+ id={`${config.entryId}/${config.id}`}
/>
)
})
diff --git a/apps/desktop/src/renderer/src/modules/entry-content/components/EntryReadHistory.shared.tsx b/apps/desktop/src/renderer/src/modules/entry-content/components/EntryReadHistory.shared.tsx
index 700ef2ab1..96115acb1 100644
--- a/apps/desktop/src/renderer/src/modules/entry-content/components/EntryReadHistory.shared.tsx
+++ b/apps/desktop/src/renderer/src/modules/entry-content/components/EntryReadHistory.shared.tsx
@@ -8,7 +8,7 @@ import {
import { EllipsisHorizontalTextWithTooltip } from "@follow/components/ui/typography/index.js"
import { FeedViewType } from "@follow/constants"
import { getNameInitials } from "@follow/utils/cjk"
-import { m } from "framer-motion"
+import { m } from "motion/react"
import { memo } from "react"
import { useTranslation } from "react-i18next"
diff --git a/apps/desktop/src/renderer/src/modules/entry-content/components/EntryTimelineSidebar.tsx b/apps/desktop/src/renderer/src/modules/entry-content/components/EntryTimelineSidebar.tsx
index 9595f3ccb..cc3c923fd 100644
--- a/apps/desktop/src/renderer/src/modules/entry-content/components/EntryTimelineSidebar.tsx
+++ b/apps/desktop/src/renderer/src/modules/entry-content/components/EntryTimelineSidebar.tsx
@@ -1,7 +1,7 @@
import { EllipsisHorizontalTextWithTooltip } from "@follow/components/ui/typography/EllipsisWithTooltip.js"
import { cn } from "@follow/utils/utils"
-import type { Target, TargetAndTransition } from "framer-motion"
-import { m } from "framer-motion"
+import type { Target, TargetAndTransition } from "motion/react"
+import { m } from "motion/react"
import { useIsZenMode } from "~/atoms/settings/ui"
import { useAsRead } from "~/hooks/biz/useAsRead"
diff --git a/apps/desktop/src/renderer/src/modules/entry-content/components/EntryTitle.tsx b/apps/desktop/src/renderer/src/modules/entry-content/components/EntryTitle.tsx
index 9b7ad2e76..e6d10410d 100644
--- a/apps/desktop/src/renderer/src/modules/entry-content/components/EntryTitle.tsx
+++ b/apps/desktop/src/renderer/src/modules/entry-content/components/EntryTitle.tsx
@@ -100,6 +100,7 @@ export const EntryTitle = ({ entryId, compact }: EntryLinkProps) => {
source={entry.entries.title}
target={translation.data?.title}
className="inline-block select-text hyphens-auto duration-200"
+ inline={false}
/>
diff --git a/apps/desktop/src/renderer/src/modules/entry-content/components/SourceContentView.tsx b/apps/desktop/src/renderer/src/modules/entry-content/components/SourceContentView.tsx
index c2041b8f9..73338759e 100644
--- a/apps/desktop/src/renderer/src/modules/entry-content/components/SourceContentView.tsx
+++ b/apps/desktop/src/renderer/src/modules/entry-content/components/SourceContentView.tsx
@@ -1,5 +1,5 @@
import { IN_ELECTRON } from "@follow/shared/constants"
-import { AnimatePresence } from "framer-motion"
+import { AnimatePresence } from "motion/react"
import { useEffect, useRef, useState } from "react"
import { useShowSourceContent } from "~/atoms/source-content"
diff --git a/apps/desktop/src/renderer/src/modules/entry-content/header.electron.tsx b/apps/desktop/src/renderer/src/modules/entry-content/header.electron.tsx
index 3748d1735..0f9f8a732 100644
--- a/apps/desktop/src/renderer/src/modules/entry-content/header.electron.tsx
+++ b/apps/desktop/src/renderer/src/modules/entry-content/header.electron.tsx
@@ -1,12 +1,11 @@
import { views } from "@follow/constants"
import { cn } from "@follow/utils/utils"
-import { AnimatePresence, m } from "framer-motion"
+import { AnimatePresence, m } from "motion/react"
import { memo } from "react"
import { useUISettingKey } from "~/atoms/settings/ui"
import { useEntry } from "~/store/entry/hooks"
-import { ElectronAdditionActions } from "./actions/electron-actions"
import { EntryHeaderActions } from "./actions/header-actions"
import { MoreActions } from "./actions/more-actions"
import { useEntryContentScrollToTop, useEntryTitleMeta } from "./atoms"
@@ -71,9 +70,7 @@ function EntryHeaderImpl({ view, entryId, className, compact }: EntryHeaderProps
- {!compact && }
-
-
+
diff --git a/apps/desktop/src/renderer/src/modules/entry-content/header.mobile.tsx b/apps/desktop/src/renderer/src/modules/entry-content/header.mobile.tsx
index 74b99f41f..8553f61ec 100644
--- a/apps/desktop/src/renderer/src/modules/entry-content/header.mobile.tsx
+++ b/apps/desktop/src/renderer/src/modules/entry-content/header.mobile.tsx
@@ -3,7 +3,7 @@ import { RootPortal } from "@follow/components/ui/portal/index.js"
import { findElementInShadowDOM } from "@follow/utils/dom"
import { clsx, cn } from "@follow/utils/utils"
import { DismissableLayer } from "@radix-ui/react-dismissable-layer"
-import { AnimatePresence, m } from "framer-motion"
+import { AnimatePresence, m } from "motion/react"
import { memo, useEffect, useState } from "react"
import { RemoveScroll } from "react-remove-scroll"
import { useEventCallback } from "usehooks-ts"
diff --git a/apps/desktop/src/renderer/src/modules/entry-content/index.electron.tsx b/apps/desktop/src/renderer/src/modules/entry-content/index.electron.tsx
index e260e1651..b446ed226 100644
--- a/apps/desktop/src/renderer/src/modules/entry-content/index.electron.tsx
+++ b/apps/desktop/src/renderer/src/modules/entry-content/index.electron.tsx
@@ -2,14 +2,17 @@ import { MemoedDangerousHTMLStyle } from "@follow/components/common/MemoedDanger
import { ScrollArea } from "@follow/components/ui/scroll-area/index.js"
import { useTitle } from "@follow/hooks"
import type { FeedModel, InboxModel } from "@follow/models/types"
-import { IN_ELECTRON } from "@follow/shared/constants"
import { stopPropagation } from "@follow/utils/dom"
import { cn } from "@follow/utils/utils"
import { ErrorBoundary } from "@sentry/react"
import * as React from "react"
import { useEffect, useMemo, useRef } from "react"
-import { useEntryIsInReadability } from "~/atoms/readability"
+import {
+ useEntryIsInReadability,
+ useEntryIsInReadabilitySuccess,
+ useEntryReadabilityContent,
+} from "~/atoms/readability"
import { useUISettingKey } from "~/atoms/settings/ui"
import { ShadowDOM } from "~/components/common/ShadowDOM"
import { useInPeekModal } from "~/components/ui/modal/inspire/PeekModal"
@@ -35,7 +38,7 @@ import {
ContainerToc,
NoContent,
ReadabilityAutoToggleEffect,
- ReadabilityContent,
+ ReadabilityNotice,
RenderError,
TitleMetaHandler,
ViewSourceContentAutoToggleEffect,
@@ -62,11 +65,13 @@ export const EntryContent: Component = ({
staleTime: 300_000,
},
)
+ const readabilityContent = useEntryReadabilityContent(entryId)
const readerFontFamily = useUISettingKey("readerFontFamily")
const view = useRouteParamsSelector((route) => route.view)
const isInReadabilityMode = useEntryIsInReadability(entryId)
+ const isReadabilitySuccess = useEntryIsInReadabilitySuccess(entryId)
const scrollerRef = useRef(null)
useEffect(() => {
scrollerRef.current?.scrollTo(0, 0)
@@ -114,14 +119,21 @@ export const EntryContent: Component = ({
)
const customCSS = useUISettingKey("customCSS")
- const contentTranslated = useEntryTranslation({ entry, extraFields: ["content"] })
+ const contentTranslated = useEntryTranslation({
+ entry,
+ extraFields: isReadabilitySuccess ? ["readabilityContent"] : ["content"],
+ })
const isInPeekModal = useInPeekModal()
if (!entry) return null
- const entryContent = entry?.entries.content ?? data?.entries.content
- const translatedContent = contentTranslated.data?.content
+ const entryContent = isInReadabilityMode
+ ? readabilityContent?.content
+ : (entry?.entries.content ?? data?.entries.content)
+ const translatedContent = isInReadabilityMode
+ ? contentTranslated.data?.readabilityContent
+ : contentTranslated.data?.content
const content = translatedContent || entryContent
const isInbox = !!inbox
@@ -166,34 +178,31 @@ export const EntryContent: Component = ({
- {!isInReadabilityMode ? (
-
- {!!customCSS && (
- {customCSS}
- )}
-
- {content}
-
-
- ) : (
-
- )}
+
+
+ {!!customCSS && (
+ {customCSS}
+ )}
+
+ {content}
+
+
- {entry.settings?.readability && IN_ELECTRON && (
+ {entry.settings?.readability && (
)}
{entry.settings?.sourceContent && }
diff --git a/apps/desktop/src/renderer/src/modules/entry-content/index.shared.tsx b/apps/desktop/src/renderer/src/modules/entry-content/index.shared.tsx
index 22be07ce4..d508a0629 100644
--- a/apps/desktop/src/renderer/src/modules/entry-content/index.shared.tsx
+++ b/apps/desktop/src/renderer/src/modules/entry-content/index.shared.tsx
@@ -4,7 +4,7 @@ import { Button, MotionButtonBase } from "@follow/components/ui/button/index.js"
import { LoadingWithIcon } from "@follow/components/ui/loading/index.jsx"
import { RootPortal } from "@follow/components/ui/portal/index.jsx"
import { useScrollViewElement } from "@follow/components/ui/scroll-area/hooks.js"
-import { IN_ELECTRON, WEB_BUILD } from "@follow/shared/constants"
+import { WEB_BUILD } from "@follow/shared/constants"
import { EventBus } from "@follow/utils/event-bus"
import { springScrollTo } from "@follow/utils/scroller"
import { cn } from "@follow/utils/utils"
@@ -17,12 +17,12 @@ import {
ReadabilityStatus,
setReadabilityStatus,
useEntryInReadabilityStatus,
+ useEntryIsInReadability,
useEntryReadabilityContent,
} from "~/atoms/readability"
import { enableShowSourceContent } from "~/atoms/source-content"
import { Toc } from "~/components/ui/markdown/components/Toc"
-import { useEntryReadabilityToggle } from "~/hooks/biz/useEntryActions"
-import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
+import { toggleEntryReadability } from "~/hooks/biz/useEntryActions"
import { getNewIssueUrl } from "~/lib/issues"
import {
useIsSoFWrappedElement,
@@ -33,7 +33,6 @@ import { useEntry } from "~/store/entry"
import { useFeedById } from "~/store/feed"
import { useInboxById } from "~/store/inbox"
-import { EntryContentHTMLRenderer } from "../renderer/html"
import { setEntryContentScrollToTop, setEntryTitleMeta } from "./atoms"
export interface EntryContentProps {
@@ -85,10 +84,13 @@ export const TitleMetaHandler: Component<{
return null
}
-export const ReadabilityContent = ({ entryId, feedId }: { entryId: string; feedId: string }) => {
+export const ReadabilityNotice = ({ entryId }: { entryId: string }) => {
const { t } = useTranslation()
const result = useEntryReadabilityContent(entryId)
- const view = useRouteParamsSelector((route) => route.view)
+ const isInReadability = useEntryIsInReadability(entryId)
+ if (!isInReadability) {
+ return null
+ }
return (
@@ -103,16 +105,6 @@ export const ReadabilityContent = ({ entryId, feedId }: { entryId: string; feedI
{t("entry_content.fetching_content")}
)}
-
-
- {result?.content ?? ""}
-
)
}
@@ -134,12 +126,7 @@ export const NoContent: FC<{
{(WEB_BUILD || status === ReadabilityStatus.FAILURE) && (
{t("entry_content.no_content")}
)}
- {WEB_BUILD && (
-
- {t("entry_content.web_app_notice")}
-
- )}
- {!sourceContent && url && IN_ELECTRON && }
+ {!sourceContent && url && }
)
@@ -159,10 +146,6 @@ export const ViewSourceContentAutoToggleEffect = () => {
}
export const ReadabilityAutoToggleEffect = ({ url, id }: { url: string; id: string }) => {
- const toggle = useEntryReadabilityToggle({
- id,
- url,
- })
const onceRef = useRef(false)
useEffect(() => {
@@ -171,9 +154,9 @@ export const ReadabilityAutoToggleEffect = ({ url, id }: { url: string; id: stri
setReadabilityStatus({
[id]: ReadabilityStatus.INITIAL,
})
- toggle()
+ toggleEntryReadability({ id, url })
}
- }, [toggle])
+ }, [id, url])
return null
}
diff --git a/apps/desktop/src/renderer/src/modules/feed/feed-icon.tsx b/apps/desktop/src/renderer/src/modules/feed/feed-icon.tsx
index d0cee7d53..6b7974009 100644
--- a/apps/desktop/src/renderer/src/modules/feed/feed-icon.tsx
+++ b/apps/desktop/src/renderer/src/modules/feed/feed-icon.tsx
@@ -4,7 +4,7 @@ import { getBackgroundGradient } from "@follow/utils/color"
import { getImageProxyUrl } from "@follow/utils/img-proxy"
import { cn, getUrlIcon } from "@follow/utils/utils"
import { Avatar, AvatarFallback, AvatarImage } from "@radix-ui/react-avatar"
-import { m } from "framer-motion"
+import { m } from "motion/react"
import type { ReactNode } from "react"
import { forwardRef, useMemo } from "react"
diff --git a/apps/desktop/src/renderer/src/modules/modal/shortcuts.tsx b/apps/desktop/src/renderer/src/modules/modal/shortcuts.tsx
index f5717b13c..d04a6f49d 100644
--- a/apps/desktop/src/renderer/src/modules/modal/shortcuts.tsx
+++ b/apps/desktop/src/renderer/src/modules/modal/shortcuts.tsx
@@ -2,7 +2,7 @@ import { MotionButtonBase } from "@follow/components/ui/button/index.js"
import { KbdCombined } from "@follow/components/ui/kbd/Kbd.js"
import { ScrollArea } from "@follow/components/ui/scroll-area/index.js"
import { clsx, cn } from "@follow/utils/utils"
-import { m, useDragControls } from "framer-motion"
+import { m, useDragControls } from "motion/react"
import { useCallback, useEffect } from "react"
import { useTranslation } from "react-i18next"
diff --git a/apps/desktop/src/renderer/src/modules/new-user-guide/guide-modal-content.tsx b/apps/desktop/src/renderer/src/modules/new-user-guide/guide-modal-content.tsx
index 56b65de1e..551d3da79 100644
--- a/apps/desktop/src/renderer/src/modules/new-user-guide/guide-modal-content.tsx
+++ b/apps/desktop/src/renderer/src/modules/new-user-guide/guide-modal-content.tsx
@@ -3,7 +3,7 @@ import { Button } from "@follow/components/ui/button/index.js"
import { Kbd } from "@follow/components/ui/kbd/Kbd.js"
import { tracker } from "@follow/tracker"
import { cn } from "@follow/utils/utils"
-import { AnimatePresence, m } from "framer-motion"
+import { AnimatePresence, m } from "motion/react"
import type { ComponentProps, FunctionComponentElement } from "react"
import { createElement, useCallback, useEffect, useMemo, useState } from "react"
import { Trans, useTranslation } from "react-i18next"
diff --git a/apps/desktop/src/renderer/src/modules/panel/cmdf.tsx b/apps/desktop/src/renderer/src/modules/panel/cmdf.tsx
index d03d5812c..1fe8f4745 100644
--- a/apps/desktop/src/renderer/src/modules/panel/cmdf.tsx
+++ b/apps/desktop/src/renderer/src/modules/panel/cmdf.tsx
@@ -6,7 +6,7 @@ import { RootPortal } from "@follow/components/ui/portal/index.jsx"
import { useInputComposition, useRefValue } from "@follow/hooks"
import { useSubscribeElectronEvent } from "@follow/shared/event"
import { nextFrame } from "@follow/utils/dom"
-import { AnimatePresence, m } from "framer-motion"
+import { AnimatePresence, m } from "motion/react"
import type { FC } from "react"
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"
import { useDebounceCallback, useEventCallback } from "usehooks-ts"
diff --git a/apps/desktop/src/renderer/src/modules/player/corner-player.tsx b/apps/desktop/src/renderer/src/modules/player/corner-player.tsx
index 9d176706b..c2addc84a 100644
--- a/apps/desktop/src/renderer/src/modules/player/corner-player.tsx
+++ b/apps/desktop/src/renderer/src/modules/player/corner-player.tsx
@@ -6,7 +6,7 @@ import { tracker } from "@follow/tracker"
import { cn } from "@follow/utils/utils"
import * as Slider from "@radix-ui/react-slider"
import dayjs from "dayjs"
-import { AnimatePresence, m } from "framer-motion"
+import { AnimatePresence, m } from "motion/react"
import { useEffect, useMemo, useState } from "react"
import Marquee from "react-fast-marquee"
import { useHotkeys } from "react-hotkeys-hook"
@@ -26,7 +26,6 @@ import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry"
import { FeedIcon } from "~/modules/feed/feed-icon"
import { useEntry } from "~/store/entry"
import { useFeedById } from "~/store/feed"
-import { useInboxById } from "~/store/inbox"
import { useListById } from "~/store/list"
const handleClickPlay = () => {
@@ -112,6 +111,7 @@ const CornerPlayerImpl = ({ hideControls, rounded }: ControlButtonProps) => {
const playerValue = { entryId, status, isMute }
const entry = useEntry(playerValue.entryId)
+ const isInbox = !!entry?.inboxId
const feed = useFeedById(entry?.feedId)
const list = useListById(listId)
@@ -141,8 +141,6 @@ const CornerPlayerImpl = ({ hideControls, rounded }: ControlButtonProps) => {
}
}, [])
- const isInbox = useInboxById(entry?.feedId, (inbox) => inbox !== null)
-
const navigateToEntry = useNavigateEntry()
usePlayerTracker()
@@ -170,7 +168,7 @@ const CornerPlayerImpl = ({ hideControls, rounded }: ControlButtonProps) => {
return null
}
return options
- }, [entry, feed, list])
+ }, [entry, feed, isInbox, list])
if (!entry || !feed) return null
return (
diff --git a/apps/desktop/src/renderer/src/modules/profile/email-management.tsx b/apps/desktop/src/renderer/src/modules/profile/email-management.tsx
index 8e87e1758..3658010df 100644
--- a/apps/desktop/src/renderer/src/modules/profile/email-management.tsx
+++ b/apps/desktop/src/renderer/src/modules/profile/email-management.tsx
@@ -12,7 +12,7 @@ import { Label } from "@follow/components/ui/label/index.js"
import { cn } from "@follow/utils/utils"
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation } from "@tanstack/react-query"
-import { m } from "framer-motion"
+import { m } from "motion/react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
diff --git a/apps/desktop/src/renderer/src/modules/profile/two-factor.tsx b/apps/desktop/src/renderer/src/modules/profile/two-factor.tsx
index 0447b3ce3..5ac0eb152 100644
--- a/apps/desktop/src/renderer/src/modules/profile/two-factor.tsx
+++ b/apps/desktop/src/renderer/src/modules/profile/two-factor.tsx
@@ -11,7 +11,7 @@ import { Input, InputOTP, InputOTPGroup, InputOTPSlot } from "@follow/components
import { Label } from "@follow/components/ui/label/index.js"
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation } from "@tanstack/react-query"
-import { m, useAnimation } from "framer-motion"
+import { m, useAnimation } from "motion/react"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
diff --git a/apps/desktop/src/renderer/src/modules/profile/user-profile-modal.electron.tsx b/apps/desktop/src/renderer/src/modules/profile/user-profile-modal.electron.tsx
index b27c366bd..8ec04140d 100644
--- a/apps/desktop/src/renderer/src/modules/profile/user-profile-modal.electron.tsx
+++ b/apps/desktop/src/renderer/src/modules/profile/user-profile-modal.electron.tsx
@@ -7,9 +7,9 @@ import { nextFrame, stopPropagation } from "@follow/utils/dom"
import { getStorageNS } from "@follow/utils/ns"
import { clsx, cn } from "@follow/utils/utils"
import { throttle } from "es-toolkit/compat"
-import { useAnimationControls } from "framer-motion"
import { useAtom } from "jotai"
import { atomWithStorage } from "jotai/utils"
+import { useAnimationControls } from "motion/react"
import type { FC } from "react"
import { Fragment, useEffect, useMemo, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
@@ -220,7 +220,7 @@ export const UserProfileModalContent: FC = ({ use
window.open(UrlBuilder.profile(user.data.handle ?? user.data.id))
}}
>
-
+
diff --git a/apps/desktop/src/renderer/src/modules/profile/user-profile-modal.shared.tsx b/apps/desktop/src/renderer/src/modules/profile/user-profile-modal.shared.tsx
index fba486dc4..d43519df2 100644
--- a/apps/desktop/src/renderer/src/modules/profile/user-profile-modal.shared.tsx
+++ b/apps/desktop/src/renderer/src/modules/profile/user-profile-modal.shared.tsx
@@ -7,7 +7,7 @@ import { LoadingWithIcon } from "@follow/components/ui/loading/index.jsx"
import { EllipsisHorizontalTextWithTooltip } from "@follow/components/ui/typography/index.js"
import type { SubscriptionModel } from "@follow/models/types"
import { cn } from "@follow/utils/utils"
-import { AnimatePresence } from "framer-motion"
+import { AnimatePresence } from "motion/react"
import type { FC } from "react"
import { Fragment, memo, useState } from "react"
import { useEventCallback } from "usehooks-ts"
diff --git a/apps/desktop/src/renderer/src/modules/rsshub/add-modal-content.tsx b/apps/desktop/src/renderer/src/modules/rsshub/add-modal-content.tsx
index 016b5e611..1a6f92279 100644
--- a/apps/desktop/src/renderer/src/modules/rsshub/add-modal-content.tsx
+++ b/apps/desktop/src/renderer/src/modules/rsshub/add-modal-content.tsx
@@ -66,6 +66,13 @@ export function AddModalContent({
}
}, [details.data])
+ const codes = [
+ `FOLLOW_OWNER_USER_ID=${me?.handle || me?.id} # User id or handle of your follow account`,
+ `FOLLOW_DESCRIPTION=${instance?.description || `${me?.name}'s instance`} # The description of your instance`,
+ `FOLLOW_PRICE=${instance?.price || 100} # The monthly price of your instance, set to 0 means free.`,
+ `FOLLOW_USER_LIMIT=${instance?.userLimit || 1000} # The user limit of your instance, set it to 0 or 1 can make your instance private, leaving it empty means no restriction`,
+ ]
+
return (
{t("rsshub.addModal.description")}
@@ -73,11 +80,8 @@ export function AddModalContent({
{details.isLoading ? (
diff --git a/apps/desktop/src/renderer/src/modules/rsshub/delete-modal-content.tsx b/apps/desktop/src/renderer/src/modules/rsshub/delete-modal-content.tsx
new file mode 100644
index 000000000..230d115b6
--- /dev/null
+++ b/apps/desktop/src/renderer/src/modules/rsshub/delete-modal-content.tsx
@@ -0,0 +1,43 @@
+import { Button } from "@follow/components/ui/button/index.js"
+import { useMutation } from "@tanstack/react-query"
+import { useTranslation } from "react-i18next"
+import { toast } from "sonner"
+
+import { apiClient } from "~/lib/api-fetch"
+import { Queries } from "~/queries"
+
+export const ConfirmDeleteModalContent = ({ id, dismiss }: { dismiss: () => void; id: string }) => {
+ const { t } = useTranslation("settings")
+ const deleteMutation = useMutation({
+ mutationFn: () => {
+ return apiClient.rsshub.$delete({ json: { id } })
+ },
+ onSuccess: () => {
+ Queries.rsshub.list().invalidate()
+ toast.success(t("rsshub.table.delete.success"))
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ })
+
+ return (
+
+
+
+ {t("rsshub.table.delete.confirm")}
+
+
+ {
+ deleteMutation.mutate()
+ dismiss()
+ }}
+ >
+ {t("rsshub.table.delete.label")}
+
+
+
+ )
+}
diff --git a/apps/desktop/src/renderer/src/modules/settings/modal/layout.tsx b/apps/desktop/src/renderer/src/modules/settings/modal/layout.tsx
index 6051c6780..063422943 100644
--- a/apps/desktop/src/renderer/src/modules/settings/modal/layout.tsx
+++ b/apps/desktop/src/renderer/src/modules/settings/modal/layout.tsx
@@ -3,7 +3,7 @@ import { LetsIconsResizeDownRightLight } from "@follow/components/icons/resize.j
import { IN_ELECTRON } from "@follow/shared/constants"
import { preventDefault } from "@follow/utils/dom"
import { cn, getOS } from "@follow/utils/utils"
-import type { BoundingBox } from "framer-motion"
+import type { BoundingBox } from "motion/react"
import { Resizable } from "re-resizable"
import type { PropsWithChildren } from "react"
import { memo, Suspense, useCallback, useEffect, useRef } from "react"
diff --git a/apps/desktop/src/renderer/src/modules/settings/tabs/apperance.tsx b/apps/desktop/src/renderer/src/modules/settings/tabs/apperance.tsx
index c6d267c54..bc823e4ab 100644
--- a/apps/desktop/src/renderer/src/modules/settings/tabs/apperance.tsx
+++ b/apps/desktop/src/renderer/src/modules/settings/tabs/apperance.tsx
@@ -13,7 +13,7 @@ import { useIsDark, useThemeAtomValue } from "@follow/hooks"
import { ELECTRON_BUILD, IN_ELECTRON } from "@follow/shared/constants"
import { capitalizeFirstLetter, getOS } from "@follow/utils/utils"
import dayjs from "dayjs"
-import { useForceUpdate } from "framer-motion"
+import { useForceUpdate } from "motion/react"
import { lazy, Suspense, useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { bundledThemesInfo } from "shiki/themes"
diff --git a/apps/desktop/src/renderer/src/modules/settings/tabs/integration.tsx b/apps/desktop/src/renderer/src/modules/settings/tabs/integration.tsx
index a12d31158..b32b5e220 100644
--- a/apps/desktop/src/renderer/src/modules/settings/tabs/integration.tsx
+++ b/apps/desktop/src/renderer/src/modules/settings/tabs/integration.tsx
@@ -210,6 +210,9 @@ export const SettingIntegration = () => {
label: t("integration.cubox.autoMemo.label"),
description: t("integration.cubox.autoMemo.description"),
}),
+ defineSettingItem("saveSummaryAsDescription", {
+ label: t("integration.save_ai_summary_as_description.label"),
+ }),
BottomTip,
]}
/>
diff --git a/apps/desktop/src/renderer/src/modules/timeline-column/FeedCategory.tsx b/apps/desktop/src/renderer/src/modules/timeline-column/FeedCategory.tsx
index 79442cc13..880c031d6 100644
--- a/apps/desktop/src/renderer/src/modules/timeline-column/FeedCategory.tsx
+++ b/apps/desktop/src/renderer/src/modules/timeline-column/FeedCategory.tsx
@@ -9,7 +9,7 @@ import { useInputComposition, useRefValue } from "@follow/hooks"
import { stopPropagation } from "@follow/utils/dom"
import { cn, sortByAlphabet } from "@follow/utils/utils"
import { useMutation } from "@tanstack/react-query"
-import { AnimatePresence, m } from "framer-motion"
+import { AnimatePresence, m } from "motion/react"
import type { FC } from "react"
import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
diff --git a/apps/desktop/src/renderer/src/modules/timeline-column/FeedList.shared.tsx b/apps/desktop/src/renderer/src/modules/timeline-column/FeedList.shared.tsx
index 6313fed2b..0adaac6ca 100644
--- a/apps/desktop/src/renderer/src/modules/timeline-column/FeedList.shared.tsx
+++ b/apps/desktop/src/renderer/src/modules/timeline-column/FeedList.shared.tsx
@@ -5,7 +5,7 @@ import { stopPropagation } from "@follow/utils/dom"
import { cn } from "@follow/utils/utils"
import * as HoverCard from "@radix-ui/react-hover-card"
import { useQuery } from "@tanstack/react-query"
-import { AnimatePresence, m } from "framer-motion"
+import { AnimatePresence, m } from "motion/react"
import { memo, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router"
diff --git a/apps/desktop/src/renderer/src/modules/timeline-column/TimelineColumnHeader.tsx b/apps/desktop/src/renderer/src/modules/timeline-column/TimelineColumnHeader.tsx
index 9c934095a..afece99e5 100644
--- a/apps/desktop/src/renderer/src/modules/timeline-column/TimelineColumnHeader.tsx
+++ b/apps/desktop/src/renderer/src/modules/timeline-column/TimelineColumnHeader.tsx
@@ -4,7 +4,7 @@ import { ActionButton } from "@follow/components/ui/button/index.js"
import { Popover, PopoverContent, PopoverTrigger } from "@follow/components/ui/popover/index.jsx"
import { stopPropagation } from "@follow/utils/dom"
import { cn } from "@follow/utils/utils"
-import { m } from "framer-motion"
+import { m } from "motion/react"
import type { FC, PropsWithChildren } from "react"
import { memo, useCallback, useEffect, useRef, useState } from "react"
import { useHotkeys } from "react-hotkeys-hook"
diff --git a/apps/desktop/src/renderer/src/modules/timeline-column/index.tsx b/apps/desktop/src/renderer/src/modules/timeline-column/index.tsx
index afa43e065..429103084 100644
--- a/apps/desktop/src/renderer/src/modules/timeline-column/index.tsx
+++ b/apps/desktop/src/renderer/src/modules/timeline-column/index.tsx
@@ -6,8 +6,8 @@ import { useRegisterGlobalContext } from "@follow/shared/bridge"
import { ELECTRON_BUILD } from "@follow/shared/constants"
import { clamp, cn } from "@follow/utils/utils"
import { useWheel } from "@use-gesture/react"
-import { AnimatePresence, m } from "framer-motion"
import { Lethargy } from "lethargy"
+import { AnimatePresence, m } from "motion/react"
import type { FC, PropsWithChildren } from "react"
import { memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"
import { isHotkeyPressed, useHotkeys } from "react-hotkeys-hook"
diff --git a/apps/desktop/src/renderer/src/modules/update-notice/UpdateNotice.mobile.tsx b/apps/desktop/src/renderer/src/modules/update-notice/UpdateNotice.mobile.tsx
index 03ece9048..29ff1f755 100644
--- a/apps/desktop/src/renderer/src/modules/update-notice/UpdateNotice.mobile.tsx
+++ b/apps/desktop/src/renderer/src/modules/update-notice/UpdateNotice.mobile.tsx
@@ -1,4 +1,4 @@
-import { m } from "framer-motion"
+import { m } from "motion/react"
import { useCallback } from "react"
import { useTranslation } from "react-i18next"
diff --git a/apps/desktop/src/renderer/src/modules/update-notice/UpdateNotice.tsx b/apps/desktop/src/renderer/src/modules/update-notice/UpdateNotice.tsx
index f23af93eb..d8a63643e 100644
--- a/apps/desktop/src/renderer/src/modules/update-notice/UpdateNotice.tsx
+++ b/apps/desktop/src/renderer/src/modules/update-notice/UpdateNotice.tsx
@@ -1,6 +1,6 @@
import { tracker } from "@follow/tracker"
import { cn } from "@follow/utils/utils"
-import { m, useMotionTemplate, useMotionValue } from "framer-motion"
+import { m, useMotionTemplate, useMotionValue } from "motion/react"
import { useCallback, useEffect } from "react"
import { useTranslation } from "react-i18next"
diff --git a/apps/desktop/src/renderer/src/modules/user/ProfileButton.electron.tsx b/apps/desktop/src/renderer/src/modules/user/ProfileButton.electron.tsx
index 8b67cc4d1..0cee2a0d1 100644
--- a/apps/desktop/src/renderer/src/modules/user/ProfileButton.electron.tsx
+++ b/apps/desktop/src/renderer/src/modules/user/ProfileButton.electron.tsx
@@ -22,12 +22,11 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu/dropdown-menu"
-import { useSignOut } from "~/hooks/biz/useSignOut"
import { UrlBuilder } from "~/lib/url-builder"
import { useAchievementModal } from "~/modules/achievement/hooks"
import { usePresentUserProfileModal } from "~/modules/profile/hooks"
import { useSettingModal } from "~/modules/settings/modal/use-setting-modal-hack"
-import { useSession } from "~/queries/auth"
+import { signOut, useSession } from "~/queries/auth"
import { useWallet } from "~/queries/wallet"
import { useActivationModal } from "../activation"
@@ -47,7 +46,6 @@ export type ProfileButtonProps = LoginProps & {
export const ProfileButton: FC
= memo((props) => {
const { status, session } = useSession()
const { user } = session || {}
- const signOut = useSignOut()
const settingModalPresent = useSettingModal()
const presentUserProfile = usePresentUserProfileModal("dialog")
const presentAchievement = useAchievementModal()
diff --git a/apps/desktop/src/renderer/src/modules/user/ProfileButton.mobile.tsx b/apps/desktop/src/renderer/src/modules/user/ProfileButton.mobile.tsx
index 002f6e89a..892821f8c 100644
--- a/apps/desktop/src/renderer/src/modules/user/ProfileButton.mobile.tsx
+++ b/apps/desktop/src/renderer/src/modules/user/ProfileButton.mobile.tsx
@@ -9,8 +9,8 @@ import { Link } from "react-router"
import rsshubLogoUrl from "~/assets/rsshub-icon.png?url"
import { useUserRole, useWhoami } from "~/atoms/user"
-import { useSignOut } from "~/hooks/biz/useSignOut"
import { UrlBuilder } from "~/lib/url-builder"
+import { signOut } from "~/queries/auth"
import { useWallet } from "~/queries/wallet"
import { useAchievementModal } from "../achievement/hooks"
@@ -26,8 +26,6 @@ import { UserAvatar } from "./UserAvatar"
export const ProfileButton: FC = () => {
const user = useWhoami()
- const signOut = useSignOut()
-
const presentUserProfile = usePresentUserProfileModal("dialog")
const presentAchievement = useAchievementModal()
const { t } = useTranslation()
diff --git a/apps/desktop/src/renderer/src/pages/(main)/(layer)/(subview)/rsshub/index.tsx b/apps/desktop/src/renderer/src/pages/(main)/(layer)/(subview)/rsshub/index.tsx
index a42842c22..40f036eca 100644
--- a/apps/desktop/src/renderer/src/pages/(main)/(layer)/(subview)/rsshub/index.tsx
+++ b/apps/desktop/src/renderer/src/pages/(main)/(layer)/(subview)/rsshub/index.tsx
@@ -19,6 +19,7 @@ import { useAuthQuery } from "~/hooks/common"
import { useSubViewTitle } from "~/modules/app-layout/subview/hooks"
import { useTOTPModalWrapper } from "~/modules/profile/hooks"
import { AddModalContent } from "~/modules/rsshub/add-modal-content"
+import { ConfirmDeleteModalContent } from "~/modules/rsshub/delete-modal-content"
import { SetModalContent } from "~/modules/rsshub/set-modal-content"
import { UserAvatar } from "~/modules/user/UserAvatar"
import { Queries } from "~/queries"
@@ -145,6 +146,10 @@ function List({ data }: { data?: RSSHubModel[] }) {
return a.price - b.price || loadA - loadB
})
.map((instance) => {
+ const instanceUserCountExceptOwner = instance.userCount
+ ? instance.userCount - (instance.ownerUserId === me?.id ? 1 : 0)
+ : 0
+
return (
@@ -226,6 +231,22 @@ function List({ data }: { data?: RSSHubModel[] }) {
{t("rsshub.table.edit")}
)}
+ {me?.id === instance.ownerUserId && (
+
+ present({
+ title: t("rsshub.table.delete.label"),
+ content: ({ dismiss }) => (
+
+ ),
+ })
+ }
+ >
+ {t("rsshub.table.delete.label")}
+
+ )}
diff --git a/apps/desktop/src/renderer/src/pages/settings/(settings)/profile.tsx b/apps/desktop/src/renderer/src/pages/settings/(settings)/profile.tsx
index 39a1c1813..1616be1ca 100644
--- a/apps/desktop/src/renderer/src/pages/settings/(settings)/profile.tsx
+++ b/apps/desktop/src/renderer/src/pages/settings/(settings)/profile.tsx
@@ -3,7 +3,6 @@ import { Divider } from "@follow/components/ui/divider/Divider.js"
import { Label } from "@follow/components/ui/label/index.js"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
-import { signOut } from "~/lib/auth"
import { isInMAS } from "~/lib/utils"
import { AccountManagement } from "~/modules/profile/account-management"
import { EmailManagement } from "~/modules/profile/email-management"
@@ -12,6 +11,7 @@ import { TwoFactor } from "~/modules/profile/two-factor"
import { UpdatePasswordForm } from "~/modules/profile/update-password-form"
import { SettingsTitle } from "~/modules/settings/title"
import { defineSettingPageData } from "~/modules/settings/utils"
+import { signOut } from "~/queries/auth"
const iconName = "i-mgc-user-setting-cute-re"
const priority = 1090
@@ -51,13 +51,7 @@ export function Component() {
Are you sure you want to delete your account? This action is irreversible
and may take up to two days to take effect.
- {
- await signOut()
- window.location.reload()
- }}
- >
+
Delete
diff --git a/apps/desktop/src/renderer/src/providers/extension-expose-provider.tsx b/apps/desktop/src/renderer/src/providers/extension-expose-provider.tsx
index 782e75363..e2945ac5b 100644
--- a/apps/desktop/src/renderer/src/providers/extension-expose-provider.tsx
+++ b/apps/desktop/src/renderer/src/providers/extension-expose-provider.tsx
@@ -11,8 +11,10 @@ import { setUpdaterStatus } from "~/atoms/updater"
import { useDialog, useModalStack } from "~/components/ui/modal/stacked/hooks"
import { useDiscoverRSSHubRouteModal } from "~/hooks/biz/useDiscoverRSSHubRoute"
import { useFollow } from "~/hooks/biz/useFollow"
+import { oneTimeToken } from "~/lib/auth"
import { usePresentUserProfileModal } from "~/modules/profile/hooks"
import { useSettingModal } from "~/modules/settings/modal/use-setting-modal"
+import { handleSessionChanges } from "~/queries/auth"
import { clearDataIfLoginOtherAccount } from "~/store/utils/clear"
declare module "@follow/components/providers/stable-router-provider.js" {
@@ -42,6 +44,11 @@ export const ExtensionExposeProvider = () => {
clearIfLoginOtherAccount(newUserId: string) {
clearDataIfLoginOtherAccount(newUserId)
},
+ async applyOneTimeToken(token: string) {
+ await oneTimeToken.apply({ token })
+ handleSessionChanges()
+ },
+
readyToUpdate() {
setUpdaterStatus({
type: "renderer",
diff --git a/apps/desktop/src/renderer/src/providers/user-provider.tsx b/apps/desktop/src/renderer/src/providers/user-provider.tsx
index d77bff776..a960b4b80 100644
--- a/apps/desktop/src/renderer/src/providers/user-provider.tsx
+++ b/apps/desktop/src/renderer/src/providers/user-provider.tsx
@@ -3,7 +3,6 @@ import { useEffect } from "react"
import { setUserRole, setWhoami } from "~/atoms/user"
import { setIntegrationIdentify } from "~/initialize/helper"
-import { tipcClient } from "~/lib/client"
import { useSession } from "~/queries/auth"
import { CleanerService } from "~/services/cleaner"
@@ -18,9 +17,6 @@ export const UserProvider = () => {
}
setIntegrationIdentify(session.user)
- tipcClient?.trackerIdentify({
- user: session.user,
- })
CleanerService.cleanRemainingData(session.user.id)
}, [session?.role, session?.user])
diff --git a/apps/desktop/src/renderer/src/queries/ai.ts b/apps/desktop/src/renderer/src/queries/ai.ts
index d52cd2850..2451a2878 100644
--- a/apps/desktop/src/renderer/src/queries/ai.ts
+++ b/apps/desktop/src/renderer/src/queries/ai.ts
@@ -22,12 +22,21 @@ export const ai = {
defineQuery(["translation", entry?.entries.id, view, language, extraFields, part], () =>
translate({ entry, view, language, extraFields, part }),
),
- summary: ({ entryId, language }: { entryId: string; language?: SupportedLanguages }) =>
- defineQuery(["summary", entryId, language], async () => {
+ summary: ({
+ entryId,
+ language,
+ target = "content",
+ }: {
+ entryId: string
+ language?: SupportedLanguages
+ target?: "content" | "readabilityContent"
+ }) =>
+ defineQuery(["summary", entryId, language, target], async () => {
const res = await apiClient.ai.summary.$get({
query: {
id: entryId,
language,
+ target,
},
})
return res.data
diff --git a/apps/desktop/src/renderer/src/queries/auth.ts b/apps/desktop/src/renderer/src/queries/auth.ts
index abf229dac..a8e7ed076 100644
--- a/apps/desktop/src/renderer/src/queries/auth.ts
+++ b/apps/desktop/src/renderer/src/queries/auth.ts
@@ -1,9 +1,14 @@
import type { AuthSession } from "@follow/shared/hono"
+import { clearStorage } from "@follow/utils/ns"
import type { FetchError } from "ofetch"
+import { setWhoami } from "~/atoms/user"
+import { QUERY_PERSIST_KEY } from "~/constants"
import { useAuthQuery } from "~/hooks/common"
-import { getAccountInfo, getSession } from "~/lib/auth"
+import { getAccountInfo, getSession, signOut as signOutFn } from "~/lib/auth"
+import { tipcClient } from "~/lib/client"
import { defineQuery } from "~/lib/defineQuery"
+import { clearLocalPersistStoreData } from "~/store/utils/clear"
export const auth = {
getSession: () => defineQuery(["auth", "session"], () => getSession()),
@@ -65,3 +70,31 @@ export const useSession = (options?: { enabled?: boolean }) => {
: "unknown",
}
}
+
+export const handleSessionChanges = () => {
+ tipcClient?.sessionChanged()
+ window.location.reload()
+}
+
+export const signOut = async () => {
+ if (window.__RN__) {
+ window.ReactNativeWebView?.postMessage("sign-out")
+ return
+ }
+
+ // Clear query cache
+ localStorage.removeItem(QUERY_PERSIST_KEY)
+
+ // setLoginModalShow(true)
+ setWhoami(null)
+
+ // Clear local storage
+ clearStorage()
+
+ // clear local store data
+ await clearLocalPersistStoreData()
+ // Sign out
+ await tipcClient?.signOut()
+ await signOutFn()
+ window.location.reload()
+}
diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts
index cb364ed53..16c3df42d 100644
--- a/apps/desktop/vite.config.ts
+++ b/apps/desktop/vite.config.ts
@@ -47,6 +47,43 @@ const isWebBuild = process.env.WEB_BUILD === "1"
// eslint-disable-next-line no-console
console.log(green("Build type:"), isWebBuild ? "Web" : "Unknown")
+const proxyConfig = {
+ target: "http://localhost:2234",
+ changeOrigin: true,
+ selfHandleResponse: true,
+ configure: (proxy, _options) => {
+ proxy.on("proxyRes", (proxyRes, req, res) => {
+ const body = [] as any[]
+ proxyRes.on("data", (chunk: any) => body.push(chunk))
+ proxyRes.on("end", () => {
+ const html = parseHTML(Buffer.concat(body).toString())
+ const doc = html.document
+
+ const $scripts = doc.querySelectorAll("script")
+ $scripts.forEach((script) => {
+ const src = script.getAttribute("src")
+ if (src) {
+ script.setAttribute("src", `http://localhost:2234${src}`)
+ }
+ })
+
+ const $links = doc.querySelectorAll("link")
+ $links.forEach((link) => {
+ const href = link.getAttribute("href")
+ if (href) {
+ link.setAttribute("href", `http://localhost:2234${href}`)
+ }
+ })
+
+ res.setHeader("Content-Type", "text/html; charset=utf-8")
+
+ const modifiedHtml = doc.toString()
+ res.end(modifiedHtml)
+ })
+ })
+ },
+}
+
export default ({ mode }) => {
const env = loadEnv(mode, process.cwd())
const typedEnv = env as typeof EnvType
@@ -73,42 +110,10 @@ export default ({ mode }) => {
},
cors: true,
proxy: {
- "/login": {
- target: "http://localhost:2234",
- changeOrigin: true,
- selfHandleResponse: true,
- configure: (proxy, _options) => {
- proxy.on("proxyRes", (proxyRes, req, res) => {
- const body = [] as any[]
- proxyRes.on("data", (chunk: any) => body.push(chunk))
- proxyRes.on("end", () => {
- const html = parseHTML(Buffer.concat(body).toString())
- const doc = html.document
-
- const $scripts = doc.querySelectorAll("script")
- $scripts.forEach((script) => {
- const src = script.getAttribute("src")
- if (src) {
- script.setAttribute("src", `http://localhost:2234${src}`)
- }
- })
-
- const $links = doc.querySelectorAll("link")
- $links.forEach((link) => {
- const href = link.getAttribute("href")
- if (href) {
- link.setAttribute("href", `http://localhost:2234${href}`)
- }
- })
-
- res.setHeader("Content-Type", "text/html; charset=utf-8")
-
- const modifiedHtml = doc.toString()
- res.end(modifiedHtml)
- })
- })
- },
- },
+ "/login": proxyConfig,
+ "/forget-password": proxyConfig,
+ "/reset-password": proxyConfig,
+ "/register": proxyConfig,
...(env.VITE_DEV_PROXY
? {
@@ -236,7 +241,7 @@ export default ({ mode }) => {
],
["vfile", "unified"],
["es-toolkit/compat"],
- ["framer-motion"],
+ ["motion/react"],
["clsx", "tailwind-merge", "class-variance-authority"],
[
diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts
index 292004202..4a4ddc0c4 100644
--- a/apps/mobile/app.config.ts
+++ b/apps/mobile/app.config.ts
@@ -160,6 +160,17 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
enableBackgroundRemoteNotifications: true,
},
],
+ [
+ // Fix status bar flash issue on Android
+ // Learn more: https://github.com/expo/expo/blob/main/packages/expo-status-bar/src/StatusBar.android.tsx#L21
+ "react-native-edge-to-edge",
+ {
+ android: {
+ parentTheme: "Default",
+ enforceNavigationBarContrast: false,
+ },
+ },
+ ],
],
experiments: {
typedRoutes: true,
diff --git a/apps/mobile/bump.config.ts b/apps/mobile/bump.config.ts
new file mode 100644
index 000000000..b7e17bfaf
--- /dev/null
+++ b/apps/mobile/bump.config.ts
@@ -0,0 +1,23 @@
+/* eslint-disable no-template-curly-in-string */
+import { defineConfig } from "nbump"
+
+export default defineConfig({
+ leading: [
+ "git pull --rebase",
+ "tsx scripts/apply-changelog.ts ${NEW_VERSION}",
+ "git add changelog",
+ "pnpm eslint --fix package.json",
+ "pnpm prettier --ignore-unknown --write package.json",
+ "git add package.json",
+ ],
+ trailing: ["git checkout -b release/mobile/${NEW_VERSION}"],
+ finally: [
+ "git push origin release/mobile/${NEW_VERSION}",
+ "gh pr create --title 'release(mobile): Release v${NEW_VERSION}' --body 'v${NEW_VERSION}' --base main --head release/mobile/${NEW_VERSION}",
+ ],
+ push: false,
+ commitMessage: "release(mobile): release v${NEW_VERSION}",
+ tagPrefix: "mobile@",
+ changelog: false,
+ allowedBranches: ["dev"],
+})
diff --git a/apps/mobile/changelog/0.1.6.md b/apps/mobile/changelog/0.1.6.md
new file mode 100644
index 000000000..74de0b245
--- /dev/null
+++ b/apps/mobile/changelog/0.1.6.md
@@ -0,0 +1,19 @@
+# What's New in v0.1.6
+
+## Shiny new things
+
+- Server‑side readability with AI summaries and instant translation (#3498)
+- Discover page completely overhauled for a vibrant, streamlined experience (cce400c)
+- Added openLinksInExternalApp setting to let you choose where links open (61c099b)
+
+## Improvements
+
+- React re‑renders cut by 10×, delivering lightning‑fast, buttery‑smooth interactions (22964c7, a758e43)
+- Added action icons and refreshed share & AI icons for a cleaner interface (ece2637, 3bc6585)
+
+## No longer broken
+
+- Fixed an issue that prevented video preview images from loading (bcf752b)
+- Resolved the iOS swipe‑back gesture conflict with React Native’s Pressable, restoring silky‑smooth navigation
+- Corrected a bug where some images failed to display in the entry body (4654dc9)
+- Fixed an issue causing entry timestamps to display incorrectly in the timeline (4ee5944)
diff --git a/apps/mobile/changelog/next.md b/apps/mobile/changelog/next.md
index 4ef2c94b6..efca4370b 100644
--- a/apps/mobile/changelog/next.md
+++ b/apps/mobile/changelog/next.md
@@ -1,7 +1,7 @@
# What's New in vNEXT_VERSION
-## Shiny new things
+## New Features
## Improvements
-## No longer broken
+## Bug Fixes
diff --git a/apps/mobile/drizzle/0020_little_marauders.sql b/apps/mobile/drizzle/0020_little_marauders.sql
new file mode 100644
index 000000000..9a6f6ae3d
--- /dev/null
+++ b/apps/mobile/drizzle/0020_little_marauders.sql
@@ -0,0 +1,2 @@
+ALTER TABLE `summaries` ADD `readability_summary` text;--> statement-breakpoint
+ALTER TABLE `translations` ADD `readability_content` text;
\ No newline at end of file
diff --git a/apps/mobile/drizzle/meta/0020_snapshot.json b/apps/mobile/drizzle/meta/0020_snapshot.json
new file mode 100644
index 000000000..0b7d8b6e4
--- /dev/null
+++ b/apps/mobile/drizzle/meta/0020_snapshot.json
@@ -0,0 +1,708 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "493dc0df-ddca-4c5c-96bc-f74802251bc6",
+ "prevId": "0295d33b-ce34-41aa-a428-1dc63c6bb447",
+ "tables": {
+ "collections": {
+ "name": "collections",
+ "columns": {
+ "feed_id": {
+ "name": "feed_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "entry_id": {
+ "name": "entry_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "view": {
+ "name": "view",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "entries": {
+ "name": "entries",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source_content": {
+ "name": "source_content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "guid": {
+ "name": "guid",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "author": {
+ "name": "author",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "author_url": {
+ "name": "author_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "author_avatar": {
+ "name": "author_avatar",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "inserted_at": {
+ "name": "inserted_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "media": {
+ "name": "media",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "categories": {
+ "name": "categories",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "attachments": {
+ "name": "attachments",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "extra": {
+ "name": "extra",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "language": {
+ "name": "language",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "feed_id": {
+ "name": "feed_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "inbox_handle": {
+ "name": "inbox_handle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "read": {
+ "name": "read",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "sources": {
+ "name": "sources",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "settings": {
+ "name": "settings",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "feeds": {
+ "name": "feeds",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "error_at": {
+ "name": "error_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "site_url": {
+ "name": "site_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "owner_user_id": {
+ "name": "owner_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "images": {
+ "name": "images",
+ "columns": {
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "colors": {
+ "name": "colors",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(CURRENT_TIMESTAMP)"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "inboxes": {
+ "name": "inboxes",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "lists": {
+ "name": "lists",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "feed_ids": {
+ "name": "feed_ids",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "view": {
+ "name": "view",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "fee": {
+ "name": "fee",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "owner_user_id": {
+ "name": "owner_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "subscriptions": {
+ "name": "subscriptions",
+ "columns": {
+ "feed_id": {
+ "name": "feed_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "list_id": {
+ "name": "list_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "inbox_id": {
+ "name": "inbox_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "view": {
+ "name": "view",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "is_private": {
+ "name": "is_private",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "category": {
+ "name": "category",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "summaries": {
+ "name": "summaries",
+ "columns": {
+ "entry_id": {
+ "name": "entry_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "summary": {
+ "name": "summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "readability_summary": {
+ "name": "readability_summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "language": {
+ "name": "language",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "unq": {
+ "name": "unq",
+ "columns": ["entry_id", "language"],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "translations": {
+ "name": "translations",
+ "columns": {
+ "entry_id": {
+ "name": "entry_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "language": {
+ "name": "language",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "readability_content": {
+ "name": "readability_content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "translation-unique-index": {
+ "name": "translation-unique-index",
+ "columns": ["entry_id", "language"],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "unread": {
+ "name": "unread",
+ "columns": {
+ "subscription_id": {
+ "name": "subscription_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "count": {
+ "name": "count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "users": {
+ "name": "users",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "handle": {
+ "name": "handle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "is_me": {
+ "name": "is_me",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
diff --git a/apps/mobile/drizzle/meta/_journal.json b/apps/mobile/drizzle/meta/_journal.json
index c3a1b8b57..69e0d760f 100644
--- a/apps/mobile/drizzle/meta/_journal.json
+++ b/apps/mobile/drizzle/meta/_journal.json
@@ -141,6 +141,13 @@
"when": 1743153830369,
"tag": "0019_wonderful_shape",
"breakpoints": true
+ },
+ {
+ "idx": 20,
+ "version": "6",
+ "when": 1744793226628,
+ "tag": "0020_little_marauders",
+ "breakpoints": true
}
]
}
diff --git a/apps/mobile/drizzle/migrations.js b/apps/mobile/drizzle/migrations.js
index f66e18730..3ac3f929d 100644
--- a/apps/mobile/drizzle/migrations.js
+++ b/apps/mobile/drizzle/migrations.js
@@ -20,6 +20,7 @@ import m0016 from "./0016_curious_carnage.sql"
import m0017 from "./0017_talented_captain_cross.sql"
import m0018 from "./0018_dashing_the_fury.sql"
import m0019 from "./0019_wonderful_shape.sql"
+import m0020 from "./0020_little_marauders.sql"
import journal from "./meta/_journal.json"
export default {
@@ -45,5 +46,6 @@ export default {
m0017,
m0018,
m0019,
+ m0020,
},
}
diff --git a/apps/mobile/eas.json b/apps/mobile/eas.json
index 8c4881cbf..e38bbddc9 100644
--- a/apps/mobile/eas.json
+++ b/apps/mobile/eas.json
@@ -42,6 +42,9 @@
"appleId": "diygod@rss3.io",
"ascAppId": "6739802604",
"appleTeamId": "492J8Q67PF"
+ },
+ "android": {
+ "releaseStatus": "draft"
}
}
}
diff --git a/apps/mobile/native/expo-module.config.json b/apps/mobile/native/expo-module.config.json
index 904b0a42b..5806f648b 100644
--- a/apps/mobile/native/expo-module.config.json
+++ b/apps/mobile/native/expo-module.config.json
@@ -12,7 +12,8 @@
"TabScreenModule",
"TabBarPortalModule",
"EnhancePagerViewModule",
- "EnhancePageViewModule"
+ "EnhancePageViewModule",
+ "ItemPressableModule"
]
},
"android": {
diff --git a/apps/mobile/native/ios/Modules/ItemPressable/ItemPressableModule.swift b/apps/mobile/native/ios/Modules/ItemPressable/ItemPressableModule.swift
new file mode 100644
index 000000000..5a06b57b8
--- /dev/null
+++ b/apps/mobile/native/ios/Modules/ItemPressable/ItemPressableModule.swift
@@ -0,0 +1,60 @@
+//
+// ItemPressableModule.swift
+// Pods
+//
+// Created by Innei on 2025/4/15.
+//
+
+import ExpoModulesCore
+import UIKit
+
+public class ItemPressableModule: Module {
+ public func definition() -> ModuleDefinition {
+ Name("ItemPressable")
+
+ View(ItemPressableView.self) {
+ OnViewDidUpdateProps { view in
+ view.initizlize()
+ }
+
+ Prop("touchHighlight") { (view: ItemPressableView, value: Bool) in
+ view.setTouchHighlight(value)
+ }
+
+ Events("onItemPress")
+ }
+ }
+}
+
+class ItemPressableView: ExpoView {
+ private var onItemPress: EventDispatcher!
+ private var touchHighlight = true
+ required init(appContext: AppContext? = nil) {
+ self.onItemPress = EventDispatcher()
+ super.init(appContext: appContext)
+ }
+
+ func setTouchHighlight(_ value: Bool) {
+ touchHighlight = value
+ }
+
+ func initizlize() {
+ let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
+ tapGesture.delegate = self
+ gestureRecognizers = [tapGesture]
+ }
+
+ @objc func handleTap() {
+ let bgColor = layer.backgroundColor
+ if touchHighlight {
+ layer.backgroundColor = UIColor.systemGray5.cgColor
+
+ UIView.animate(withDuration: 0.2, delay: 0.1, options: [.curveEaseOut]) {
+ self.layer.backgroundColor = bgColor
+ }
+ }
+ onItemPress()
+ }
+}
+
+extension ItemPressableView: UIGestureRecognizerDelegate {}
diff --git a/apps/mobile/native/ios/Modules/PagerView/EnhancePagerViewModule.swift b/apps/mobile/native/ios/Modules/PagerView/EnhancePagerViewModule.swift
index c77edffc8..bb628a753 100644
--- a/apps/mobile/native/ios/Modules/PagerView/EnhancePagerViewModule.swift
+++ b/apps/mobile/native/ios/Modules/PagerView/EnhancePagerViewModule.swift
@@ -13,15 +13,15 @@ public class EnhancePagerViewModule: Module {
View(EnhancePagerView.self) {
OnViewDidUpdateProps { view in
- view.initizlize()
+ view.initialize()
}
Prop("page") { (view: EnhancePagerView, index: Int) in
view.page = index
}
- Prop("pageGap") { (view: EnhancePagerView, gap: Int) in
- view.pageGap = gap
+ Prop("initialPageIndex") { (view: EnhancePagerView, index: Int) in
+ view.initialPageIndex = index
}
Prop("transitionStyle") { (view: EnhancePagerView, style: TransitionStyle?) in
@@ -98,20 +98,23 @@ private class EnhancePagerView: ExpoView, UIGestureRecognizerDelegate {
}
}
+ var initialPageIndex: Int = 0
+
var pageGap = 20
var transitionStyle: TransitionStyle = .scroll
var panGestureRecognizer: UIGestureRecognizer?
- func initizlize() {
+ func initialize() {
let panGestureRecognizer = UIPanGestureRecognizer()
panGestureRecognizer.delegate = self
self.panGestureRecognizer = panGestureRecognizer
addGestureRecognizer(panGestureRecognizer)
- pageController = EnhancePagerController(pageViews: pageViews, initialPageIndex: page,
- transitionStyle: transitionStyle.toUIPageViewControllerTransitionStyle(),
- options: [
- .interPageSpacing: pageGap,
- ])
+ pageController = EnhancePagerController(
+ pageViews: pageViews, initialPageIndex: initialPageIndex,
+ transitionStyle: transitionStyle.toUIPageViewControllerTransitionStyle(),
+ options: [
+ .interPageSpacing: pageGap
+ ])
guard let pageController = pageController else { return }
addSubview(pageController.view)
pageController.view.snp.makeConstraints { make in
@@ -149,12 +152,16 @@ private class EnhancePagerView: ExpoView, UIGestureRecognizerDelegate {
}
#endif
- func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
+ func gestureRecognizer(
+ _ gestureRecognizer: UIGestureRecognizer,
+ shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
+ ) -> Bool {
guard let pageController = pageController else { return false }
let currentIndex = pageController.getCurrentPageIndex()
guard let scrollView = pageController.scrollView else { return false }
if gestureRecognizer == panGestureRecognizer,
- NSStringFromClass(type(of: otherGestureRecognizer)) == "RNSPanGestureRecognizer" {
+ NSStringFromClass(type(of: otherGestureRecognizer)) == "RNSPanGestureRecognizer"
+ {
if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer {
let velocity = panGestureRecognizer.velocity(in: self)
let isLTR = true
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index db16888a2..76548219e 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "@follow/mobile",
- "version": "0.1.5",
+ "version": "0.1.6",
"private": true,
"main": "src/main.tsx",
"scripts": {
@@ -18,7 +18,7 @@
"web": "expo start --web"
},
"dependencies": {
- "@better-auth/expo": "1.2.6-beta.1",
+ "@better-auth/expo": "1.2.7",
"@expo/metro-runtime": "4.0.1",
"@expo/react-native-action-sheet": "4.1.1",
"@follow/components": "workspace:*",
@@ -33,25 +33,25 @@
"@hookform/resolvers": "4.1.3",
"@openpanel/sdk": "1.0.0",
"@react-native-community/image-editor": "4.3.0",
- "@react-native-firebase/analytics": "21.13.0",
- "@react-native-firebase/app": "21.13.0",
- "@react-native-firebase/app-check": "21.13.0",
- "@react-native-firebase/crashlytics": "21.13.0",
- "@react-native-firebase/messaging": "21.13.0",
+ "@react-native-firebase/analytics": "21.14.0",
+ "@react-native-firebase/app": "21.14.0",
+ "@react-native-firebase/app-check": "21.14.0",
+ "@react-native-firebase/crashlytics": "21.14.0",
+ "@react-native-firebase/messaging": "21.14.0",
"@react-native-masked-view/masked-view": "0.3.2",
"@react-native-menu/menu": "1.2.3",
"@react-native-picker/picker": "2.11.0",
- "@shopify/flash-list": "1.7.3",
- "@tanstack/query-sync-storage-persister": "5.71.10",
- "@tanstack/react-query": "5.71.10",
- "@tanstack/react-query-persist-client": "5.71.10",
+ "@shopify/flash-list": "1.8.0",
+ "@tanstack/query-sync-storage-persister": "5.74.4",
+ "@tanstack/react-query": "5.74.4",
+ "@tanstack/react-query-persist-client": "5.74.4",
"@types/qrcode": "1.5.5",
- "better-auth": "1.2.6-beta.2",
- "cookie-es": "2.0.0",
+ "better-auth": "1.2.7",
+ "camelcase-keys": "9.1.3",
"dayjs": "1.11.13",
"dnum": "2.14.0",
- "es-toolkit": "1.34.1",
- "expo": "52.0.44",
+ "es-toolkit": "1.35.0",
+ "expo": "52.0.46",
"expo-apple-authentication": "7.1.3",
"expo-application": "6.0.2",
"expo-av": "15.0.2",
@@ -83,10 +83,10 @@
"expo-updates": "0.27.4",
"expo-web-browser": "14.0.2",
"franc-min": "6.2.0",
- "hono": "4.7.5",
- "i18next": "24.2.3",
+ "hono": "4.7.6",
+ "i18next": "25.0.0",
"immer": "10.1.1",
- "jotai": "2.12.2",
+ "jotai": "2.12.3",
"lru-cache": "11.1.0",
"nanoid": "5.1.5",
"nativewind": "4.1.23",
@@ -101,31 +101,32 @@
"react-native-awesome-slider": "2.9.0",
"react-native-bouncy-checkbox": "4.1.2",
"react-native-color-matrix-image-filters": "7.0.2",
+ "react-native-edge-to-edge": "1.6.0",
"react-native-gesture-handler": "2.25.0",
"react-native-image-colors": "2.4.0",
"react-native-ios-context-menu": "3.1.1",
"react-native-ios-utilities": "5.1.4",
- "react-native-keyboard-controller": "1.16.8",
+ "react-native-keyboard-controller": "1.17.0",
"react-native-otp-entry": "1.8.4",
"react-native-pager-view": "6.7.0",
- "react-native-reanimated": "3.17.2",
+ "react-native-reanimated": "3.17.4",
"react-native-root-siblings": "5.0.1",
- "react-native-safe-area-context": "5.3.0",
+ "react-native-safe-area-context": "5.4.0",
"react-native-screens": "4.10.0",
"react-native-sheet-transitions": "0.1.2",
"react-native-svg": "15.11.2",
"react-native-track-player": "4.1.1",
"react-native-uikit-colors": "0.3.10",
- "react-native-video": "6.11.0",
+ "react-native-video": "6.12.0",
"react-native-volume-manager": "2.0.8",
"react-native-web": "0.20.0",
"react-native-webview": "13.13.5",
- "shiki": "3.2.1",
+ "shiki": "3.2.2",
"tailwindcss": "3.4.17",
"use-sync-external-store": "1.5.0",
"usehooks-ts": "3.1.1",
"zeego": "3.0.6",
- "zod": "3.24.2",
+ "zod": "3.24.3",
"zustand": "5.0.3"
},
"devDependencies": {
@@ -133,11 +134,12 @@
"@types/react": "18.3.12",
"babel-plugin-inline-import": "3.0.0",
"babel-plugin-module-resolver": "5.0.2",
- "drizzle-kit": "0.30.6",
- "eas-cli": "16.2.1",
+ "drizzle-kit": "0.31.0",
+ "eas-cli": "16.3.2",
"expo-drizzle-studio-plugin": "0.1.2",
- "nbump": "2.0.7",
- "postcss": "8.5.3"
+ "nbump": "2.1.0",
+ "postcss": "8.5.3",
+ "sf-symbols-typescript": "2.1.0"
},
"appName": "Folo",
"expo": {
@@ -153,21 +155,5 @@
"autolinking": {
"nativeModulesDir": "./native"
}
- },
- "bump": {
- "before": [
- "git pull --rebase",
- "tsx scripts/apply-changelog.ts ${NEW_VERSION}",
- "git add changelog",
- "pnpm eslint --fix package.json",
- "pnpm prettier --ignore-unknown --write package.json",
- "git add package.json"
- ],
- "commit_message": "release(mobile): release v${NEW_VERSION}",
- "tag": false,
- "changelog": false,
- "allowed_branches": [
- "dev"
- ]
}
}
diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx
index f228e9697..0cb63e64e 100644
--- a/apps/mobile/src/App.tsx
+++ b/apps/mobile/src/App.tsx
@@ -4,6 +4,7 @@ import Animated, { interpolate, useAnimatedStyle } from "react-native-reanimated
import { RootSiblingParent } from "react-native-root-siblings"
import { useSheet } from "react-native-sheet-transitions"
+import { useBackHandler } from "./hooks/useBackHandler"
import { useIntentHandler } from "./hooks/useIntentHandler"
import { DebugButton, EnvProfileIndicator } from "./modules/debug"
import { usePrefetchActions } from "./store/action/hooks"
@@ -15,6 +16,7 @@ export function App({ children }: { children: React.ReactNode }) {
useIntentHandler()
useOnboarding()
useUnreadCountBadge()
+ useBackHandler()
// prefetch actions to detect if the user has any actions contains notifications
usePrefetchActions()
diff --git a/apps/mobile/src/api/trending.ts b/apps/mobile/src/api/trending.ts
new file mode 100644
index 000000000..59db4e0ad
--- /dev/null
+++ b/apps/mobile/src/api/trending.ts
@@ -0,0 +1,17 @@
+import type { Models } from "@follow/models/src"
+import camelcaseKeys from "camelcase-keys"
+
+import { apiFetch } from "../lib/api-fetch"
+
+const v1ApiPrefix = "/v1"
+export const getTrendingAggregates = async (params: { language: string }) => {
+ const data = await apiFetch(
+ `${v1ApiPrefix}/trendings?language=${params.language}`,
+ {
+ params: {
+ language: params.language,
+ },
+ },
+ )
+ return camelcaseKeys(data as any, { deep: true }) as Models.TrendingAggregates
+}
diff --git a/apps/mobile/src/components/common/ErrorBoundary.tsx b/apps/mobile/src/components/common/ErrorBoundary.tsx
index bc5f6ce51..a8f5c0bfa 100644
--- a/apps/mobile/src/components/common/ErrorBoundary.tsx
+++ b/apps/mobile/src/components/common/ErrorBoundary.tsx
@@ -34,7 +34,7 @@ export const ErrorBoundary = ({
const defaultFallbackRender = ({ error }: { error: Error }) => {
return (
- {error.message}
+ {error.message}
)
}
diff --git a/apps/mobile/src/components/common/NoLoginInfo.tsx b/apps/mobile/src/components/common/NoLoginInfo.tsx
index bbf61b530..c7b5e7172 100644
--- a/apps/mobile/src/components/common/NoLoginInfo.tsx
+++ b/apps/mobile/src/components/common/NoLoginInfo.tsx
@@ -2,7 +2,7 @@ import { Pressable, Text } from "react-native"
import { useColor } from "react-native-uikit-colors"
import { useNavigation } from "@/src/lib/navigation/hooks"
-import { LoginScreen } from "@/src/screens/(modal)/login"
+import { LoginScreen } from "@/src/screens/(modal)/LoginScreen"
import { Logo } from "../ui/logo"
diff --git a/apps/mobile/src/components/icons/PhUsersBold.tsx b/apps/mobile/src/components/icons/PhUsersBold.tsx
new file mode 100644
index 000000000..0fe5581ad
--- /dev/null
+++ b/apps/mobile/src/components/icons/PhUsersBold.tsx
@@ -0,0 +1,15 @@
+import * as React from "react"
+import type { SvgProps } from "react-native-svg"
+import Svg, { Path } from "react-native-svg"
+
+export function PhUsersBold(props: SvgProps & { size?: number; color?: string }) {
+ const { size = 24, color = "currentColor", ...rest } = props
+ return (
+
+
+
+ )
+}
diff --git a/apps/mobile/src/components/layouts/header/NavigationHeader.tsx b/apps/mobile/src/components/layouts/header/NavigationHeader.tsx
index ea01753c2..664a17f00 100644
--- a/apps/mobile/src/components/layouts/header/NavigationHeader.tsx
+++ b/apps/mobile/src/components/layouts/header/NavigationHeader.tsx
@@ -1,4 +1,5 @@
import { cn } from "@follow/utils"
+import { EventBus } from "@follow/utils/src/event-bus"
import type { FC, PropsWithChildren, ReactNode } from "react"
import {
createElement,
@@ -35,7 +36,6 @@ import {
useScreenIsInSheetModal,
} from "@/src/lib/navigation/hooks"
import { ScreenItemContext } from "@/src/lib/navigation/ScreenItemContext"
-import { useHorizontalScrolling } from "@/src/modules/screen/atoms"
import { ThemedBlurView } from "../../common/ThemedBlurView"
import { PlatformActivityIndicator } from "../../ui/loading/PlatformActivityIndicator"
@@ -105,12 +105,11 @@ const useHideableBottom = (
},
)
- const horizontalScrolling = useHorizontalScrolling()
useEffect(() => {
- if (horizontalScrolling) {
+ EventBus.subscribe("SELECT_TIMELINE", () => {
largeHeaderHeight.value = withTiming(largeDefaultHeaderHeightRef.current)
- }
- }, [horizontalScrolling, largeHeaderHeight])
+ })
+ }, [largeHeaderHeight])
const layoutHeightOnceRef = useRef(false)
const onLayout = useCallback(
@@ -335,7 +334,7 @@ export const InternalNavigationHeader = ({
{/* Only show loading indicator when headerTitle is not absolute */}
{!headerTitleAbsolute && isLoading && (
diff --git a/apps/mobile/src/components/layouts/tabbar/BottomTabProvider.tsx b/apps/mobile/src/components/layouts/tabbar/BottomTabProvider.tsx
index 484c710cb..ae5d79f4b 100644
--- a/apps/mobile/src/components/layouts/tabbar/BottomTabProvider.tsx
+++ b/apps/mobile/src/components/layouts/tabbar/BottomTabProvider.tsx
@@ -1,6 +1,11 @@
+import { atom } from "jotai"
import { useMemo, useState } from "react"
import { useSharedValue } from "react-native-reanimated"
+import type { BottomTabContextType } from "@/src/lib/navigation/bottom-tab/BottomTabContext"
+import { BottomTabContext } from "@/src/lib/navigation/bottom-tab/BottomTabContext"
+import type { TabScreenProps } from "@/src/lib/navigation/bottom-tab/types"
+
import { BottomTabHeightProvider } from "./BottomTabHeightProvider"
import { BottomTabBarBackgroundContext } from "./contexts/BottomTabBarBackgroundContext"
import {
@@ -8,17 +13,37 @@ import {
SetBottomTabBarVisibleContext,
} from "./contexts/BottomTabBarVisibleContext"
-export const BottomTabProvider = ({ children }: { children: React.ReactNode }) => {
+export const BottomTabProvider = ({
+ children,
+ initialTabIndex = 0,
+}: {
+ children: React.ReactNode
+ initialTabIndex?: number
+}) => {
const opacity = useSharedValue(1)
const [tabBarVisible, setTabBarVisible] = useState(true)
+ const [tabIndexAtom] = useState(() => atom(initialTabIndex))
+
+ const ctxValue = useMemo(
+ () => ({
+ currentIndexAtom: tabIndexAtom,
+ loadedableIndexAtom: atom(new Set()),
+ tabScreensAtom: atom([]),
+ tabHeightAtom: atom(0),
+ }),
+ [tabIndexAtom],
+ )
+
return (
- ({ opacity }), [opacity])}>
-
-
- {children}
-
-
-
+
+ ({ opacity }), [opacity])}>
+
+
+ {children}
+
+
+
+
)
}
diff --git a/apps/mobile/src/components/layouts/tabbar/Tabbar.tsx b/apps/mobile/src/components/layouts/tabbar/Tabbar.tsx
index 395657467..d191e9f66 100644
--- a/apps/mobile/src/components/layouts/tabbar/Tabbar.tsx
+++ b/apps/mobile/src/components/layouts/tabbar/Tabbar.tsx
@@ -1,6 +1,6 @@
import { useAtom, useAtomValue } from "jotai"
import type { FC } from "react"
-import { memo, useContext, useEffect, useMemo } from "react"
+import { memo, useCallback, useContext, useEffect, useMemo } from "react"
import type { StyleProp, TextStyle } from "react-native"
import { Platform, Pressable, StyleSheet, Text, View } from "react-native"
import Animated, {
@@ -10,6 +10,7 @@ import Animated, {
withSpring,
} from "react-native-reanimated"
import { useSafeAreaInsets } from "react-native-safe-area-context"
+import { useEventCallback } from "usehooks-ts"
import { SetBottomTabBarHeightContext } from "@/src/components/layouts/tabbar/contexts/BottomTabBarHeightContext"
import { gentleSpringPreset, quickSpringPreset, softSpringPreset } from "@/src/constants/spring"
@@ -57,6 +58,11 @@ export const Tabbar: FC<{
const renderTabScreens = tabScreens.length > 0 ? tabScreens : placeholderTabScreens
+ const onPress = useEventCallback((index: number) => {
+ setSelectedIndex(index)
+ onPressProp?.(index)
+ })
+
return (
0 ? "auto" : "none"}
@@ -76,53 +82,16 @@ export const Tabbar: FC<{
{renderTabScreens.map((route, index) => {
const focused = index === selectedIndex
-
- const inactiveTintColor = "#999"
- const onPress = () => {
- setSelectedIndex(index)
- onPressProp?.(index)
- }
-
const label = route.title
- const accessibilityLabel =
- typeof label === "string" && Platform.OS === "ios"
- ? `${label}, tab, ${index + 1} of ${tabScreens.length}`
- : undefined
-
- const renderIcon = ({ focused }: { focused: boolean }) => {
- const iconSize = ICON_SIZE_ROUND
- return (
-
- )
- }
-
- const renderLabel = ({ focused }: { focused: boolean }) => {
- return (
-
- )
- }
-
return (
-
)
})}
@@ -131,6 +100,62 @@ export const Tabbar: FC<{
)
}
+const MemoedTabItem: FC<{
+ focused: boolean
+ index: number
+ label: string
+ renderIcon?: (options: TabbarIconProps) => React.ReactNode
+ onPress: (index: number) => void
+}> = memo(({ focused, index, label, renderIcon: renderIconFn, onPress: onPressProp }) => {
+ const inactiveTintColor = "#999"
+ const onPress = () => {
+ onPressProp?.(index)
+ }
+
+ const accessibilityLabel =
+ typeof label === "string" && Platform.OS === "ios" ? `${label}, tab` : undefined
+
+ const renderIcon = useCallback(
+ ({ focused }: { focused: boolean }) => {
+ const iconSize = ICON_SIZE_ROUND
+ return (
+
+ )
+ },
+ [renderIconFn],
+ )
+
+ const renderLabel = useCallback(
+ ({ focused }: { focused: boolean }) => {
+ return (
+
+ )
+ },
+ [label, accessibilityLabel, inactiveTintColor],
+ )
+
+ return (
+
+ )
+})
+
const TextLabel = (props: {
focused: boolean
accessibilityLabel: string | undefined
@@ -165,33 +190,10 @@ const TabIcon = ({
const activeOpacity = focused ? 1 : 0
const inactiveOpacity = focused ? 0 : 1
- const opacity = useSharedValue(focused ? 1 : 0.8)
- const scale = useSharedValue(focused ? 1 : 0.92)
- const rotate = useSharedValue(focused ? 0 : 0.2)
-
- useEffect(() => {
- if (focused) {
- opacity.value = withSpring(1)
- scale.value = withSpring(1)
- rotate.value = withSpring(0)
- } else {
- opacity.value = 0.8
- scale.value = 0.92
- rotate.value = 0.2
- }
- }, [focused, opacity, scale, rotate])
-
- const animatedStyle = useAnimatedStyle(() => {
- return {
- opacity: opacity.value,
- transform: [{ scale: scale.value }, { rotate: `${rotate.value}rad` }],
- }
- })
-
return (
{focused && (
-
+
{renderIcon({
focused: true,
size: iconSize,
@@ -258,7 +260,7 @@ const TabBarBackground = () => {
{
const TabItem = memo(
({
- route,
focused,
onPress,
@@ -280,12 +281,11 @@ const TabItem = memo(
originalRenderLabel,
accessibilityLabel,
}: {
- route: any
focused: boolean
onPress: () => void
- originalRenderIcon: (scene: { route: any; focused: boolean }) => React.ReactNode
- originalRenderLabel: (scene: { route: any; focused: boolean }) => React.ReactNode
+ originalRenderIcon: (scene: { focused: boolean }) => React.ReactNode
+ originalRenderLabel: (scene: { focused: boolean }) => React.ReactNode
accessibilityLabel?: string
}) => {
const pressed = useSharedValue(0)
@@ -296,23 +296,32 @@ const TabItem = memo(
}
})
- const scene = { route, focused }
+ const scene = { focused }
return (
{
+ onPress()
+ cancelAnimation(pressed)
+ pressed.value = withSpring(0, quickSpringPreset)
+ }}
onPressIn={() => {
pressed.value = withSpring(1, gentleSpringPreset)
}}
onPressOut={() => {
- pressed.value = withSpring(0, gentleSpringPreset)
+ cancelAnimation(pressed)
+ pressed.value = withSpring(0, quickSpringPreset)
}}
className="flex-1 flex-col items-center justify-center"
accessibilityLabel={accessibilityLabel}
accessibilityRole="button"
accessibilityState={{ selected: focused }}
>
- {originalRenderIcon(scene)}
+
+ {useMemo(() => {
+ return originalRenderIcon(scene)
+ }, [JSON.stringify(scene), originalRenderIcon])}
+
{originalRenderLabel(scene)}
)
diff --git a/apps/mobile/src/components/layouts/tabbar/hooks.ts b/apps/mobile/src/components/layouts/tabbar/hooks.ts
index 2eafaa968..3404cd076 100644
--- a/apps/mobile/src/components/layouts/tabbar/hooks.ts
+++ b/apps/mobile/src/components/layouts/tabbar/hooks.ts
@@ -1,5 +1,5 @@
import type { RefObject } from "react"
-import { useCallback, useContext, useEffect, useRef } from "react"
+import { useCallback, useContext, useEffect, useLayoutEffect, useRef } from "react"
import type { FlatList, ScrollView } from "react-native"
import { findNodeHandle, Platform } from "react-native"
@@ -8,8 +8,9 @@ import {
SetAttachNavigationScrollViewContext,
useAttachNavigationScrollView,
} from "@/src/lib/navigation/AttachNavigationScrollViewContext"
-import { useScreenIsAppeared } from "@/src/lib/navigation/bottom-tab/hooks"
+import { useScreenIsAppeared, useTabScreenIsFocused } from "@/src/lib/navigation/bottom-tab/hooks"
+import { BottomTabBarBackgroundContext } from "./contexts/BottomTabBarBackgroundContext"
import { BottomTabBarHeightContext } from "./contexts/BottomTabBarHeightContext"
export const useBottomTabBarHeight = () => {
@@ -62,8 +63,26 @@ export const useRegisterNavigationScrollView = (active = true) => {
if (!active) return
if (!setAttachNavigationScrollViewRef) return
if (!tabScreenIsFocused) return
+ if (
+ scrollViewRef.current &&
+ typeof scrollViewRef.current === "object" &&
+ "checkScrollToBottom" in scrollViewRef.current &&
+ typeof scrollViewRef.current.checkScrollToBottom === "function"
+ ) {
+ scrollViewRef.current.checkScrollToBottom()
+ }
setAttachNavigationScrollViewRef(scrollViewRef as unknown as RefObject)
}, [setAttachNavigationScrollViewRef, scrollViewRef, active, tabScreenIsFocused])
return scrollViewRef
}
+
+export const useResetTabOpacityWhenFocused = () => {
+ const { opacity } = useContext(BottomTabBarBackgroundContext)
+ const tabScreenIsFocus = useTabScreenIsFocused()
+ useLayoutEffect(() => {
+ if (tabScreenIsFocus) {
+ opacity.value = 1
+ }
+ }, [tabScreenIsFocus, opacity])
+}
diff --git a/apps/mobile/src/components/layouts/views/SafeNavigationScrollView.tsx b/apps/mobile/src/components/layouts/views/SafeNavigationScrollView.tsx
index ba649340d..100029a4c 100644
--- a/apps/mobile/src/components/layouts/views/SafeNavigationScrollView.tsx
+++ b/apps/mobile/src/components/layouts/views/SafeNavigationScrollView.tsx
@@ -48,6 +48,9 @@ type SafeNavigationScrollViewProps = Omit & {
Header?: React.ReactNode
} & PropsWithChildren
+export interface ForwardedSafeNavigationScrollView extends ScrollView {
+ checkScrollToBottom: () => void
+}
export const SafeNavigationScrollView = forwardRef(
(
{
@@ -74,7 +77,11 @@ export const SafeNavigationScrollView = forwardRef(null)
- useImperativeHandle(forwardedRef, () => ref.current!)
+ useImperativeHandle(forwardedRef, () => {
+ return Object.assign({}, ref.current!, {
+ checkScrollToBottom,
+ })
+ })
const { opacity } = useContext(BottomTabBarBackgroundContext)
const inTabScreen = useInTabScreen()
@@ -172,6 +179,13 @@ export const NavigationBlurEffectHeaderView = ({
)
}
+/**
+ * @deprecated
+ * please use `NavigationBlurEffectHeaderView` instead, pass ` ` in `SafeNavigationScrollView`'s `Header` prop
+ *
+ * e.g. ` } />`
+ * @see NavigationBlurEffectHeaderView
+ */
export const NavigationBlurEffectHeader = ({
headerHideableBottom,
headerHideableBottomHeight,
diff --git a/apps/mobile/src/components/native/PagerView/index.tsx b/apps/mobile/src/components/native/PagerView/index.tsx
index d0a789482..f9a3c0782 100644
--- a/apps/mobile/src/components/native/PagerView/index.tsx
+++ b/apps/mobile/src/components/native/PagerView/index.tsx
@@ -1,4 +1,5 @@
import { cn } from "@follow/utils"
+import { cssInterop } from "nativewind"
import type {
FC,
ForwardRefExoticComponent,
@@ -6,13 +7,16 @@ import type {
ReactNode,
RefAttributes,
} from "react"
-import { forwardRef, useEffect, useRef, useState } from "react"
-import type { ViewStyle } from "react-native"
+import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react"
+import type { StyleProp, ViewStyle } from "react-native"
import { StyleSheet } from "react-native"
import type { PagerRef } from "./specs"
import { EnhancePagerView, EnhancePageView } from "./specs"
+cssInterop(EnhancePagerView, {
+ className: "style",
+})
interface PagerViewProps {
pageContainerStyle?: ViewStyle
pageContainerClassName?: string
@@ -24,27 +28,32 @@ interface PagerViewProps {
onPageChange?: (index: number) => void
onScroll?: (percent: number, direction: "left" | "right") => void
onScrollBegin?: () => void
- onScrollEnd?: () => void
+ onScrollEnd?: (index: number) => void
onPageWillAppear?: (index: number) => void
- containerStyle?: ViewStyle
+ containerStyle?: StyleProp
containerClassName?: string
+ initialPageIndex?: number
}
-const PagerViewImpl: FC = ({
- pageContainerStyle,
- pageContainerClassName,
- renderPage,
- pageTotal,
- pageGap,
- transitionStyle,
- containerStyle,
- containerClassName,
- page,
- onPageChange,
- onScroll,
- onScrollBegin,
- onScrollEnd,
- onPageWillAppear,
-}) => {
+const PagerViewImpl: FC = (
+ {
+ pageContainerStyle,
+ pageContainerClassName,
+ renderPage,
+ pageTotal,
+ pageGap,
+ transitionStyle,
+ containerStyle,
+ containerClassName,
+ page,
+ onPageChange,
+ onScroll,
+ onScrollBegin,
+ onScrollEnd,
+ onPageWillAppear,
+ initialPageIndex,
+ },
+ ref: any,
+) => {
const [currentPage, setCurrentPage] = useState(page ?? 0)
const nativeRef = useRef(null)
@@ -56,8 +65,17 @@ const PagerViewImpl: FC = ({
}
}
}, [currentPage])
+ useImperativeHandle(ref, () => ({
+ setPage: (index: number) => {
+ setCurrentPage(index)
+ nativeRef.current?.setPage(index)
+ },
+ getPage: () => currentPage,
+ getState: () => nativeRef.current?.getState(),
+ }))
return (
{
@@ -70,15 +88,14 @@ const PagerViewImpl: FC = ({
onScrollBegin={() => {
onScrollBegin?.()
}}
- onScrollEnd={() => {
- onScrollEnd?.()
+ onScrollEnd={(e) => {
+ onScrollEnd?.(e.nativeEvent.index)
}}
onPageWillAppear={(e) => {
onPageWillAppear?.(e.nativeEvent.index)
}}
className={cn("flex-1", containerClassName)}
style={containerStyle}
- // @ts-expect-error
ref={nativeRef}
>
{Array.from({ length: pageTotal }).map((_, index) => (
diff --git a/apps/mobile/src/components/native/PagerView/specs.ts b/apps/mobile/src/components/native/PagerView/specs.ts
index 17cb03d2a..2f46f88f6 100644
--- a/apps/mobile/src/components/native/PagerView/specs.ts
+++ b/apps/mobile/src/components/native/PagerView/specs.ts
@@ -1,18 +1,24 @@
import { requireNativeView } from "expo"
import type { NativeSyntheticEvent, ViewProps } from "react-native"
-export const EnhancePagerView = requireNativeView("EnhancePagerView")
+export const EnhancePagerView = requireNativeView<
+ ViewProps &
+ PagerProps & {
+ ref: React.RefObject
+ }
+>("EnhancePagerView")
export const EnhancePageView = requireNativeView("EnhancePageView")
export interface PagerProps {
onPageChange?: (e: NativeSyntheticEvent<{ index: number }>) => void
onScroll?: (e: NativeSyntheticEvent<{ percent: number; direction: "left" | "right" }>) => void
onScrollBegin?: () => void
- onScrollEnd?: () => void
+ onScrollEnd?: (e: NativeSyntheticEvent<{ index: number }>) => void
onPageWillAppear?: (e: NativeSyntheticEvent<{ index: number }>) => void
page?: number
pageGap?: number
transitionStyle?: "scroll" | "pageCurl"
+ initialPageIndex?: number
}
export interface PagerRef {
setPage: (index: number) => void
diff --git a/apps/mobile/src/components/native/webview/EntryContentWebView.tsx b/apps/mobile/src/components/native/webview/EntryContentWebView.tsx
index 3027c5d7f..55c5c7ad9 100644
--- a/apps/mobile/src/components/native/webview/EntryContentWebView.tsx
+++ b/apps/mobile/src/components/native/webview/EntryContentWebView.tsx
@@ -66,21 +66,16 @@ export function EntryContentWebView(props: EntryContentWebViewProps) {
}, [codeThemeLight, codeThemeDark, mode])
const entryInWebview = React.useMemo(() => {
- if (showReadability) {
- return {
- ...entry,
- content: entry.readabilityContent,
- }
- }
+ const entryContent = showReadability ? entry.readabilityContent : entry.content
+ const translatedContent = showReadability
+ ? entry.translation?.readabilityContent
+ : entry.translation?.content
+ const content = showTranslation ? translatedContent || entryContent : entryContent
- if (showTranslation) {
- return {
- ...entry,
- content: entry.translation?.content || entry.content,
- }
+ return {
+ ...entry,
+ content,
}
-
- return entry
}, [entry, showReadability, showTranslation])
useEffect(() => {
diff --git a/apps/mobile/src/components/ui/datetime/RelativeDateTime.tsx b/apps/mobile/src/components/ui/datetime/RelativeDateTime.tsx
index f754df054..ff945345a 100644
--- a/apps/mobile/src/components/ui/datetime/RelativeDateTime.tsx
+++ b/apps/mobile/src/components/ui/datetime/RelativeDateTime.tsx
@@ -2,9 +2,10 @@ import dayjs from "dayjs"
import { useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import type { TextProps } from "react-native"
-import { Pressable } from "react-native"
import Animated, { FadeOut } from "react-native-reanimated"
+import { NativePressable } from "../pressable/NativePressable"
+
const formatTemplateString = "lll"
const formatTime = (
@@ -48,7 +49,7 @@ interface RelativeDateTimeProps extends TextProps {
export const RelativeDateTime = ({
date,
- displayAbsoluteTimeAfterDay,
+ displayAbsoluteTimeAfterDay = 29,
dateFormatTemplate,
postfixText,
...props
@@ -76,7 +77,7 @@ export const RelativeDateTime = ({
}, [date, displayAbsoluteTimeAfterDay, dateFormatTemplate, mode])
return (
- {
setMode((mode) => (mode === "relative" ? "absolute" : "relative"))
@@ -87,6 +88,6 @@ export const RelativeDateTime = ({
? `${relative}${t("space")}${postfixText ?? t("words.ago")}`
: memoizedFormatTime}
-
+
)
}
diff --git a/apps/mobile/src/components/ui/grouped/GroupedList.tsx b/apps/mobile/src/components/ui/grouped/GroupedList.tsx
index 0a1d12ce1..e7da097c7 100644
--- a/apps/mobile/src/components/ui/grouped/GroupedList.tsx
+++ b/apps/mobile/src/components/ui/grouped/GroupedList.tsx
@@ -1,10 +1,12 @@
import { cn } from "@follow/utils"
+import { SymbolView } from "expo-symbols"
import type { FC, PropsWithChildren } from "react"
import * as React from "react"
import { Fragment } from "react"
import type { PressableProps, ViewProps } from "react-native"
import { Pressable, StyleSheet, Text, View } from "react-native"
import Animated, { FadeIn, FadeOut } from "react-native-reanimated"
+import type { SFSymbol } from "sf-symbols-typescript"
import { CheckFilledIcon } from "@/src/icons/check_filled"
import { MingcuteRightLine } from "@/src/icons/mingcute_right_line"
@@ -23,6 +25,7 @@ import { GroupedInsetListCardItemStyle } from "./GroupedInsetListCardItemStyle"
interface GroupedInsetListCardProps {
showSeparator?: boolean
SeparatorComponent?: FC
+ SeparatorElement?: React.ReactNode
}
interface BaseCellClassNames {
@@ -38,7 +41,14 @@ export const GroupedOutlineDescription: FC<{
export const GroupedInsetListCard: FC<
PropsWithChildren & ViewProps & GroupedInsetListCardProps
-> = ({ children, className, showSeparator = true, SeparatorComponent, ...props }) => {
+> = ({
+ children,
+ className,
+ showSeparator = true,
+ SeparatorComponent,
+ SeparatorElement,
+ ...props
+}) => {
const nextChildren = React.useMemo(
() => React.Children.toArray(children).filter(Boolean),
[children],
@@ -63,15 +73,24 @@ export const GroupedInsetListCard: FC<
((child.type as Function).name === GroupedInsetListNavigationLink.name ||
(child.type as any).itemStyle === GroupedInsetListCardItemStyle.NavigationLink)
+ const NextSeparatorComponent =
+ typeof SeparatorComponent === "function" ? : undefined
+ const NextSeparatorElement = SeparatorElement
+ ? React.isValidElement(SeparatorElement)
+ ? SeparatorElement
+ : NextSeparatorComponent
+ : NextSeparatorComponent
+
return (
{child}
{!isLast &&
- (SeparatorComponent ? (
-
- ) : (
+ (NextSeparatorElement ?? (
))}
@@ -178,12 +197,16 @@ export const GroupedInsetListCell: FC<
label: string
description?: string
children?: React.ReactNode
+ icon?: SFSymbol
} & BaseCellClassNames
-> = ({ label, description, children, leftClassName, rightClassName }) => {
+> = ({ label, description, children, leftClassName, rightClassName, icon }) => {
return (
- {label}
+
+ {!!icon && }
+ {label}
+
{!!description && (
{description}
)}
@@ -254,7 +277,8 @@ export const GroupedInsetListActionCell: FC<{
description?: string
onPress?: () => void
disabled?: boolean
-}> = ({ label, description, onPress, disabled }) => {
+ icon?: SFSymbol
+}> = ({ label, description, onPress, disabled, icon }) => {
const rightIconColor = useColor("tertiaryLabel")
return (
- {label}
+
+ {!!icon && }
+ {label}
+
{!!description && (
{description}
)}
diff --git a/apps/mobile/src/components/ui/pressable/IosItemPressable.ios.tsx b/apps/mobile/src/components/ui/pressable/IosItemPressable.ios.tsx
new file mode 100644
index 000000000..afa8f1bf1
--- /dev/null
+++ b/apps/mobile/src/components/ui/pressable/IosItemPressable.ios.tsx
@@ -0,0 +1,14 @@
+import { requireNativeView } from "expo"
+import { cssInterop } from "nativewind"
+import type { ViewProps } from "react-native"
+
+const NativeItemPressable = requireNativeView<
+ ViewProps & {
+ onItemPress: () => any
+ touchHighlight?: boolean
+ }
+>("ItemPressable")
+cssInterop(NativeItemPressable, {
+ className: "style",
+})
+export { NativeItemPressable }
diff --git a/apps/mobile/src/components/ui/pressable/IosItemPressable.tsx b/apps/mobile/src/components/ui/pressable/IosItemPressable.tsx
new file mode 100644
index 000000000..27da52fc6
--- /dev/null
+++ b/apps/mobile/src/components/ui/pressable/IosItemPressable.tsx
@@ -0,0 +1,12 @@
+import type { FC } from "react"
+import type { ViewProps } from "react-native"
+
+const NativeItemPressable: FC<
+ ViewProps & {
+ onItemPress?: () => any
+ touchHighlight?: boolean
+ }
+> = () => {
+ throw new Error("NativeItemPressable is not supported on iOS")
+}
+export { NativeItemPressable }
diff --git a/apps/mobile/src/components/ui/pressable/ItemPressable.ios.tsx b/apps/mobile/src/components/ui/pressable/ItemPressable.ios.tsx
new file mode 100644
index 000000000..24aee7a2f
--- /dev/null
+++ b/apps/mobile/src/components/ui/pressable/ItemPressable.ios.tsx
@@ -0,0 +1,42 @@
+import { cn } from "@follow/utils"
+import type { FC } from "react"
+import { memo } from "react"
+import type { ViewProps } from "react-native"
+
+import { ItemPressableStyle } from "./enum"
+import { NativeItemPressable } from "./IosItemPressable"
+
+export interface ItemPressableProps extends ViewProps {
+ itemStyle?: ItemPressableStyle
+ touchHighlight?: boolean
+ onPress?: () => any
+}
+
+export const ItemPressable: FC = memo(
+ ({ children, itemStyle = ItemPressableStyle.Grouped, className, ...props }) => {
+ const isUnStyled = itemStyle === ItemPressableStyle.UnStyled
+ return (
+ {
+ props.onPress?.()
+ }}
+ >
+ {children}
+
+ )
+ },
+)
diff --git a/apps/mobile/src/components/ui/pressable/ItemPressable.tsx b/apps/mobile/src/components/ui/pressable/ItemPressable.tsx
index 90b61872f..e1779a6a3 100644
--- a/apps/mobile/src/components/ui/pressable/ItemPressable.tsx
+++ b/apps/mobile/src/components/ui/pressable/ItemPressable.tsx
@@ -19,7 +19,7 @@ import { useColor } from "@/src/theme/colors"
import { ReAnimatedPressable } from "../../common/AnimatedComponents"
import { ItemPressableStyle } from "./enum"
-interface ItemPressableProps extends PressableProps {
+export interface ItemPressableProps extends PressableProps {
itemStyle?: ItemPressableStyle
touchHighlight?: boolean
}
@@ -67,6 +67,7 @@ export const ItemPressable: FC = memo(
}, 100)
})
}, [setIsPressing])
+ const isUnStyled = itemStyle === ItemPressableStyle.UnStyled
return (
@@ -77,7 +78,10 @@ export const ItemPressable: FC = memo(
onLongPress={composeEventHandlers(props.onLongPress, () => {})}
delayLongPress={props.delayLongPress ?? 100}
className={cn("relative overflow-hidden", props.className)}
- style={StyleSheet.flatten([props.style, { backgroundColor: itemNormalColor }])}
+ style={StyleSheet.flatten([
+ props.style,
+ !isUnStyled && { backgroundColor: itemNormalColor },
+ ])}
>
{useTypeScriptHappyCallback(
(props) => {
diff --git a/apps/mobile/src/components/ui/pressable/NativePressable.ios.tsx b/apps/mobile/src/components/ui/pressable/NativePressable.ios.tsx
new file mode 100644
index 000000000..2771a9df7
--- /dev/null
+++ b/apps/mobile/src/components/ui/pressable/NativePressable.ios.tsx
@@ -0,0 +1,10 @@
+import { NativeItemPressable } from "./IosItemPressable"
+import type { NativePressableProps } from "./NativePressable.types"
+
+export const NativePressable = ({ children, onPress, ...props }: NativePressableProps) => {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/apps/mobile/src/components/ui/pressable/NativePressable.tsx b/apps/mobile/src/components/ui/pressable/NativePressable.tsx
new file mode 100644
index 000000000..233e91634
--- /dev/null
+++ b/apps/mobile/src/components/ui/pressable/NativePressable.tsx
@@ -0,0 +1,11 @@
+import { Pressable } from "react-native"
+
+import type { NativePressableProps } from "./NativePressable.types"
+
+/**
+ * In order to resolve the conflict between the gesture handling of the native view and the RCTSurfaceGestureHandler in React Native.
+ *
+ */
+export const NativePressable = ({ children, ...props }: NativePressableProps) => {
+ return {children}
+}
diff --git a/apps/mobile/src/components/ui/pressable/NativePressable.types.tsx b/apps/mobile/src/components/ui/pressable/NativePressable.types.tsx
new file mode 100644
index 000000000..ac0533fa5
--- /dev/null
+++ b/apps/mobile/src/components/ui/pressable/NativePressable.types.tsx
@@ -0,0 +1,5 @@
+import type { ViewProps } from "react-native"
+
+export interface NativePressableProps extends ViewProps {
+ onPress?: () => any
+}
diff --git a/apps/mobile/src/components/ui/pressable/enum.ts b/apps/mobile/src/components/ui/pressable/enum.ts
index f777c26fb..f1d972865 100644
--- a/apps/mobile/src/components/ui/pressable/enum.ts
+++ b/apps/mobile/src/components/ui/pressable/enum.ts
@@ -1,4 +1,5 @@
export enum ItemPressableStyle {
+ UnStyled,
Plain,
Grouped,
}
diff --git a/apps/mobile/src/components/ui/tabview/TabView.tsx b/apps/mobile/src/components/ui/tabview/TabView.tsx
index f38e5ab6d..1752f5653 100644
--- a/apps/mobile/src/components/ui/tabview/TabView.tsx
+++ b/apps/mobile/src/components/ui/tabview/TabView.tsx
@@ -20,7 +20,10 @@ type Tab = {
value: string
}
-export type TabComponent = FC<{ isSelected: boolean; tab: Tab } & Pick>
+// eslint-disable-next-line @typescript-eslint/no-empty-object-type
+export type TabComponent = FC<
+ { isSelected: boolean; tab: Tab } & T & Pick
+>
interface TabViewProps {
tabs: Tab[]
Tab?: TabComponent
diff --git a/apps/mobile/src/database/schemas/index.ts b/apps/mobile/src/database/schemas/index.ts
index 4af8ed1c4..9a628c2b6 100644
--- a/apps/mobile/src/database/schemas/index.ts
+++ b/apps/mobile/src/database/schemas/index.ts
@@ -109,12 +109,11 @@ export const summariesTable = sqliteTable(
{
entryId: text("entry_id").notNull().primaryKey(),
summary: text("summary").notNull(),
+ readabilitySummary: text("readability_summary"),
createdAt: text("created_at").$defaultFn(() => new Date().toISOString()),
language: text("language"),
},
- (table) => ({
- unq: uniqueIndex("unq").on(table.entryId, table.language),
- }),
+ (t) => [uniqueIndex("unq").on(t.entryId, t.language)],
)
export const translationsTable = sqliteTable(
@@ -125,6 +124,7 @@ export const translationsTable = sqliteTable(
title: t.text("title").notNull(),
description: t.text("description").notNull(),
content: t.text("content").notNull(),
+ readabilityContent: t.text("readability_content"),
createdAt: t
.text("created_at")
.notNull()
diff --git a/apps/mobile/src/hooks/useBackHandler.ts b/apps/mobile/src/hooks/useBackHandler.ts
new file mode 100644
index 000000000..4e286af4a
--- /dev/null
+++ b/apps/mobile/src/hooks/useBackHandler.ts
@@ -0,0 +1,28 @@
+import { useEffect } from "react"
+import { BackHandler } from "react-native"
+
+import { useCanDismiss, useNavigation } from "../lib/navigation/hooks"
+import { isAndroid } from "../lib/platform"
+
+export const useBackHandler = () => {
+ const navigation = useNavigation()
+ const canDismiss = useCanDismiss()
+
+ useEffect(() => {
+ if (!isAndroid) return
+
+ // eslint-disable-next-line @eslint-react/web-api/no-leaked-event-listener -- listener.remove() handles cleanup
+ const listener = BackHandler.addEventListener("hardwareBackPress", () => {
+ if (canDismiss) {
+ navigation.dismiss()
+ } else {
+ navigation.back()
+ }
+ return true
+ })
+
+ return () => {
+ listener.remove()
+ }
+ }, [canDismiss, navigation])
+}
diff --git a/apps/mobile/src/hooks/useIntentHandler.ts b/apps/mobile/src/hooks/useIntentHandler.ts
index b4d482c1c..eccd2067f 100644
--- a/apps/mobile/src/hooks/useIntentHandler.ts
+++ b/apps/mobile/src/hooks/useIntentHandler.ts
@@ -2,7 +2,7 @@ import * as Linking from "expo-linking"
import { useEffect } from "react"
import { useNavigation } from "../lib/navigation/hooks"
-import { FollowScreen } from "../screens/(modal)/follow"
+import { FollowScreen } from "../screens/(modal)/FollowScreen"
// This needs to stay outside of react to persist between account switches
let previousIntentUrl = ""
diff --git a/apps/mobile/src/icons/ai_cute_re.tsx b/apps/mobile/src/icons/ai_cute_re.tsx
new file mode 100644
index 000000000..0b9a480b4
--- /dev/null
+++ b/apps/mobile/src/icons/ai_cute_re.tsx
@@ -0,0 +1,20 @@
+import * as React from "react"
+import Svg, { Path } from "react-native-svg"
+
+interface AiCuteReIconProps {
+ width?: number
+ height?: number
+ color?: string
+}
+
+export const AiCuteReIcon = ({ width = 24, height = 24, color = "#10161F" }: AiCuteReIconProps) => {
+ return (
+
+
+
+ )
+}
diff --git a/apps/mobile/src/icons/bell_ringing_cute_re.tsx b/apps/mobile/src/icons/bell_ringing_cute_re.tsx
new file mode 100644
index 000000000..2cf23cf22
--- /dev/null
+++ b/apps/mobile/src/icons/bell_ringing_cute_re.tsx
@@ -0,0 +1,24 @@
+import * as React from "react"
+import Svg, { Path } from "react-native-svg"
+
+interface BellRingingCuteReIconProps {
+ width?: number
+ height?: number
+ color?: string
+}
+
+export const BellRingingCuteReIcon = ({
+ width = 24,
+ height = 24,
+ color = "#10161F",
+}: BellRingingCuteReIconProps) => {
+ return (
+
+
+
+ )
+}
diff --git a/apps/mobile/src/icons/close_circle_fill.tsx b/apps/mobile/src/icons/close_circle_fill.tsx
index 8597960ba..8e8d2c096 100644
--- a/apps/mobile/src/icons/close_circle_fill.tsx
+++ b/apps/mobile/src/icons/close_circle_fill.tsx
@@ -1,12 +1,16 @@
-import type { SvgProps } from "react-native-svg"
import { G, Path, Svg } from "react-native-svg"
-export const CloseCircleFillIcon = (props: SvgProps & { color?: string }) => (
-
+interface IconProps {
+ width?: number
+ height?: number
+ color?: string
+}
+export const CloseCircleFillIcon = ({ width = 24, height = 24, color = "#10161F" }: IconProps) => (
+
diff --git a/apps/mobile/src/icons/ghost_cute_re.tsx b/apps/mobile/src/icons/ghost_cute_re.tsx
new file mode 100644
index 000000000..6bb2d459a
--- /dev/null
+++ b/apps/mobile/src/icons/ghost_cute_re.tsx
@@ -0,0 +1,24 @@
+import * as React from "react"
+import Svg, { Path } from "react-native-svg"
+
+interface GhostCuteReIconProps {
+ width?: number
+ height?: number
+ color?: string
+}
+
+export const GhostCuteReIcon = ({
+ width = 24,
+ height = 24,
+ color = "#10161F",
+}: GhostCuteReIconProps) => {
+ return (
+
+
+
+ )
+}
diff --git a/apps/mobile/src/icons/info_circle_fill.tsx b/apps/mobile/src/icons/info_circle_fill.tsx
index 22381e0b1..135fa0849 100644
--- a/apps/mobile/src/icons/info_circle_fill.tsx
+++ b/apps/mobile/src/icons/info_circle_fill.tsx
@@ -1,12 +1,16 @@
-import type { SvgProps } from "react-native-svg"
import { G, Path, Svg } from "react-native-svg"
-export const InfoCircleFillIcon = (props: SvgProps & { color?: string }) => (
-
+interface IconProps {
+ width?: number
+ height?: number
+ color?: string
+}
+export const InfoCircleFillIcon = ({ width = 24, height = 24, color = "#10161F" }: IconProps) => (
+
diff --git a/apps/mobile/src/icons/quill_pen_cute_re.tsx b/apps/mobile/src/icons/quill_pen_cute_re.tsx
new file mode 100644
index 000000000..aa5b8060c
--- /dev/null
+++ b/apps/mobile/src/icons/quill_pen_cute_re.tsx
@@ -0,0 +1,24 @@
+import * as React from "react"
+import Svg, { Path } from "react-native-svg"
+
+interface QuillPenCuteReIconProps {
+ width?: number
+ height?: number
+ color?: string
+}
+
+export const QuillPenCuteReIcon = ({
+ width = 24,
+ height = 24,
+ color = "#10161F",
+}: QuillPenCuteReIconProps) => {
+ return (
+
+
+
+ )
+}
diff --git a/apps/mobile/src/icons/share_3_cute_re.tsx b/apps/mobile/src/icons/share_3_cute_re.tsx
deleted file mode 100644
index 40e255ee6..000000000
--- a/apps/mobile/src/icons/share_3_cute_re.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import * as React from "react"
-import Svg, { Path } from "react-native-svg"
-
-interface Share3CuteReIconProps {
- width?: number
- height?: number
- color?: string
-}
-
-export const Share3CuteReIcon = ({
- width = 24,
- height = 24,
- color = "#10161F",
-}: Share3CuteReIconProps) => {
- return (
-
-
-
- )
-}
diff --git a/apps/mobile/src/icons/translate_2_ai_cute_re.tsx b/apps/mobile/src/icons/translate_2_ai_cute_re.tsx
new file mode 100644
index 000000000..8df834904
--- /dev/null
+++ b/apps/mobile/src/icons/translate_2_ai_cute_re.tsx
@@ -0,0 +1,24 @@
+import * as React from "react"
+import Svg, { Path } from "react-native-svg"
+
+interface Translate2AiCuteReIconProps {
+ width?: number
+ height?: number
+ color?: string
+}
+
+export const Translate2AiCuteReIcon = ({
+ width = 24,
+ height = 24,
+ color = "#10161F",
+}: Translate2AiCuteReIconProps) => {
+ return (
+
+
+
+ )
+}
diff --git a/apps/mobile/src/icons/webhook_cute_re.tsx b/apps/mobile/src/icons/webhook_cute_re.tsx
new file mode 100644
index 000000000..b89b6d1fb
--- /dev/null
+++ b/apps/mobile/src/icons/webhook_cute_re.tsx
@@ -0,0 +1,24 @@
+import * as React from "react"
+import Svg, { Path } from "react-native-svg"
+
+interface WebhookCuteReIconProps {
+ width?: number
+ height?: number
+ color?: string
+}
+
+export const WebhookCuteReIcon = ({
+ width = 24,
+ height = 24,
+ color = "#10161F",
+}: WebhookCuteReIconProps) => {
+ return (
+
+
+
+ )
+}
diff --git a/apps/mobile/src/lib/api-fetch.ts b/apps/mobile/src/lib/api-fetch.ts
index 5b93d6573..f74eeaf1b 100644
--- a/apps/mobile/src/lib/api-fetch.ts
+++ b/apps/mobile/src/lib/api-fetch.ts
@@ -2,7 +2,7 @@
import type { AppType } from "@follow/shared"
import { FetchError, ofetch } from "ofetch"
-import { InvitationScreen } from "../screens/(modal)/invitation"
+import { InvitationScreen } from "../screens/(modal)/InvitationScreen"
import { userActions } from "../store/user/store"
import { getCookie } from "./auth"
import { getUserAgent } from "./native/user-agent"
diff --git a/apps/mobile/src/lib/auth.ts b/apps/mobile/src/lib/auth.ts
index c13bc4fd0..2507a66ad 100644
--- a/apps/mobile/src/lib/auth.ts
+++ b/apps/mobile/src/lib/auth.ts
@@ -22,14 +22,14 @@ const serverPlugins = [
id: "customGetProviders",
$InferServerPlugin: {} as Extract,
},
- {
- id: "customCreateSession",
- $InferServerPlugin: {} as Extract,
- },
{
id: "getAccountInfo",
$InferServerPlugin: {} as Extract,
},
+ {
+ id: "oneTimeToken",
+ $InferServerPlugin: {} as Extract,
+ },
inferAdditionalFields({
user: {
handle: {
@@ -75,6 +75,7 @@ export const {
getCookie,
getProviders,
linkSocial,
+ oneTimeToken,
sendVerificationEmail,
signIn,
signOut,
diff --git a/apps/mobile/src/lib/native/index.ios.ts b/apps/mobile/src/lib/native/index.ios.ts
index 9cf99f8c9..3ad767ea4 100644
--- a/apps/mobile/src/lib/native/index.ios.ts
+++ b/apps/mobile/src/lib/native/index.ios.ts
@@ -13,8 +13,8 @@ interface NativeModule {
}
const nativeModule = requireNativeModule("Helper") as NativeModule
export const openLink = (url: string, onDismiss?: () => void) => {
- const { openLinksInApp } = getGeneralSettings()
- if (!openLinksInApp) {
+ const { openLinksInExternalApp } = getGeneralSettings()
+ if (openLinksInExternalApp) {
openURL(url)
return
}
diff --git a/apps/mobile/src/lib/navigation/WrappedScreenItem.tsx b/apps/mobile/src/lib/navigation/WrappedScreenItem.tsx
index 8a627643b..58c2768c5 100644
--- a/apps/mobile/src/lib/navigation/WrappedScreenItem.tsx
+++ b/apps/mobile/src/lib/navigation/WrappedScreenItem.tsx
@@ -3,7 +3,7 @@ import type { PrimitiveAtom } from "jotai"
import { atom, useAtomValue, useSetAtom } from "jotai"
import type { FC, ReactNode } from "react"
import { memo, useCallback, useContext, useMemo, useRef } from "react"
-import type { NativeSyntheticEvent } from "react-native"
+import type { NativeSyntheticEvent, StyleProp, ViewStyle } from "react-native"
import { StyleSheet, View } from "react-native"
import { useSharedValue } from "react-native-reanimated"
import type { ScreenStackHeaderConfigProps, StackPresentationTypes } from "react-native-screens"
@@ -33,6 +33,7 @@ export const WrappedScreenItem: FC<
headerConfig?: ScreenStackHeaderConfigProps
screenOptions?: NavigationControllerViewExtraProps
+ style?: StyleProp
} & ScreenOptionsContextType
> = memo(
({
@@ -41,6 +42,7 @@ export const WrappedScreenItem: FC<
stackPresentation,
headerConfig,
screenOptions: screenOptionsProp,
+ style,
...rest
}) => {
const navigation = useNavigation()
@@ -143,6 +145,7 @@ export const WrappedScreenItem: FC<
style={[
StyleSheet.absoluteFill,
{ backgroundColor: screenOptionsProp?.transparent ? undefined : backgroundColor },
+ style,
]}
{...rest}
{...mergedScreenOptions}
diff --git a/apps/mobile/src/lib/navigation/bottom-tab/TabRoot.ios.tsx b/apps/mobile/src/lib/navigation/bottom-tab/TabRoot.ios.tsx
index 099dd8f8b..49109a475 100644
--- a/apps/mobile/src/lib/navigation/bottom-tab/TabRoot.ios.tsx
+++ b/apps/mobile/src/lib/navigation/bottom-tab/TabRoot.ios.tsx
@@ -1,19 +1,13 @@
import { requireNativeView } from "expo"
-import { atom, useAtom } from "jotai"
+import { useAtom } from "jotai"
import type { FC, PropsWithChildren } from "react"
import * as React from "react"
-import { useCallback, useMemo, useState } from "react"
+import { useCallback, useMemo } from "react"
import type { NativeSyntheticEvent, ViewProps } from "react-native"
import { StyleSheet } from "react-native"
-import type { BottomTabContextType } from "./BottomTabContext"
import { BottomTabContext } from "./BottomTabContext"
import { TabScreen } from "./TabScreen.ios"
-import type { TabScreenProps } from "./types"
-
-interface TabRootProps {
- initialTabIndex?: number
-}
const TabBarRoot = requireNativeView<
{
@@ -22,22 +16,9 @@ const TabBarRoot = requireNativeView<
} & ViewProps
>("TabBarRoot")
-export const TabRoot: FC = ({
- children,
- initialTabIndex = 0,
-}) => {
- const [tabIndexAtom] = useState(() => atom(initialTabIndex))
- const [tabIndex, setTabIndex] = useAtom(tabIndexAtom)
-
- const ctxValue = useMemo(
- () => ({
- currentIndexAtom: tabIndexAtom,
- loadedableIndexAtom: atom(new Set()),
- tabScreensAtom: atom([]),
- tabHeightAtom: atom(0),
- }),
- [tabIndexAtom],
- )
+export const TabRoot: FC = ({ children }) => {
+ const { currentIndexAtom } = React.useContext(BottomTabContext)
+ const [tabIndex, setTabIndex] = useAtom(currentIndexAtom)
const MapChildren = useMemo(() => {
let cnt = 0
@@ -51,19 +32,17 @@ export const TabRoot: FC = ({
})
}, [children])
return (
-
- {
- setTabIndex(e.nativeEvent.index)
- },
- [setTabIndex],
- )}
- selectedIndex={tabIndex}
- >
- {MapChildren}
-
-
+ {
+ setTabIndex(e.nativeEvent.index)
+ },
+ [setTabIndex],
+ )}
+ selectedIndex={tabIndex}
+ >
+ {MapChildren}
+
)
}
diff --git a/apps/mobile/src/lib/navigation/bottom-tab/TabRoot.tsx b/apps/mobile/src/lib/navigation/bottom-tab/TabRoot.tsx
index 6be3ddae4..eb99934f5 100644
--- a/apps/mobile/src/lib/navigation/bottom-tab/TabRoot.tsx
+++ b/apps/mobile/src/lib/navigation/bottom-tab/TabRoot.tsx
@@ -1,34 +1,11 @@
-import { atom } from "jotai"
import type { FC, PropsWithChildren } from "react"
import * as React from "react"
-import { useMemo, useState } from "react"
+import { useMemo } from "react"
import { StyleSheet, View } from "react-native"
-import type { BottomTabContextType } from "./BottomTabContext"
-import { BottomTabContext } from "./BottomTabContext"
import { TabScreen } from "./TabScreen"
-import type { TabScreenProps } from "./types"
-
-interface TabRootProps {
- initialTabIndex?: number
-}
-
-export const TabRoot: FC = ({
- children,
- initialTabIndex = 0,
-}) => {
- const [tabIndexAtom] = useState(() => atom(initialTabIndex))
-
- const ctxValue = useMemo(
- () => ({
- currentIndexAtom: tabIndexAtom,
- loadedableIndexAtom: atom(new Set()),
- tabScreensAtom: atom([]),
- tabHeightAtom: atom(0),
- }),
- [tabIndexAtom],
- )
+export const TabRoot: FC = ({ children }) => {
const MapChildren = useMemo(() => {
let cnt = 0
return React.Children.map(children, (child) => {
@@ -40,9 +17,5 @@ export const TabRoot: FC = ({
return child
})
}, [children])
- return (
-
- {MapChildren}
-
- )
+ return {MapChildren}
}
diff --git a/apps/mobile/src/lib/navigation/bottom-tab/TabScreen.ios.tsx b/apps/mobile/src/lib/navigation/bottom-tab/TabScreen.ios.tsx
index c128a9bc5..0ed63427a 100644
--- a/apps/mobile/src/lib/navigation/bottom-tab/TabScreen.ios.tsx
+++ b/apps/mobile/src/lib/navigation/bottom-tab/TabScreen.ios.tsx
@@ -64,7 +64,7 @@ export const TabScreen: FC tabScreen.tabScreenIndex !== tabScreenIndex),
)
}
- }, [setTabScreens, props, tabScreenIndex, children, mergedProps])
+ }, [mergedProps, setTabScreens, tabScreenIndex])
const currentSelectedIndex = useAtomValue(currentIndexAtom)
@@ -95,15 +95,15 @@ export const TabScreen: FC
- {shouldLoadReact && (
+ {shouldLoadReact && render && (
{children}
- {/* */}
)}
diff --git a/apps/mobile/src/main.tsx b/apps/mobile/src/main.tsx
index 880ccf08f..327509098 100644
--- a/apps/mobile/src/main.tsx
+++ b/apps/mobile/src/main.tsx
@@ -2,6 +2,7 @@ import "./global.css"
import { registerRootComponent } from "expo"
import { Image } from "expo-image"
+import { LinearGradient } from "expo-linear-gradient"
import { cssInterop } from "nativewind"
import { useTranslation } from "react-i18next"
import { enableFreeze } from "react-native-screens"
@@ -23,10 +24,11 @@ import { SubscriptionsTabScreen } from "./screens/(stack)/(tabs)/subscriptions"
import { registerSitemap } from "./sitemap"
enableFreeze(true)
-cssInterop(Image, { className: "style" })
+;[Image, LinearGradient].forEach((Component) => {
+ cssInterop(Component, { className: "style" })
+})
initializeApp()
-
registerSitemap()
initializeI18n().then(() => {
registerRootComponent(RootComponent)
diff --git a/apps/mobile/src/modules/ai/summary.tsx b/apps/mobile/src/modules/ai/summary.tsx
index baf6dea52..4a6f556a7 100644
--- a/apps/mobile/src/modules/ai/summary.tsx
+++ b/apps/mobile/src/modules/ai/summary.tsx
@@ -15,7 +15,7 @@ import Animated, {
} from "react-native-reanimated"
import { useColor } from "react-native-uikit-colors"
-import { Magic2CuteReIcon } from "@/src/icons/magic_2_cute_re"
+import { AiCuteReIcon } from "@/src/icons/ai_cute_re"
export const AISummary: FC<{
className?: string
@@ -69,7 +69,7 @@ export const AISummary: FC<{
style={styles.card}
>
-
+
diff --git a/apps/mobile/src/modules/context-menu/entry.tsx b/apps/mobile/src/modules/context-menu/entry.tsx
index 85ab13840..8c8511140 100644
--- a/apps/mobile/src/modules/context-menu/entry.tsx
+++ b/apps/mobile/src/modules/context-menu/entry.tsx
@@ -12,8 +12,7 @@ import {
import { ContextMenu } from "@/src/components/ui/context-menu"
import { useNavigation } from "@/src/lib/navigation/hooks"
import { toast } from "@/src/lib/toast"
-import { getHorizontalScrolling } from "@/src/modules/screen/atoms"
-import { EntryDetailScreen } from "@/src/screens/(stack)/entries/[entryId]"
+import { EntryDetailScreen } from "@/src/screens/(stack)/entries/[entryId]/EntryDetailScreen"
import { useIsEntryStarred } from "@/src/store/collection/hooks"
import { collectionSyncService } from "@/src/store/collection/store"
import { useEntry } from "@/src/store/entry/hooks"
@@ -31,8 +30,7 @@ export const EntryItemContextMenu = ({
const navigation = useNavigation()
const handlePressPreview = useCallback(() => {
- const isHorizontalScrolling = getHorizontalScrolling()
- if (entry && !isHorizontalScrolling) {
+ if (entry) {
preloadWebViewEntry(entry)
navigation.pushControllerView(EntryDetailScreen, {
entryId: id,
diff --git a/apps/mobile/src/modules/context-menu/feeds.tsx b/apps/mobile/src/modules/context-menu/feeds.tsx
index 0237f8420..7f87f9626 100644
--- a/apps/mobile/src/modules/context-menu/feeds.tsx
+++ b/apps/mobile/src/modules/context-menu/feeds.tsx
@@ -9,11 +9,12 @@ import type { ListRenderItemInfo } from "react-native"
import { Alert, FlatList, View } from "react-native"
import { ContextMenu } from "@/src/components/ui/context-menu"
+import { PlatformActivityIndicator } from "@/src/components/ui/loading/PlatformActivityIndicator"
import { views } from "@/src/constants/views"
import { useNavigation } from "@/src/lib/navigation/hooks"
import type { Navigation } from "@/src/lib/navigation/Navigation"
import { toast } from "@/src/lib/toast"
-import { FeedScreen } from "@/src/screens/(stack)/feeds/[feedId]"
+import { FeedScreen } from "@/src/screens/(stack)/feeds/[feedId]/FeedScreen"
import { useEntryIdsByFeedId, usePrefetchEntries } from "@/src/store/entry/hooks"
import { getFeed } from "@/src/store/feed/getter"
import { getSubscription } from "@/src/store/subscription/getter"
@@ -322,16 +323,19 @@ export const SubscriptionFeedCategoryContextMenu = ({
const PreviewFeeds = (props: { id: string; view: FeedViewType }) => {
const { id: feedId } = props
const entryIds = useEntryIdsByFeedId(feedId)
- usePrefetchEntries({ feedId, limit: 5 })
+ const { isLoading } = usePrefetchEntries({ feedId, limit: 5 })
const renderItem = useCallback(
({ item: id }: ListRenderItemInfo) => (
),
- [],
+ [props.view],
)
return (
+ {isLoading && !entryIds?.length && (
+
+ )}
entryIds?.slice(0, 5), [entryIds])}
diff --git a/apps/mobile/src/modules/context-menu/lists.tsx b/apps/mobile/src/modules/context-menu/lists.tsx
index 98bdba536..4e3559270 100644
--- a/apps/mobile/src/modules/context-menu/lists.tsx
+++ b/apps/mobile/src/modules/context-menu/lists.tsx
@@ -1,4 +1,4 @@
-import { env } from "@follow/shared/src/env"
+import { env } from "@follow/shared/src/env.rn"
import type { FC, PropsWithChildren } from "react"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
@@ -33,7 +33,7 @@ export const SubscriptionListItemContextMenu: FC<
const list = getList(id)
if (!list) return
toast.success("Link copied to clipboard")
- Clipboard.setString(`${env.VITE_WEB_URL}/share/lists/${list.id}`)
+ Clipboard.setString(`${env.WEB_URL}/share/lists/${list.id}`)
},
},
{
diff --git a/apps/mobile/src/modules/dialogs/AddFeedDialog.tsx b/apps/mobile/src/modules/dialogs/AddFeedDialog.tsx
index a708e53bd..ac736e1e8 100644
--- a/apps/mobile/src/modules/dialogs/AddFeedDialog.tsx
+++ b/apps/mobile/src/modules/dialogs/AddFeedDialog.tsx
@@ -6,7 +6,7 @@ import type { DialogComponent } from "@/src/lib/dialog"
import { Dialog } from "@/src/lib/dialog"
import { useNavigation } from "@/src/lib/navigation/hooks"
import { Navigation } from "@/src/lib/navigation/Navigation"
-import { FollowScreen } from "@/src/screens/(modal)/follow"
+import { FollowScreen } from "@/src/screens/(modal)/FollowScreen"
import { accentColor } from "@/src/theme/colors"
export const AddFeedDialog: DialogComponent<{
diff --git a/apps/mobile/src/modules/discover/DiscoverContext.ts b/apps/mobile/src/modules/discover/DiscoverContext.ts
index 32c33e7f0..96fc869d1 100644
--- a/apps/mobile/src/modules/discover/DiscoverContext.ts
+++ b/apps/mobile/src/modules/discover/DiscoverContext.ts
@@ -1,12 +1,6 @@
import type { PrimitiveAtom } from "jotai"
import { createContext } from "react"
-import type { Animated } from "react-native"
-import type { SharedValue } from "react-native-reanimated"
export const DiscoverContext = createContext<{
- animatedX: Animated.Value
- currentTabAtom: PrimitiveAtom
-
headerHeightAtom: PrimitiveAtom
- animatedY: SharedValue
}>(null!)
diff --git a/apps/mobile/src/modules/discover/GoodLuck.tsx b/apps/mobile/src/modules/discover/GoodLuck.tsx
new file mode 100644
index 000000000..c38068837
--- /dev/null
+++ b/apps/mobile/src/modules/discover/GoodLuck.tsx
@@ -0,0 +1,53 @@
+import { RSSHubCategories } from "@follow/constants"
+import type { RSSHubRouteDeclaration } from "@follow/models/src/rsshub"
+import { useQuery } from "@tanstack/react-query"
+import { shuffle } from "es-toolkit/compat"
+import { useMemo } from "react"
+import { View } from "react-native"
+
+import { PlatformActivityIndicator } from "@/src/components/ui/loading/PlatformActivityIndicator"
+
+import { fetchRsshubPopular } from "./api"
+import { RecommendationListItem } from "./RecommendationListItem"
+
+export const GoodLuck = () => {
+ const randomTab = useMemo(
+ () => RSSHubCategories[Math.floor(Math.random() * RSSHubCategories.length)]!,
+ [],
+ )
+ const { data, isLoading } = useQuery({
+ queryKey: ["rsshub-popular", "cache", randomTab],
+ queryFn: () => fetchRsshubPopular(randomTab, "all").then((res) => res.data),
+ })
+
+ const arr = useMemo(() => {
+ if (!data) return []
+ const keys = shuffle(Object.keys(data)).slice(0, 2)
+
+ return keys.map((key) => {
+ const route = data[key]
+ return {
+ key,
+ route,
+ }
+ })
+ }, [data])
+
+ return (
+
+ {isLoading ? (
+
+
+
+ ) : (
+ arr.map((item) => (
+
+ ))
+ )}
+
+ )
+}
diff --git a/apps/mobile/src/modules/discover/RecommendationListItem.tsx b/apps/mobile/src/modules/discover/RecommendationListItem.tsx
index b075f9e67..542aa0f96 100644
--- a/apps/mobile/src/modules/discover/RecommendationListItem.tsx
+++ b/apps/mobile/src/modules/discover/RecommendationListItem.tsx
@@ -12,7 +12,7 @@ import { FeedIcon } from "@/src/components/ui/icon/feed-icon"
import { openLink } from "@/src/lib/native"
import { useNavigation } from "@/src/lib/navigation/hooks"
import { toast } from "@/src/lib/toast"
-import { RsshubFormScreen } from "@/src/screens/(modal)/rsshub-form"
+import { RsshubFormScreen } from "@/src/screens/(modal)/RsshubFormScreen"
export const RecommendationListItem: FC<{
data: RSSHubRouteDeclaration
diff --git a/apps/mobile/src/modules/discover/Recommendations.tsx b/apps/mobile/src/modules/discover/Recommendations.tsx
index 1d745ae83..434295c46 100644
--- a/apps/mobile/src/modules/discover/Recommendations.tsx
+++ b/apps/mobile/src/modules/discover/Recommendations.tsx
@@ -1,34 +1,46 @@
import { RSSHubCategories } from "@follow/constants"
import type { RSSHubRouteDeclaration } from "@follow/models/src/rsshub"
import { isASCII } from "@follow/utils"
+import type { ContentStyle } from "@shopify/flash-list"
import { FlashList } from "@shopify/flash-list"
import { useQuery } from "@tanstack/react-query"
-import { useAtomValue } from "jotai"
import type { FC } from "react"
-import { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import type { ScrollView } from "react-native"
-import { Animated, Text, TouchableOpacity, useWindowDimensions, View } from "react-native"
+import {
+ Animated,
+ Text,
+ TouchableOpacity,
+ useAnimatedValue,
+ useWindowDimensions,
+ View,
+} from "react-native"
import type { PanGestureHandlerGestureEvent } from "react-native-gesture-handler"
import { PanGestureHandler } from "react-native-gesture-handler"
+import type { SharedValue } from "react-native-reanimated"
+import { useSharedValue } from "react-native-reanimated"
+import { useSafeAreaInsets } from "react-native-safe-area-context"
import { AnimatedScrollView } from "@/src/components/common/AnimatedComponents"
-import {
- useBottomTabBarHeight,
- useRegisterNavigationScrollView,
-} from "@/src/components/layouts/tabbar/hooks"
+import { BlurEffect } from "@/src/components/common/BlurEffect"
+import { UINavigationHeaderActionButton } from "@/src/components/layouts/header/NavigationHeader"
+import { useRegisterNavigationScrollView } from "@/src/components/layouts/tabbar/hooks"
import { PlatformActivityIndicator } from "@/src/components/ui/loading/PlatformActivityIndicator"
+import { TabBar } from "@/src/components/ui/tabview/TabBar"
import type { TabComponent } from "@/src/components/ui/tabview/TabView"
-import { apiClient } from "@/src/lib/api-fetch"
+import { MingcuteLeftLineIcon } from "@/src/icons/mingcute_left_line"
+import { useNavigation } from "@/src/lib/navigation/hooks"
+import { useColor } from "@/src/theme/colors"
-import { DiscoverContext } from "./DiscoverContext"
+import { fetchRsshubPopular } from "./api"
import { RecommendationListItem } from "./RecommendationListItem"
export const Recommendations = () => {
const { t } = useTranslation("common")
- const { animatedX, currentTabAtom } = useContext(DiscoverContext)
- const currentTab = useAtomValue(currentTabAtom)
+ const animatedX = useAnimatedValue(0)
+ const [currentTab, setCurrentTab] = useState(0)
const windowWidth = useWindowDimensions().width
const ref = useRef(null)
@@ -44,63 +56,71 @@ export const Recommendations = () => {
})
}, [currentTab])
+ const insets = useSafeAreaInsets()
+ const navigation = useNavigation()
+ const label = useColor("label")
return (
-
- {RSSHubCategories.map((category, index) => (
-
- {loadedTabIndex.has(index) && (
-
- )}
-
- ))}
-
+
+
+
+
+ {
+ navigation.back()
+ }}
+ className="-mb-2 ml-4"
+ >
+
+
+ ({
+ name: t(`discover.category.${category}`),
+ value: category,
+ }))}
+ currentTab={currentTab}
+ onTabItemPress={(tab) => {
+ setCurrentTab(tab)
+ }}
+ />
+
+
+ {RSSHubCategories.map((category, index) => (
+
+ {loadedTabIndex.has(index) && (
+
+ )}
+
+ ))}
+
+
)
}
-const _languageOptions = [
- {
- label: "All",
- value: "all",
- },
- {
- label: "English",
- value: "en",
- },
- {
- label: "中文",
- value: "zh-CN",
- },
-] as const
-
-type Language = (typeof _languageOptions)[number]["value"]
-type DiscoverCategories = (typeof RSSHubCategories)[number] | string
-
-const fetchRsshubPopular = (category: DiscoverCategories, lang: Language) => {
- return apiClient.discover.rsshub.$get({
- query: {
- category: "popular",
- categories: category === "all" ? "popular" : `popular,${category}`,
- lang: lang === "all" ? undefined : lang,
- },
- })
-}
-
-const Tab: TabComponent = ({ tab, isSelected, ...rest }) => {
- const tabHeight = useBottomTabBarHeight()
+export const RecommendationTab: TabComponent<{
+ contentContainerStyle?: ContentStyle
+ reanimatedScrollY?: SharedValue
+}> = ({ tab, isSelected, contentContainerStyle, reanimatedScrollY, ...rest }) => {
const { data, isLoading } = useQuery({
queryKey: ["rsshub-popular", "cache", tab.value],
queryFn: () => fetchRsshubPopular(tab.value, "all").then((res) => res.data),
@@ -175,19 +195,16 @@ const Tab: TabComponent = ({ tab, isSelected, ...rest }) => {
FlashList<{ key: string; data: RSSHubRouteDeclaration } | string>
>(isSelected)
- const getItemType = useCallback((item: string | { key: string }) => {
+ const getItemType = useRef((item: string | { key: string }) => {
return typeof item === "string" ? "sectionHeader" : "row"
- }, [])
+ }).current
- const keyExtractor = useCallback((item: string | { key: string }) => {
+ const keyExtractor = useRef((item: string | { key: string }) => {
return typeof item === "string" ? item : item.key
- }, [])
-
- const { headerHeightAtom } = useContext(DiscoverContext)
- const headerHeight = useAtomValue(headerHeightAtom)
+ }).current
const scrollOffsetRef = useRef(0)
- const { animatedY } = useContext(DiscoverContext)
+ const animatedY = useSharedValue(0)
useEffect(() => {
if (isSelected) {
@@ -195,6 +212,8 @@ const Tab: TabComponent = ({ tab, isSelected, ...rest }) => {
}
}, [animatedY, isSelected])
+ const insets = useSafeAreaInsets()
+
if (isLoading) {
return
}
@@ -205,6 +224,9 @@ const Tab: TabComponent = ({ tab, isSelected, ...rest }) => {
onScroll={(e) => {
scrollOffsetRef.current = e.nativeEvent.contentOffset.y
animatedY.value = scrollOffsetRef.current
+ if (reanimatedScrollY) {
+ reanimatedScrollY.value = scrollOffsetRef.current
+ }
}}
scrollEventThrottle={16}
estimatedItemSize={150}
@@ -213,13 +235,15 @@ const Tab: TabComponent = ({ tab, isSelected, ...rest }) => {
keyExtractor={keyExtractor}
getItemType={getItemType}
renderItem={ItemRenderer}
+ automaticallyAdjustContentInsets={false}
+ contentInsetAdjustmentBehavior="never"
automaticallyAdjustsScrollIndicatorInsets={false}
+ contentContainerStyle={contentContainerStyle}
scrollIndicatorInsets={{
right: -2,
- top: headerHeight,
- bottom: tabHeight,
+ top: 0,
+ bottom: insets.bottom,
}}
- contentContainerStyle={{ paddingBottom: tabHeight, paddingTop: headerHeight }}
removeClippedSubviews
/>
{/* Right Sidebar */}
@@ -304,15 +328,8 @@ const NavigationSidebar: FC<{
[scrollToLetter, titles],
)
- const { headerHeightAtom } = useContext(DiscoverContext)
- const headerHeight = useAtomValue(headerHeightAtom)
- const tabHeight = useBottomTabBarHeight()
-
return (
-
+
{titles.map((title) => (
diff --git a/apps/mobile/src/modules/discover/api.ts b/apps/mobile/src/modules/discover/api.ts
new file mode 100644
index 000000000..5deeabe55
--- /dev/null
+++ b/apps/mobile/src/modules/discover/api.ts
@@ -0,0 +1,13 @@
+import { apiClient } from "@/src/lib/api-fetch"
+
+import type { DiscoverCategories, Language } from "./constants"
+
+export const fetchRsshubPopular = (category: DiscoverCategories, lang: Language) => {
+ return apiClient.discover.rsshub.$get({
+ query: {
+ category: "popular",
+ categories: category === "all" ? "popular" : `popular,${category}`,
+ lang: lang === "all" ? undefined : lang,
+ },
+ })
+}
diff --git a/apps/mobile/src/modules/discover/constants.ts b/apps/mobile/src/modules/discover/constants.ts
index 39dcfb891..606abec2f 100644
--- a/apps/mobile/src/modules/discover/constants.ts
+++ b/apps/mobile/src/modules/discover/constants.ts
@@ -1,3 +1,5 @@
+import type { RSSHubCategories } from "@follow/constants"
+
export enum SearchType {
Feed = "feed",
List = "list",
@@ -9,3 +11,21 @@ export const SearchTabs = [
{ name: "List", value: SearchType.List },
// { name: "User", value: SearchType.User },
]
+
+const _languageOptions = [
+ {
+ label: "All",
+ value: "all",
+ },
+ {
+ label: "English",
+ value: "en",
+ },
+ {
+ label: "中文",
+ value: "zh-CN",
+ },
+] as const
+
+export type Language = (typeof _languageOptions)[number]["value"]
+export type DiscoverCategories = (typeof RSSHubCategories)[number] | string
diff --git a/apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx b/apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx
index b02dd061f..fe1ebc2f3 100644
--- a/apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx
+++ b/apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx
@@ -13,7 +13,7 @@ import { ItemPressableStyle } from "@/src/components/ui/pressable/enum"
import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable"
import { apiClient } from "@/src/lib/api-fetch"
import { useNavigation } from "@/src/lib/navigation/hooks"
-import { FollowScreen } from "@/src/screens/(modal)/follow"
+import { FollowScreen } from "@/src/screens/(modal)/FollowScreen"
import { useSubscriptionByFeedId } from "@/src/store/subscription/hooks"
import { useSearchPageContext } from "../ctx"
diff --git a/apps/mobile/src/modules/discover/search-tabs/SearchList.tsx b/apps/mobile/src/modules/discover/search-tabs/SearchList.tsx
index 029165fb6..1abd68e35 100644
--- a/apps/mobile/src/modules/discover/search-tabs/SearchList.tsx
+++ b/apps/mobile/src/modules/discover/search-tabs/SearchList.tsx
@@ -9,7 +9,7 @@ import { ItemPressableStyle } from "@/src/components/ui/pressable/enum"
import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable"
import { apiClient } from "@/src/lib/api-fetch"
import { useNavigation } from "@/src/lib/navigation/hooks"
-import { FollowScreen } from "@/src/screens/(modal)/follow"
+import { FollowScreen } from "@/src/screens/(modal)/FollowScreen"
import { useSubscriptionByListId } from "@/src/store/subscription/hooks"
import { useSearchPageContext } from "../ctx"
diff --git a/apps/mobile/src/modules/discover/search-tabs/__base.tsx b/apps/mobile/src/modules/discover/search-tabs/__base.tsx
index 73ff8d4f5..2dcbbc098 100644
--- a/apps/mobile/src/modules/discover/search-tabs/__base.tsx
+++ b/apps/mobile/src/modules/discover/search-tabs/__base.tsx
@@ -74,7 +74,10 @@ export function BaseSearchPageFlatList({
}
const itemSeparator = (
-
+
)
export const ItemSeparator = () => itemSeparator
diff --git a/apps/mobile/src/modules/discover/search.tsx b/apps/mobile/src/modules/discover/search.tsx
index e1f9de14f..5147df13f 100644
--- a/apps/mobile/src/modules/discover/search.tsx
+++ b/apps/mobile/src/modules/discover/search.tsx
@@ -1,4 +1,3 @@
-import { RSSHubCategories } from "@follow/constants"
import { useAtom, useAtomValue, useSetAtom } from "jotai"
import type { FC } from "react"
import { useContext, useEffect, useRef, useState } from "react"
@@ -15,10 +14,10 @@ import { useSafeAreaFrame, useSafeAreaInsets } from "react-native-safe-area-cont
import { BlurEffect } from "@/src/components/common/BlurEffect"
import { getDefaultHeaderHeight } from "@/src/components/layouts/utils"
-import { TabBar } from "@/src/components/ui/tabview/TabBar"
import { Search2CuteReIcon } from "@/src/icons/search_2_cute_re"
import { useScreenIsInSheetModal } from "@/src/lib/navigation/hooks"
import { Navigation } from "@/src/lib/navigation/Navigation"
+import { ScreenItemContext } from "@/src/lib/navigation/ScreenItemContext"
import SearchScreen from "@/src/screens/(headless)/search"
import { accentColor, useColor } from "@/src/theme/colors"
@@ -54,9 +53,9 @@ export const SearchHeader: FC<{
}
const DynamicBlurEffect = () => {
- const { animatedY } = useContext(DiscoverContext)
+ const { reAnimatedScrollY } = useContext(ScreenItemContext)
const blurStyle = useAnimatedStyle(() => ({
- opacity: Math.max(0, Math.min(1, animatedY.value / 50)),
+ opacity: Math.max(0, Math.min(1, reAnimatedScrollY.value / 50)),
}))
return (
@@ -64,17 +63,14 @@ const DynamicBlurEffect = () => {
)
}
+
export const DiscoverHeader = () => {
- return
-}
-const DiscoverHeaderImpl = () => {
- const { t } = useTranslation("common")
const frame = useSafeAreaFrame()
const insets = useSafeAreaInsets()
const sheetModal = useScreenIsInSheetModal()
const headerHeight = getDefaultHeaderHeight(frame, sheetModal, insets.top)
- const { animatedX, currentTabAtom, headerHeightAtom } = useContext(DiscoverContext)
- const setCurrentTab = useSetAtom(currentTabAtom)
+ const { headerHeightAtom } = useContext(DiscoverContext)
+
const setHeaderHeight = useSetAtom(headerHeightAtom)
return (
@@ -87,26 +83,10 @@ const DiscoverHeaderImpl = () => {
>
-
+
-
- {/* Right actions group */}
-
-
-
+
-
- ({
- name: t(`discover.category.${category}`),
- value: category,
- }))}
- tabScrollContainerAnimatedX={animatedX}
- onTabItemPress={(index) => {
- setCurrentTab(index)
- }}
- tabbarClassName="border-b border-b-quaternary-system-fill"
- />
)
}
@@ -279,10 +259,6 @@ const styles = StyleSheet.create({
position: "relative",
},
- discoverHeader: {
- marginRight: 0,
- },
-
searchbar: {
flex: 1,
display: "flex",
diff --git a/apps/mobile/src/modules/entry-content/EntryAISummary.tsx b/apps/mobile/src/modules/entry-content/EntryAISummary.tsx
index d6cdf6707..8e30eb9b0 100644
--- a/apps/mobile/src/modules/entry-content/EntryAISummary.tsx
+++ b/apps/mobile/src/modules/entry-content/EntryAISummary.tsx
@@ -13,10 +13,13 @@ export const EntryAISummary: FC<{
entryId: string
}> = ({ entryId }) => {
const ctx = useEntryContentContext()
+ const showReadability = useAtomValue(ctx.showReadabilityAtom)
const showAISummaryOnce = useAtomValue(ctx.showAISummaryAtom)
const showAISummary = useGeneralSettingKey("summary") || showAISummaryOnce
const summary = useSummary(entryId)
- usePrefetchSummary(entryId, { enabled: showAISummary })
+ usePrefetchSummary(entryId, showReadability ? "readabilityContent" : "content", {
+ enabled: showAISummary,
+ })
const status = useSummaryStore((state) => state.generatingStatus[entryId])
if (!showAISummary) return null
@@ -24,7 +27,11 @@ export const EntryAISummary: FC<{
return (
diff --git a/apps/mobile/src/modules/entry-content/EntryContentHeaderRightActions.tsx b/apps/mobile/src/modules/entry-content/EntryContentHeaderRightActions.tsx
index e4f52ca91..4b2a317ea 100644
--- a/apps/mobile/src/modules/entry-content/EntryContentHeaderRightActions.tsx
+++ b/apps/mobile/src/modules/entry-content/EntryContentHeaderRightActions.tsx
@@ -11,10 +11,10 @@ import type { MenuItemIconProps } from "zeego/lib/typescript/menu"
import { getGeneralSettings, useGeneralSettingKey } from "@/src/atoms/settings/general"
import { ActionBarItem } from "@/src/components/ui/action-bar/ActionBarItem"
import { DropdownMenu } from "@/src/components/ui/context-menu"
+import { AiCuteReIcon } from "@/src/icons/ai_cute_re"
import { DocmentCuteReIcon } from "@/src/icons/docment_cute_re"
-import { Magic2CuteReIcon } from "@/src/icons/magic_2_cute_re"
import { More1CuteReIcon } from "@/src/icons/more_1_cute_re"
-import { Share3CuteReIcon } from "@/src/icons/share_3_cute_re"
+import { ShareForwardCuteReIcon } from "@/src/icons/share_forward_cute_re"
import { StarCuteFiIcon } from "@/src/icons/star_cute_fi"
import { StarCuteReIcon } from "@/src/icons/star_cute_re"
import { Translate2CuteReIcon } from "@/src/icons/translate_2_cute_re"
@@ -111,7 +111,10 @@ const HeaderRightActionsImpl = ({
if (hasSummary) return
const hideGlowEffect = showIntelligenceGlowEffect()
- await summarySyncService.generateSummary(entryId)
+ await summarySyncService.generateSummary(
+ entryId,
+ showReadability ? "readabilityContent" : "content",
+ )
hideGlowEffect()
}
@@ -127,6 +130,7 @@ const HeaderRightActionsImpl = ({
entryId,
language: getGeneralSettings().actionLanguage as SupportedLanguages,
withContent: true,
+ target: showReadability ? "readabilityContent" : "content",
})
setShowTranslation((prev) => !prev)
}
@@ -173,12 +177,12 @@ const HeaderRightActionsImpl = ({
onPress: toggleReadability,
active: showReadability,
isCheckbox: true,
- inMenu: true,
+ // inMenu: true,
},
!showAISummarySetting && {
key: "GenerateSummary",
title: "Generate Summary",
- icon: ,
+ icon: ,
iconIOS: { name: "sparkles" },
onPress: toggleAISummary,
active: showAISummary,
@@ -198,7 +202,7 @@ const HeaderRightActionsImpl = ({
{
key: "Share",
title: t("operation.share"),
- icon: ,
+ icon: ,
iconIOS: { name: "square.and.arrow.up" },
onPress: handleShare,
},
diff --git a/apps/mobile/src/modules/entry-content/EntryGridFooter.tsx b/apps/mobile/src/modules/entry-content/EntryGridFooter.tsx
index f70d9857c..c312a1823 100644
--- a/apps/mobile/src/modules/entry-content/EntryGridFooter.tsx
+++ b/apps/mobile/src/modules/entry-content/EntryGridFooter.tsx
@@ -1,12 +1,9 @@
import { FeedViewType } from "@follow/constants"
import { cn } from "@follow/utils"
-import { useEffect } from "react"
import { Text, View } from "react-native"
-import ReAnimated, { useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated"
import { RelativeDateTime } from "@/src/components/ui/datetime/RelativeDateTime"
import { FeedIcon } from "@/src/components/ui/icon/feed-icon"
-import { gentleSpringPreset } from "@/src/constants/spring"
import { useEntry } from "@/src/store/entry/hooks"
import { useFeed } from "@/src/store/feed/hooks"
import { useEntryTranslation } from "@/src/store/translation/hooks"
@@ -27,34 +24,12 @@ export const EntryGridFooter = ({
const translation = useEntryTranslation(entryId)
const feed = useFeed(entry?.feedId || "")
- const unreadZoomSharedValue = useSharedValue(entry?.read ? 0 : 1)
-
- useEffect(() => {
- if (!entry) return
-
- if (entry.read) {
- unreadZoomSharedValue.value = withSpring(0, gentleSpringPreset)
- } else {
- unreadZoomSharedValue.value = withSpring(1, gentleSpringPreset)
- }
- }, [entry, entry?.read, unreadZoomSharedValue])
-
- const unreadIndicatorStyle = useAnimatedStyle(() => {
- return {
- width: unreadZoomSharedValue.value * 8,
- height: unreadZoomSharedValue.value * 8,
- }
- })
-
if (!entry) return null
return (
-
+ {!entry.read && }
{entry.title && (
= ({ entryId }) => {
+ const opacityAnimatedValue = useSharedValue(0)
+
+ const headerHeight = useHeaderHeight()
+
+ const title = useEntry(entryId, (entry) => {
+ return entry.title
+ })
+
+ const [isHeaderTitleVisible, setIsHeaderTitleVisible] = useState(true)
+
+ const reanimatedScrollY = useContext(ScreenItemContext).reAnimatedScrollY
+
+ const ctxValue = useContext(EntryContentContext)
+ const titleHeight = useAtomValue(ctxValue.titleHeightAtom)
+ useAnimatedReaction(
+ () => reanimatedScrollY.value,
+ (value) => {
+ if (value > titleHeight + headerHeight) {
+ opacityAnimatedValue.value = withTiming(1, { duration: 100 })
+ runOnJS(setIsHeaderTitleVisible)(true)
+ } else {
+ opacityAnimatedValue.value = withTiming(0, { duration: 100 })
+ runOnJS(setIsHeaderTitleVisible)(false)
+ }
+ },
+ )
+ const headerBarWidth = useWindowDimensions().width
+
+ return (
+ (
+
+ ),
+ [entryId],
+ )}
+ headerRight={
+
+
+
+ }
+ headerTitle={
+
+
+ {title}
+
+
+ }
+ />
+ )
+}
+interface EntryLeftGroupProps {
+ canGoBack: boolean
+ entryId: string
+ titleOpacityShareValue: SharedValue
+}
+
+const EntryLeftGroup = ({ canGoBack, entryId, titleOpacityShareValue }: EntryLeftGroupProps) => {
+ const hideRecentReader = useUISettingKey("hideRecentReader")
+ const animatedOpacity = useAnimatedStyle(() => {
+ return {
+ opacity: interpolate(titleOpacityShareValue.value, [0, 1], [1, 0]),
+ }
+ })
+ return (
+
+
+
+ {!hideRecentReader && (
+
+
+
+ )}
+
+ )
+}
diff --git a/apps/mobile/src/modules/entry-content/EntryReadHistory.tsx b/apps/mobile/src/modules/entry-content/EntryReadHistory.tsx
index d6f99e542..59273d66f 100644
--- a/apps/mobile/src/modules/entry-content/EntryReadHistory.tsx
+++ b/apps/mobile/src/modules/entry-content/EntryReadHistory.tsx
@@ -4,7 +4,7 @@ import { Pressable, View } from "react-native"
import { UserAvatar } from "@/src/components/ui/avatar/UserAvatar"
import { apiClient } from "@/src/lib/api-fetch"
import { useNavigation } from "@/src/lib/navigation/hooks"
-import { ProfileScreen } from "@/src/screens/(modal)/profile"
+import { ProfileScreen } from "@/src/screens/(modal)/ProfileScreen"
import { useEntry } from "@/src/store/entry/hooks"
import { isInboxEntry } from "@/src/store/entry/utils"
import { userActions } from "@/src/store/user/store"
diff --git a/apps/mobile/src/modules/entry-content/EntryTitle.tsx b/apps/mobile/src/modules/entry-content/EntryTitle.tsx
index 4b32faac4..2875c36b0 100644
--- a/apps/mobile/src/modules/entry-content/EntryTitle.tsx
+++ b/apps/mobile/src/modules/entry-content/EntryTitle.tsx
@@ -1,118 +1,38 @@
-import { useTypeScriptHappyCallback } from "@follow/hooks"
-import { useCallback, useContext, useState } from "react"
-import { Text, useWindowDimensions, View } from "react-native"
-import type { SharedValue } from "react-native-reanimated"
-import Animated, {
- interpolate,
- runOnJS,
- useAnimatedReaction,
- useAnimatedStyle,
- useSharedValue,
- withTiming,
-} from "react-native-reanimated"
+import { useSetAtom } from "jotai"
+import { useContext } from "react"
+import { Text, View } from "react-native"
-import { useUISettingKey } from "@/src/atoms/settings/ui"
-import { DefaultHeaderBackButton } from "@/src/components/layouts/header/NavigationHeader"
-import { NavigationBlurEffectHeader } from "@/src/components/layouts/views/SafeNavigationScrollView"
import { UserAvatar } from "@/src/components/ui/avatar/UserAvatar"
import { FeedIcon } from "@/src/components/ui/icon/feed-icon"
-import { ScreenItemContext } from "@/src/lib/navigation/ScreenItemContext"
-import { EntryContentContext, useEntryContentContext } from "@/src/modules/entry-content/ctx"
-import { EntryContentHeaderRightActions } from "@/src/modules/entry-content/EntryContentHeaderRightActions"
+import { EntryContentContext } from "@/src/modules/entry-content/ctx"
import { useEntry } from "@/src/store/entry/hooks"
import { useFeed } from "@/src/store/feed/hooks"
import { useEntryTranslation } from "@/src/store/translation/hooks"
import { EntryTranslation } from "../entry-list/templates/EntryTranslation"
-import { useHeaderHeight } from "../screen/hooks/useHeaderHeight"
-import { EntryReadHistory } from "./EntryReadHistory"
export const EntryTitle = ({ title, entryId }: { title: string; entryId: string }) => {
const translation = useEntryTranslation(entryId)
- const reanimatedScrollY = useContext(ScreenItemContext).reAnimatedScrollY
- const [titleHeight, setTitleHeight] = useState(0)
- const opacityAnimatedValue = useSharedValue(0)
+ const { titleHeightAtom } = useContext(EntryContentContext)
+ const setTitleHeight = useSetAtom(titleHeightAtom)
- const headerHeight = useHeaderHeight()
-
- const [isHeaderTitleVisible, setIsHeaderTitleVisible] = useState(true)
-
- useAnimatedReaction(
- () => reanimatedScrollY.value,
- (value) => {
- if (value > titleHeight + headerHeight) {
- opacityAnimatedValue.value = withTiming(1, { duration: 100 })
- runOnJS(setIsHeaderTitleVisible)(true)
- } else {
- opacityAnimatedValue.value = withTiming(0, { duration: 100 })
- runOnJS(setIsHeaderTitleVisible)(false)
- }
- },
- )
-
- const ctxValue = useEntryContentContext()
- const headerBarWidth = useWindowDimensions().width
return (
- <>
- (
-
- ),
- [entryId],
- )}
- headerRight={
-
-
-
- }
- headerTitle={
-
-
- {title}
-
-
- }
+ {
+ setTitleHeight(e.nativeEvent.layout.height)
+ }}
+ >
+
- {
- setTitleHeight(titleHeight)
- }}
- >
-
-
- >
+
)
}
export const EntrySocialTitle = ({ entryId }: { entryId: string }) => {
- const reanimatedScrollY = useContext(ScreenItemContext).reAnimatedScrollY
-
- const opacityAnimatedValue = useSharedValue(0)
-
const entry = useEntry(entryId, (entry) => {
return {
authorAvatar: entry.authorAvatar,
@@ -123,87 +43,16 @@ export const EntrySocialTitle = ({ entryId }: { entryId: string }) => {
const feed = useFeed(entry?.feedId as string)
- const headerHeight = useHeaderHeight()
-
- const titleHeight = -60
- const [isHeaderTitleVisible, setIsHeaderTitleVisible] = useState(true)
-
- useAnimatedReaction(
- () => reanimatedScrollY.value,
- (value) => {
- if (value > titleHeight + headerHeight) {
- opacityAnimatedValue.value = withTiming(1, { duration: 100 })
- runOnJS(setIsHeaderTitleVisible)(true)
- } else {
- opacityAnimatedValue.value = withTiming(0, { duration: 100 })
- runOnJS(setIsHeaderTitleVisible)(false)
- }
- },
- )
-
- const ctxValue = useEntryContentContext()
return (
- <>
- (
-
-
-
- ),
- [ctxValue, entryId, opacityAnimatedValue, isHeaderTitleVisible],
- )}
- headerTitle={() => (
-
- Post
-
- )}
- />
-
- {entry?.authorAvatar ? (
-
- ) : (
- feed &&
- )}
-
- {entry?.author || feed?.title || ""}
-
-
- >
- )
-}
-
-interface EntryLeftGroupProps {
- canGoBack: boolean
- entryId: string
- titleOpacityShareValue: SharedValue
-}
-
-const EntryLeftGroup = ({ canGoBack, entryId, titleOpacityShareValue }: EntryLeftGroupProps) => {
- const hideRecentReader = useUISettingKey("hideRecentReader")
- const animatedOpacity = useAnimatedStyle(() => {
- return {
- opacity: interpolate(titleOpacityShareValue.value, [0, 1], [1, 0]),
- }
- })
- return (
-
-
-
- {!hideRecentReader && (
-
-
-
+
+ {entry?.authorAvatar ? (
+
+ ) : (
+ feed &&
)}
+
+ {entry?.author || feed?.title || ""}
+
)
}
diff --git a/apps/mobile/src/modules/entry-content/ctx.ts b/apps/mobile/src/modules/entry-content/ctx.ts
index 2f92a5663..82e36bda7 100644
--- a/apps/mobile/src/modules/entry-content/ctx.ts
+++ b/apps/mobile/src/modules/entry-content/ctx.ts
@@ -5,6 +5,7 @@ interface EntryContentContextType {
showAISummaryAtom: PrimitiveAtom
showAITranslationAtom: PrimitiveAtom
showReadabilityAtom: PrimitiveAtom
+ titleHeightAtom: PrimitiveAtom
}
export const EntryContentContext = createContext(null!)
export const useEntryContentContext = () => {
diff --git a/apps/mobile/src/modules/entry-list/EntryListContentArticle.tsx b/apps/mobile/src/modules/entry-list/EntryListContentArticle.tsx
index 4ffc3a3ad..a7582912f 100644
--- a/apps/mobile/src/modules/entry-list/EntryListContentArticle.tsx
+++ b/apps/mobile/src/modules/entry-list/EntryListContentArticle.tsx
@@ -1,7 +1,7 @@
import type { FeedViewType } from "@follow/constants"
import type { ListRenderItemInfo } from "@shopify/flash-list"
import type { ElementRef } from "react"
-import { forwardRef, useCallback, useMemo } from "react"
+import { forwardRef, useCallback, useImperativeHandle, useMemo } from "react"
import { View } from "react-native"
import { usePlayingUrl } from "@/src/lib/player"
@@ -10,14 +10,14 @@ import { usePrefetchEntryTranslation } from "@/src/store/translation/hooks"
import { useFetchEntriesControls } from "../screen/atoms"
import { TimelineSelectorList } from "../screen/TimelineSelectorList"
import { EntryListFooter } from "./EntryListFooter"
-import { useOnViewableItemsChanged } from "./hooks"
+import { useOnViewableItemsChanged, usePagerListPerformanceHack } from "./hooks"
import { ItemSeparator } from "./ItemSeparator"
import { EntryNormalItem } from "./templates/EntryNormalItem"
export const EntryListContentArticle = forwardRef<
ElementRef,
{ entryIds: string[] | null; active?: boolean; view: FeedViewType }
->(({ entryIds, active, view }, ref) => {
+>(({ entryIds, active, view }, forwardRef) => {
const playingAudioUrl = usePlayingUrl()
const { fetchNextPage, isFetching, refetch, isRefetching, hasNextPage } =
@@ -35,11 +35,16 @@ export const EntryListContentArticle = forwardRef<
[hasNextPage],
)
+ const { onScroll: hackOnScroll, ref, style: hackStyle } = usePagerListPerformanceHack()
+
const { onViewableItemsChanged, onScroll, viewableItems } = useOnViewableItemsChanged({
disabled: active === false || isFetching,
+ onScroll: hackOnScroll,
})
- usePrefetchEntryTranslation(active ? viewableItems.map((item) => item.key) : [])
+ useImperativeHandle(forwardRef, () => ref.current!)
+
+ usePrefetchEntryTranslation({ entryIds: active ? viewableItems.map((item) => item.key) : [] })
return (
id}
+ keyExtractor={defaultKeyExtractor}
estimatedItemSize={100}
renderItem={renderItem}
onEndReached={fetchNextPage}
@@ -56,10 +61,13 @@ export const EntryListContentArticle = forwardRef<
onViewableItemsChanged={onViewableItemsChanged}
ItemSeparatorComponent={ItemSeparator}
ListFooterComponent={ListFooterComponent}
+ style={hackStyle}
/>
)
})
+const defaultKeyExtractor = (id: string) => id
+
export function EntryItemSkeleton() {
return (
diff --git a/apps/mobile/src/modules/entry-list/EntryListContentPicture.tsx b/apps/mobile/src/modules/entry-list/EntryListContentPicture.tsx
index fc69da2ff..3ef90c133 100644
--- a/apps/mobile/src/modules/entry-list/EntryListContentPicture.tsx
+++ b/apps/mobile/src/modules/entry-list/EntryListContentPicture.tsx
@@ -1,7 +1,7 @@
import { useTypeScriptHappyCallback } from "@follow/hooks"
import type { MasonryFlashListProps } from "@shopify/flash-list"
import type { ElementRef } from "react"
-import { forwardRef } from "react"
+import { forwardRef, useImperativeHandle } from "react"
import { View } from "react-native"
import { PlatformActivityIndicator } from "@/src/components/ui/loading/PlatformActivityIndicator"
@@ -10,7 +10,7 @@ import { usePrefetchEntryTranslation } from "@/src/store/translation/hooks"
import { TimelineSelectorMasonryList } from "../screen/TimelineSelectorList"
import { GridEntryListFooter } from "./EntryListFooter"
-import { useOnViewableItemsChanged } from "./hooks"
+import { useOnViewableItemsChanged, usePagerListPerformanceHack } from "./hooks"
// import type { MasonryItem } from "./templates/EntryGridItem"
import { EntryPictureItem } from "./templates/EntryPictureItem"
@@ -20,14 +20,17 @@ export const EntryListContentPicture = forwardRef<
MasonryFlashListProps,
"data" | "renderItem"
>
->(({ entryIds, active, ...rest }, ref) => {
+>(({ entryIds, active, ...rest }, forwardRef) => {
+ const { onScroll: hackOnScroll, ref, style: hackStyle } = usePagerListPerformanceHack()
+ useImperativeHandle(forwardRef, () => ref.current!)
const { fetchNextPage, refetch, isRefetching, hasNextPage, isFetching } =
useFetchEntriesControls()
const { onViewableItemsChanged, onScroll, viewableItems } = useOnViewableItemsChanged({
disabled: active === false || isFetching,
+ onScroll: hackOnScroll,
})
- usePrefetchEntryTranslation(active ? viewableItems.map((item) => item.key) : [])
+ usePrefetchEntryTranslation({ entryIds: active ? viewableItems.map((item) => item.key) : [] })
return (
,
{ entryIds: string[] | null; active?: boolean }
->(({ entryIds, active }, ref) => {
+>(({ entryIds, active }, forwardRef) => {
const { fetchNextPage, isFetching, refetch, isRefetching, hasNextPage } =
useFetchEntriesControls()
+ const { onScroll: hackOnScroll, ref, style: hackStyle } = usePagerListPerformanceHack()
+ useImperativeHandle(forwardRef, () => ref.current!)
+ // eslint-disable-next-line @eslint-react/hooks-extra/no-unnecessary-use-callback
const renderItem = useCallback(
({ item: id }: ListRenderItemInfo) => ,
[],
@@ -31,9 +34,10 @@ export const EntryListContentSocial = forwardRef<
const { onViewableItemsChanged, onScroll, viewableItems } = useOnViewableItemsChanged({
disabled: active === false || isFetching,
+ onScroll: hackOnScroll,
})
- usePrefetchEntryTranslation(active ? viewableItems.map((item) => item.key) : [])
+ usePrefetchEntryTranslation({ entryIds: active ? viewableItems.map((item) => item.key) : [] })
return (
)
})
diff --git a/apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx b/apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx
index 8101c6432..7580c739d 100644
--- a/apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx
+++ b/apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx
@@ -1,7 +1,7 @@
import { useTypeScriptHappyCallback } from "@follow/hooks"
import type { MasonryFlashListProps } from "@shopify/flash-list"
import type { ElementRef } from "react"
-import { forwardRef, useMemo } from "react"
+import { forwardRef, useImperativeHandle, useMemo } from "react"
import { View } from "react-native"
import { useFetchEntriesControls } from "@/src/modules/screen/atoms"
@@ -9,7 +9,7 @@ import { usePrefetchEntryTranslation } from "@/src/store/translation/hooks"
import { TimelineSelectorMasonryList } from "../screen/TimelineSelectorList"
import { GridEntryListFooter } from "./EntryListFooter"
-import { useOnViewableItemsChanged } from "./hooks"
+import { useOnViewableItemsChanged, usePagerListPerformanceHack } from "./hooks"
import { EntryVideoItem } from "./templates/EntryVideoItem"
export const EntryListContentVideo = forwardRef<
@@ -18,14 +18,17 @@ export const EntryListContentVideo = forwardRef<
MasonryFlashListProps,
"data" | "renderItem"
>
->(({ entryIds, active, ...rest }, ref) => {
+>(({ entryIds, active, ...rest }, forwardRef) => {
+ const { onScroll: hackOnScroll, ref, style: hackStyle } = usePagerListPerformanceHack()
+ useImperativeHandle(forwardRef, () => ref.current!)
const { fetchNextPage, refetch, isRefetching, isFetching, hasNextPage } =
useFetchEntriesControls()
const { onViewableItemsChanged, onScroll, viewableItems } = useOnViewableItemsChanged({
disabled: active === false || isFetching,
+ onScroll: hackOnScroll,
})
- usePrefetchEntryTranslation(active ? viewableItems.map((item) => item.key) : [])
+ usePrefetchEntryTranslation({ entryIds: active ? viewableItems.map((item) => item.key) : [] })
const ListFooterComponent = useMemo(
() =>
@@ -57,6 +60,7 @@ export const EntryListContentVideo = forwardRef<
ListFooterComponent={ListFooterComponent}
{...rest}
onRefresh={refetch}
+ style={hackStyle}
/>
)
})
diff --git a/apps/mobile/src/modules/entry-list/EntryListFooter.tsx b/apps/mobile/src/modules/entry-list/EntryListFooter.tsx
index aadc8e2e3..dd2bcf660 100644
--- a/apps/mobile/src/modules/entry-list/EntryListFooter.tsx
+++ b/apps/mobile/src/modules/entry-list/EntryListFooter.tsx
@@ -4,6 +4,7 @@ import { Text, TouchableOpacity } from "react-native"
import { CheckCircleCuteReIcon } from "@/src/icons/check_circle_cute_re"
import { getFetchEntryPayload } from "@/src/store/entry/getter"
import { unreadSyncService } from "@/src/store/unread/store"
+import { useColor } from "@/src/theme/colors"
import { useSelectedFeed, useSelectedView } from "../screen/atoms"
import { ItemSeparator } from "./ItemSeparator"
@@ -13,11 +14,12 @@ export const EntryListFooter = () => {
const selectedView = useSelectedView()
const selectedFeed = useSelectedFeed()
+ const labelColor = useColor("label")
return (
<>
{
if (typeof selectedView === "number") {
const payload = getFetchEntryPayload(selectedFeed, selectedView)
@@ -25,8 +27,8 @@ export const EntryListFooter = () => {
}
}}
>
-
-
+
+
{t("operation.mark_all_as_read_which", {
which: t("operation.mark_all_as_read_which_above"),
})}
diff --git a/apps/mobile/src/modules/entry-list/ItemSeparator.tsx b/apps/mobile/src/modules/entry-list/ItemSeparator.tsx
index e4a836087..bf6e6f06c 100644
--- a/apps/mobile/src/modules/entry-list/ItemSeparator.tsx
+++ b/apps/mobile/src/modules/entry-list/ItemSeparator.tsx
@@ -3,7 +3,7 @@ import { View } from "react-native"
const el = (
@@ -16,7 +16,7 @@ export const ItemSeparator = () => {
const el2 = (
diff --git a/apps/mobile/src/modules/entry-list/hooks.ts b/apps/mobile/src/modules/entry-list/hooks.ts
index 96ce3ed64..a999111e2 100644
--- a/apps/mobile/src/modules/entry-list/hooks.ts
+++ b/apps/mobile/src/modules/entry-list/hooks.ts
@@ -1,18 +1,33 @@
+import type { FlashList } from "@shopify/flash-list"
import type ViewToken from "@shopify/flash-list/dist/viewability/ViewToken"
-import { useCallback, useEffect, useInsertionEffect, useMemo, useRef, useState } from "react"
-import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native"
+import type { RefObject } from "react"
+import {
+ useCallback,
+ useContext,
+ useEffect,
+ useInsertionEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react"
+import type { NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle } from "react-native"
+import { useEventCallback } from "usehooks-ts"
import { useGeneralSettingKey } from "@/src/atoms/settings/general"
import { debouncedFetchEntryContentByStream } from "@/src/store/entry/store"
import { unreadSyncService } from "@/src/store/unread/store"
+import { PagerListVisibleContext, PagerListWillVisibleContext } from "../screen/PagerListContext"
+
const defaultIdExtractor = (item: ViewToken) => item.key
export function useOnViewableItemsChanged({
disabled,
idExtractor = defaultIdExtractor,
+ onScroll: onScrollProp,
}: {
disabled?: boolean
idExtractor?: (item: ViewToken) => string
+ onScroll?: (e: NativeSyntheticEvent) => void
} = {}) {
const orientation = useRef<"down" | "up">("down")
const lastOffset = useRef(0)
@@ -82,12 +97,16 @@ export function useOnViewableItemsChanged({
stableIdExtractor,
])
- const onScroll = useCallback((e: NativeSyntheticEvent) => {
- const currentOffset = e.nativeEvent.contentOffset.y
- const currentOrientation = currentOffset > lastOffset.current ? "down" : "up"
- orientation.current = currentOrientation
- lastOffset.current = currentOffset
- }, [])
+ const onScroll = useCallback(
+ (e: NativeSyntheticEvent) => {
+ const currentOffset = e.nativeEvent.contentOffset.y
+ const currentOrientation = currentOffset > lastOffset.current ? "down" : "up"
+ orientation.current = currentOrientation
+ lastOffset.current = currentOffset
+ onScrollProp?.(e)
+ },
+ [onScrollProp],
+ )
return useMemo(
() => ({ onViewableItemsChanged, onScroll, viewableItems }),
@@ -108,3 +127,33 @@ function useNonReactiveCallback any>(fn: T): T {
[ref],
) as unknown as T
}
+
+export const usePagerListPerformanceHack = (provideRef?: RefObject>) => {
+ const lastY = useRef(0)
+
+ const onScroll = useEventCallback((e: NativeSyntheticEvent) => {
+ if (!visible) return
+
+ lastY.current = e.nativeEvent.contentOffset.y
+ })
+
+ const visible = useContext(PagerListVisibleContext)
+ const willVisible = useContext(PagerListWillVisibleContext)
+
+ const nextVisible = visible || willVisible
+
+ const ref = useRef>(null)
+
+ const usingRef = provideRef ?? ref
+ const [style, setStyle] = useState>({})
+ useEffect(() => {
+ setStyle({ display: nextVisible ? "flex" : "none" })
+ if (nextVisible && lastY.current > 0) {
+ requestAnimationFrame(() => {
+ usingRef.current?.scrollToOffset({ offset: lastY.current, animated: false })
+ })
+ }
+ }, [nextVisible, usingRef])
+
+ return { onScroll, ref, style }
+}
diff --git a/apps/mobile/src/modules/entry-list/index.tsx b/apps/mobile/src/modules/entry-list/index.tsx
index c23aa75da..a3cb47ece 100644
--- a/apps/mobile/src/modules/entry-list/index.tsx
+++ b/apps/mobile/src/modules/entry-list/index.tsx
@@ -2,7 +2,7 @@ import { FeedViewType } from "@follow/constants"
import { memo, useMemo } from "react"
import { useSelectedFeed, useSelectedView } from "@/src/modules/screen/atoms"
-import { PagerList } from "@/src/modules/screen/PageList"
+import { PagerList } from "@/src/modules/screen/PagerList"
import { TimelineHeader } from "@/src/modules/screen/TimelineSelectorProvider"
import {
useEntryIdsByCategory,
diff --git a/apps/mobile/src/modules/entry-list/templates/EntryNormalItem.tsx b/apps/mobile/src/modules/entry-list/templates/EntryNormalItem.tsx
index c43e94e4c..42b434bce 100644
--- a/apps/mobile/src/modules/entry-list/templates/EntryNormalItem.tsx
+++ b/apps/mobile/src/modules/entry-list/templates/EntryNormalItem.tsx
@@ -1,9 +1,8 @@
import { FeedViewType } from "@follow/constants"
import { tracker } from "@follow/tracker"
import { cn, formatEstimatedMins, formatTimeToSeconds } from "@follow/utils"
-import { memo, useCallback, useEffect, useMemo } from "react"
-import { StyleSheet, Text, TouchableOpacity, View } from "react-native"
-import ReAnimated, { useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated"
+import { memo, useCallback, useMemo } from "react"
+import { StyleSheet, Text, View } from "react-native"
import { useUISettingKey } from "@/src/atoms/settings/ui"
import { ThemedBlurView } from "@/src/components/common/ThemedBlurView"
@@ -14,13 +13,12 @@ import { Image } from "@/src/components/ui/image/Image"
import { PlatformActivityIndicator } from "@/src/components/ui/loading/PlatformActivityIndicator"
import { ItemPressableStyle } from "@/src/components/ui/pressable/enum"
import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable"
-import { gentleSpringPreset } from "@/src/constants/spring"
+import { NativePressable } from "@/src/components/ui/pressable/NativePressable"
import { PauseCuteFiIcon } from "@/src/icons/pause_cute_fi"
import { PlayCuteFiIcon } from "@/src/icons/play_cute_fi"
import { useNavigation } from "@/src/lib/navigation/hooks"
import { getAttachmentState, player } from "@/src/lib/player"
-import { getHorizontalScrolling } from "@/src/modules/screen/atoms"
-import { EntryDetailScreen } from "@/src/screens/(stack)/entries/[entryId]"
+import { EntryDetailScreen } from "@/src/screens/(stack)/entries/[entryId]/EntryDetailScreen"
import { useEntry } from "@/src/store/entry/hooks"
import { getInboxFrom } from "@/src/store/entry/utils"
import { useFeed } from "@/src/store/feed/hooks"
@@ -38,8 +36,7 @@ export const EntryNormalItem = memo(
const feed = useFeed(entry?.feedId as string)
const navigation = useNavigation()
const handlePress = useCallback(() => {
- const isHorizontalScrolling = getHorizontalScrolling()
- if (entry && !isHorizontalScrolling) {
+ if (entry) {
preloadWebViewEntry(entry)
tracker.navigateEntry({
feedId: entry.feedId!,
@@ -53,32 +50,10 @@ export const EntryNormalItem = memo(
}
}, [entryId, entry, navigation, view])
- const unreadZoomSharedValue = useSharedValue(entry?.read ? 0 : 1)
-
- const unreadIndicatorStyle = useAnimatedStyle(() => {
- return {
- transform: [
- {
- scale: unreadZoomSharedValue.value,
- },
- ],
- }
- })
-
- useEffect(() => {
- if (!entry) return
-
- if (entry.read) {
- unreadZoomSharedValue.value = withSpring(0, gentleSpringPreset)
- } else {
- unreadZoomSharedValue.value = withSpring(1, gentleSpringPreset)
- }
- }, [entry, entry?.read, unreadZoomSharedValue])
-
const thumbnailRatio = useUISettingKey("thumbnailRatio")
const coverImage = entry?.media?.[0]
- const image = coverImage?.url
+ const image = coverImage?.url || (view === FeedViewType.Audios ? feed?.image : null)
const blurhash = coverImage?.blurhash
const audio = entry?.attachments?.find((attachment) =>
@@ -105,14 +80,14 @@ export const EntryNormalItem = memo(
)}
onPress={handlePress}
>
-
-
+ {!entry.read && (
+
+ )}
@@ -158,7 +133,7 @@ export const EntryNormalItem = memo(
)}
{view !== FeedViewType.Notifications && (
-
+
{image &&
(thumbnailRatio === "square" ? (
@@ -172,7 +147,7 @@ export const EntryNormalItem = memo(
))}
{audio && (
- {
if (isLoading) return
@@ -198,7 +173,7 @@ export const EntryNormalItem = memo(
)}
-
+
)}
)}
diff --git a/apps/mobile/src/modules/entry-list/templates/EntrySocialItem.tsx b/apps/mobile/src/modules/entry-list/templates/EntrySocialItem.tsx
index 1fe48e0d7..06729dae0 100644
--- a/apps/mobile/src/modules/entry-list/templates/EntrySocialItem.tsx
+++ b/apps/mobile/src/modules/entry-list/templates/EntrySocialItem.tsx
@@ -1,8 +1,7 @@
import { FeedViewType } from "@follow/constants"
import { tracker } from "@follow/tracker"
-import { memo, useCallback, useEffect, useMemo } from "react"
+import { memo, useCallback, useMemo } from "react"
import { Pressable, Text, View } from "react-native"
-import ReAnimated, { useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated"
import { useGeneralSettingKey } from "@/src/atoms/settings/general"
import { UserAvatar } from "@/src/components/ui/avatar/UserAvatar"
@@ -12,11 +11,9 @@ import { Galeria } from "@/src/components/ui/image/galeria"
import { Image } from "@/src/components/ui/image/Image"
import { ItemPressableStyle } from "@/src/components/ui/pressable/enum"
import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable"
-import { gentleSpringPreset } from "@/src/constants/spring"
import { useNavigation } from "@/src/lib/navigation/hooks"
-import { getHorizontalScrolling } from "@/src/modules/screen/atoms"
-import { EntryDetailScreen } from "@/src/screens/(stack)/entries/[entryId]"
-import { FeedScreen } from "@/src/screens/(stack)/feeds/[feedId]"
+import { EntryDetailScreen } from "@/src/screens/(stack)/entries/[entryId]/EntryDetailScreen"
+import { FeedScreen } from "@/src/screens/(stack)/feeds/[feedId]/FeedScreen"
import { useEntry } from "@/src/store/entry/hooks"
import { useFeed } from "@/src/store/feed/hooks"
import { useEntryTranslation } from "@/src/store/translation/hooks"
@@ -34,46 +31,25 @@ export const EntrySocialItem = memo(({ entryId }: { entryId: string }) => {
const navigation = useNavigation()
const handlePress = useCallback(() => {
- const isHorizontalScrolling = getHorizontalScrolling()
- if (!isHorizontalScrolling) {
- unreadSyncService.markEntryAsRead(entryId)
- tracker.navigateEntry({
- feedId: entry?.feedId!,
- entryId,
- })
- navigation.pushControllerView(EntryDetailScreen, {
- entryId,
- view: FeedViewType.SocialMedia,
- })
- }
+ unreadSyncService.markEntryAsRead(entryId)
+ tracker.navigateEntry({
+ feedId: entry?.feedId!,
+ entryId,
+ })
+ navigation.pushControllerView(EntryDetailScreen, {
+ entryId,
+ view: FeedViewType.SocialMedia,
+ })
}, [entry?.feedId, entryId, navigation])
- const unreadZoomSharedValue = useSharedValue(entry?.read ? 0 : 1)
-
- const unreadIndicatorStyle = useAnimatedStyle(() => {
- return {
- transform: [
- {
- scale: unreadZoomSharedValue.value,
- },
- ],
- }
- })
-
- useEffect(() => {
- if (!entry) return
-
- if (entry.read) {
- unreadZoomSharedValue.value = withSpring(0, gentleSpringPreset)
- } else {
- unreadZoomSharedValue.value = withSpring(1, gentleSpringPreset)
- }
- }, [entry, entry?.read, unreadZoomSharedValue])
-
const autoExpandLongSocialMedia = useGeneralSettingKey("autoExpandLongSocialMedia")
const memoedMediaUrlList = useMemo(() => {
- return entry?.media?.map((i) => i.url) ?? []
+ return (entry?.media
+ ?.map((i) =>
+ i.type === "video" ? i.preview_image_url : i.type === "photo" ? i.url : undefined,
+ )
+ .filter(Boolean) || []) as string[]
}, [entry])
if (!entry) return
@@ -87,10 +63,7 @@ export const EntrySocialItem = memo(({ entryId }: { entryId: string }) => {
className="flex flex-col gap-2 p-4 pl-6"
onPress={handlePress}
>
-
+ {!entry.read && }
{
{media && media.length > 0 && (
- {media.map((image, index) => {
+ {media.map((mediaItem, index) => {
+ // TODO: support video
+ const imageUrl =
+ mediaItem.type === "video"
+ ? mediaItem.preview_image_url
+ : mediaItem.type === "photo"
+ ? mediaItem.url
+ : undefined
const fullWidth = index === media.length - 1 && media.length % 2 === 1
+ if (!imageUrl) return null
return (
@@ -143,11 +124,13 @@ export const EntrySocialItem = memo(({ entryId }: { entryId: string }) => {
proxy={{
width: fullWidth ? 400 : 200,
}}
- source={{ uri: image.url }}
- blurhash={image.blurhash}
+ source={{ uri: imageUrl }}
+ blurhash={mediaItem.blurhash}
className="border-secondary-system-background w-full rounded-lg border"
aspectRatio={
- fullWidth && image.width && image.height ? image.width / image.height : 1
+ fullWidth && mediaItem.width && mediaItem.height
+ ? mediaItem.width / mediaItem.height
+ : 1
}
/>
diff --git a/apps/mobile/src/modules/entry-list/templates/EntryTranslation.tsx b/apps/mobile/src/modules/entry-list/templates/EntryTranslation.tsx
index ccff4bb6d..7cc94ca0b 100644
--- a/apps/mobile/src/modules/entry-list/templates/EntryTranslation.tsx
+++ b/apps/mobile/src/modules/entry-list/templates/EntryTranslation.tsx
@@ -1,4 +1,3 @@
-import { cn } from "@follow/utils"
import { useMemo } from "react"
import type { TextProps } from "react-native"
import { Text, View } from "react-native"
@@ -47,19 +46,14 @@ export const EntryTranslation = ({
return (
- {nextTarget && (
- <>
-
- {nextTarget}
-
-
- ⇋
-
- >
- )}
{nextSource}
+ {nextTarget && (
+
+ {nextTarget}
+
+ )}
)
}
diff --git a/apps/mobile/src/modules/entry-list/templates/EntryVideoItem.tsx b/apps/mobile/src/modules/entry-list/templates/EntryVideoItem.tsx
index 6a0f3b8e2..ab0998bf1 100644
--- a/apps/mobile/src/modules/entry-list/templates/EntryVideoItem.tsx
+++ b/apps/mobile/src/modules/entry-list/templates/EntryVideoItem.tsx
@@ -82,8 +82,8 @@ const parseSchemeLink = (url: string) => {
}
const openVideo = async (url: string) => {
- const { openLinksInApp } = getGeneralSettings()
- if (!openLinksInApp) {
+ const { openLinksInExternalApp } = getGeneralSettings()
+ if (openLinksInExternalApp) {
const schemeLink = parseSchemeLink(url)
try {
const isInstalled = !!schemeLink && (await Linking.canOpenURL(schemeLink))
diff --git a/apps/mobile/src/modules/login/email.tsx b/apps/mobile/src/modules/login/email.tsx
index cf369ba9e..cb370ab88 100644
--- a/apps/mobile/src/modules/login/email.tsx
+++ b/apps/mobile/src/modules/login/email.tsx
@@ -11,9 +11,9 @@ import { signIn } from "@/src/lib/auth"
import { useNavigation } from "@/src/lib/navigation/hooks"
import { Navigation } from "@/src/lib/navigation/Navigation"
import { getTokenHeaders } from "@/src/lib/token"
-import { TwoFactorAuthScreen } from "@/src/screens/(modal)/2fa"
-import { ForgetPasswordScreen } from "@/src/screens/(modal)/forget-password"
-import { SignUpScreen } from "@/src/screens/(modal)/sign-up"
+import { ForgetPasswordScreen } from "@/src/screens/(modal)/ForgetPasswordScreen"
+import { SignUpScreen } from "@/src/screens/(modal)/SignUpScreen"
+import { TwoFactorAuthScreen } from "@/src/screens/(modal)/TwoFactorAuthScreen"
import { accentColor } from "@/src/theme/colors"
const formSchema = z.object({
diff --git a/apps/mobile/src/modules/onboarding/step-preferences.tsx b/apps/mobile/src/modules/onboarding/step-preferences.tsx
index 896d6ecad..eba5537ef 100644
--- a/apps/mobile/src/modules/onboarding/step-preferences.tsx
+++ b/apps/mobile/src/modules/onboarding/step-preferences.tsx
@@ -11,7 +11,7 @@ import { MingcuteRightLine } from "@/src/icons/mingcute_right_line"
import { Settings1CuteReIcon } from "@/src/icons/settings_1_cute_re"
import { Translate2CuteReIcon } from "@/src/icons/translate_2_cute_re"
import { useNavigation } from "@/src/lib/navigation/hooks"
-import { SelectReadingModeScreen } from "@/src/screens/(modal)/onboarding/select-reading-mode"
+import { SelectReadingModeScreen } from "@/src/screens/(modal)/onboarding/SelectReadingModeScreen"
import { accentColor } from "@/src/theme/colors"
import { EditProfileScreen } from "../settings/routes/EditProfile"
diff --git a/apps/mobile/src/modules/player/PlayerTabBar.tsx b/apps/mobile/src/modules/player/PlayerTabBar.tsx
index fa86623a6..75f6980ab 100644
--- a/apps/mobile/src/modules/player/PlayerTabBar.tsx
+++ b/apps/mobile/src/modules/player/PlayerTabBar.tsx
@@ -13,7 +13,7 @@ import { Image } from "@/src/components/ui/image/Image"
import { BottomTabContext } from "@/src/lib/navigation/bottom-tab/BottomTabContext"
import { useNavigation } from "@/src/lib/navigation/hooks"
import { useActiveTrack } from "@/src/lib/player"
-import { PlayerScreen } from "@/src/screens/player"
+import { PlayerScreen } from "@/src/screens/PlayerScreen"
import { usePrefetchImageColors } from "@/src/store/image/hooks"
import { PlayPauseButton, SeekButton } from "./control"
@@ -47,7 +47,10 @@ export function PlayerTabBar({ className }: { className?: string }) {
return (
{
diff --git a/apps/mobile/src/modules/screen/PagerList.ios.tsx b/apps/mobile/src/modules/screen/PagerList.ios.tsx
new file mode 100644
index 000000000..d05550015
--- /dev/null
+++ b/apps/mobile/src/modules/screen/PagerList.ios.tsx
@@ -0,0 +1,72 @@
+import type { FeedViewType } from "@follow/constants"
+import { useTypeScriptHappyCallback } from "@follow/hooks"
+import { EventBus } from "@follow/utils/src/event-bus"
+import * as Haptics from "expo-haptics"
+import { useEffect, useId, useMemo, useRef, useState } from "react"
+import type { StyleProp, ViewStyle } from "react-native"
+
+import { PagerView } from "@/src/components/native/PagerView"
+import type { PagerRef } from "@/src/components/native/PagerView/specs"
+import { useViewWithSubscription } from "@/src/store/subscription/hooks"
+
+import { selectTimeline, useSelectedFeed } from "./atoms"
+import { PagerListVisibleContext, PagerListWillVisibleContext } from "./PagerListContext"
+
+export function PagerList({
+ renderItem,
+ style,
+}: {
+ renderItem: (view: FeedViewType, active: boolean) => React.ReactNode
+ style?: StyleProp | undefined
+}) {
+ const selectedFeed = useSelectedFeed()
+ const viewId = selectedFeed?.type === "view" ? selectedFeed.viewId : undefined
+
+ const activeViews = useViewWithSubscription()
+ const activeViewIndex = useMemo(
+ () => activeViews.findIndex((view) => view.view === viewId),
+ [activeViews, viewId],
+ )
+ const pagerRef = useRef(null)
+ const rid = useId()
+ useEffect(() => {
+ return EventBus.subscribe("SELECT_TIMELINE", (data) => {
+ if (data.target !== rid) {
+ pagerRef.current?.setPage(activeViews.findIndex((view) => view.view === data.view.viewId))
+ }
+ })
+ }, [activeViews, pagerRef, rid])
+ const [dragging, setDragging] = useState(false)
+
+ return (
+ setDragging(true)}
+ onScrollEnd={() => setDragging(false)}
+ pageContainerClassName="flex-1"
+ containerClassName="flex-1 absolute inset-0"
+ containerStyle={style}
+ onPageChange={(targetIndex) => {
+ selectTimeline({ type: "view", viewId: activeViews[targetIndex]!.view }, rid)
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
+ }}
+ renderPage={useTypeScriptHappyCallback(
+ (index) => (
+
+
+ {renderItem(activeViews[index]!.view, index === activeViewIndex)}
+
+
+ ),
+ [activeViews, activeViewIndex, dragging, renderItem],
+ )}
+ pageTotal={activeViews.length}
+ />
+ )
+}
diff --git a/apps/mobile/src/modules/screen/PageList.tsx b/apps/mobile/src/modules/screen/PagerList.tsx
similarity index 74%
rename from apps/mobile/src/modules/screen/PageList.tsx
rename to apps/mobile/src/modules/screen/PagerList.tsx
index 40fa71584..f56a49905 100644
--- a/apps/mobile/src/modules/screen/PageList.tsx
+++ b/apps/mobile/src/modules/screen/PagerList.tsx
@@ -1,7 +1,7 @@
import type { FeedViewType } from "@follow/constants"
import { EventBus } from "@follow/utils/src/event-bus"
import * as Haptics from "expo-haptics"
-import { useCallback, useEffect, useId, useMemo, useRef } from "react"
+import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"
import type { StyleProp, ViewStyle } from "react-native"
import { Animated, StyleSheet } from "react-native"
import PagerView from "react-native-pager-view"
@@ -10,7 +10,7 @@ import { useSharedValue } from "react-native-reanimated"
import { selectTimeline, useSelectedFeed } from "@/src/modules/screen/atoms"
import { useViewWithSubscription } from "@/src/store/subscription/hooks"
-import { setHorizontalScrolling } from "./atoms"
+import { PagerListVisibleContext, PagerListWillVisibleContext } from "./PagerListContext"
const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
@@ -43,6 +43,7 @@ export function PagerList({
}, [activeViews, pagerRef, rid])
const userInitiatedDragRef = useSharedValue(false)
+ const [dragging, setDragging] = useState(false)
const pageScrollHandler = useCallback(
(e: {
nativeEvent: {
@@ -86,11 +87,15 @@ export function PagerList({
overdrag
onPageScroll={pageScrollHandler}
onPageScrollStateChanged={(e) => {
- if (e.nativeEvent.pageScrollState === "dragging") {
+ const { pageScrollState } = e.nativeEvent
+ if (pageScrollState === "dragging") {
+ setDragging(true)
userInitiatedDragRef.value = true
+ } else if (pageScrollState === "idle") {
+ setDragging(false)
}
- setHorizontalScrolling(e.nativeEvent.pageScrollState !== "idle")
- if (e.nativeEvent.pageScrollState === "settling") {
+
+ if (pageScrollState === "settling") {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
}
}}
@@ -98,8 +103,17 @@ export function PagerList({
orientation="horizontal"
>
{useMemo(
- () => activeViews.map((view, index) => renderItem(view.view, index === activeViewIndex)),
- [activeViews, activeViewIndex, renderItem],
+ () =>
+ activeViews.map((view, index) => (
+
+
+ {renderItem(view.view, index === activeViewIndex)}
+
+
+ )),
+ [activeViews, activeViewIndex, dragging, renderItem],
)}
)
diff --git a/apps/mobile/src/modules/screen/PagerListContext.ts b/apps/mobile/src/modules/screen/PagerListContext.ts
new file mode 100644
index 000000000..e02890910
--- /dev/null
+++ b/apps/mobile/src/modules/screen/PagerListContext.ts
@@ -0,0 +1,4 @@
+import { createContext } from "react"
+
+export const PagerListVisibleContext = createContext(true)
+export const PagerListWillVisibleContext = createContext(false)
diff --git a/apps/mobile/src/modules/screen/TimelineSelectorList.tsx b/apps/mobile/src/modules/screen/TimelineSelectorList.tsx
index 166a4321a..7c363eb3f 100644
--- a/apps/mobile/src/modules/screen/TimelineSelectorList.tsx
+++ b/apps/mobile/src/modules/screen/TimelineSelectorList.tsx
@@ -10,13 +10,11 @@ import * as Haptics from "expo-haptics"
import type { ElementRef, RefObject } from "react"
import { forwardRef, useCallback, useContext, useImperativeHandle, useRef } from "react"
import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native"
-import { findNodeHandle, RefreshControl } from "react-native"
+import { RefreshControl, View } from "react-native"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { useColor } from "react-native-uikit-colors"
-import { BottomTabBarBackgroundContext } from "@/src/components/layouts/tabbar/contexts/BottomTabBarBackgroundContext"
import { useBottomTabBarHeight } from "@/src/components/layouts/tabbar/hooks"
-import { isScrollToEnd } from "@/src/lib/native"
import { ScreenItemContext } from "@/src/lib/navigation/ScreenItemContext"
import { useHeaderHeight } from "@/src/modules/screen/hooks/useHeaderHeight"
import { usePrefetchSubscription } from "@/src/store/subscription/hooks"
@@ -39,30 +37,18 @@ export const TimelineSelectorList = forwardRef<
const { refetch: subscriptionRefetch } = usePrefetchSubscription()
const headerHeight = useHeaderHeight()
- const { reAnimatedScrollY, scrollViewHeight, scrollViewContentHeight } =
+ const { scrollViewHeight, scrollViewContentHeight, reAnimatedScrollY } =
useContext(ScreenItemContext)!
- const { opacity } = useContext(BottomTabBarBackgroundContext)
- const checkScrollToBottom = useCallback(() => {
- const handle = findNodeHandle(ref.current!)
- if (!handle) {
- return
- }
- isScrollToEnd(handle).then((isEnd) => {
- opacity.value = isEnd ? 0 : 1
- })
- }, [opacity])
+
+ const tabBarHeight = useBottomTabBarHeight()
const onScroll = useCallback(
(e: NativeSyntheticEvent) => {
props.onScroll?.(e)
reAnimatedScrollY.value = e.nativeEvent.contentOffset.y
- checkScrollToBottom()
},
- [props, reAnimatedScrollY, checkScrollToBottom],
+ [props, reAnimatedScrollY],
)
-
- const tabBarHeight = useBottomTabBarHeight()
-
const systemFill = useColor("secondaryLabel")
const onLayout = useTypeScriptHappyCallback(
@@ -84,42 +70,44 @@ export const TimelineSelectorList = forwardRef<
}
return (
- {
- unreadRefetch()
- subscriptionRefetch()
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
- onRefresh()
- }}
- refreshing={isRefetching}
- />
- }
- scrollIndicatorInsets={{
- top: headerHeight,
- bottom: tabBarHeight,
- }}
- contentContainerStyle={{
- paddingTop: headerHeight,
- paddingBottom: tabBarHeight,
- }}
- {...props}
- onScroll={onScroll}
- onEndReached={() => {
- nextFrame(() => {
- props.onEndReached?.()
- })
- }}
- />
+
+ {
+ unreadRefetch()
+ subscriptionRefetch()
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
+ onRefresh()
+ }}
+ refreshing={isRefetching}
+ />
+ }
+ scrollIndicatorInsets={{
+ top: headerHeight,
+ bottom: tabBarHeight,
+ }}
+ contentContainerStyle={{
+ paddingTop: headerHeight,
+ paddingBottom: tabBarHeight,
+ }}
+ {...props}
+ onScroll={onScroll}
+ onEndReached={() => {
+ nextFrame(() => {
+ props.onEndReached?.()
+ })
+ }}
+ />
+
)
})
diff --git a/apps/mobile/src/modules/screen/action.tsx b/apps/mobile/src/modules/screen/action.tsx
index 0488e8728..c2aa347b2 100644
--- a/apps/mobile/src/modules/screen/action.tsx
+++ b/apps/mobile/src/modules/screen/action.tsx
@@ -12,13 +12,13 @@ import { AddCuteReIcon } from "@/src/icons/add_cute_re"
import { CheckCircleCuteReIcon } from "@/src/icons/check_circle_cute_re"
import { RoundCuteFiIcon } from "@/src/icons/round_cute_fi"
import { RoundCuteReIcon } from "@/src/icons/round_cute_re"
-import { Share3CuteReIcon } from "@/src/icons/share_3_cute_re"
+import { ShareForwardCuteReIcon } from "@/src/icons/share_forward_cute_re"
import { Dialog } from "@/src/lib/dialog"
import { useNavigation } from "@/src/lib/navigation/hooks"
import { proxyEnv } from "@/src/lib/proxy-env"
import { toast } from "@/src/lib/toast"
-import { LoginScreen } from "@/src/screens/(modal)/login"
-import { ProfileScreen } from "@/src/screens/(modal)/profile"
+import { LoginScreen } from "@/src/screens/(modal)/LoginScreen"
+import { ProfileScreen } from "@/src/screens/(modal)/ProfileScreen"
import { getFeed } from "@/src/store/feed/getter"
import { useWhoami } from "@/src/store/user/hooks"
import { accentColor, useColor } from "@/src/theme/colors"
@@ -133,7 +133,7 @@ export const FeedShareActionButton = ({
return (
}
+ normalIcon={ }
onPress={() => {
const feed = getFeed(feedId)
if (!feed) return
diff --git a/apps/mobile/src/modules/screen/atoms.ts b/apps/mobile/src/modules/screen/atoms.ts
index e1083f5a8..93fed3326 100644
--- a/apps/mobile/src/modules/screen/atoms.ts
+++ b/apps/mobile/src/modules/screen/atoms.ts
@@ -184,16 +184,3 @@ export const useViewDefinition = (view?: FeedViewType) => {
const viewDef = useMemo(() => views.find((v) => v.view === view), [view])
return viewDef
}
-
-// horizontal scrolling state
-
-const horizontalScrollingAtom = atom(false)
-
-export const setHorizontalScrolling = (value: boolean) =>
- jotaiStore.set(horizontalScrollingAtom, value)
-
-export const getHorizontalScrolling = () => jotaiStore.get(horizontalScrollingAtom)
-
-export const useHorizontalScrolling = () => {
- return useAtomValue(horizontalScrollingAtom)
-}
diff --git a/apps/mobile/src/modules/screen/usePagerView.ts b/apps/mobile/src/modules/screen/usePagerView.ts
deleted file mode 100644
index e3caaa9db..000000000
--- a/apps/mobile/src/modules/screen/usePagerView.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * @Author: czy0729
- * @Date: 2023-12-27 15:42:54
- * @Last Modified by: czy0729
- * @Last Modified time: 2024-08-13 16:14:43
- *
- * Ported from https://github.com/czy0729/Bangumi/blob/6f2c46bba0bb2c1ffa98fb54d9609d87cb5f06a2/src/components/nested-scroll/hooks/usePagerView.tsx
- * License: MIT
- */
-import { useCallback, useMemo, useRef, useState } from "react"
-import { Animated, Platform, useAnimatedValue } from "react-native"
-import type {
- default as PagerView,
- PagerViewOnPageScrollEventData,
- PagerViewOnPageSelectedEvent,
- PageScrollStateChangedNativeEvent,
-} from "react-native-pager-view"
-
-export function usePagerView({
- initialPage = 0,
- onIndexChange,
-}: {
- initialPage?: number
- onIndexChange?: (index: number) => void
-}) {
- const pagerRef = useRef(null)
- const [activePage, setActivePage] = useState(initialPage)
- const [isIdle, setIdle] = useState(true)
-
- const setPage = useCallback((page: number, animated = true) => {
- if (animated) {
- pagerRef.current?.setPage(page)
- } else {
- pagerRef.current?.setPageWithoutAnimation(page)
- }
- setActivePage(page)
- }, [])
-
- const offset = useAnimatedValue(0)
- const position = useAnimatedValue(initialPage)
-
- const onPageScroll = useMemo(
- () =>
- Animated.event(
- [
- {
- nativeEvent: {
- offset,
- position,
- },
- },
- ],
- {
- useNativeDriver: true,
- },
- ),
- [offset, position],
- )
-
- const onPageSelected = useCallback(
- (e: PagerViewOnPageSelectedEvent) => {
- const { position } = e.nativeEvent
- setActivePage(position)
- if (Platform.OS === "ios") setIdle(true)
-
- if (typeof onIndexChange === "function") onIndexChange(position)
- },
- [onIndexChange],
- )
-
- const onPageScrollStateChanged = useCallback(
- ({ nativeEvent: { pageScrollState } }: PageScrollStateChangedNativeEvent) => {
- setIdle(pageScrollState === "idle")
- },
- [],
- )
-
- return {
- pagerRef,
- setPage,
- page: activePage,
- isIdle,
- position,
- offset,
- onPageScroll,
- onPageSelected,
- onPageScrollStateChanged,
- }
-}
diff --git a/apps/mobile/src/modules/settings/SettingsList.tsx b/apps/mobile/src/modules/settings/SettingsList.tsx
index f84eed9d0..8e49005e7 100644
--- a/apps/mobile/src/modules/settings/SettingsList.tsx
+++ b/apps/mobile/src/modules/settings/SettingsList.tsx
@@ -27,7 +27,7 @@ import { UserSettingCuteFiIcon } from "@/src/icons/user_setting_cute_fi"
import { signOut } from "@/src/lib/auth"
import { useNavigation } from "@/src/lib/navigation/hooks"
import type { Navigation } from "@/src/lib/navigation/Navigation"
-import { InvitationScreen } from "@/src/screens/(modal)/invitation"
+import { InvitationScreen } from "@/src/screens/(modal)/InvitationScreen"
import { useRole, useWhoami } from "@/src/store/user/hooks"
import { AboutScreen } from "./routes/About"
diff --git a/apps/mobile/src/modules/settings/UserHeaderBanner.tsx b/apps/mobile/src/modules/settings/UserHeaderBanner.tsx
index 5a948e269..14b528c1e 100644
--- a/apps/mobile/src/modules/settings/UserHeaderBanner.tsx
+++ b/apps/mobile/src/modules/settings/UserHeaderBanner.tsx
@@ -9,7 +9,7 @@ import { useColor } from "react-native-uikit-colors"
import { UserAvatar } from "@/src/components/ui/avatar/UserAvatar"
import { useNavigation } from "@/src/lib/navigation/hooks"
-import { LoginScreen } from "@/src/screens/(modal)/login"
+import { LoginScreen } from "@/src/screens/(modal)/LoginScreen"
import { useImageColors, usePrefetchImageColors } from "@/src/store/image/hooks"
import { useUser } from "@/src/store/user/hooks"
@@ -64,7 +64,7 @@ export const UserHeaderBanner = ({
if (!gradientColors) return {}
return {
transform: [{ scale: scaleValue }],
- height: 250 + (scrollY.value < 0 ? -scrollY.value : 0),
+ height: 250,
}
})
@@ -118,8 +118,8 @@ export const UserHeaderBanner = ({
)}
, `actions.action_card.${string}`>
onEnable?: (index: number) => void
onNavigate?: (router: Navigation, index: number) => void
- component?: React.FC<{ rule: ActionRule }>
+ icon: SFSymbol
}> = [
{
value: "summary",
label: "actions.action_card.generate_summary",
+ icon: "sparkles",
},
{
value: "translation",
label: "actions.action_card.translate_into",
+ icon: "translate",
},
{
value: "readability",
label: "actions.action_card.enable_readability",
+ icon: "text.document",
},
{
value: "sourceContent",
label: "actions.action_card.source_content",
+ icon: "macwindow",
},
{
value: "newEntryNotification",
label: "actions.action_card.new_entry_notification",
+ icon: "bell.and.waves.left.and.right",
},
{
value: "silence",
label: "actions.action_card.silence",
+ icon: "speaker.slash",
},
{
value: "block",
label: "actions.action_card.block",
+ icon: "xmark.circle",
},
{
value: "rewriteRules",
label: "actions.action_card.rewrite_rules",
+ icon: "pencil.and.outline",
onEnable: (index: number) => {
actionActions.patchRule(index, {
result: {
@@ -156,6 +165,7 @@ export const availableActionList: Array<{
{
value: "webhooks",
label: "actions.action_card.webhooks",
+ icon: "arrow.up.right.square",
onEnable: (index) => {
actionActions.patchRule(index, { result: { webhooks: [""] } })
},
diff --git a/apps/mobile/src/modules/settings/hooks/useShareSubscription.tsx b/apps/mobile/src/modules/settings/hooks/useShareSubscription.tsx
index 9ebf43764..acd7c69a3 100644
--- a/apps/mobile/src/modules/settings/hooks/useShareSubscription.tsx
+++ b/apps/mobile/src/modules/settings/hooks/useShareSubscription.tsx
@@ -1,10 +1,15 @@
-import { useQuery } from "@tanstack/react-query"
+import type { UseQueryResult } from "@tanstack/react-query"
+import { useQuery, useQueryClient } from "@tanstack/react-query"
+import { useCallback, useMemo } from "react"
import { apiClient } from "@/src/lib/api-fetch"
+// eslint-disable-next-line unused-imports/no-unused-vars
+type ExtractQueryData = T extends UseQueryResult ? T : never
export const useShareSubscription = ({ userId }: { userId: string }) => {
- return useQuery({
- queryKey: ["public", "subscription", userId],
+ const queryKey = useMemo(() => ["public", "subscription", userId], [userId])
+ const query = useQuery({
+ queryKey,
queryFn: async () => {
const subscriptions = await apiClient.subscriptions.$get({
query: {
@@ -15,4 +20,22 @@ export const useShareSubscription = ({ userId }: { userId: string }) => {
return subscriptions
},
})
+ const queryClient = useQueryClient()
+ return {
+ ...query,
+ removeItemById: useCallback(
+ (id: string) => {
+ type QueryData = ExtractQueryData
+ queryClient.cancelQueries({ queryKey })
+ queryClient.setQueryData(queryKey, (data) => {
+ if (!data) return
+ return {
+ ...data,
+ data: data?.data.filter((item) => item.feedId !== id),
+ }
+ })
+ },
+ [queryClient, queryKey],
+ ),
+ }
}
diff --git a/apps/mobile/src/modules/settings/hooks/useTOTPModalWrapper.tsx b/apps/mobile/src/modules/settings/hooks/useTOTPModalWrapper.tsx
index 037e5b364..963fa0985 100644
--- a/apps/mobile/src/modules/settings/hooks/useTOTPModalWrapper.tsx
+++ b/apps/mobile/src/modules/settings/hooks/useTOTPModalWrapper.tsx
@@ -4,7 +4,7 @@ import Siblings from "react-native-root-siblings"
import { getFetchErrorInfo } from "@/src/lib/error-parser"
import { useNavigation } from "@/src/lib/navigation/hooks"
import { toast } from "@/src/lib/toast"
-import { TwoFactorAuthScreen } from "@/src/screens/(modal)/2fa"
+import { TwoFactorAuthScreen } from "@/src/screens/(modal)/TwoFactorAuthScreen"
import { useWhoami } from "@/src/store/user/hooks"
import { OTPWindow } from "../components/OTPWindow"
diff --git a/apps/mobile/src/modules/settings/routes/Actions.tsx b/apps/mobile/src/modules/settings/routes/Actions.tsx
index 02702f3c8..d92f2ad6e 100644
--- a/apps/mobile/src/modules/settings/routes/Actions.tsx
+++ b/apps/mobile/src/modules/settings/routes/Actions.tsx
@@ -135,7 +135,7 @@ const SaveRuleButton = ({ disabled }: { disabled?: boolean }) => {
const ItemSeparatorComponent = () => {
return (
diff --git a/apps/mobile/src/modules/settings/routes/EditProfile.tsx b/apps/mobile/src/modules/settings/routes/EditProfile.tsx
index 5c74ea157..d18f91f22 100644
--- a/apps/mobile/src/modules/settings/routes/EditProfile.tsx
+++ b/apps/mobile/src/modules/settings/routes/EditProfile.tsx
@@ -23,7 +23,7 @@ import { CheckCircleCuteReIcon } from "@/src/icons/check_circle_cute_re"
import { CloseCircleFillIcon } from "@/src/icons/close_circle_fill"
import { useNavigation } from "@/src/lib/navigation/hooks"
import { toast } from "@/src/lib/toast"
-import { EditEmailScreen } from "@/src/screens/(modal)/edit-email"
+import { EditEmailScreen } from "@/src/screens/(modal)/EditEmailScreen"
import { useWhoami } from "@/src/store/user/hooks"
import type { MeModel } from "@/src/store/user/store"
import { userSyncService } from "@/src/store/user/store"
diff --git a/apps/mobile/src/modules/settings/routes/EditRule.tsx b/apps/mobile/src/modules/settings/routes/EditRule.tsx
index 500c9ed05..41c9309da 100644
--- a/apps/mobile/src/modules/settings/routes/EditRule.tsx
+++ b/apps/mobile/src/modules/settings/routes/EditRule.tsx
@@ -255,20 +255,22 @@ const ActionSection: React.FC<{ rule: ActionRule }> = ({ rule }) => {
},
]}
>
- {action.component ? (
-
- ) : action.onNavigate ? (
- action.onNavigate?.(navigation, rule.index)}
- />
- ) : (
-
- )}
+
+ {action.onNavigate ? (
+ action.onNavigate?.(navigation, rule.index)}
+ />
+ ) : (
+
+ )}
+
))}
{notEnabledActions.length > 0 && (
@@ -288,6 +290,11 @@ const ActionSection: React.FC<{ rule: ActionRule }> = ({ rule }) => {
}
}}
>
+
{t(action.label)}
))}
diff --git a/apps/mobile/src/modules/settings/routes/General.tsx b/apps/mobile/src/modules/settings/routes/General.tsx
index e40699709..18620bea0 100644
--- a/apps/mobile/src/modules/settings/routes/General.tsx
+++ b/apps/mobile/src/modules/settings/routes/General.tsx
@@ -84,7 +84,7 @@ export const GeneralScreen: NavigationControllerView = () => {
const expandLongSocialMedia = useGeneralSettingKey("autoExpandLongSocialMedia")
const markAsReadWhenScrolling = useGeneralSettingKey("scrollMarkUnread")
const markAsReadWhenInView = useGeneralSettingKey("renderMarkUnread")
- const openLinksInApp = useGeneralSettingKey("openLinksInApp")
+ const openLinksInExternalApp = useGeneralSettingKey("openLinksInExternalApp")
return (
{
-
+
{
- setGeneralSetting("openLinksInApp", value)
+ setGeneralSetting("openLinksInExternalApp", value)
}}
/>
diff --git a/apps/mobile/src/modules/settings/routes/Lists.tsx b/apps/mobile/src/modules/settings/routes/Lists.tsx
index 06de1facd..425ca2e7a 100644
--- a/apps/mobile/src/modules/settings/routes/Lists.tsx
+++ b/apps/mobile/src/modules/settings/routes/Lists.tsx
@@ -26,7 +26,7 @@ import { UserAdd2CuteFiIcon } from "@/src/icons/user_add_2_cute_fi"
import { Wallet2CuteFiIcon } from "@/src/icons/wallet_2_cute_fi"
import { useNavigation } from "@/src/lib/navigation/hooks"
import type { HonoApiClient } from "@/src/morph/types"
-import { ListScreen } from "@/src/screens/(modal)/list"
+import { ListScreen } from "@/src/screens/(modal)/ListScreen"
import { useOwnedLists, usePrefetchOwnedLists } from "@/src/store/list/hooks"
import type { ListModel } from "@/src/store/list/store"
import { accentColor } from "@/src/theme/colors"
diff --git a/apps/mobile/src/modules/settings/routes/ManageList.tsx b/apps/mobile/src/modules/settings/routes/ManageList.tsx
index b53d4b6e9..45540ffe0 100644
--- a/apps/mobile/src/modules/settings/routes/ManageList.tsx
+++ b/apps/mobile/src/modules/settings/routes/ManageList.tsx
@@ -120,7 +120,7 @@ const ListImpl: React.FC<{ id: string }> = ({ id }) => {
const SeparatorComponent = () => {
return (
diff --git a/apps/mobile/src/modules/subscription/CategoryGrouped.tsx b/apps/mobile/src/modules/subscription/CategoryGrouped.tsx
index 9143625b5..1fad0bbad 100644
--- a/apps/mobile/src/modules/subscription/CategoryGrouped.tsx
+++ b/apps/mobile/src/modules/subscription/CategoryGrouped.tsx
@@ -1,15 +1,16 @@
import { cn } from "@follow/utils"
import { memo, useState } from "react"
-import { Text, TouchableOpacity, View } from "react-native"
+import { Text, View } from "react-native"
import Animated, { useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated"
import { GROUPED_LIST_MARGIN } from "@/src/components/ui/grouped/constants"
import { ItemPressableStyle } from "@/src/components/ui/pressable/enum"
import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable"
+import { NativePressable } from "@/src/components/ui/pressable/NativePressable"
import { RightCuteFiIcon } from "@/src/icons/right_cute_fi"
import { useNavigation } from "@/src/lib/navigation/hooks"
-import { closeDrawer, getHorizontalScrolling, selectFeed } from "@/src/modules/screen/atoms"
-import { FeedScreen } from "@/src/screens/(stack)/feeds/[feedId]"
+import { closeDrawer, selectFeed } from "@/src/modules/screen/atoms"
+import { FeedScreen } from "@/src/screens/(stack)/feeds/[feedId]/FeedScreen"
import { useUnreadCounts } from "@/src/store/unread/hooks"
import { useColor } from "@/src/theme/colors"
@@ -50,10 +51,6 @@ export const CategoryGrouped = memo(
{
- const isHorizontalScrolling = getHorizontalScrolling()
- if (isHorizontalScrolling) {
- return
- }
selectFeed({
type: "category",
categoryName: category,
@@ -68,7 +65,7 @@ export const CategoryGrouped = memo(
"rounded-b-[10px]": isLast && !expanded,
})}
>
- {
rotateSharedValue.value = withSpring(expanded ? 0 : 90, {})
@@ -79,7 +76,7 @@ export const CategoryGrouped = memo(
-
+
{category}
diff --git a/apps/mobile/src/modules/subscription/ItemSeparator.tsx b/apps/mobile/src/modules/subscription/ItemSeparator.tsx
index fe4027f02..120f47094 100644
--- a/apps/mobile/src/modules/subscription/ItemSeparator.tsx
+++ b/apps/mobile/src/modules/subscription/ItemSeparator.tsx
@@ -8,7 +8,7 @@ const el = (
style={{ marginHorizontal: GROUPED_LIST_MARGIN }}
>
@@ -24,7 +24,7 @@ const el2 = (
style={{ marginHorizontal: GROUPED_LIST_MARGIN }}
>
diff --git a/apps/mobile/src/modules/subscription/SubscriptionLists.tsx b/apps/mobile/src/modules/subscription/SubscriptionLists.tsx
index fa97eee97..ef36897a2 100644
--- a/apps/mobile/src/modules/subscription/SubscriptionLists.tsx
+++ b/apps/mobile/src/modules/subscription/SubscriptionLists.tsx
@@ -19,9 +19,9 @@ import { ItemPressableStyle } from "@/src/components/ui/pressable/enum"
import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable"
import { StarCuteFiIcon } from "@/src/icons/star_cute_fi"
import { useNavigation } from "@/src/lib/navigation/hooks"
-import { closeDrawer, getHorizontalScrolling, selectFeed } from "@/src/modules/screen/atoms"
+import { closeDrawer, selectFeed } from "@/src/modules/screen/atoms"
import { TimelineSelectorList } from "@/src/modules/screen/TimelineSelectorList"
-import { FeedScreen } from "@/src/screens/(stack)/feeds/[feedId]"
+import { FeedScreen } from "@/src/screens/(stack)/feeds/[feedId]/FeedScreen"
import { FEED_COLLECTION_LIST } from "@/src/store/entry/utils"
import {
useGroupedSubscription,
@@ -33,6 +33,7 @@ import {
} from "@/src/store/subscription/hooks"
import { subscriptionSyncService } from "@/src/store/subscription/store"
+import { usePagerListPerformanceHack } from "../entry-list/hooks"
import { useFeedListSortMethod, useFeedListSortOrder } from "./atoms"
import { CategoryGrouped } from "./CategoryGrouped"
import { InboxItem } from "./items/InboxItem"
@@ -103,6 +104,8 @@ const SubscriptionListImpl = ({
const scrollViewRef = useRegisterNavigationScrollView>(active)
+ const { onScroll, style } = usePagerListPerformanceHack(scrollViewRef)
+
return (
)
}
@@ -232,10 +236,6 @@ const StarItem = () => {
{
- const isHorizontalScrolling = getHorizontalScrolling()
- if (isHorizontalScrolling) {
- return
- }
selectFeed({ type: "feed", feedId: FEED_COLLECTION_LIST })
closeDrawer()
navigation.pushControllerView(FeedScreen, {
diff --git a/apps/mobile/src/modules/subscription/items/InboxItem.tsx b/apps/mobile/src/modules/subscription/items/InboxItem.tsx
index e1a4ccd35..424f46c8d 100644
--- a/apps/mobile/src/modules/subscription/items/InboxItem.tsx
+++ b/apps/mobile/src/modules/subscription/items/InboxItem.tsx
@@ -9,8 +9,8 @@ import { ItemPressableStyle } from "@/src/components/ui/pressable/enum"
import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable"
import { InboxCuteFiIcon } from "@/src/icons/inbox_cute_fi"
import { useNavigation } from "@/src/lib/navigation/hooks"
-import { getHorizontalScrolling, selectFeed } from "@/src/modules/screen/atoms"
-import { FeedScreen } from "@/src/screens/(stack)/feeds/[feedId]"
+import { selectFeed } from "@/src/modules/screen/atoms"
+import { FeedScreen } from "@/src/screens/(stack)/feeds/[feedId]/FeedScreen"
import { useSubscription } from "@/src/store/subscription/hooks"
import { getInboxStoreId } from "@/src/store/subscription/utils"
import { useUnreadCount } from "@/src/store/unread/hooks"
@@ -37,10 +37,6 @@ export const InboxItem = memo(({ id, isFirst, isLast }: SubscriptionItemBaseProp
itemStyle={ItemPressableStyle.Grouped}
className="h-12 flex-row items-center px-3"
onPress={() => {
- const isHorizontalScrolling = getHorizontalScrolling()
- if (isHorizontalScrolling) {
- return
- }
selectFeed({ type: "inbox", inboxId: id })
navigation.pushControllerView(FeedScreen, {
feedId: id,
diff --git a/apps/mobile/src/modules/subscription/items/ListSubscriptionItem.tsx b/apps/mobile/src/modules/subscription/items/ListSubscriptionItem.tsx
index b174b9e60..d95db5805 100644
--- a/apps/mobile/src/modules/subscription/items/ListSubscriptionItem.tsx
+++ b/apps/mobile/src/modules/subscription/items/ListSubscriptionItem.tsx
@@ -9,12 +9,12 @@ import { Image } from "@/src/components/ui/image/Image"
import { ItemPressableStyle } from "@/src/components/ui/pressable/enum"
import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable"
import { useNavigation } from "@/src/lib/navigation/hooks"
-import { FeedScreen } from "@/src/screens/(stack)/feeds/[feedId]"
+import { FeedScreen } from "@/src/screens/(stack)/feeds/[feedId]/FeedScreen"
import { useList } from "@/src/store/list/hooks"
import { useListUnreadCount } from "@/src/store/unread/hooks"
import { SubscriptionListItemContextMenu } from "../../context-menu/lists"
-import { getHorizontalScrolling, selectFeed } from "../../screen/atoms"
+import { selectFeed } from "../../screen/atoms"
import { ItemSeparator } from "../ItemSeparator"
import type { SubscriptionItemBaseProps } from "./types"
import { UnreadCount } from "./UnreadCount"
@@ -41,10 +41,6 @@ export const ListSubscriptionItem = memo(({ id, isFirst, isLast }: ListSubscript
itemStyle={ItemPressableStyle.Grouped}
className="h-12 flex-row items-center px-3"
onPress={() => {
- const isHorizontalScrolling = getHorizontalScrolling()
- if (isHorizontalScrolling) {
- return
- }
selectFeed({
type: "list",
listId: id,
diff --git a/apps/mobile/src/modules/subscription/items/SubscriptionItem.tsx b/apps/mobile/src/modules/subscription/items/SubscriptionItem.tsx
index 1efc20b9e..d95217986 100644
--- a/apps/mobile/src/modules/subscription/items/SubscriptionItem.tsx
+++ b/apps/mobile/src/modules/subscription/items/SubscriptionItem.tsx
@@ -11,8 +11,8 @@ import { ItemPressableStyle } from "@/src/components/ui/pressable/enum"
import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable"
import { WifiOffCuteReIcon } from "@/src/icons/wifi_off_cute_re"
import { useNavigation } from "@/src/lib/navigation/hooks"
-import { closeDrawer, getHorizontalScrolling, selectFeed } from "@/src/modules/screen/atoms"
-import { FeedScreen } from "@/src/screens/(stack)/feeds/[feedId]"
+import { closeDrawer, selectFeed } from "@/src/modules/screen/atoms"
+import { FeedScreen } from "@/src/screens/(stack)/feeds/[feedId]/FeedScreen"
import { useFeed, usePrefetchFeed } from "@/src/store/feed/hooks"
import { useSubscription } from "@/src/store/subscription/hooks"
import { useUnreadCount } from "@/src/store/unread/hooks"
@@ -63,10 +63,6 @@ export const SubscriptionItem = memo(
className,
)}
onPress={() => {
- const isHorizontalScrolling = getHorizontalScrolling()
- if (isHorizontalScrolling) {
- return
- }
selectFeed({
type: "feed",
feedId: id,
diff --git a/apps/mobile/src/screens/(modal)/edit-email.tsx b/apps/mobile/src/screens/(modal)/EditEmailScreen.tsx
similarity index 100%
rename from apps/mobile/src/screens/(modal)/edit-email.tsx
rename to apps/mobile/src/screens/(modal)/EditEmailScreen.tsx
diff --git a/apps/mobile/src/screens/(modal)/follow.tsx b/apps/mobile/src/screens/(modal)/FollowScreen.tsx
similarity index 100%
rename from apps/mobile/src/screens/(modal)/follow.tsx
rename to apps/mobile/src/screens/(modal)/FollowScreen.tsx
diff --git a/apps/mobile/src/screens/(modal)/forget-password.tsx b/apps/mobile/src/screens/(modal)/ForgetPasswordScreen.tsx
similarity index 100%
rename from apps/mobile/src/screens/(modal)/forget-password.tsx
rename to apps/mobile/src/screens/(modal)/ForgetPasswordScreen.tsx
diff --git a/apps/mobile/src/screens/(modal)/invitation.tsx b/apps/mobile/src/screens/(modal)/InvitationScreen.tsx
similarity index 100%
rename from apps/mobile/src/screens/(modal)/invitation.tsx
rename to apps/mobile/src/screens/(modal)/InvitationScreen.tsx
diff --git a/apps/mobile/src/screens/(modal)/list.tsx b/apps/mobile/src/screens/(modal)/ListScreen.tsx
similarity index 100%
rename from apps/mobile/src/screens/(modal)/list.tsx
rename to apps/mobile/src/screens/(modal)/ListScreen.tsx
diff --git a/apps/mobile/src/screens/(modal)/login.tsx b/apps/mobile/src/screens/(modal)/LoginScreen.tsx
similarity index 100%
rename from apps/mobile/src/screens/(modal)/login.tsx
rename to apps/mobile/src/screens/(modal)/LoginScreen.tsx
diff --git a/apps/mobile/src/screens/(modal)/profile.tsx b/apps/mobile/src/screens/(modal)/ProfileScreen.tsx
similarity index 66%
rename from apps/mobile/src/screens/(modal)/profile.tsx
rename to apps/mobile/src/screens/(modal)/ProfileScreen.tsx
index 48056e0b0..95ba64545 100644
--- a/apps/mobile/src/screens/(modal)/profile.tsx
+++ b/apps/mobile/src/screens/(modal)/ProfileScreen.tsx
@@ -1,7 +1,8 @@
import type { FeedViewType } from "@follow/constants"
-import { Fragment, useCallback, useEffect, useMemo } from "react"
+import { useQuery } from "@tanstack/react-query"
+import { createContext, Fragment, useCallback, useContext, useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
-import { FlatList, Image, Share, Text, TouchableOpacity, View } from "react-native"
+import { Alert, FlatList, Image, Share, Text, TouchableOpacity, View } from "react-native"
import Animated, {
interpolate,
useAnimatedScrollHandler,
@@ -12,6 +13,7 @@ import { useSafeAreaFrame, useSafeAreaInsets } from "react-native-safe-area-cont
import { ReAnimatedScrollView } from "@/src/components/common/AnimatedComponents"
import { BlurEffect } from "@/src/components/common/BlurEffect"
+import { SwipeableItem } from "@/src/components/common/SwipeableItem"
import {
InternalNavigationHeader,
UINavigationHeaderActionButton,
@@ -30,8 +32,10 @@ import { FallbackIcon } from "@/src/components/ui/icon/fallback-icon"
import type { FeedIconRequiredFeed } from "@/src/components/ui/icon/feed-icon"
import { FeedIcon } from "@/src/components/ui/icon/feed-icon"
import { PlatformActivityIndicator } from "@/src/components/ui/loading/PlatformActivityIndicator"
-import { Share3CuteReIcon } from "@/src/icons/share_3_cute_re"
+import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable"
+import { ShareForwardCuteReIcon } from "@/src/icons/share_forward_cute_re"
import type { apiClient } from "@/src/lib/api-fetch"
+import { Navigation } from "@/src/lib/navigation/Navigation"
import type { NavigationControllerView } from "@/src/lib/navigation/types"
import { toast } from "@/src/lib/toast"
import { useShareSubscription } from "@/src/modules/settings/hooks/useShareSubscription"
@@ -39,9 +43,15 @@ import { UserHeaderBanner } from "@/src/modules/settings/UserHeaderBanner"
import { ItemSeparator } from "@/src/modules/subscription/ItemSeparator"
import type { FeedModel } from "@/src/store/feed/types"
import type { ListModel } from "@/src/store/list/store"
+import { getSubscription } from "@/src/store/subscription/getter"
+import { subscriptionSyncService } from "@/src/store/subscription/store"
import { useUser, useWhoami } from "@/src/store/user/hooks"
+import { userSyncService } from "@/src/store/user/store"
import { useColor } from "@/src/theme/colors"
+import { FeedScreen } from "../(stack)/feeds/[feedId]/FeedScreen"
+import { FollowScreen } from "./FollowScreen"
+
type Subscription = Awaited>["data"][number]
export const ProfileScreen: NavigationControllerView<{
@@ -55,6 +65,22 @@ export const ProfileScreen: NavigationControllerView<{
return
}
+const IsMyProfileContext = createContext(false)
+
+const ActionContext = createContext<{
+ removeItemById: (id: string) => void
+}>({
+ removeItemById: () => {},
+})
+const usePrefetchUser = (userId: string) => {
+ const { data } = useQuery({
+ queryKey: ["user", userId],
+ queryFn: () => userSyncService.fetchUser(userId),
+ enabled: !!userId,
+ staleTime: 1000 * 60 * 5,
+ })
+ return data
+}
function ProfileScreenImpl(props: { userId: string }) {
const { t } = useTranslation()
const scrollY = useSharedValue(0)
@@ -62,6 +88,8 @@ function ProfileScreenImpl(props: { userId: string }) {
data: subscriptions,
isLoading,
isError,
+
+ removeItemById,
} = useShareSubscription({
userId: props.userId,
})
@@ -72,6 +100,7 @@ function ProfileScreenImpl(props: { userId: string }) {
headerOpacity.value = scrollY.value / 100
})
+ usePrefetchUser(props.userId)
const user = useUser(props.userId)
useEffect(() => {
if (isError) {
@@ -93,6 +122,10 @@ function ProfileScreenImpl(props: { userId: string }) {
const frame = useSafeAreaFrame()
const headerHeight = getDefaultHeaderHeight(frame, false, 0)
+ const whoami = useWhoami()
+ const isMyProfile = user?.id === whoami?.id
+ const actionCtx = useMemo(() => ({ removeItemById }), [removeItemById])
+
return (
+
-
+
}
/>
@@ -120,7 +154,11 @@ function ProfileScreenImpl(props: { userId: string }) {
{isLoading && }
- {!isLoading && subscriptions && }
+
+
+ {!isLoading && subscriptions && }
+
+
-
+
@@ -247,9 +285,21 @@ const SubscriptionList = ({ subscriptions }: { subscriptions: Subscription[] })
)
}
const renderListItems = ({ item }: { item: PickedListModel }) => (
- {
+ if (getSubscription(item.id))
+ Navigation.rootNavigation.pushControllerView(FeedScreen, {
+ feedId: item.id,
+ })
+ else {
+ Navigation.rootNavigation.pushControllerView(FollowScreen, {
+ type: "list",
+ id: item.id,
+ })
+ }
+ }}
>
{!!item.image && (
@@ -258,38 +308,98 @@ const renderListItems = ({ item }: { item: PickedListModel }) => (
{!item.image && }
-
+
{item.title}
-
+
)
const renderFeedItems = ({ item }: { item: PickedFeedModel }) => (
-
-
-
+ {
+ if (getSubscription(item.id))
+ Navigation.rootNavigation.pushControllerView(FeedScreen, {
+ feedId: item.id,
+ })
+ else {
+ Navigation.rootNavigation.pushControllerView(FollowScreen, {
+ type: "feed",
id: item.id,
- title: item.title,
- url: item.url,
- image: item.image,
- type: item.view,
- siteUrl: item.siteUrl || "",
- } as FeedIconRequiredFeed
+ })
}
- size={24}
- />
-
-
- {item.title}
-
-
+ }}
+ >
+
+
+
+
+ {item.title}
+
+
+
)
+const MaybeSwipeable = ({ id, children }: { id: string; children: React.ReactNode }) => {
+ const isMyProfile = useContext(IsMyProfileContext)
+ const { t } = useTranslation()
+ const { removeItemById } = useContext(ActionContext)
+ if (!isMyProfile) {
+ return children
+ }
+ return (
+ {
+ // unsubscribe
+ Alert.alert("Unsubscribe?", "This will remove the feed from your subscriptions", [
+ {
+ text: "Cancel",
+ style: "cancel",
+ },
+ {
+ text: t("operation.unfollow"),
+ style: "destructive",
+ onPress: () => {
+ subscriptionSyncService.unsubscribe(id)
+ removeItemById(id)
+ },
+ },
+ ])
+ },
+ backgroundColor: "red",
+ label: t("operation.unfollow"),
+ },
+ ]}
+ >
+ {children}
+
+ )
+}
+
const SectionHeader = ({ title }: { title: string }) => (
atom(0))[0]
const headerHeightAtom = useState(() => atom(0))[0]
- const animatedY = useSharedValue(0)
- const ctxValue = useMemo(
- () => ({ animatedX, currentTabAtom, headerHeightAtom, animatedY }),
- [animatedX, currentTabAtom, headerHeightAtom, animatedY],
- )
+
+ const ctxValue = useMemo(() => ({ headerHeightAtom }), [headerHeightAtom])
+
return (
-
-
-
-
-
+
+
+
+ }
+ >
+
+ Good Luck
+
-
+
+
+
+ Categories
+
+
+
+
)
}
+const DiscoverGrid = () => {
+ return (
+
+
+ {RSSHubCategories.map((category) => (
+
+ ))}
+
+
+ )
+}
+
+const CategoryItem = memo(({ category }: { category: string }) => {
+ const { t } = useTranslation("common")
+ const name = t(`discover.category.${category}` as any)
+ const navigation = useNavigation()
+
+ return (
+ {
+ if (category === "all") {
+ navigation.pushControllerView(Recommendations)
+ } else {
+ navigation.pushControllerView(RecommendationCategoryScreen, {
+ category,
+ })
+ }
+ }}
+ >
+
+
+
+ {CategoryMap[category as keyof typeof CategoryMap].emoji}
+
+ {name}
+
+
+
+ )
+})
+
+const styles = StyleSheet.create({
+ cardItem: {
+ aspectRatio: 16 / 9,
+ },
+})
export const DiscoverTabScreen: TabScreenComponent = Discover
DiscoverTabScreen.tabBarIcon = ({ focused, color }) => {
const Icon = !focused ? Search3CuteReIcon : Search3CuteFiIcon
diff --git a/apps/mobile/src/screens/(stack)/(tabs)/index.tsx b/apps/mobile/src/screens/(stack)/(tabs)/index.tsx
index f5668dc01..bf13451de 100644
--- a/apps/mobile/src/screens/(stack)/(tabs)/index.tsx
+++ b/apps/mobile/src/screens/(stack)/(tabs)/index.tsx
@@ -1,3 +1,4 @@
+import { useResetTabOpacityWhenFocused } from "@/src/components/layouts/tabbar/hooks"
import { usePrepareEntryRenderWebView } from "@/src/components/native/webview/hooks"
import { Home5CuteFiIcon } from "@/src/icons/home_5_cute_fi"
import { Home5CuteReIcon } from "@/src/icons/home_5_cute_re"
@@ -6,7 +7,7 @@ import { EntryList } from "@/src/modules/entry-list"
export const IndexTabScreen: TabScreenComponent = () => {
usePrepareEntryRenderWebView()
-
+ useResetTabOpacityWhenFocused()
return
}
diff --git a/apps/mobile/src/screens/(stack)/(tabs)/subscriptions.tsx b/apps/mobile/src/screens/(stack)/(tabs)/subscriptions.tsx
index 637a8b005..d0f54e580 100644
--- a/apps/mobile/src/screens/(stack)/(tabs)/subscriptions.tsx
+++ b/apps/mobile/src/screens/(stack)/(tabs)/subscriptions.tsx
@@ -6,11 +6,12 @@ import { useColor } from "react-native-uikit-colors"
import { ErrorBoundary } from "@/src/components/common/ErrorBoundary"
import { NoLoginInfo } from "@/src/components/common/NoLoginInfo"
import { ListErrorView } from "@/src/components/errors/ListErrorView"
+import { useResetTabOpacityWhenFocused } from "@/src/components/layouts/tabbar/hooks"
import { BlackBoard2CuteFiIcon } from "@/src/icons/black_board_2_cute_fi"
import { BlackBoard2CuteReIcon } from "@/src/icons/black_board_2_cute_re"
import type { TabScreenComponent } from "@/src/lib/navigation/bottom-tab/types"
import { EntryListContext } from "@/src/modules/screen/atoms"
-import { PagerList } from "@/src/modules/screen/PageList"
+import { PagerList } from "@/src/modules/screen/PagerList"
import { TimelineHeader } from "@/src/modules/screen/TimelineSelectorProvider"
import { SubscriptionList } from "@/src/modules/subscription/SubscriptionLists"
import { useWhoami } from "@/src/store/user/hooks"
@@ -18,7 +19,7 @@ import { useWhoami } from "@/src/store/user/hooks"
export default function Subscriptions() {
const whoami = useWhoami()
const systemGroupedBackground = useColor("systemGroupedBackground")
-
+ useResetTabOpacityWhenFocused()
return (
({ type: "subscriptions" }), [])}>
diff --git a/apps/mobile/src/screens/(stack)/entries/[entryId]/index.tsx b/apps/mobile/src/screens/(stack)/entries/[entryId]/EntryDetailScreen.tsx
similarity index 83%
rename from apps/mobile/src/screens/(stack)/entries/[entryId]/index.tsx
rename to apps/mobile/src/screens/(stack)/entries/[entryId]/EntryDetailScreen.tsx
index beed0c695..ad84ddea7 100644
--- a/apps/mobile/src/screens/(stack)/entries/[entryId]/index.tsx
+++ b/apps/mobile/src/screens/(stack)/entries/[entryId]/EntryDetailScreen.tsx
@@ -2,8 +2,7 @@ import { FeedViewType } from "@follow/constants"
import { PortalProvider } from "@gorhom/portal"
import { atom, useAtomValue } from "jotai"
import { useEffect, useMemo } from "react"
-import { Pressable, Text, View } from "react-native"
-import Animated, { FadeIn, FadeOut } from "react-native-reanimated"
+import { Text, View } from "react-native"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { useColor } from "react-native-uikit-colors"
@@ -13,11 +12,14 @@ import { SafeNavigationScrollView } from "@/src/components/layouts/views/SafeNav
import { EntryContentWebView } from "@/src/components/native/webview/EntryContentWebView"
import { RelativeDateTime } from "@/src/components/ui/datetime/RelativeDateTime"
import { FeedIcon } from "@/src/components/ui/icon/feed-icon"
+import { ItemPressableStyle } from "@/src/components/ui/pressable/enum"
+import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable"
import { CalendarTimeAddCuteReIcon } from "@/src/icons/calendar_time_add_cute_re"
import { openLink } from "@/src/lib/native"
import type { NavigationControllerView } from "@/src/lib/navigation/types"
import { EntryContentContext, useEntryContentContext } from "@/src/modules/entry-content/ctx"
import { EntryAISummary } from "@/src/modules/entry-content/EntryAISummary"
+import { EntryNavigationHeader } from "@/src/modules/entry-content/EntryNavigationHeader"
import { useEntry, usePrefetchEntryDetail } from "@/src/store/entry/hooks"
import { entrySyncServices } from "@/src/store/entry/store"
import type { EntryWithTranslation } from "@/src/store/entry/types"
@@ -32,7 +34,6 @@ export const EntryDetailScreen: NavigationControllerView<{
view: FeedViewType
}> = ({ entryId, view: viewType }) => {
usePrefetchEntryDetail(entryId)
- usePrefetchEntryTranslation([entryId], true)
useAutoMarkAsRead(entryId)
const entry = useEntry(entryId)
const translation = useEntryTranslation(entryId)
@@ -50,6 +51,8 @@ export const EntryDetailScreen: NavigationControllerView<{
showAISummaryAtom: atom(entry?.settings?.summary || false),
showAITranslationAtom: atom(!!entry?.settings?.translation || false),
showReadabilityAtom: atom(entry?.settings?.readability || false),
+
+ titleHeightAtom: atom(0),
}),
[entry?.settings?.readability, entry?.settings?.summary, entry?.settings?.translation],
)
@@ -65,30 +68,24 @@ export const EntryDetailScreen: NavigationControllerView<{
}
automaticallyAdjustContentInsets={false}
className="bg-system-background"
>
- entry?.url && openLink(entry.url)} className="relative py-4">
- {({ pressed }) => (
+ entry?.url && openLink(entry.url)}
+ className="relative rounded-xl py-4"
+ >
+ {viewType === FeedViewType.SocialMedia ? (
+
+ ) : (
<>
- {pressed && (
-
- )}
- {viewType === FeedViewType.SocialMedia ? (
-
- ) : (
- <>
-
-
- >
- )}
+
+
>
)}
-
+
{entryWithTranslation && (
@@ -112,6 +109,11 @@ const EntryContentWebViewWithContext = ({ entry }: { entry: EntryWithTranslation
const showReadability = useAtomValue(showReadabilityAtom)
const translationSetting = useGeneralSettingKey("translation")
const showTranslation = useAtomValue(showAITranslationAtom)
+ usePrefetchEntryTranslation({
+ entryIds: [entry.id],
+ withContent: true,
+ target: showReadability ? "readabilityContent" : "content",
+ })
return (
= ({ category }) => {
+ const { t } = useTranslation("common")
+ const { reAnimatedScrollY } = useContext(ScreenItemContext)!
+ const defaultHeaderHeight = useDefaultHeaderHeight()
+ const insets = useSafeAreaInsets()
+ return (
+
+
+
+
+
+ )
+}
diff --git a/apps/mobile/src/screens/onboarding.tsx b/apps/mobile/src/screens/OnboardingScreen.tsx
similarity index 100%
rename from apps/mobile/src/screens/onboarding.tsx
rename to apps/mobile/src/screens/OnboardingScreen.tsx
diff --git a/apps/mobile/src/screens/player.tsx b/apps/mobile/src/screens/PlayerScreen.tsx
similarity index 100%
rename from apps/mobile/src/screens/player.tsx
rename to apps/mobile/src/screens/PlayerScreen.tsx
diff --git a/apps/mobile/src/services/summary.ts b/apps/mobile/src/services/summary.ts
index db98e042f..d3d95ddfd 100644
--- a/apps/mobile/src/services/summary.ts
+++ b/apps/mobile/src/services/summary.ts
@@ -6,6 +6,13 @@ import type { SummarySchema } from "../database/schemas/types"
class SummaryServiceStatic {
async insertSummary(data: Omit) {
+ const updateExceptEmpty = Object.fromEntries(
+ Object.entries({
+ summary: data.summary,
+ readabilitySummary: data.readabilitySummary,
+ }).filter(([_, value]) => !!value),
+ )
+
await db
.insert(summariesTable)
.values({
@@ -14,9 +21,7 @@ class SummaryServiceStatic {
})
.onConflictDoUpdate({
target: [summariesTable.entryId, summariesTable.language],
- set: {
- summary: data.summary,
- },
+ set: updateExceptEmpty,
})
}
diff --git a/apps/mobile/src/services/translation.ts b/apps/mobile/src/services/translation.ts
index 22e1e6d73..7bd1744b9 100644
--- a/apps/mobile/src/services/translation.ts
+++ b/apps/mobile/src/services/translation.ts
@@ -21,6 +21,7 @@ class TranslationServiceStatic implements Hydratable, Resetable {
title: data.title,
description: data.description,
content: data.content,
+ readabilityContent: data.readabilityContent,
}).filter(([_, value]) => !!value),
)
diff --git a/apps/mobile/src/sitemap.tsx b/apps/mobile/src/sitemap.tsx
index 108255f17..7a02091f9 100644
--- a/apps/mobile/src/sitemap.tsx
+++ b/apps/mobile/src/sitemap.tsx
@@ -7,12 +7,12 @@ import { NavigationSitemapRegistry } from "./lib/navigation/sitemap/registry"
import type { NavigationControllerView } from "./lib/navigation/types"
import { OTPWindow } from "./modules/settings/components/OTPWindow"
import { TermsScreen } from "./screens/(headless)/terms"
-import { TwoFactorAuthScreen } from "./screens/(modal)/2fa"
-import { ForgetPasswordScreen } from "./screens/(modal)/forget-password"
-import { InvitationScreen } from "./screens/(modal)/invitation"
-import { LoginScreen } from "./screens/(modal)/login"
-import { SignUpScreen } from "./screens/(modal)/sign-up"
-import { OnboardingScreen } from "./screens/onboarding"
+import { ForgetPasswordScreen } from "./screens/(modal)/ForgetPasswordScreen"
+import { InvitationScreen } from "./screens/(modal)/InvitationScreen"
+import { LoginScreen } from "./screens/(modal)/LoginScreen"
+import { SignUpScreen } from "./screens/(modal)/SignUpScreen"
+import { TwoFactorAuthScreen } from "./screens/(modal)/TwoFactorAuthScreen"
+import { OnboardingScreen } from "./screens/OnboardingScreen"
export function registerSitemap() {
;[TermsScreen].forEach((Component) => {
diff --git a/apps/mobile/src/store/entry/store.ts b/apps/mobile/src/store/entry/store.ts
index 165f7c302..3d06d96f9 100644
--- a/apps/mobile/src/store/entry/store.ts
+++ b/apps/mobile/src/store/entry/store.ts
@@ -1,5 +1,4 @@
import { FeedViewType } from "@follow/constants"
-import { readability } from "@follow/utils"
import { debounce } from "es-toolkit/compat"
import { fetch as expoFetch } from "expo/fetch"
@@ -436,8 +435,19 @@ class EntrySyncServices {
const entry = honoMorph.toEntry(res.data)
if (!currentEntry && entry) {
await entryActions.upsertMany([entry])
- } else if (entry?.content && currentEntry?.content !== entry.content) {
- await entryActions.updateEntryContent({ entryId, content: entry.content })
+ } else {
+ if (entry?.content && currentEntry?.content !== entry.content) {
+ await entryActions.updateEntryContent({ entryId, content: entry.content })
+ }
+ if (
+ entry?.readabilityContent &&
+ currentEntry?.readabilityContent !== entry.readabilityContent
+ ) {
+ await entryActions.updateEntryContent({
+ entryId,
+ readabilityContent: entry.readabilityContent,
+ })
+ }
}
return entry
}
@@ -445,8 +455,12 @@ class EntrySyncServices {
async fetchEntryReadabilityContent(entryId: EntryId) {
const entry = getEntry(entryId)
- if (entry?.url && !entry?.readabilityContent) {
- const contentByFetch = await readability(entry.url)
+ if (entry?.url && entry?.readabilityContent === null) {
+ const { data: contentByFetch } = await apiClient.entries.readability.$get({
+ query: {
+ id: entryId,
+ },
+ })
if (contentByFetch?.content && entry?.readabilityContent !== contentByFetch.content) {
await entryActions.updateEntryContent({
entryId,
diff --git a/apps/mobile/src/store/messaging/hooks.ts b/apps/mobile/src/store/messaging/hooks.ts
index 4315e8618..6c6f23b47 100644
--- a/apps/mobile/src/store/messaging/hooks.ts
+++ b/apps/mobile/src/store/messaging/hooks.ts
@@ -8,7 +8,7 @@ import { apiClient } from "@/src/lib/api-fetch"
import { kv } from "@/src/lib/kv"
import { useNavigation } from "@/src/lib/navigation/hooks"
import { requestNotificationPermission } from "@/src/lib/permission"
-import { EntryDetailScreen } from "@/src/screens/(stack)/entries/[entryId]"
+import { EntryDetailScreen } from "@/src/screens/(stack)/entries/[entryId]/EntryDetailScreen"
import { useHasNotificationActions } from "../action/hooks"
import { useWhoami } from "../user/hooks"
diff --git a/apps/mobile/src/store/summary/hooks.ts b/apps/mobile/src/store/summary/hooks.ts
index 8f5ac68d4..4fccbfa62 100644
--- a/apps/mobile/src/store/summary/hooks.ts
+++ b/apps/mobile/src/store/summary/hooks.ts
@@ -12,11 +12,15 @@ export const useSummaryStatus = (entryId: string) => {
return status
}
-export const usePrefetchSummary = (entryId: string, options?: { enabled?: boolean }) => {
+export const usePrefetchSummary = (
+ entryId: string,
+ target: "content" | "readabilityContent",
+ options?: { enabled?: boolean },
+) => {
return useQuery({
- queryKey: ["summary", entryId],
+ queryKey: ["summary", entryId, target],
queryFn: () => {
- return summarySyncService.generateSummary(entryId)
+ return summarySyncService.generateSummary(entryId, target)
},
enabled: options?.enabled,
staleTime: 1000 * 60 * 60 * 24,
diff --git a/apps/mobile/src/store/summary/store.ts b/apps/mobile/src/store/summary/store.ts
index 63ada4166..e56760ada 100644
--- a/apps/mobile/src/store/summary/store.ts
+++ b/apps/mobile/src/store/summary/store.ts
@@ -13,6 +13,7 @@ type SummaryModel = Omit
interface SummaryData {
lang?: string
summary: string
+ readabilitySummary: string | null
lastAccessed: number
}
@@ -41,7 +42,9 @@ class SummaryActions {
immerSet((state) => {
state.data[summary.entryId] = {
lang: summary.language ?? undefined,
- summary: summary.summary,
+ summary: summary.summary || state.data[summary.entryId]?.summary || "",
+ readabilitySummary:
+ summary.readabilitySummary || state.data[summary.entryId]?.readabilitySummary || null,
lastAccessed: now,
}
})
@@ -96,7 +99,7 @@ export const summaryActions = new SummaryActions()
class SummarySyncService {
private pendingPromises: Record> = {}
- async generateSummary(entryId: string) {
+ async generateSummary(entryId: string, target: "content" | "readabilityContent") {
const entry = getEntry(entryId)
if (!entry) return
@@ -116,6 +119,7 @@ class SummarySyncService {
query: {
id: entryId,
language: actionLanguage as SupportedLanguages,
+ target,
},
})
.then((summary) => {
@@ -127,7 +131,11 @@ class SummarySyncService {
state.data[entryId] = {
lang: actionLanguage,
- summary: summary.data,
+ summary: target === "content" ? summary.data : state.data[entryId]?.summary || "",
+ readabilitySummary:
+ target === "readabilityContent"
+ ? summary.data
+ : state.data[entryId]?.readabilitySummary || null,
lastAccessed: Date.now(),
}
state.generatingStatus[entryId] = SummaryGeneratingStatus.Success
@@ -153,8 +161,9 @@ class SummarySyncService {
summaryActions.upsertMany([
{
entryId,
- summary,
+ summary: target === "content" ? summary : "",
language: actionLanguage ?? null,
+ readabilitySummary: target === "readabilityContent" ? summary : null,
},
])
}
diff --git a/apps/mobile/src/store/translation/hooks.ts b/apps/mobile/src/store/translation/hooks.ts
index 4861f711d..126e658f7 100644
--- a/apps/mobile/src/store/translation/hooks.ts
+++ b/apps/mobile/src/store/translation/hooks.ts
@@ -7,7 +7,15 @@ import type { SupportedLanguages } from "@/src/lib/language"
import { useEntryList } from "../entry/hooks"
import { translationSyncService, useTranslationStore } from "./store"
-export const usePrefetchEntryTranslation = (entryIds: string[], withContent?: boolean) => {
+export const usePrefetchEntryTranslation = ({
+ entryIds,
+ withContent,
+ target = "content",
+}: {
+ entryIds: string[]
+ withContent?: boolean
+ target?: "content" | "readabilityContent"
+}) => {
const translation = useGeneralSettingKey("translation")
const entryList =
useEntryList(entryIds)
@@ -18,12 +26,13 @@ export const usePrefetchEntryTranslation = (entryIds: string[], withContent?: bo
return useQueries({
queries: entryList.map((entryId) => ({
- queryKey: ["translation", entryId, actionLanguage, withContent],
+ queryKey: ["translation", entryId, actionLanguage, withContent, target],
queryFn: () =>
translationSyncService.generateTranslation({
entryId,
language: actionLanguage,
withContent,
+ target,
}),
})),
})
diff --git a/apps/mobile/src/store/translation/store.ts b/apps/mobile/src/store/translation/store.ts
index 0fb48895c..412d112f8 100644
--- a/apps/mobile/src/store/translation/store.ts
+++ b/apps/mobile/src/store/translation/store.ts
@@ -30,6 +30,7 @@ class TranslationActions {
title: translation.title,
description: translation.description,
content: translation.content,
+ readabilityContent: translation.readabilityContent,
}
if (!state.data[translation.entryId]) {
@@ -61,17 +62,19 @@ class TranslationSyncService {
entryId,
language,
withContent,
+ target,
}: {
entryId: string
language: SupportedLanguages
withContent?: boolean
+ target: "content" | "readabilityContent"
}) {
const entry = getEntry(entryId)
if (!entry) return
const translationSession = translationActions.getTranslation(entryId, language)
const fields = (
- ["title", "description", ...(withContent ? ["content"] : [])] as Array<
+ ["title", "description", ...(withContent ? [target] : [])] as Array<
"title" | "description" | "content"
>
).filter((field) => {
@@ -100,6 +103,7 @@ class TranslationSyncService {
title: res.data.title || "",
description: res.data.description || "",
content: res.data.content || "",
+ readabilityContent: res.data.readabilityContent || "",
}
await translationActions.upsertMany([translation])
diff --git a/apps/mobile/src/store/translation/types.ts b/apps/mobile/src/store/translation/types.ts
index 02b675d56..fbb86ad40 100644
--- a/apps/mobile/src/store/translation/types.ts
+++ b/apps/mobile/src/store/translation/types.ts
@@ -2,4 +2,5 @@ export interface EntryTranslation {
title: string
description: string
content: string
+ readabilityContent: string | null
}
diff --git a/apps/mobile/src/store/user/hooks.ts b/apps/mobile/src/store/user/hooks.ts
index 8d72f2c59..b8039fd43 100644
--- a/apps/mobile/src/store/user/hooks.ts
+++ b/apps/mobile/src/store/user/hooks.ts
@@ -5,7 +5,7 @@ import { useEffect } from "react"
import { apiClient } from "@/src/lib/api-fetch"
import { kv } from "@/src/lib/kv"
import { useNavigation } from "@/src/lib/navigation/hooks"
-import { OnboardingScreen } from "@/src/screens/onboarding"
+import { OnboardingScreen } from "@/src/screens/OnboardingScreen"
import { isNewUserQueryKey, isOnboardingFinishedStorageKey } from "./constants"
import { userSyncService, useUserStore } from "./store"
diff --git a/apps/mobile/src/store/user/store.ts b/apps/mobile/src/store/user/store.ts
index e8a7ed3ba..dab251149 100644
--- a/apps/mobile/src/store/user/store.ts
+++ b/apps/mobile/src/store/user/store.ts
@@ -98,9 +98,9 @@ class UserSyncService {
if (!me) throw new Error("user not login")
- const method = enabled ? twoFactor.enable : twoFactor.disable
-
- const res = await method({ password })
+ const res = enabled
+ ? await twoFactor.enable({ password })
+ : await twoFactor.disable({ password })
if (!res.error) {
immerSet((state) => {
@@ -153,6 +153,22 @@ class UserSyncService {
return res
}
+
+ async fetchUser(userId: string) {
+ const res = await apiClient.profiles.$get({ query: { id: userId } })
+ if (res.code === 0) {
+ const { whoami } = get()
+ immerSet((state) => {
+ state.users[userId] = {
+ email: null,
+ isMe: whoami?.id === userId ? 1 : 0,
+ ...res.data,
+ }
+ })
+ }
+
+ return res.data
+ }
}
class UserActions {
diff --git a/apps/mobile/web-app/html-renderer/src/components/image.tsx b/apps/mobile/web-app/html-renderer/src/components/image.tsx
index b8747efad..593d6366c 100644
--- a/apps/mobile/web-app/html-renderer/src/components/image.tsx
+++ b/apps/mobile/web-app/html-renderer/src/components/image.tsx
@@ -53,7 +53,7 @@ export const MarkdownImage = (props: HTMLProps<"img">) => {
})
}}
>
- {image?.blurhash && (
+ {image?.blurhash && scaleWidth && scaleHeight && (
) => {
{...rest}
onLoad={() => setIsLoading(false)}
style={{
- width: scaleWidth,
- height: scaleHeight,
+ width: scaleWidth || undefined,
+ height: scaleHeight || undefined,
}}
loading="lazy"
className={clsx(
- "absolute inset-0 !my-0 transition-opacity duration-500",
+ "!my-0 transition-opacity duration-500",
isLoading && "opacity-0",
+ scaleWidth && scaleHeight && "absolute inset-0",
)}
crossOrigin="anonymous"
src={src}
diff --git a/apps/ssr/client/@types/default-resource.ts b/apps/ssr/client/@types/default-resource.ts
index 80dc3ca98..5b7954e1f 100644
--- a/apps/ssr/client/@types/default-resource.ts
+++ b/apps/ssr/client/@types/default-resource.ts
@@ -21,6 +21,7 @@ import common_zhCN from "@locales/common/zh-CN.json"
import common_zhHK from "@locales/common/zh-HK.json"
import common_zhTW from "@locales/common/zh-TW.json"
import external_en from "@locales/external/en.json"
+import external_zhCN from "@locales/external/zh-CN.json"
import type { ns, SSRSupportedLanguages } from "./constants"
@@ -38,6 +39,7 @@ export const defaultResources = {
},
"zh-CN": {
common: common_zhCN,
+ external: external_zhCN,
},
"zh-HK": {
common: common_zhHK,
diff --git a/apps/ssr/client/components/items/picture.tsx b/apps/ssr/client/components/items/picture.tsx
index e8098b2b0..d9b9c2c73 100644
--- a/apps/ssr/client/components/items/picture.tsx
+++ b/apps/ssr/client/components/items/picture.tsx
@@ -19,8 +19,8 @@ import { nextFrame } from "@follow/utils/dom"
import { cn } from "@follow/utils/utils"
import dayjs from "dayjs"
import { throttle } from "es-toolkit/compat"
-import { AnimatePresence, m } from "framer-motion"
import type { RenderComponentProps } from "masonic"
+import { AnimatePresence, m } from "motion/react"
import type { FC, PropsWithChildren } from "react"
import { memo, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
import { PhotoProvider, PhotoView } from "react-photo-view"
diff --git a/apps/ssr/client/components/layout/header/index.tsx b/apps/ssr/client/components/layout/header/index.tsx
index 41e0565d5..5f00b837b 100644
--- a/apps/ssr/client/components/layout/header/index.tsx
+++ b/apps/ssr/client/components/layout/header/index.tsx
@@ -4,8 +4,8 @@ import { Logo } from "@follow/components/icons/logo.jsx"
import { Button } from "@follow/components/ui/button/index.jsx"
import { Kbd } from "@follow/components/ui/kbd/Kbd.jsx"
import { cn } from "@follow/utils/utils"
-import type { MotionValue } from "framer-motion"
-import { useMotionValueEvent, useScroll } from "framer-motion"
+import type { MotionValue } from "motion/react"
+import { useMotionValueEvent, useScroll } from "motion/react"
import * as React from "react"
import { useState } from "react"
import { useHotkeys } from "react-hotkeys-hook"
diff --git a/apps/ssr/client/initialize/analytics.ts b/apps/ssr/client/initialize/analytics.ts
index 12d738a7c..976884bee 100644
--- a/apps/ssr/client/initialize/analytics.ts
+++ b/apps/ssr/client/initialize/analytics.ts
@@ -1,4 +1,4 @@
-import { env } from "@follow/shared/env"
+import { env } from "@follow/shared/env.ssr"
import { setOpenPanelTracker } from "@follow/tracker"
import { op } from "./op"
diff --git a/apps/ssr/client/initialize/op.ts b/apps/ssr/client/initialize/op.ts
index 5cbf50cc2..489367073 100644
--- a/apps/ssr/client/initialize/op.ts
+++ b/apps/ssr/client/initialize/op.ts
@@ -1,4 +1,4 @@
-import { env } from "@follow/shared/env"
+import { env } from "@follow/shared/env.ssr"
import { OpenPanel } from "@openpanel/web"
export const op = new OpenPanel({
diff --git a/apps/ssr/client/initialize/sentry.ts b/apps/ssr/client/initialize/sentry.ts
index 2919bfe23..47586b5f3 100644
--- a/apps/ssr/client/initialize/sentry.ts
+++ b/apps/ssr/client/initialize/sentry.ts
@@ -1,4 +1,4 @@
-import { env } from "@follow/shared/env"
+import { env } from "@follow/shared/env.ssr"
import type { BrowserOptions } from "@sentry/react"
import { FetchError } from "ofetch"
diff --git a/apps/ssr/client/lib/api-fetch.ts b/apps/ssr/client/lib/api-fetch.ts
index 7afdd3d5f..396917c4c 100644
--- a/apps/ssr/client/lib/api-fetch.ts
+++ b/apps/ssr/client/lib/api-fetch.ts
@@ -1,4 +1,4 @@
-import { env } from "@follow/shared/env"
+import { env } from "@follow/shared/env.ssr"
import type { AppType } from "@follow/shared/hono"
import { hc } from "hono/client"
import { ofetch } from "ofetch"
diff --git a/apps/ssr/client/lib/auth.ts b/apps/ssr/client/lib/auth.ts
index 1b0c17fd6..c8d937488 100644
--- a/apps/ssr/client/lib/auth.ts
+++ b/apps/ssr/client/lib/auth.ts
@@ -1,50 +1,22 @@
-import { env } from "@follow/shared/env"
-import type { authPlugins } from "@follow/shared/hono"
-import type { BetterAuthClientPlugin } from "better-auth/client"
-import { inferAdditionalFields, twoFactorClient } from "better-auth/client/plugins"
-import { createAuthClient } from "better-auth/react"
+import { Auth } from "@follow/shared/auth"
+import { env } from "@follow/shared/env.ssr"
-const WEB_URL = env.VITE_WEB_URL
-type AuthPlugin = (typeof authPlugins)[number]
-const serverPlugins = [
- {
- id: "customGetProviders",
- $InferServerPlugin: {} as Extract,
- },
- {
- id: "customCreateSession",
- $InferServerPlugin: {} as Extract,
- },
- {
- id: "getAccountInfo",
- $InferServerPlugin: {} as Extract,
- },
- inferAdditionalFields({
- user: {
- handle: {
- type: "string",
- required: false,
- },
- },
- }),
-] satisfies BetterAuthClientPlugin[]
-
-const authClient = createAuthClient({
- baseURL: `${env.VITE_API_URL}/better-auth`,
- plugins: [...serverPlugins, twoFactorClient()],
+const auth = new Auth({
+ apiURL: env.VITE_API_URL,
+ webURL: env.VITE_WEB_URL,
})
// @keep-sorted
export const {
changeEmail,
changePassword,
- createSession,
forgetPassword,
getAccountInfo,
getProviders,
getSession,
linkSocial,
listAccounts,
+ oneTimeToken,
resetPassword,
sendVerificationEmail,
signIn,
@@ -53,30 +25,6 @@ export const {
twoFactor,
unlinkAccount,
updateUser,
-} = authClient
+} = auth.authClient
-export type LoginRuntime = "browser" | "app"
-export const loginHandler = async (
- provider: string,
- runtime?: LoginRuntime,
- args?: {
- email?: string
- password?: string
- headers?: Record
- },
-) => {
- const { email, password, headers } = args ?? {}
-
- if (provider === "credential") {
- if (!email || !password) {
- window.location.href = "/login"
- return
- }
- return signIn.email({ email, password }, { headers })
- }
-
- signIn.social({
- provider: provider as "google" | "github" | "apple",
- callbackURL: runtime === "app" ? `${WEB_URL}/login` : WEB_URL,
- })
-}
+export const { loginHandler } = auth
diff --git a/apps/ssr/client/lib/url-builder.ts b/apps/ssr/client/lib/url-builder.ts
index 69a5a6bf3..58b20d091 100644
--- a/apps/ssr/client/lib/url-builder.ts
+++ b/apps/ssr/client/lib/url-builder.ts
@@ -1,4 +1,4 @@
-import { env } from "@follow/shared/env"
+import { env } from "@follow/shared/env.ssr"
import { UrlBuilder as UrlBuilderClass } from "@follow/utils/url-builder"
export const UrlBuilder = new UrlBuilderClass(env.VITE_WEB_URL)
diff --git a/apps/ssr/client/modules/login/index.tsx b/apps/ssr/client/modules/login/index.tsx
index c47ded507..04c9e7577 100644
--- a/apps/ssr/client/modules/login/index.tsx
+++ b/apps/ssr/client/modules/login/index.tsx
@@ -1,5 +1,5 @@
import { UserAvatar } from "@client/components/ui/user-avatar"
-import { createSession, loginHandler, signOut, twoFactor } from "@client/lib/auth"
+import { loginHandler, oneTimeToken, signOut, twoFactor } from "@client/lib/auth"
import { queryClient } from "@client/lib/query-client"
import { useSession } from "@client/query/auth"
import { useAuthProviders } from "@client/query/users"
@@ -18,7 +18,7 @@ import { Input } from "@follow/components/ui/input/index.js"
import { LoadingCircle } from "@follow/components/ui/loading/index.jsx"
import { authProvidersConfig } from "@follow/constants"
import { DEEPLINK_SCHEME } from "@follow/shared/constants"
-import { env } from "@follow/shared/env"
+import { env } from "@follow/shared/env.ssr"
import { cn } from "@follow/utils/utils"
import { zodResolver } from "@hookform/resolvers/zod"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
@@ -29,6 +29,27 @@ import { Link, useLocation, useNavigate } from "react-router"
import { toast } from "sonner"
import { z } from "zod"
+function closeRecaptcha(recaptchaRef: React.RefObject, resetLoadingState: () => void) {
+ const handleClick = (e: MouseEvent) => {
+ const recaptchaIframeSelector =
+ 'iframe[src*="recaptcha/api2"], iframe[src*="www.recaptcha.net"], iframe[src*="google.com/recaptcha"]'
+ const recaptchaChallengeIframe = document.querySelector(recaptchaIframeSelector)
+
+ if (
+ e.target instanceof Element &&
+ recaptchaChallengeIframe &&
+ !recaptchaChallengeIframe.contains(e.target) &&
+ !e.target.closest(".g-recaptcha")
+ ) {
+ recaptchaRef.current?.reset()
+ resetLoadingState()
+ }
+ }
+
+ document.addEventListener("click", handleClick)
+ return () => document.removeEventListener("click", handleClick)
+}
+
export function Login() {
const { status, refetch } = useSession()
@@ -53,11 +74,10 @@ export function Login() {
}, [isCredentialProvider, provider, status])
const getCallbackUrl = useCallback(async () => {
- const { data } = await createSession()
+ const { data } = await oneTimeToken.generate()
if (!data) return null
return {
- url: `${DEEPLINK_SCHEME}auth?ck=${data.ck}&userId=${data.userId}`,
- userId: data.userId,
+ url: `${DEEPLINK_SCHEME}auth?token=${data.token}`,
}
}, [])
@@ -223,41 +243,67 @@ function LoginWithPassword() {
},
})
const [needTwoFactor, setNeedTwoFactor] = useState(false)
+ const [isButtonLoading, setIsButtonLoading] = useState(false)
- const { isSubmitting } = form.formState
const navigate = useNavigate()
-
const recaptchaRef = useRef(null)
+ const resetLoadingState = useCallback(() => {
+ setIsButtonLoading(false)
+ }, [])
+
+ useEffect(() => {
+ if (isButtonLoading) {
+ return closeRecaptcha(recaptchaRef, resetLoadingState)
+ }
+ }, [isButtonLoading, resetLoadingState])
+
async function onSubmit(values: z.infer) {
- if (needTwoFactor && values.code) {
- const res = await twoFactor.verifyTotp({ code: values.code })
+ setIsButtonLoading(true)
+ try {
+ if (needTwoFactor && values.code) {
+ const res = await twoFactor.verifyTotp({ code: values.code })
+ if (res?.error) {
+ toast.error(res.error.message)
+ setIsButtonLoading(false)
+ } else {
+ queryClient.invalidateQueries({ queryKey: ["auth", "session"] })
+ }
+ return
+ }
+
+ const token = await recaptchaRef.current?.executeAsync()
+ if (!token) {
+ setIsButtonLoading(false)
+ return
+ }
+
+ const res = await loginHandler("credential", "app", {
+ ...values,
+ headers: {
+ "x-token": `r2:${token}`,
+ },
+ })
+
if (res?.error) {
toast.error(res.error.message)
+ setIsButtonLoading(false)
+ return
+ }
+
+ if ((res?.data as any)?.twoFactorRedirect) {
+ setNeedTwoFactor(true)
+ form.setValue("code", "")
+ setTimeout(() => form.setFocus("code"), 0)
+ setIsButtonLoading(false)
+ return
} else {
queryClient.invalidateQueries({ queryKey: ["auth", "session"] })
}
- return
- }
-
- const token = await recaptchaRef.current?.executeAsync()
- const res = await loginHandler("credential", "app", {
- ...values,
- headers: {
- "x-token": `r2:${token}`,
- },
- })
- if (res?.error) {
- toast.error(res.error.message)
- return
- }
- if ((res?.data as any)?.twoFactorRedirect) {
- setNeedTwoFactor(true)
- form.setValue("code", "")
- form.setFocus("code")
- return
- } else {
- queryClient.invalidateQueries({ queryKey: ["auth", "session"] })
+ } catch (error) {
+ console.error("Login error:", error)
+ toast.error(t("login.errors.unknown"))
+ setIsButtonLoading(false)
}
}
@@ -271,7 +317,7 @@ function LoginWithPassword() {
{t("login.email")}
-
+
@@ -292,7 +338,7 @@ function LoginWithPassword() {
-
+
@@ -306,7 +352,7 @@ function LoginWithPassword() {
{t("login.two_factor.code")}
-
+
@@ -314,8 +360,16 @@ function LoginWithPassword() {
/>
)}
-
- {t("login.continueWith", { provider: t("words.email") })}
+
+ {needTwoFactor
+ ? t("login.two_factor.verify")
+ : t("login.continueWith", { provider: t("words.email") })}
{t("login.signUp")}
diff --git a/apps/ssr/client/pages/(login)/forget-password.tsx b/apps/ssr/client/pages/(login)/forget-password.tsx
index a9bab2f77..a09520746 100644
--- a/apps/ssr/client/pages/(login)/forget-password.tsx
+++ b/apps/ssr/client/pages/(login)/forget-password.tsx
@@ -16,10 +16,10 @@ import {
FormMessage,
} from "@follow/components/ui/form/index.jsx"
import { Input } from "@follow/components/ui/input/index.js"
-import { env } from "@follow/shared/env"
+import { env } from "@follow/shared/env.ssr"
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation } from "@tanstack/react-query"
-import { useRef } from "react"
+import { useEffect, useRef } from "react"
import ReCAPTCHA from "react-google-recaptcha"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
@@ -27,24 +27,57 @@ import { useNavigate } from "react-router"
import { toast } from "sonner"
import { z } from "zod"
-const forgetPasswordFormSchema = z.object({
- email: z.string().email(),
-})
+function closeRecaptcha(
+ recaptchaRef: React.RefObject,
+ mutation?: { reset: () => void },
+) {
+ const handleClick = (e: MouseEvent) => {
+ const recaptchaIframeSelector =
+ 'iframe[src*="recaptcha/api2"], iframe[src*="www.recaptcha.net"], iframe[src*="google.com/recaptcha"]'
+ const recaptchaChallengeIframe = document.querySelector(recaptchaIframeSelector)
+
+ if (
+ e.target instanceof Element &&
+ (!recaptchaChallengeIframe || !recaptchaChallengeIframe.contains(e.target)) &&
+ !e.target.closest(".g-recaptcha") &&
+ recaptchaChallengeIframe
+ ) {
+ recaptchaRef.current?.reset()
+ mutation?.reset()
+ }
+ }
+
+ document.addEventListener("click", handleClick)
+ return () => document.removeEventListener("click", handleClick)
+}
+
+const createEmailSchema = (t: any) =>
+ z.object({
+ email: z
+ .string()
+ .min(1, t("login.forget_password.email_required"))
+ .email(t("login.forget_password.email_invalid")),
+ })
export function Component() {
const { t } = useTranslation()
- const form = useForm>({
- resolver: zodResolver(forgetPasswordFormSchema),
+ const navigate = useNavigate()
+ const recaptchaRef = useRef(null)
+
+ const EmailSchema = createEmailSchema(t)
+
+ const form = useForm>({
+ resolver: zodResolver(EmailSchema),
defaultValues: {
email: "",
},
+ mode: "onChange",
+ delayError: 500,
})
- const recaptchaRef = useRef(null)
-
const { isValid } = form.formState
const updateMutation = useMutation({
- mutationFn: async (values: z.infer) => {
+ mutationFn: async (values: z.infer) => {
const token = await recaptchaRef.current?.executeAsync()
const res = await forgetPassword(
{
@@ -62,19 +95,25 @@ export function Component() {
}
},
onError: (error) => {
+ recaptchaRef.current?.reset()
toast.error(error.message)
},
onSuccess: () => {
toast.success(t("login.forget_password.success"))
},
+ onSettled: () => {
+ recaptchaRef.current?.reset()
+ },
})
- function onSubmit(values: z.infer) {
+ useEffect(() => {
+ return closeRecaptcha(recaptchaRef, updateMutation)
+ }, [updateMutation])
+
+ function onSubmit(values: z.infer) {
updateMutation.mutate(values)
}
- const navigate = useNavigate()
-
return (
@@ -116,7 +155,11 @@ export function Component() {
size="invisible"
/>
-
+
{t("login.submit")}
diff --git a/apps/ssr/client/pages/(login)/register.tsx b/apps/ssr/client/pages/(login)/register.tsx
index 29fd66aa2..d8d7a4b7d 100644
--- a/apps/ssr/client/pages/(login)/register.tsx
+++ b/apps/ssr/client/pages/(login)/register.tsx
@@ -10,10 +10,10 @@ import {
FormMessage,
} from "@follow/components/ui/form/index.jsx"
import { Input } from "@follow/components/ui/input/index.js"
-import { env } from "@follow/shared/env"
+import { env } from "@follow/shared/env.ssr"
import { tracker } from "@follow/tracker"
import { zodResolver } from "@hookform/resolvers/zod"
-import { useRef } from "react"
+import { useEffect, useRef, useState } from "react"
import ReCAPTCHA from "react-google-recaptcha"
import { useForm } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next"
@@ -21,6 +21,30 @@ import { Link, useNavigate } from "react-router"
import { toast } from "sonner"
import { z } from "zod"
+function closeRecaptcha(
+ recaptchaRef: React.RefObject,
+ setIsSubmitting: (value: boolean) => void,
+) {
+ const handleClick = (e: MouseEvent) => {
+ const recaptchaIframeSelector =
+ 'iframe[src*="recaptcha/api2"], iframe[src*="www.recaptcha.net"], iframe[src*="google.com/recaptcha"]'
+ const recaptchaChallengeIframe = document.querySelector(recaptchaIframeSelector)
+
+ if (
+ e.target instanceof Element &&
+ recaptchaChallengeIframe &&
+ !recaptchaChallengeIframe.contains(e.target) &&
+ !e.target.closest(".g-recaptcha")
+ ) {
+ recaptchaRef.current?.reset()
+ setIsSubmitting(false)
+ }
+ }
+
+ document.addEventListener("click", handleClick)
+ return () => document.removeEventListener("click", handleClick)
+}
+
export function Component() {
return (
@@ -43,6 +67,9 @@ const formSchema = z
function RegisterForm() {
const { t } = useTranslation()
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const navigate = useNavigate()
+ const recaptchaRef = useRef
(null)
const form = useForm>({
resolver: zodResolver(formSchema),
@@ -53,34 +80,48 @@ function RegisterForm() {
},
})
- const { isValid } = form.formState
-
- const navigate = useNavigate()
-
- const recaptchaRef = useRef(null)
+ useEffect(() => {
+ if (isSubmitting) {
+ return closeRecaptcha(recaptchaRef, setIsSubmitting)
+ }
+ }, [isSubmitting])
async function onSubmit(values: z.infer) {
- const token = await recaptchaRef.current?.executeAsync()
- return signUp.email({
- email: values.email,
- password: values.password,
- name: values.email.split("@")[0]!,
- callbackURL: "/",
- fetchOptions: {
- onSuccess() {
- tracker.register({
- type: "email",
- })
- navigate("/login")
+ setIsSubmitting(true)
+
+ try {
+ const token = await recaptchaRef.current?.executeAsync()
+
+ if (!token) {
+ return
+ }
+
+ await signUp.email({
+ email: values.email,
+ password: values.password,
+ name: values.email.split("@")[0]!,
+ callbackURL: "/",
+ fetchOptions: {
+ onSuccess() {
+ tracker.register({
+ type: "email",
+ })
+ navigate("/login")
+ },
+ onError(context) {
+ toast.error(context.error.message)
+ },
+ headers: {
+ "x-token": `r2:${token}`,
+ },
},
- onError(context) {
- toast.error(context.error.message)
- },
- headers: {
- "x-token": `r2:${token}`,
- },
- },
- })
+ })
+ } finally {
+ if (recaptchaRef.current) {
+ recaptchaRef.current.reset()
+ }
+ setIsSubmitting(false)
+ }
}
return (
@@ -109,7 +150,7 @@ function RegisterForm() {
{t("register.email")}
-
+
@@ -122,7 +163,7 @@ function RegisterForm() {
{t("register.password")}
-
+
@@ -135,14 +176,20 @@ function RegisterForm() {
{t("register.confirm_password")}
-
+
)}
/>
-
+
{t("register.submit")}
diff --git a/apps/ssr/index.ts b/apps/ssr/index.ts
index 6f30c0250..49954217d 100644
--- a/apps/ssr/index.ts
+++ b/apps/ssr/index.ts
@@ -2,7 +2,7 @@ import "./src/lib/load-env"
import middie from "@fastify/middie"
import { fastifyRequestContext } from "@fastify/request-context"
-import { env } from "@follow/shared/env"
+import { env } from "@follow/shared/env.ssr"
import type { FastifyRequest } from "fastify"
import Fastify from "fastify"
import { nanoid } from "nanoid"
diff --git a/apps/ssr/package.json b/apps/ssr/package.json
index 14afaab48..81ccfa5dd 100644
--- a/apps/ssr/package.json
+++ b/apps/ssr/package.json
@@ -15,31 +15,31 @@
"@fontsource/sn-pro": "5.2.5",
"@openpanel/web": "1.0.1",
"@resvg/resvg-js": "2.6.2",
- "@sentry/react": "9.11.0",
- "@tanstack/react-query": "5.71.10",
+ "@sentry/react": "9.13.0",
+ "@tanstack/react-query": "5.74.4",
"blurhash": "2.0.5",
"click-to-react-component": "1.1.2",
"dayjs": "1.11.13",
- "fastify": "5.2.2",
- "framer-motion": "12.6.3",
- "i18next": "24.2.3",
- "jotai": "2.12.2",
+ "fastify": "5.3.0",
+ "i18next": "25.0.0",
+ "jotai": "2.12.3",
"kose-font": "1.0.0",
"linkedom": "0.18.9",
+ "motion": "12.7.4",
"ofetch": "1.4.1",
- "rc-modal-sheet": "0.3.2",
+ "rc-modal-sheet": "1.0.0",
"react-blurhash": "0.3.0",
"react-google-recaptcha": "3.1.0",
"react-hook-form": "7.55.0",
- "react-hotkeys-hook": "4.6.1",
+ "react-hotkeys-hook": "5.0.1",
"react-i18next": "15.4.1",
"react-photo-view": "1.2.7",
- "react-router": "7.5.0",
+ "react-router": "7.5.1",
"satori": "0.12.2",
"sonner": "2.0.3",
"use-context-selector": "2.0.0",
"xss": "1.0.15",
- "zod": "3.24.2"
+ "zod": "3.24.3"
},
"devDependencies": {
"@follow/components": "workspace:*",
@@ -49,13 +49,13 @@
"@follow/shared": "workspace:*",
"@follow/types": "workspace:*",
"@follow/utils": "workspace:*",
- "@radix-ui/react-avatar": "1.1.3",
+ "@radix-ui/react-avatar": "1.1.4",
"@types/html-minifier-terser": "7.0.2",
"@types/react-google-recaptcha": "2.1.9",
"chokidar": "4.0.3",
"daisyui": "4.12.24",
"dotenv-flow": "4.1.0",
- "es-toolkit": "1.34.1",
+ "es-toolkit": "1.35.0",
"fast-glob": "3.3.3",
"foxact": "0.2.45",
"html-minifier-terser": "7.2.0",
@@ -65,6 +65,6 @@
"path-to-regexp": "8.2.0",
"tsup": "8.4.0",
"tsx": "4.19.3",
- "vite": "6.2.5"
+ "vite": "6.3.2"
}
}
diff --git a/apps/ssr/src/lib/api-client.ts b/apps/ssr/src/lib/api-client.ts
index 969fe43af..c5187f36a 100644
--- a/apps/ssr/src/lib/api-client.ts
+++ b/apps/ssr/src/lib/api-client.ts
@@ -1,7 +1,7 @@
import "./load-env"
import { requestContext } from "@fastify/request-context"
-import { env } from "@follow/shared/env"
+import { env } from "@follow/shared/env.ssr"
import type { AppType } from "@follow/shared/hono"
import { hc } from "hono/client"
import { ofetch } from "ofetch"
diff --git a/apps/ssr/src/router/global.ts b/apps/ssr/src/router/global.ts
index 75a9d4cbc..5a9402c87 100644
--- a/apps/ssr/src/router/global.ts
+++ b/apps/ssr/src/router/global.ts
@@ -1,7 +1,7 @@
import { readFileSync } from "node:fs"
import path, { resolve } from "node:path"
-import { env } from "@follow/shared/env"
+import { env } from "@follow/shared/env.ssr"
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"
import { minify } from "html-minifier-terser"
import { parseHTML } from "linkedom"
diff --git a/eslint.config.mjs b/eslint.config.mjs
index d21de4f80..e595ace97 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -43,6 +43,7 @@ export default defineConfig(
"@eslint-react/no-clone-element": 0,
"@eslint-react/hooks-extra/no-direct-set-state-in-use-effect": 0,
"@eslint-react/dom/no-flush-sync": 1,
+ "@eslint-react/hooks-extra/no-unnecessary-use-callback": "warn",
// NOTE: Disable this temporarily
"react-compiler/react-compiler": 0,
"no-restricted-syntax": 0,
diff --git a/icons/mgc/ai_cute_re.svg b/icons/mgc/ai_cute_re.svg
new file mode 100644
index 000000000..fb43faeb6
--- /dev/null
+++ b/icons/mgc/ai_cute_re.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/mgc/bell_ringing_cute_re.svg b/icons/mgc/bell_ringing_cute_re.svg
new file mode 100644
index 000000000..5bb67e1c0
--- /dev/null
+++ b/icons/mgc/bell_ringing_cute_re.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/mgc/ghost_cute_re.svg b/icons/mgc/ghost_cute_re.svg
new file mode 100644
index 000000000..f528cd9c6
--- /dev/null
+++ b/icons/mgc/ghost_cute_re.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/mgc/quill_pen_cute_re.svg b/icons/mgc/quill_pen_cute_re.svg
new file mode 100644
index 000000000..a9f59e67c
--- /dev/null
+++ b/icons/mgc/quill_pen_cute_re.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/mgc/share_3_cute_re.svg b/icons/mgc/share_3_cute_re.svg
deleted file mode 100644
index 9d4d7c73d..000000000
--- a/icons/mgc/share_3_cute_re.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/icons/mgc/translate_2_ai_cute_re.svg b/icons/mgc/translate_2_ai_cute_re.svg
new file mode 100644
index 000000000..024fe5b70
--- /dev/null
+++ b/icons/mgc/translate_2_ai_cute_re.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/mgc/webhook_cute_re.svg b/icons/mgc/webhook_cute_re.svg
new file mode 100644
index 000000000..734d0a406
--- /dev/null
+++ b/icons/mgc/webhook_cute_re.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/locales/app/en.json b/locales/app/en.json
index 870846870..e7bfdeecc 100644
--- a/locales/app/en.json
+++ b/locales/app/en.json
@@ -312,6 +312,7 @@
"sidebar.feed_actions.claim": "Claim",
"sidebar.feed_actions.claim_feed": "Claim Feed",
"sidebar.feed_actions.copy_email_address": "Copy Email Address",
+ "sidebar.feed_actions.copy_feed_badge": "Copy Feed Badge",
"sidebar.feed_actions.copy_feed_id": "Copy Feed ID",
"sidebar.feed_actions.copy_feed_url": "Copy Feed URL",
"sidebar.feed_actions.copy_list_id": "Copy List ID",
diff --git a/locales/app/zh-CN.json b/locales/app/zh-CN.json
index 9205e1d1f..8fcc96497 100644
--- a/locales/app/zh-CN.json
+++ b/locales/app/zh-CN.json
@@ -102,6 +102,7 @@
"entry_actions.open_in_browser": "在{{which}}打开",
"entry_actions.recent_reader": "最近阅读者:",
"entry_actions.save_media_to_eagle": "保存到 Eagle",
+ "entry_actions.save_to_cubox": "保存到 Cubox",
"entry_actions.save_to_instapaper": "保存到 Instapaper",
"entry_actions.save_to_obsidian": "保存到 Obsidian",
"entry_actions.save_to_outline": "保存到 Outline",
@@ -118,7 +119,7 @@
"entry_actions.star": "收藏",
"entry_actions.starred": "已收藏",
"entry_actions.tip": "打赏",
- "entry_actions.toggle_ai_summary": "切换 AI 摘要",
+ "entry_actions.toggle_ai_summary": "切换 AI 总结",
"entry_actions.toggle_ai_translation": "切换 AI 翻译",
"entry_actions.unstar": "取消收藏",
"entry_actions.unstarred": "取消收藏",
@@ -154,6 +155,8 @@
"entry_list_header.unread": "未读",
"feed.actions.follow": "订阅",
"feed.actions.followed": "已订阅",
+ "feed.entry_week_one": "{{count}} 条目/周",
+ "feed.entry_week_other": "{{count}} 条目/周",
"feed.follower_one": "订阅者",
"feed.follower_other": "订阅者",
"feed.followsAndFeeds": "在 {{appName}} 上有 {{subscriptionCount}} 个{{subscriptionNoun}}和 {{feedsCount}} 个{{feedsNoun}}",
@@ -220,6 +223,7 @@
"mark_all_read_button.confirm": "确认",
"mark_all_read_button.confirm_mark_all": "将 标记为已读?",
"mark_all_read_button.confirm_mark_all_info": "确认将全部标记为已读?",
+ "mark_all_read_button.done": "已标记",
"mark_all_read_button.mark_all_as_read": "全部标记为已读",
"mark_all_read_button.mark_as_read": "标记 为已读",
"mark_all_read_button.undo": "撤销",
@@ -239,7 +243,7 @@
"new_user_guide.step.behavior.unread_question.option1": "主动:显示时自动标记为已读。",
"new_user_guide.step.behavior.unread_question.option2": "平衡:悬停或滚出视野时自动标记为已读。",
"new_user_guide.step.behavior.unread_question.option3": "被动:仅在点击时标记为已读。",
- "new_user_guide.step.features.actions.description": "自动化规则允许你对不同的订阅执行不同的操作。\n- 使用 AI 进行摘要或翻译\n- 配置阅读条目的方式\n- 启用新条目的通知或静音\n- 重写或屏蔽特定条目\n- 将新条目发送到 webhook 地址",
+ "new_user_guide.step.features.actions.description": "自动化规则允许你对不同的订阅执行不同的操作。\n- 使用 AI 进行总结或翻译\n- 配置阅读条目的方式\n- 启用新条目的通知或静音\n- 重写或屏蔽特定条目\n- 将新条目发送到 webhook 地址",
"new_user_guide.step.features.integration.description": "集成允许你将条目保存到其他服务。目前支持的服务有:\n- Eagle\n- Readwise\n- Instapaper\n- Obsidian\n- Outline\n- Readeck",
"new_user_guide.step.migrate.profile": "设置你的个人资料",
"new_user_guide.step.migrate.title": "从 OPML 文件导入",
@@ -308,6 +312,7 @@
"sidebar.feed_actions.claim": "认证",
"sidebar.feed_actions.claim_feed": "认证订阅",
"sidebar.feed_actions.copy_email_address": "复制邮件地址",
+ "sidebar.feed_actions.copy_feed_badge": "复制订阅源徽章",
"sidebar.feed_actions.copy_feed_id": "复制 ID",
"sidebar.feed_actions.copy_feed_url": "复制链接",
"sidebar.feed_actions.copy_list_id": "复制列表 ID",
diff --git a/locales/app/zh-TW.json b/locales/app/zh-TW.json
index 43e98cd05..c5821d2d6 100644
--- a/locales/app/zh-TW.json
+++ b/locales/app/zh-TW.json
@@ -89,9 +89,10 @@
"entry_actions.deleted": "已刪除。",
"entry_actions.export_as_pdf": "匯出為 PDF",
"entry_actions.failed_to_delete": "刪除失敗。",
+ "entry_actions.failed_to_save_to_cubox": "無法儲存到 Cubox。",
"entry_actions.failed_to_save_to_eagle": "無法儲存到 Eagle。",
"entry_actions.failed_to_save_to_instapaper": "無法儲存到 Instapaper。",
- "entry_actions.failed_to_save_to_obsidian": "儲存到 Obsidian 失敗",
+ "entry_actions.failed_to_save_to_obsidian": "無法儲存到 Obsidian。",
"entry_actions.failed_to_save_to_outline": "無法儲存到 Outline。",
"entry_actions.failed_to_save_to_readeck": "無法儲存到 Readeck。",
"entry_actions.failed_to_save_to_readwise": "無法儲存到 Readwise。",
@@ -101,17 +102,19 @@
"entry_actions.open_in_browser": "在{{which}}中打開",
"entry_actions.recent_reader": "最近閲讀者:",
"entry_actions.save_media_to_eagle": "儲存媒體至 Eagle",
+ "entry_actions.save_to_cubox": "儲存到 Cubox",
"entry_actions.save_to_instapaper": "儲存到 Instapaper",
"entry_actions.save_to_obsidian": "儲存到 Obsidian",
"entry_actions.save_to_outline": "儲存到 Outline",
"entry_actions.save_to_readeck": "儲存到 Readeck",
"entry_actions.save_to_readwise": "儲存到 Readwise",
- "entry_actions.saved_to_eagle": "已儲存至 Eagle。",
- "entry_actions.saved_to_instapaper": "已儲存至 Instapaper。",
+ "entry_actions.saved_to_cubox": "已儲存到 Cubox",
+ "entry_actions.saved_to_eagle": "已儲存到 Eagle",
+ "entry_actions.saved_to_instapaper": "已儲存到 Instapaper",
"entry_actions.saved_to_obsidian": "已儲存到 Obsidian",
- "entry_actions.saved_to_outline": "已儲存至 Outline。",
- "entry_actions.saved_to_readeck": "已儲存至 Readeck。",
- "entry_actions.saved_to_readwise": "已儲存至 Readwise。",
+ "entry_actions.saved_to_outline": "已儲存到 Outline",
+ "entry_actions.saved_to_readeck": "已儲存到 Readeck",
+ "entry_actions.saved_to_readwise": "已儲存到 Readwise",
"entry_actions.share": "分享",
"entry_actions.star": "收藏",
"entry_actions.starred": "已收藏",
@@ -152,12 +155,15 @@
"entry_list_header.unread": "未讀",
"feed.actions.follow": "跟隨",
"feed.actions.followed": "已跟隨",
+ "feed.entry_week_one": "{{count}} 條目/週",
+ "feed.entry_week_other": "{{count}} 條目/週",
"feed.follower_one": "跟隨者",
"feed.follower_other": "跟隨者",
"feed.followsAndFeeds": "在 {{appName}} 上有 {{subscriptionCount}} 個 {{subscriptionNoun}} 和 {{feedsCount}} 個 {{feedsNoun}}",
"feed.followsAndReads": "在 {{appName}} 上有 {{subscriptionCount}} 個 {{subscriptionNoun}} 和 {{readCount}} 次近期 {{readNoun}}",
"feed.read_one": "閱讀",
"feed.read_other": "閱讀",
+ "feed.updated_at": "已更新",
"feed_claim_modal.choose_verification_method": "有三種驗證方式,您可以選擇其中一種進行驗證。",
"feed_claim_modal.claim_button": "認領",
"feed_claim_modal.content_instructions": "複製以下內容,發佈到需要驗證的 RSS 摘要。",
@@ -217,6 +223,7 @@
"mark_all_read_button.confirm": "確認",
"mark_all_read_button.confirm_mark_all": "將 標記為已讀?",
"mark_all_read_button.confirm_mark_all_info": "確認將全部標記為已讀?",
+ "mark_all_read_button.done": "已標記",
"mark_all_read_button.mark_all_as_read": "全部標記為已讀",
"mark_all_read_button.mark_as_read": "標記 為已讀",
"mark_all_read_button.undo": "復原",
diff --git a/locales/common/en.json b/locales/common/en.json
index fa2fed7e1..2ee5cca52 100644
--- a/locales/common/en.json
+++ b/locales/common/en.json
@@ -1,6 +1,6 @@
{
"app.copied_to_clipboard": "Copied to clipboard",
- "discover.category.all": "All",
+ "discover.category.all": "Popular",
"discover.category.anime": "ACG",
"discover.category.bbs": "BBS",
"discover.category.blog": "Blog",
diff --git a/locales/common/zh-CN.json b/locales/common/zh-CN.json
index bb4098a42..81c676aa4 100644
--- a/locales/common/zh-CN.json
+++ b/locales/common/zh-CN.json
@@ -49,6 +49,7 @@
"words.create": "创建",
"words.default": "默认",
"words.delete": "删除",
+ "words.documentation": "文档",
"words.download": "下载",
"words.edit": "编辑",
"words.entry": "条目",
diff --git a/locales/common/zh-TW.json b/locales/common/zh-TW.json
index d3ec2ad98..b2027e69f 100644
--- a/locales/common/zh-TW.json
+++ b/locales/common/zh-TW.json
@@ -49,19 +49,23 @@
"words.create": "創建",
"words.default": "預設",
"words.delete": "刪除",
+ "words.documentation": "文件",
"words.download": "下載",
"words.edit": "編輯",
"words.entry": "條目",
"words.expand": "展開",
"words.feeds": "RSS 摘要",
+ "words.finishSetup": "完成設置",
"words.follow": "跟隨",
"words.id": "ID",
"words.inbox": "收件匣",
"words.items_one": "內容",
"words.items_other": "內容",
+ "words.letsGo": "開始吧",
"words.lists": "列表",
"words.local": "本地",
"words.manage": "管理",
+ "words.next": "下一步",
"words.record": "記錄",
"words.record_one": "記錄",
"words.record_other": "記錄",
@@ -75,5 +79,6 @@
"words.starred": "收藏",
"words.submit": "提交",
"words.update": "更新",
+ "words.which.above": "以上",
"words.which.all": "全部"
}
diff --git a/locales/errors/zh-TW.json b/locales/errors/zh-TW.json
index 2919af85a..afa10c234 100644
--- a/locales/errors/zh-TW.json
+++ b/locales/errors/zh-TW.json
@@ -8,6 +8,7 @@
"1004": "無權限",
"1005": "內部錯誤",
"1006": "電子信箱未驗證",
+ "1007": "人機驗證失敗",
"1100": "超出預覽版 RSS 摘要最大數量限制",
"1101": "超出預覽版訂閲列表最大數量限制",
"1102": "超出預覽版收件箱最大數量限制",
@@ -46,6 +47,7 @@
"8001": "沒有權限存取列表",
"8002": "超出列表限制",
"8003": "此 RSS 摘要已經加入列表",
+ "8004": "無法刪除包含訂閱的列表。",
"9000": "尚未完成成就",
"9001": "成就已領取",
"9002": "成就審核中",
diff --git a/locales/external/en.json b/locales/external/en.json
index 45035373b..1ae0afa93 100644
--- a/locales/external/en.json
+++ b/locales/external/en.json
@@ -38,7 +38,10 @@
"login.confirm_password.label": "Confirm Password",
"login.continueWith": "Continue with {{provider}}",
"login.email": "Email",
+ "login.errors.unknown": "Errors Unknown",
"login.forget_password.description": "Enter the email address associated with your account and we’ll send you an email about how to reset your password.",
+ "login.forget_password.email_invalid": "Invalid email",
+ "login.forget_password.email_required": "Email required",
"login.forget_password.label": "Forget Password",
"login.forget_password.note": "Forgot your password?",
"login.forget_password.success": "Email has been sent successfully",
@@ -57,6 +60,7 @@
"login.signUp": "Sign up with email",
"login.submit": "Submit",
"login.two_factor.code": "Two Factor Code",
+ "login.two_factor.verify": "Complete Verification",
"login.welcomeTo": "Welcome to ",
"redirect.continueInBrowser": "Continue in Browser",
"redirect.instruction": "Now is the time to open {{app_name}} and safely close this page.",
diff --git a/locales/external/zh-CN.json b/locales/external/zh-CN.json
index dba4dd4db..5ecbf1105 100644
--- a/locales/external/zh-CN.json
+++ b/locales/external/zh-CN.json
@@ -38,7 +38,10 @@
"login.confirm_password.label": "确认密码",
"login.continueWith": "使用 {{provider}} 登入",
"login.email": "邮件地址",
+ "login.errors.unknown": "未知错误",
"login.forget_password.description": "请输入与你的帐户关联的邮件地址,我们将向你发送一封关于如何重置密码的邮件。",
+ "login.forget_password.email_invalid": "无效的邮箱地址",
+ "login.forget_password.email_required": "请输入邮箱",
"login.forget_password.label": "忘记密码",
"login.forget_password.note": "忘记了密码?",
"login.forget_password.success": "邮件已成功发送。",
@@ -57,6 +60,7 @@
"login.signUp": "使用邮件地址注册",
"login.submit": "提交",
"login.two_factor.code": "双重身份验证码",
+ "login.two_factor.verify": "完成验证",
"login.welcomeTo": "欢迎来到 ",
"redirect.continueInBrowser": "在浏览器中继续",
"redirect.instruction": "现在可以打开 {{app_name}} 并关闭此页面。",
diff --git a/locales/mobile/default/zh-CN.json b/locales/mobile/default/zh-CN.json
index a0a1f6bb9..f43346c3b 100644
--- a/locales/mobile/default/zh-CN.json
+++ b/locales/mobile/default/zh-CN.json
@@ -32,9 +32,11 @@
"operation.mark_all_as_read_which": "将{{which}}标记为已读",
"operation.mark_all_as_read_which_above": "以上",
"operation.mark_as_read": "标记为已读",
+ "operation.mark_as_unread": "标记为未读",
"operation.open_link": "打开链接",
"operation.rename_category": "重命名分类",
"operation.share": "分享",
+ "operation.show_error_message": "显示错误信息",
"operation.star": "收藏",
"operation.toggle_unread_only.show_all.label": "显示全部",
"operation.toggle_unread_only.show_all.success": "正在显示所有条目",
diff --git a/locales/mobile/default/zh-TW.json b/locales/mobile/default/zh-TW.json
index 92f5508c1..4ff5a65de 100644
--- a/locales/mobile/default/zh-TW.json
+++ b/locales/mobile/default/zh-TW.json
@@ -30,15 +30,24 @@
"operation.mark_all_as_read": "全部標記為已讀",
"operation.mark_all_as_read_confirm": "確認將全部標記為已讀?",
"operation.mark_all_as_read_which": "將 {{which}} 標記為已讀",
+ "operation.mark_all_as_read_which_above": "以上",
"operation.mark_as_read": "標記為已讀",
+ "operation.mark_as_unread": "標記為未讀",
+ "operation.open_link": "開啟連結",
"operation.rename_category": "重新命名類別",
"operation.share": "分享",
+ "operation.show_error_message": "顯示錯誤訊息",
"operation.star": "收藏",
+ "operation.toggle_unread_only.show_all.label": "顯示全部",
+ "operation.toggle_unread_only.show_all.success": "顯示全部條目",
+ "operation.toggle_unread_only.show_unread_only.label": "只顯示未讀",
+ "operation.toggle_unread_only.show_unread_only.success": "顯示未讀條目",
"operation.unfollow": "取消跟隨",
+ "operation.unstar": "取消收藏",
"profile.title": "{{name}} 的個人資料",
"profile.uncategorized_feeds": "未分類的 RSS 摘要",
"tabs.discover": "探索",
"tabs.home": "首頁",
"tabs.settings": "設定",
- "tabs.subscriptions": "跟隨"
+ "tabs.subscriptions": "訂閱"
}
diff --git a/locales/native/en.json b/locales/native/en.json
index 88bccdc32..b7dd0d843 100644
--- a/locales/native/en.json
+++ b/locales/native/en.json
@@ -65,6 +65,7 @@
"menu.toggleDevTools": "Toggle Developer Tools",
"menu.toggleFullScreen": "Toggle Full Screen",
"menu.undo": "Undo",
+ "menu.unread": "Unread",
"menu.view": "View",
"menu.window": "Window",
"menu.zenMode": "Zen Mode",
diff --git a/locales/native/zh-CN.json b/locales/native/zh-CN.json
index d205d52f0..b4d9e0b72 100644
--- a/locales/native/zh-CN.json
+++ b/locales/native/zh-CN.json
@@ -65,6 +65,7 @@
"menu.toggleDevTools": "切换开发者工具",
"menu.toggleFullScreen": "切换全屏",
"menu.undo": "撤销",
+ "menu.unread": "未读",
"menu.view": "视图",
"menu.window": "窗口",
"menu.zenMode": "禅定模式",
diff --git a/locales/settings/en.json b/locales/settings/en.json
index 14cc4abe2..633110d13 100644
--- a/locales/settings/en.json
+++ b/locales/settings/en.json
@@ -38,7 +38,7 @@
"actions.action_card.operation_options.matches_regex": "matches regex",
"actions.action_card.operator": "Operator",
"actions.action_card.or": "Or",
- "actions.action_card.rewrite_rules": "Rewrite Rules",
+ "actions.action_card.rewrite_rules": "Rewrite rules",
"actions.action_card.silence": "Silence",
"actions.action_card.source_content": "View source content",
"actions.action_card.then_do": "Then do…",
@@ -192,7 +192,7 @@
"general.minimize_to_tray.description": "Minimize to system tray when closing window",
"general.minimize_to_tray.label": "Minimize to tray",
"general.network": "Network",
- "general.open_links_in_app.label": "Open links in app",
+ "general.open_links_in_external_app.label": "Open links in external app",
"general.privacy": "Privacy",
"general.proxy.description": "Set proxy for network traffic routing, e.g., socks://proxy.example.com:1080",
"general.proxy.label": "Proxy",
@@ -259,6 +259,7 @@
"integration.readwise.title": "Readwise",
"integration.readwise.token.description": "You can get it here: ",
"integration.readwise.token.label": "Readwise Access Token",
+ "integration.save_ai_summary_as_description.label": "Save AI Summary as description",
"integration.sidebar_title": "Integration",
"integration.tip": "Tip: Your sensitive data is stored locally and is not uploaded to the server.",
"integration.title": "Integration",
@@ -378,6 +379,9 @@
"rsshub.add_new_instance": "Add New Instance",
"rsshub.description": "RSSHub is a community-driven open-source RSS network. Folo provides a built-in dedicated instance and uses it to support thousands of subscription contents, you can also achieve more stable content acquisition by using your own or third-party instances.",
"rsshub.public_instances": "Available Instances",
+ "rsshub.table.delete.confirm": "Are you sure you want to delete this instance?",
+ "rsshub.table.delete.label": "Delete",
+ "rsshub.table.delete.success": "Instance deleted successfully.",
"rsshub.table.description": "Description",
"rsshub.table.edit": "Edit",
"rsshub.table.inuse": "In Use",
diff --git a/locales/settings/zh-CN.json b/locales/settings/zh-CN.json
index 818f6d778..fe9ef5b43 100644
--- a/locales/settings/zh-CN.json
+++ b/locales/settings/zh-CN.json
@@ -161,8 +161,8 @@
"general.data_persist.description": "在本地保留数据以启用离线访问和本地搜索。",
"general.data_persist.label": "保留数据以供离线使用",
"general.enhanced.description": "启用增强设置可提供更多自定义选项,但也可能引发不可预见的问题。",
- "general.enhanced.disabled.tip": "增强设置已停用,你可以在「常规」设置的「高级」选项中将其启用。",
- "general.enhanced.enabled.tip": "增强设置已启用,你可以在「常规」设置的「高级」选项中将其停用。",
+ "general.enhanced.disabled.tip": "增强设置已停用,你可以在「通用」设置的「高级」选项中将其启用。",
+ "general.enhanced.enabled.tip": "增强设置已启用,你可以在「通用」设置的「高级」选项中将其停用。",
"general.enhanced.label": "增强设置",
"general.export.button": "导出",
"general.export.description": "将你的订阅源导出到 OPML 文件。",
@@ -192,7 +192,7 @@
"general.minimize_to_tray.description": "关闭窗口时最小化到系统托盘。",
"general.minimize_to_tray.label": "最小化到托盘",
"general.network": "网络",
- "general.open_links_in_app.label": "在应用内打开链接",
+ "general.open_links_in_external_app.label": "在外部应用内打开链接",
"general.privacy": "隐私",
"general.proxy.description": "代理网络请求,例如:socks://proxy.example.com:1080",
"general.proxy.label": "代理",
@@ -259,6 +259,7 @@
"integration.readwise.title": "Readwise",
"integration.readwise.token.description": "在这里获取令牌",
"integration.readwise.token.label": "Readwise 访问令牌",
+ "integration.save_ai_summary_as_description.label": "将 AI 总结保存为描述",
"integration.sidebar_title": "第三方接入",
"integration.tip": "提示:敏感数据仅在本地存储,不会被收集或上传到云端。",
"integration.title": "第三方接入",
@@ -329,7 +330,10 @@
"profile.current_password.label": "当前密码",
"profile.danger_zone": "危险区",
"profile.delete_account.label": "注销账户",
+ "profile.edit_email": "编辑邮件地址",
+ "profile.edit_profile": "编辑个人资料",
"profile.email.change": "更改邮件地址",
+ "profile.email.change_note": "如需更改邮件地址,请先验证你的新邮件地址。",
"profile.email.changed": "邮件地址已更改。",
"profile.email.changed_verification_sent": "已发送验证新邮件地址的邮件。",
"profile.email.label": "邮件地址",
@@ -338,6 +342,7 @@
"profile.email.verification_sent": "验证邮件已发送。",
"profile.email.verified": "已验证",
"profile.email.verify_email": "请验证你的邮件地址 ({{email_address}}) 以继续。",
+ "profile.email.verify_status": "你的邮件地址{{status}}",
"profile.handle.description": "你的唯一标识。",
"profile.handle.label": "唯一标识",
"profile.link_social.authentication": "身份验证",
@@ -374,6 +379,9 @@
"rsshub.add_new_instance": "添加新实例",
"rsshub.description": "RSSHub 是由社区驱动的开源 RSS 网络。Folo 提供了内建的专用实例来支持数以千计的订阅内容,你也可以通过使用自己的或第三方的实例来实现更稳定的内容获取。",
"rsshub.public_instances": "实例",
+ "rsshub.table.delete.confirm": "确定要删除此实例吗?",
+ "rsshub.table.delete.label": "删除",
+ "rsshub.table.delete.success": "实例删除成功。",
"rsshub.table.description": "描述",
"rsshub.table.edit": "编辑",
"rsshub.table.inuse": "使用中",
diff --git a/locales/settings/zh-TW.json b/locales/settings/zh-TW.json
index a65911957..5d63cb220 100644
--- a/locales/settings/zh-TW.json
+++ b/locales/settings/zh-TW.json
@@ -47,6 +47,14 @@
"actions.action_card.value": "值",
"actions.action_card.webhooks": "Webhooks",
"actions.action_card.when_feeds_match": "當 RSS 摘要匹配時…",
+ "actions.condition": "條件",
+ "actions.conditions": "條件",
+ "actions.edit_condition": "編輯條件",
+ "actions.edit_rewrite_rule": "編輯覆寫規則",
+ "actions.edit_rule": "編輯規則",
+ "actions.edit_webhook": "編輯 Webhook",
+ "actions.info": "自動化操作是您可以自動化以在伺服器或客戶端執行任務的規則集合。",
+ "actions.navigate.prompt": "您有未儲存的自動化操作變更。您確定要離開嗎?",
"actions.newRule": "新增規則",
"actions.save": "儲存",
"actions.saveSuccess": "🎉 規則已儲存",
@@ -89,6 +97,7 @@
"appearance.save": "儲存",
"appearance.sidebar": "側邊欄",
"appearance.sidebar_title": "外觀",
+ "appearance.subscriptions": "訂閱",
"appearance.text_size.default": "預設",
"appearance.text_size.label": "介面字體大小",
"appearance.text_size.large": "大",
@@ -121,25 +130,40 @@
"data_control.app_cache_limit.description": "程式快取大小的上限。一旦快取達到此上限,最早的項目將被刪除以釋放空間。",
"data_control.app_cache_limit.label": "程式快取限制",
"data_control.clean_cache.button": "清理快取",
+ "data_control.clean_cache.cancel": "取消",
+ "data_control.clean_cache.clear": "清理",
"data_control.clean_cache.description": "清理程式快取以釋放空間。",
"data_control.clean_cache.description_web": "清理網頁應用服務快取以釋放空間。",
+ "data_control.data_sources": "資料來源",
+ "data_control.export_local_database.label": "匯出本地資料庫",
+ "data_control.import_opml.label": "從 OPML 匯入訂閱",
+ "data_control.utils": "工具",
"feeds.claimTips": "要認領您的 RSS 摘要並接收贊助,請在您的訂閱列表中右鍵點擊該 RSS 摘要,然後選擇「認領」。",
"feeds.noFeeds": "沒有已認領的 RSS 摘要",
"feeds.tableHeaders.name": "名稱",
"feeds.tableHeaders.subscriptionCount": "訂閱數",
"feeds.tableHeaders.tipAmount": "收到的贊助",
+ "general.action.summary": "AI 總結",
+ "general.action.title": "自動化操作",
+ "general.action.translation": "AI 翻譯",
"general.action_language.default": "預設(介面語言)",
- "general.action_language.label": "自動化語言",
+ "general.action_language.label": "自動化翻譯語言",
+ "general.advanced": "進階",
"general.app": "App",
"general.auto_expand_long_social_media.description": "自動擴展包含長文字的社群媒體條目。",
"general.auto_expand_long_social_media.label": "拓展長社群媒體",
"general.auto_group.description": "自動依照網址域名分類 RSS 摘要。",
"general.auto_group.label": "自動分類",
"general.cache": "快取",
+ "general.content": "內容",
"general.data": "資料",
"general.data_file.label": "資料檔案",
"general.data_persist.description": "本機儲存資料以啟用離線瀏覽和離線搜尋。",
- "general.data_persist.label": "離線時儲存資料",
+ "general.data_persist.label": "儲存資料以供離線使用",
+ "general.enhanced.description": "啟用增強設定可提供更多自訂選項,但也可能引入不可預見的問題。",
+ "general.enhanced.disabled.tip": "增強設定已停用,您可以在「一般設定 - 進階」中啟用。",
+ "general.enhanced.enabled.tip": "增強設定已啟用,您可以在「一般設定 - 進階」中停用。",
+ "general.enhanced.label": "增強設定",
"general.export.button": "匯出",
"general.export.description": "匯出你的訂閱到 OPML 文件。",
"general.export.folder_mode.description": "決定您想要如何組織匯出資料夾。",
@@ -168,10 +192,12 @@
"general.minimize_to_tray.description": "關閉視窗時最小化到工作列通知區域",
"general.minimize_to_tray.label": "最小化到通知區域",
"general.network": "網路",
+ "general.open_links_in_external_app.label": "在外部 app 開啟連結",
"general.privacy": "隱私",
"general.proxy.description": "設定 Proxy 伺服器來處理網路流量,例如:socks://proxy.example.com:1080",
"general.proxy.label": "Proxy 伺服器",
"general.rebuild_database.button": "重置",
+ "general.rebuild_database.cancel": "取消",
"general.rebuild_database.description": "如果您遇到渲染問題,重置資料庫可能可以解決。",
"general.rebuild_database.label": "重置資料庫",
"general.rebuild_database.title": "重置資料庫",
@@ -188,15 +214,23 @@
"general.startup_screen.subscription": "訂閱",
"general.startup_screen.timeline": "時間軸",
"general.startup_screen.title": "開始畫面",
+ "general.subscriptions": "訂閱",
"general.timeline": "時間軸",
"general.unread": "未讀",
"general.voices": "聲音",
+ "integration.cubox.autoMemo.description": "當選取文字儲存到 Cubox 時,自動使用備忘模式。",
+ "integration.cubox.autoMemo.label": "自動備忘模式",
+ "integration.cubox.enable.description": "顯示「儲存到 Cubox」按鈕(如果可用)。",
+ "integration.cubox.enable.label": "啟用",
+ "integration.cubox.title": "Cubox",
+ "integration.cubox.token.description": "請輸入完整的 Cubox API URL,格式為:https://cubox.pro/c/api/save/xxxxxxxxx。您可以在此處獲取:",
+ "integration.cubox.token.label": "Cubox API 網址",
"integration.eagle.enable.description": "顯示「將媒體儲存到 Eagle」按鈕(如果可用)。",
"integration.eagle.enable.label": "啟用",
"integration.eagle.title": "Eagle",
"integration.instapaper.enable.description": "顯示「儲存到 Instapaper」按钮(如果可用)。",
"integration.instapaper.enable.label": "啟用",
- "integration.instapaper.password.label": "密碼",
+ "integration.instapaper.password.label": "Instapaper 密碼",
"integration.instapaper.title": "Instapaper",
"integration.instapaper.username.label": "Instapaper 使用者",
"integration.obsidian.enable.description": "顯示「儲存到 Obsidian」按鈕(如果可用)",
@@ -206,17 +240,17 @@
"integration.obsidian.vaultPath.label": "Obsidian 儲存庫路徑",
"integration.outline.collection.description": "儲存文檔的文檔集的 UUID 或 urlId",
"integration.outline.collection.label": "Outline 文檔集",
- "integration.outline.enable.description": "顯示\"儲存到 Outline\"按鈕(如果可用)",
+ "integration.outline.enable.description": "顯示'儲存到 Outline'按鈕(如果可用)",
"integration.outline.enable.label": "啟用",
- "integration.outline.endpoint.description": "此網址為 \"https://<你的 OUTLINE 域名>/api\"",
- "integration.outline.endpoint.label": "Outline 端口網址",
+ "integration.outline.endpoint.description": "此網址為 'https://<你的 OUTLINE 域名>/api'。",
+ "integration.outline.endpoint.label": "Outline API 基礎網址",
"integration.outline.title": "Outline",
"integration.outline.token.description": "在你的 Outline 帳戶設置中獲取",
"integration.outline.token.label": "Outline API 密鑰",
"integration.readeck.enable.description": "顯示\"儲存到 Readeck\"按鈕(如果可用)",
"integration.readeck.enable.label": "啟用",
- "integration.readeck.endpoint.description": "此網址為 \"https://<你的 READECK 域名>\"",
- "integration.readeck.endpoint.label": "Readeck 端口網址",
+ "integration.readeck.endpoint.description": "此網址為 'https://<你的 READECK 域名>'",
+ "integration.readeck.endpoint.label": "Readeck API 基礎網址",
"integration.readeck.title": "Readeck",
"integration.readeck.token.description": "在你的 Readeck 帳戶設置中獲取",
"integration.readeck.token.label": "Readeck API 密鑰",
@@ -224,7 +258,7 @@
"integration.readwise.enable.label": "啟用",
"integration.readwise.title": "Readwise",
"integration.readwise.token.description": "您可以在這裡取得 Token",
- "integration.readwise.token.label": "Readwise Token",
+ "integration.readwise.token.label": "Readwise 存取密鑰",
"integration.sidebar_title": "整合功能",
"integration.tip": "提示:您的敏感資料儲存於本機,不會上傳到伺服器。",
"integration.title": "整合功能",
@@ -237,8 +271,10 @@
"invitation.confirmModal.continue": "繼續",
"invitation.confirmModal.message": "產生邀請碼將會花費您 {{INVITATION_PRICE}} Power 。",
"invitation.confirmModal.title": "確認",
+ "invitation.created_at": "建立者:",
"invitation.earlyAccess": "Folo 目前處於早期開發 狀態,需要邀請碼才能使用。",
"invitation.earlyAccessMessage": "😰 抱歉,關注目前處於搶先體驗階段,需要邀請碼才能使用。",
+ "invitation.generate": "產生",
"invitation.generateButton": "產生邀請碼",
"invitation.generateCost": "您可以花費 {{INVITATION_PRICE}} Power 為您的朋友產生邀請碼。",
"invitation.getCodeMessage": "您可以通過以下方式獲取邀請碼:",
@@ -279,16 +315,24 @@
"lists.feeds.title": "標題",
"lists.image": "圖片",
"lists.info": "列表是您可以分享或出售給他人訂閱的 RSS 摘要集合。訂閱者將同步並訪問列表中的所有 RSS 摘要。",
+ "lists.manage_list": "管理列表",
"lists.noLists": "沒有列表",
+ "lists.select_feeds": "選擇要新增到當前列表的 RSS 摘要",
"lists.submit": "送出",
"lists.subscriptions": "訂閱",
"lists.title": "標題",
"lists.view": "查看",
+ "privacy.terms": "條款",
"profile.avatar.label": "頭像",
"profile.change_password.label": "修改密碼",
"profile.confirm_password.label": "確認密碼",
"profile.current_password.label": "目前密碼",
+ "profile.danger_zone": "危險操作區域",
+ "profile.delete_account.label": "刪除帳號",
+ "profile.edit_email": "編輯電子信箱",
+ "profile.edit_profile": "編輯個人資料",
"profile.email.change": "變更電子信箱",
+ "profile.email.change_note": "如果你想更改電子信箱,應該驗證你的新電子信箱。",
"profile.email.changed": "電子信箱已變更。",
"profile.email.changed_verification_sent": "已發送驗證新電子信箱的信件。",
"profile.email.label": "電子信箱",
@@ -297,6 +341,7 @@
"profile.email.verification_sent": "驗證信件已發送",
"profile.email.verified": "已驗證",
"profile.email.verify_email": "請先驗證你的電子信箱 ({{email_address}}) 後繼續。",
+ "profile.email.verify_status": "你的電子信箱目前狀態為 {{status}}",
"profile.handle.description": "你的獨一無二名稱。",
"profile.handle.label": "名稱",
"profile.link_social.authentication": "驗證",
@@ -308,6 +353,8 @@
"profile.no_password": " 重設密碼以設定新密碼。",
"profile.password.label": "密碼",
"profile.reset_password_mail_sent": "重設密碼信件已發送。",
+ "profile.security": "安全性",
+ "profile.set_avatar": "設定頭像",
"profile.sidebar_title": "個人資料",
"profile.submit": "送出",
"profile.title": "個人資料設定",
diff --git a/package.json b/package.json
index 9557a90a8..b6373c1e3 100644
--- a/package.json
+++ b/package.json
@@ -36,14 +36,14 @@
"@eslint/compat": "1.2.8",
"@iconify-json/logos": "1.2.4",
"@iconify-json/mingcute": "1.2.3",
- "@iconify-json/simple-icons": "1.2.30",
+ "@iconify-json/simple-icons": "1.2.32",
"@iconify/tools": "4.1.2",
"@tailwindcss/container-queries": "0.1.1",
"@tailwindcss/typography": "0.5.16",
"@tsslint/cli": "1.5.11",
"@tsslint/config": "1.5.11",
"@tsslint/eslint": "1.5.11",
- "@types/node": "22.14.0",
+ "@types/node": "22.14.1",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@vercel/node": "5.1.14",
@@ -52,11 +52,11 @@
"cross-env": "7.0.3",
"cssnano": "7.0.6",
"eslint": "9.24.0",
- "eslint-config-hyoban": "4.0.2",
+ "eslint-config-hyoban": "4.0.3",
"eslint-plugin-react-native": "5.0.0",
"fast-glob": "3.3.3",
- "lint-staged": "15.5.0",
- "nbump": "2.0.7",
+ "lint-staged": "15.5.1",
+ "nbump": "2.1.0",
"postcss": "8.5.3",
"postcss-js": "4.0.1",
"prettier": "3.5.3",
@@ -75,7 +75,7 @@
"tsx": "4.19.3",
"turbo": "2.5.0",
"typescript": "5.8.3",
- "vite": "6.2.5",
+ "vite": "6.3.2",
"vite-bundle-analyzer": "0.18.1",
"vite-plugin-mkcert": "1.17.8",
"vite-plugin-pwa": "1.0.0",
@@ -101,8 +101,8 @@
},
"overrides": {
"@electron/node-gyp": "10.2.0-electron.2",
- "is-core-module": "npm:@nolyfill/is-core-module@1",
- "isarray": "npm:@nolyfill/isarray@1",
+ "is-core-module": "npm:@nolyfill/is-core-module@1.0.39",
+ "isarray": "npm:@nolyfill/isarray@1.0.44",
"react": "18.3.1",
"react-dom": "18.3.1",
"unist-util-visit-parents": "5.1.3",
diff --git a/packages/atoms/package.json b/packages/atoms/package.json
index e225709a8..03c1faa61 100644
--- a/packages/atoms/package.json
+++ b/packages/atoms/package.json
@@ -19,6 +19,6 @@
"@follow/hooks": "workspace:*",
"@follow/types": "workspace:*",
"@follow/utils": "workspace:*",
- "jotai": "2.12.2"
+ "jotai": "2.12.3"
}
}
diff --git a/packages/components/package.json b/packages/components/package.json
index 077e80b72..1f89c718d 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -26,39 +26,39 @@
"@follow/shared": "workspace:*",
"@follow/types": "workspace:*",
"@follow/utils": "workspace:*",
- "@headlessui/react": "2.2.1",
+ "@headlessui/react": "2.2.2",
"@microflash/remark-callout-directives": "4.4.0",
- "@radix-ui/react-avatar": "1.1.3",
- "@radix-ui/react-checkbox": "1.1.4",
- "@radix-ui/react-context-menu": "2.2.6",
- "@radix-ui/react-dialog": "1.1.6",
- "@radix-ui/react-dropdown-menu": "2.1.6",
- "@radix-ui/react-hover-card": "1.1.6",
- "@radix-ui/react-label": "2.1.2",
- "@radix-ui/react-navigation-menu": "1.2.5",
- "@radix-ui/react-popover": "1.1.6",
- "@radix-ui/react-radio-group": "1.2.3",
- "@radix-ui/react-scroll-area": "1.2.3",
- "@radix-ui/react-select": "2.1.6",
- "@radix-ui/react-slider": "1.2.3",
- "@radix-ui/react-slot": "1.1.2",
- "@radix-ui/react-switch": "1.1.3",
- "@radix-ui/react-tabs": "1.1.3",
- "@radix-ui/react-toast": "1.2.6",
- "@radix-ui/react-tooltip": "1.1.8",
+ "@radix-ui/react-avatar": "1.1.4",
+ "@radix-ui/react-checkbox": "1.1.5",
+ "@radix-ui/react-context-menu": "2.2.7",
+ "@radix-ui/react-dialog": "1.1.7",
+ "@radix-ui/react-dropdown-menu": "2.1.7",
+ "@radix-ui/react-hover-card": "1.1.7",
+ "@radix-ui/react-label": "2.1.3",
+ "@radix-ui/react-navigation-menu": "1.2.6",
+ "@radix-ui/react-popover": "1.1.7",
+ "@radix-ui/react-radio-group": "1.2.4",
+ "@radix-ui/react-scroll-area": "1.2.4",
+ "@radix-ui/react-select": "2.1.7",
+ "@radix-ui/react-slider": "1.2.4",
+ "@radix-ui/react-slot": "1.2.0",
+ "@radix-ui/react-switch": "1.1.4",
+ "@radix-ui/react-tabs": "1.1.4",
+ "@radix-ui/react-toast": "1.2.7",
+ "@radix-ui/react-tooltip": "1.2.0",
"@react-hook/window-size": "3.1.1",
"@types/hast": "3.0.4",
"@types/unist": "3.0.3",
"class-variance-authority": "0.7.1",
"dayjs": "1.11.13",
"foxact": "0.2.45",
- "framer-motion": "12.6.3",
"hast-util-to-jsx-runtime": "2.3.6",
"hast-util-to-text": "4.0.2",
"input-otp": "1.4.2",
- "jotai": "2.12.2",
- "katex": "0.16.21",
+ "jotai": "2.12.3",
+ "katex": "0.16.22",
"masonic": "4.0.1",
+ "motion": "12.7.4",
"react-blurhash": "0.3.0",
"react-fast-marquee": "1.6.5",
"react-hook-form": "7.55.0",
diff --git a/packages/components/src/common/MotionProvider.tsx b/packages/components/src/common/MotionProvider.tsx
index 7eca3d296..898c9a31a 100644
--- a/packages/components/src/common/MotionProvider.tsx
+++ b/packages/components/src/common/MotionProvider.tsx
@@ -1,4 +1,4 @@
-import { LazyMotion, MotionConfig } from "framer-motion"
+import { LazyMotion, MotionConfig } from "motion/react"
const loadFeatures = () => import("../framer-lazy-feature").then((res) => res.default)
export const MotionProvider = ({ children }: { children: React.ReactNode }) => {
diff --git a/packages/components/src/framer-lazy-feature.ts b/packages/components/src/framer-lazy-feature.ts
index b61a58f03..1c2a9c1d8 100644
--- a/packages/components/src/framer-lazy-feature.ts
+++ b/packages/components/src/framer-lazy-feature.ts
@@ -1 +1 @@
-export { domMax as default } from "framer-motion"
+export { domMax as default } from "motion/react"
diff --git a/packages/components/src/ui/auto-resize-height/index.tsx b/packages/components/src/ui/auto-resize-height/index.tsx
index cd56382dd..bd8112009 100644
--- a/packages/components/src/ui/auto-resize-height/index.tsx
+++ b/packages/components/src/ui/auto-resize-height/index.tsx
@@ -1,6 +1,6 @@
import { cn } from "@follow/utils/utils"
-import type { Spring } from "framer-motion"
-import { m } from "framer-motion"
+import type { Spring } from "motion/react"
+import { m } from "motion/react"
import { useEffect, useRef, useState } from "react"
const softSpringPreset: Spring = {
diff --git a/packages/components/src/ui/button/action-button.tsx b/packages/components/src/ui/button/action-button.tsx
index d951c9b77..9c77b88b9 100644
--- a/packages/components/src/ui/button/action-button.tsx
+++ b/packages/components/src/ui/button/action-button.tsx
@@ -7,12 +7,13 @@ import type { Options } from "react-hotkeys-hook"
import { useHotkeys } from "react-hotkeys-hook"
import { KbdCombined } from "../kbd/Kbd"
-import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from "../tooltip"
+import { Tooltip, TooltipContent, TooltipPortal, TooltipRoot, TooltipTrigger } from "../tooltip"
export interface ActionButtonProps {
icon?: React.ReactNode | ((props: { isActive?: boolean; className: string }) => React.ReactNode)
tooltip?: React.ReactNode
tooltipSide?: "top" | "bottom"
+ tooltipDefaultOpen?: boolean
active?: boolean
disabled?: boolean
clickableDisabled?: boolean
@@ -20,6 +21,7 @@ export interface ActionButtonProps {
disableTriggerShortcut?: boolean
enableHoverableContent?: boolean
size?: "sm" | "base" | "lg"
+ id?: string
/**
* @description only trigger shortcut when focus with in ` `
@@ -43,10 +45,11 @@ export const ActionButton = React.forwardRef<
(
{
icon,
-
+ id,
tooltip,
className,
tooltipSide,
+ tooltipDefaultOpen,
children,
active,
shortcut,
@@ -126,19 +129,24 @@ export const ActionButton = React.forwardRef<
)}
{tooltip ? (
-
- {Trigger}
-
-
-
- {tooltip}
- {!!finalShortcut && (
-
- {finalShortcut}
-
- )}
-
-
+
+
+ {Trigger}
+
+
+
+ {tooltip}
+ {!!finalShortcut && (
+
+ {finalShortcut}
+
+ )}
+
+
+
) : (
Trigger
diff --git a/packages/components/src/ui/button/index.tsx b/packages/components/src/ui/button/index.tsx
index d0f47c716..743ae287c 100644
--- a/packages/components/src/ui/button/index.tsx
+++ b/packages/components/src/ui/button/index.tsx
@@ -2,8 +2,8 @@ import { useMobile } from "@follow/components/hooks/useMobile.js"
import { LoadingCircle } from "@follow/components/ui/loading/index.jsx"
import { cn } from "@follow/utils/utils"
import type { VariantProps } from "class-variance-authority"
-import type { HTMLMotionProps } from "framer-motion"
-import { m } from "framer-motion"
+import type { HTMLMotionProps } from "motion/react"
+import { m } from "motion/react"
import * as React from "react"
import { styledButtonVariant } from "./variants"
diff --git a/packages/components/src/ui/feed-icon/index.tsx b/packages/components/src/ui/feed-icon/index.tsx
index bc8d2c79d..ec2195cbb 100644
--- a/packages/components/src/ui/feed-icon/index.tsx
+++ b/packages/components/src/ui/feed-icon/index.tsx
@@ -3,7 +3,7 @@ import { getColorScheme, stringToHue } from "@follow/utils/color"
import { getImageProxyUrl } from "@follow/utils/img-proxy"
import { cn } from "@follow/utils/utils"
import { Avatar, AvatarFallback, AvatarImage } from "@radix-ui/react-avatar"
-import { m } from "framer-motion"
+import { m } from "motion/react"
import type { ReactNode } from "react"
import { forwardRef, useMemo } from "react"
diff --git a/packages/components/src/ui/input/TextArea.tsx b/packages/components/src/ui/input/TextArea.tsx
index 85a383e10..e15c33dd4 100644
--- a/packages/components/src/ui/input/TextArea.tsx
+++ b/packages/components/src/ui/input/TextArea.tsx
@@ -2,7 +2,7 @@ import { useInputComposition } from "@follow/hooks"
import { stopPropagation } from "@follow/utils/dom"
import { cn } from "@follow/utils/utils"
import clsx from "clsx"
-import { useMotionValue } from "framer-motion"
+import { useMotionValue } from "motion/react"
import type { DetailedHTMLProps, PropsWithChildren, TextareaHTMLAttributes } from "react"
import { forwardRef, useCallback, useState } from "react"
diff --git a/packages/components/src/ui/loading/index.tsx b/packages/components/src/ui/loading/index.tsx
index b13b0cd17..4cff455a6 100644
--- a/packages/components/src/ui/loading/index.tsx
+++ b/packages/components/src/ui/loading/index.tsx
@@ -1,5 +1,5 @@
import { cn } from "@follow/utils/utils"
-import { m, useAnimation } from "framer-motion"
+import { m, useAnimation } from "motion/react"
import * as React from "react"
import { cloneElement, useEffect } from "react"
diff --git a/packages/components/src/ui/masonry/index.tsx b/packages/components/src/ui/masonry/index.tsx
index e9d1e161d..91bf5ad56 100644
--- a/packages/components/src/ui/masonry/index.tsx
+++ b/packages/components/src/ui/masonry/index.tsx
@@ -2,9 +2,9 @@
import { clearRequestTimeout, requestTimeout } from "@essentials/request-timeout"
import { useWindowSize } from "@react-hook/window-size"
import { isEqual, throttle } from "es-toolkit/compat"
-import { useForceUpdate } from "framer-motion"
import type { ContainerPosition, MasonryProps, MasonryScrollerProps, Positioner } from "masonic"
import { createResizeObserver, useMasonry, usePositioner, useScrollToIndex } from "masonic"
+import { useForceUpdate } from "motion/react"
import * as React from "react"
import { useScrollViewElement } from "../scroll-area/hooks.js"
diff --git a/packages/components/src/ui/segment/index.tsx b/packages/components/src/ui/segment/index.tsx
index 309e11e0e..6e4eee232 100644
--- a/packages/components/src/ui/segment/index.tsx
+++ b/packages/components/src/ui/segment/index.tsx
@@ -1,5 +1,5 @@
import { cn } from "@follow/utils/utils"
-import { m } from "framer-motion"
+import { m } from "motion/react"
import type { ReactNode } from "react"
import { useId, useMemo, useState } from "react"
import { useContextSelector } from "use-context-selector"
diff --git a/packages/components/src/ui/tooltip/index.tsx b/packages/components/src/ui/tooltip/index.tsx
index 3b8a87e9f..edb13733f 100644
--- a/packages/components/src/ui/tooltip/index.tsx
+++ b/packages/components/src/ui/tooltip/index.tsx
@@ -1,11 +1,12 @@
import { cn } from "@follow/utils/utils"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
-import { m } from "framer-motion"
+import { m } from "motion/react"
import * as React from "react"
import { tooltipStyle } from "./styles"
const TooltipProvider = TooltipPrimitive.Provider
+const TooltipRoot = TooltipPrimitive.Root
const Tooltip: typeof TooltipProvider = ({ children, ...props }) => (
@@ -45,6 +46,6 @@ const TooltipContent = React.forwardRef<
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
-export { Tooltip, TooltipContent, TooltipTrigger }
+export { Tooltip, TooltipContent, TooltipRoot, TooltipTrigger }
export { RootPortal as TooltipPortal } from "../portal"
diff --git a/packages/constants/src/rsshub.ts b/packages/constants/src/rsshub.ts
index 0646af072..d67b769a6 100644
--- a/packages/constants/src/rsshub.ts
+++ b/packages/constants/src/rsshub.ts
@@ -23,3 +23,32 @@ export const RSSHubCategories = [
"journal",
"finance",
] as const
+
+export const CategoryMap: Record<
+ (typeof RSSHubCategories)[number],
+ { fromColor: string; toColor: string; emoji: string }
+> = {
+ all: { fromColor: "#E6C07B", toColor: "#FFCA28", emoji: "⭐️" },
+ "social-media": { fromColor: "#A0C4FF", toColor: "#42A5F5", emoji: "💬" },
+ "new-media": { fromColor: "#FFB4A2", toColor: "#FF7043", emoji: "📣" },
+ "traditional-media": { fromColor: "#C0C0C0", toColor: "#BDBDBD", emoji: "📰" },
+ bbs: { fromColor: "#FFD6A5", toColor: "#FFB74D", emoji: "💭" },
+ blog: { fromColor: "#CAA6E9", toColor: "#AB47BC", emoji: "✍️" },
+ programming: { fromColor: "#A8E6CF", toColor: "#66BB6A", emoji: "💻" },
+ design: { fromColor: "#FFB7CE", toColor: "#EC407A", emoji: "🎨" },
+ live: { fromColor: "#E07A5F", toColor: "#EF5350", emoji: "📺" },
+ multimedia: { fromColor: "#FFE5D4", toColor: "#FF8A65", emoji: "🎞️" },
+ picture: { fromColor: "#B2DBBF", toColor: "#26A69A", emoji: "🖼️" },
+ anime: { fromColor: "#FFCAD4", toColor: "#F06292", emoji: "🌸" },
+ "program-update": { fromColor: "#C2CAD0", toColor: "#78909C", emoji: "🔄" },
+ university: { fromColor: "#9DB4C0", toColor: "#5C6BC0", emoji: "🎓" },
+ forecast: { fromColor: "#A2D2FF", toColor: "#4FC3F7", emoji: "⛅️" },
+ travel: { fromColor: "#BDE0FE", toColor: "#4DD0E1", emoji: "✈️" },
+ shopping: { fromColor: "#FFADAD", toColor: "#F48FB1", emoji: "🛍️" },
+ game: { fromColor: "#BDB2FF", toColor: "#7E57C2", emoji: "🎮" },
+ reading: { fromColor: "#DDBEA9", toColor: "#8D6E63", emoji: "📖" },
+ government: { fromColor: "#C4C4C4", toColor: "#90A4AE", emoji: "🏛️" },
+ study: { fromColor: "#FFE5A0", toColor: "#FFD54F", emoji: "📚" },
+ journal: { fromColor: "#D9BF77", toColor: "#A1887F", emoji: "📓" },
+ finance: { fromColor: "#A8C686", toColor: "#81C784", emoji: "💰" },
+}
diff --git a/packages/hooks/package.json b/packages/hooks/package.json
index 6e8756a71..b876b76a0 100644
--- a/packages/hooks/package.json
+++ b/packages/hooks/package.json
@@ -17,7 +17,7 @@
"@follow/types": "workspace:*",
"@follow/utils": "workspace:*",
"foxact": "0.2.45",
- "jotai": "2.12.2",
+ "jotai": "2.12.3",
"usehooks-ts": "3.1.1"
}
}
diff --git a/packages/models/package.json b/packages/models/package.json
index 92afb4bfe..00325bd13 100644
--- a/packages/models/package.json
+++ b/packages/models/package.json
@@ -24,6 +24,6 @@
"@follow/shared": "workspace:*",
"@follow/types": "workspace:*",
"@follow/utils": "workspace:*",
- "hono": "4.7.5"
+ "hono": "4.7.6"
}
}
diff --git a/packages/shared/exports.ts b/packages/shared/exports.ts
index a5bb77cb4..d22a48456 100644
--- a/packages/shared/exports.ts
+++ b/packages/shared/exports.ts
@@ -1,5 +1,4 @@
export * from "./src/bridge"
-export * from "./src/env"
export { type AppType } from "./src/hono"
export * from "./src/hono"
export * from "./src/language"
diff --git a/packages/shared/package.json b/packages/shared/package.json
index f6fdfa706..3003b52c5 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -34,12 +34,12 @@
"dependencies": {
"@electron-toolkit/preload": "3.0.1",
"@electron-toolkit/tsconfig": "1.0.1",
- "@hono/node-server": "1.14.0",
+ "@hono/node-server": "1.14.1",
"@t3-oss/env-core": "0.12.0",
- "better-auth": "1.2.6-beta.2",
- "drizzle-orm": "0.41.0",
- "hono": "4.7.5",
+ "better-auth": "1.2.7",
+ "drizzle-orm": "0.42.0",
+ "hono": "4.7.6",
"sonner": "2.0.3",
- "zod": "3.24.2"
+ "zod": "3.24.3"
}
}
diff --git a/packages/shared/src/auth.ts b/packages/shared/src/auth.ts
new file mode 100644
index 000000000..6031654e4
--- /dev/null
+++ b/packages/shared/src/auth.ts
@@ -0,0 +1,81 @@
+import { IN_ELECTRON } from "@follow/shared"
+import type { authPlugins } from "@follow/shared/hono"
+import type { BetterAuthClientPlugin } from "better-auth/client"
+import { inferAdditionalFields, twoFactorClient } from "better-auth/client/plugins"
+import { createAuthClient } from "better-auth/react"
+
+type AuthPlugin = (typeof authPlugins)[number]
+const serverPlugins = [
+ {
+ id: "customGetProviders",
+ $InferServerPlugin: {} as Extract,
+ },
+ {
+ id: "getAccountInfo",
+ $InferServerPlugin: {} as Extract,
+ },
+ {
+ id: "oneTimeToken",
+ $InferServerPlugin: {} as Extract,
+ },
+ inferAdditionalFields({
+ user: {
+ handle: {
+ type: "string",
+ required: false,
+ },
+ },
+ }),
+] satisfies BetterAuthClientPlugin[]
+const plugins = [...serverPlugins, twoFactorClient()]
+
+export type LoginRuntime = "browser" | "app"
+
+export class Auth {
+ authClient: ReturnType<
+ typeof createAuthClient<{
+ baseURL: string
+ plugins: typeof plugins
+ }>
+ >
+
+ constructor(
+ private readonly options: {
+ apiURL: string
+ webURL: string
+ },
+ ) {
+ this.authClient = createAuthClient({
+ baseURL: `${this.options.apiURL}/better-auth`,
+ plugins,
+ })
+ }
+
+ loginHandler = async (
+ provider: string,
+ runtime?: LoginRuntime,
+ args?: {
+ email?: string
+ password?: string
+ headers?: Record
+ },
+ ) => {
+ const { email, password, headers } = args ?? {}
+ if (IN_ELECTRON && provider !== "credential") {
+ window.open(`${this.options.webURL}/login?provider=${provider}`)
+ } else {
+ if (provider === "credential") {
+ if (!email || !password) {
+ window.location.href = "/login"
+ return
+ }
+ return this.authClient.signIn.email({ email, password }, { headers })
+ }
+
+ this.authClient.signIn.social({
+ provider: provider as "google" | "github" | "apple",
+ callbackURL: runtime === "app" ? `${this.options.webURL}/login` : this.options.webURL,
+ })
+ }
+ }
+}
diff --git a/packages/shared/src/bridge.ts b/packages/shared/src/bridge.ts
index a2e6af40a..ec49de605 100644
--- a/packages/shared/src/bridge.ts
+++ b/packages/shared/src/bridge.ts
@@ -48,6 +48,7 @@ interface RenderGlobalContext {
// user data
clearIfLoginOtherAccount: (newUserId: string) => void
+ applyOneTimeToken: (token: string) => void
/// Utils
toast: typeof toast
diff --git a/packages/shared/src/env.common.ts b/packages/shared/src/env.common.ts
new file mode 100644
index 000000000..786fb5399
--- /dev/null
+++ b/packages/shared/src/env.common.ts
@@ -0,0 +1,23 @@
+export const DEFAULT_VALUES = {
+ PROD: {
+ API_URL: "https://api.follow.is",
+ WEB_URL: "https://app.follow.is",
+ INBOXES_EMAIL: "@follow.re",
+ OPENPANEL_CLIENT_ID: "4382168f-b8d2-40c1-9a26-133a312d072b",
+ OPENPANEL_API_URL: "https://openpanel.follow.is/api",
+ RECAPTCHA_V2_SITE_KEY: "6LdxQ-sqAAAAAN-_za3hUdFkJEO_cu2xHSpLKVan",
+ RECAPTCHA_V3_SITE_KEY: "6LdG-asqAAAAAEXr96565MKbRvxGEv31XEykRSHV",
+ },
+ DEV: {
+ API_URL: "https://api.dev.follow.is",
+ WEB_URL: "https://dev.follow.is",
+ INBOXES_EMAIL: "__dev@follow.re",
+ },
+ STAGING: {
+ API_URL: "https://api.follow.is",
+ WEB_URL: "https://staging.follow.is",
+ INBOXES_EMAIL: "@follow.re",
+ OPENPANEL_CLIENT_ID: "4382168f-b8d2-40c1-9a26-133a312d072b",
+ OPENPANEL_API_URL: "https://openpanel.follow.is/api",
+ },
+}
diff --git a/packages/shared/src/env.desktop.ts b/packages/shared/src/env.desktop.ts
index dfeef9ced..6766b80b4 100644
--- a/packages/shared/src/env.desktop.ts
+++ b/packages/shared/src/env.desktop.ts
@@ -4,21 +4,23 @@
import { createEnv } from "@t3-oss/env-core"
import { z } from "zod"
+import { DEFAULT_VALUES } from "./env.common"
+
export const env = createEnv({
clientPrefix: "VITE_",
client: {
- VITE_WEB_URL: z.string().url().default("https://app.follow.is"),
- VITE_API_URL: z.string(),
+ VITE_WEB_URL: z.string().url().default(DEFAULT_VALUES.PROD.WEB_URL),
+ VITE_API_URL: z.string().default(DEFAULT_VALUES.PROD.API_URL),
VITE_DEV_PROXY: z.string().optional(),
VITE_SENTRY_DSN: z.string().optional(),
- VITE_INBOXES_EMAIL: z.string().default("@follow.re"),
+ VITE_INBOXES_EMAIL: z.string().default(DEFAULT_VALUES.PROD.INBOXES_EMAIL),
VITE_FIREBASE_CONFIG: z.string().optional(),
VITE_OPENPANEL_CLIENT_ID: z.string().optional(),
VITE_OPENPANEL_API_URL: z.string().url().optional(),
- VITE_RECAPTCHA_V2_SITE_KEY: z.string().default("6LdxQ-sqAAAAAN-_za3hUdFkJEO_cu2xHSpLKVan"),
- VITE_RECAPTCHA_V3_SITE_KEY: z.string().default("6LdG-asqAAAAAEXr96565MKbRvxGEv31XEykRSHV"),
+ VITE_RECAPTCHA_V2_SITE_KEY: z.string().default(DEFAULT_VALUES.PROD.RECAPTCHA_V2_SITE_KEY),
+ VITE_RECAPTCHA_V3_SITE_KEY: z.string().default(DEFAULT_VALUES.PROD.RECAPTCHA_V3_SITE_KEY),
},
emptyStringAsUndefined: true,
diff --git a/packages/shared/src/env.rn.ts b/packages/shared/src/env.rn.ts
index 00fefe392..1cd5f6c04 100644
--- a/packages/shared/src/env.rn.ts
+++ b/packages/shared/src/env.rn.ts
@@ -1,27 +1,29 @@
/**
* This env for apps/mobile
*/
+import { DEFAULT_VALUES } from "./env.common"
+
const profile = "prod"
const envProfileMap = {
prod: {
- API_URL: "https://api.follow.is",
- WEB_URL: "https://app.follow.is",
- INBOXES_EMAIL: "@follow.re",
- OPENPANEL_CLIENT_ID: "4382168f-b8d2-40c1-9a26-133a312d072b",
- OPENPANEL_API_URL: "https://openpanel.follow.is/api",
+ API_URL: DEFAULT_VALUES.PROD.API_URL,
+ WEB_URL: DEFAULT_VALUES.PROD.WEB_URL,
+ INBOXES_EMAIL: DEFAULT_VALUES.PROD.INBOXES_EMAIL,
+ OPENPANEL_CLIENT_ID: DEFAULT_VALUES.PROD.OPENPANEL_CLIENT_ID,
+ OPENPANEL_API_URL: DEFAULT_VALUES.PROD.OPENPANEL_API_URL,
},
dev: {
- API_URL: "https://api.dev.follow.is",
- WEB_URL: "https://dev.follow.is",
- INBOXES_EMAIL: "__dev@follow.re",
+ API_URL: DEFAULT_VALUES.DEV.API_URL,
+ WEB_URL: DEFAULT_VALUES.DEV.WEB_URL,
+ INBOXES_EMAIL: DEFAULT_VALUES.DEV.INBOXES_EMAIL,
},
staging: {
- API_URL: "https://api.follow.is",
- WEB_URL: "https://staging.follow.is",
- INBOXES_EMAIL: "@follow.re",
- OPENPANEL_CLIENT_ID: "4382168f-b8d2-40c1-9a26-133a312d072b",
- OPENPANEL_API_URL: "https://openpanel.follow.is/api",
+ API_URL: DEFAULT_VALUES.STAGING.API_URL,
+ WEB_URL: DEFAULT_VALUES.STAGING.WEB_URL,
+ INBOXES_EMAIL: DEFAULT_VALUES.STAGING.INBOXES_EMAIL,
+ OPENPANEL_CLIENT_ID: DEFAULT_VALUES.STAGING.OPENPANEL_CLIENT_ID,
+ OPENPANEL_API_URL: DEFAULT_VALUES.STAGING.OPENPANEL_API_URL,
},
}
export const getEnvProfiles__dangerously = () => envProfileMap
diff --git a/packages/shared/src/env.ssr.ts b/packages/shared/src/env.ssr.ts
new file mode 100644
index 000000000..484067872
--- /dev/null
+++ b/packages/shared/src/env.ssr.ts
@@ -0,0 +1,56 @@
+/**
+ * This env for apps/ssr
+ */
+import { createEnv } from "@t3-oss/env-core"
+import { z } from "zod"
+
+import { DEFAULT_VALUES } from "./env.common"
+
+export const env = createEnv({
+ clientPrefix: "VITE_",
+ client: {
+ VITE_WEB_URL: z.string().url().default(DEFAULT_VALUES.PROD.WEB_URL),
+ VITE_API_URL: z.string().default(DEFAULT_VALUES.PROD.API_URL),
+ VITE_DEV_PROXY: z.string().optional(),
+ VITE_SENTRY_DSN: z.string().optional(),
+ VITE_INBOXES_EMAIL: z.string().default(DEFAULT_VALUES.PROD.INBOXES_EMAIL),
+ VITE_FIREBASE_CONFIG: z.string().optional(),
+
+ VITE_OPENPANEL_CLIENT_ID: z.string().optional(),
+ VITE_OPENPANEL_API_URL: z.string().url().optional(),
+
+ // For external, use api_url if you don't want to fill it in.
+ VITE_EXTERNAL_PROD_API_URL: z.string().optional(),
+ VITE_EXTERNAL_DEV_API_URL: z.string().optional(),
+ VITE_EXTERNAL_API_URL: z.string().optional(),
+ VITE_WEB_PROD_URL: z.string().optional(),
+ VITE_WEB_DEV_URL: z.string().optional(),
+
+ VITE_RECAPTCHA_V2_SITE_KEY: z.string().default(DEFAULT_VALUES.PROD.RECAPTCHA_V2_SITE_KEY),
+ VITE_RECAPTCHA_V3_SITE_KEY: z.string().default(DEFAULT_VALUES.PROD.RECAPTCHA_V3_SITE_KEY),
+ },
+
+ emptyStringAsUndefined: true,
+ runtimeEnv: getRuntimeEnv() as any,
+
+ skipValidation: "process" in globalThis ? process.env.VITEST === "true" : false,
+})
+
+function metaEnvIsEmpty() {
+ try {
+ return Object.keys(import.meta.env || {}).length === 0
+ } catch {
+ return true
+ }
+}
+
+function getRuntimeEnv() {
+ try {
+ if (metaEnvIsEmpty()) {
+ return process.env
+ }
+ return import.meta.env
+ } catch {
+ return process.env
+ }
+}
diff --git a/packages/shared/src/env.ts b/packages/shared/src/env.ts
deleted file mode 100644
index 35a42343f..000000000
--- a/packages/shared/src/env.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * This is universal env
- */
-import { z } from "zod"
-
-export const envSchema = {
- VITE_WEB_URL: z.string().url().default("https://app.follow.is"),
- VITE_API_URL: z.string().default("https://api.follow.is"),
- VITE_DEV_PROXY: z.string().optional(),
- VITE_SENTRY_DSN: z.string().optional(),
- VITE_INBOXES_EMAIL: z.string().default("@follow.re"),
- VITE_FIREBASE_CONFIG: z.string().optional(),
-
- VITE_OPENPANEL_CLIENT_ID: z.string().optional(),
- VITE_OPENPANEL_API_URL: z.string().url().optional(),
-
- // For external, use api_url if you don't want to fill it in.
- VITE_EXTERNAL_PROD_API_URL: z.string().optional(),
- VITE_EXTERNAL_DEV_API_URL: z.string().optional(),
- VITE_EXTERNAL_API_URL: z.string().optional(),
- VITE_WEB_PROD_URL: z.string().optional(),
- VITE_WEB_DEV_URL: z.string().optional(),
-
- VITE_RECAPTCHA_V2_SITE_KEY: z.string().default("6LdxQ-sqAAAAAN-_za3hUdFkJEO_cu2xHSpLKVan"),
- VITE_RECAPTCHA_V3_SITE_KEY: z.string().default("6LdG-asqAAAAAEXr96565MKbRvxGEv31XEykRSHV"),
-}
-
-export const env = z.object(envSchema).parse(process.env || {})
diff --git a/packages/shared/src/hono.ts b/packages/shared/src/hono.ts
index 39d472e90..c6365760f 100644
--- a/packages/shared/src/hono.ts
+++ b/packages/shared/src/hono.ts
@@ -2,14 +2,13 @@ import * as hono_hono_base from 'hono/hono-base';
import * as hono_types from 'hono/types';
import * as hono_utils_http_status from 'hono/utils/http-status';
import { HttpBindings } from '@hono/node-server';
-import * as better_call from 'better-call';
import * as zod from 'zod';
import { z } from 'zod';
+import * as better_call from 'better-call';
import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
import { AnyPgColumn } from 'drizzle-orm/pg-core';
import * as drizzle_orm from 'drizzle-orm';
import { InferInsertModel, SQL } from 'drizzle-orm';
-import * as better_auth_adapters_drizzle from 'better-auth/adapters/drizzle';
import * as better_auth_plugins from 'better-auth/plugins';
import * as better_auth from 'better-auth';
@@ -21,15 +20,27 @@ declare const authPlugins: ({
id: "customGetProviders";
endpoints: {
customGetProviders: {
- (inputCtx_0?: ({
+ body?: undefined;
} & {
- use: any[];
- }> | undefined)?]>(...inputCtx: C): Promise | undefined;
+ } & {
+ params?: Record;
+ } & {
+ request?: Request;
+ } & {
+ headers?: HeadersInit;
+ } & {
+ asResponse?: boolean;
+ returnHeaders?: boolean;
+ use?: better_call.Middleware[];
+ path?: string;
+ } & {
+ asResponse?: AsResponse | undefined;
+ returnHeaders?: ReturnHeaders | undefined;
+ }) | undefined): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
headers: Headers;
response: any;
} : any>;
@@ -41,49 +52,31 @@ declare const authPlugins: ({
path: "/get-providers";
};
};
-} | {
- id: "customCreateSession";
- endpoints: {
- customCreateSession: {
- | undefined)?]>(...inputCtx: C): Promise;
- options: {
- method: "GET";
- } & {
- use: any[];
- };
- path: "/create-session";
- };
- };
} | {
id: "getAccountInfo";
endpoints: {
getAccountInfo: {
- (inputCtx_0?: ({
+ body?: undefined;
} & {
- use: any[];
- }> | undefined)?]>(...inputCtx: C): Promise | undefined;
+ } & {
+ params?: Record;
+ } & {
+ request?: Request;
+ } & {
+ headers?: HeadersInit;
+ } & {
+ asResponse?: boolean;
+ returnHeaders?: boolean;
+ use?: better_call.Middleware[];
+ path?: string;
+ } & {
+ asResponse?: AsResponse | undefined;
+ returnHeaders?: ReturnHeaders | undefined;
+ }) | undefined): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
headers: Headers;
response: {
id: string;
@@ -119,15 +112,27 @@ declare const authPlugins: ({
id: "customUpdateUser";
endpoints: {
customUpdateUser: {
- (inputCtx_0?: ({
+ body?: undefined;
} & {
- use: any[];
- }> | undefined)?]>(...inputCtx: C): Promise | undefined;
+ } & {
+ params?: Record;
+ } & {
+ request?: Request;
+ } & {
+ headers?: HeadersInit;
+ } & {
+ asResponse?: boolean;
+ returnHeaders?: boolean;
+ use?: better_call.Middleware[];
+ path?: string;
+ } & {
+ asResponse?: AsResponse | undefined;
+ returnHeaders?: ReturnHeaders | undefined;
+ }) | undefined): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
headers: Headers;
response: string | null;
} : string | null>;
@@ -139,6 +144,172 @@ declare const authPlugins: ({
path: "/update-user-ccc";
};
};
+} | {
+ id: "oneTimeToken";
+ endpoints: {
+ generateOneTimeToken: {
+ (inputCtx_0?: ({
+ body?: undefined;
+ } & {
+ method?: "GET" | undefined;
+ } & {
+ query?: Record | undefined;
+ } & {
+ params?: Record;
+ } & {
+ request?: Request;
+ } & {
+ headers?: HeadersInit;
+ } & {
+ asResponse?: boolean;
+ returnHeaders?: boolean;
+ use?: better_call.Middleware[];
+ path?: string;
+ } & {
+ asResponse?: AsResponse | undefined;
+ returnHeaders?: ReturnHeaders | undefined;
+ }) | undefined): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
+ headers: Headers;
+ response: {
+ token: string;
+ };
+ } : {
+ token: string;
+ }>;
+ options: {
+ method: "GET";
+ use: ((inputContext: better_call.MiddlewareInputContext) => Promise<{
+ session: {
+ session: Record & {
+ id: string;
+ createdAt: Date;
+ updatedAt: Date;
+ userId: string;
+ expiresAt: Date;
+ token: string;
+ ipAddress?: string | null | undefined;
+ userAgent?: string | null | undefined;
+ };
+ user: Record & {
+ id: string;
+ name: string;
+ email: string;
+ emailVerified: boolean;
+ createdAt: Date;
+ updatedAt: Date;
+ image?: string | null | undefined;
+ };
+ };
+ }>)[];
+ } & {
+ use: any[];
+ };
+ path: "/one-time-token/generate";
+ };
+ applyOneTimeToken: {
+ (inputCtx_0: {
+ body: {
+ token: string;
+ };
+ } & {
+ method?: "POST" | undefined;
+ } & {
+ query?: Record | undefined;
+ } & {
+ params?: Record;
+ } & {
+ request?: Request;
+ } & {
+ headers?: HeadersInit;
+ } & {
+ asResponse?: boolean;
+ returnHeaders?: boolean;
+ use?: better_call.Middleware[];
+ path?: string;
+ } & {
+ asResponse?: AsResponse | undefined;
+ returnHeaders?: ReturnHeaders | undefined;
+ }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
+ headers: Headers;
+ response: {
+ user: {
+ id: string;
+ name: string;
+ email: string;
+ emailVerified: boolean;
+ createdAt: Date;
+ updatedAt: Date;
+ image?: string | null | undefined;
+ } & Record;
+ };
+ } : {
+ user: {
+ id: string;
+ name: string;
+ email: string;
+ emailVerified: boolean;
+ createdAt: Date;
+ updatedAt: Date;
+ image?: string | null | undefined;
+ } & Record;
+ }>;
+ options: {
+ method: "POST";
+ body: zod.ZodObject<{
+ token: zod.ZodString;
+ }, "strip", zod.ZodTypeAny, {
+ token: string;
+ }, {
+ token: string;
+ }>;
+ } & {
+ use: any[];
+ };
+ path: "/one-time-token/apply";
+ };
+ };
+} | {
+ id: "customCreateSession";
+ endpoints: {
+ customCreateSession: {
+ (inputCtx_0?: ({
+ body?: undefined;
+ } & {
+ method?: "GET" | undefined;
+ } & {
+ query?: Record | undefined;
+ } & {
+ params?: Record;
+ } & {
+ request?: Request;
+ } & {
+ headers?: HeadersInit;
+ } & {
+ asResponse?: boolean;
+ returnHeaders?: boolean;
+ use?: better_call.Middleware[];
+ path?: string;
+ } & {
+ asResponse?: AsResponse | undefined;
+ returnHeaders?: ReturnHeaders | undefined;
+ }) | undefined): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
+ headers: Headers;
+ response: {
+ ck: string;
+ userId: string;
+ } | null;
+ } : {
+ ck: string;
+ userId: string;
+ } | null>;
+ options: {
+ method: "GET";
+ } & {
+ use: any[];
+ };
+ path: "/create-session";
+ };
+ };
})[];
declare const achievements: drizzle_orm_pg_core.PgTableWithColumns<{
@@ -4629,6 +4800,65 @@ type MessagingData = {
description: string;
};
+declare const readabilities: drizzle_orm_pg_core.PgTableWithColumns<{
+ name: "readabilities";
+ schema: undefined;
+ columns: {
+ id: drizzle_orm_pg_core.PgColumn<{
+ name: "id";
+ tableName: "readabilities";
+ dataType: "string";
+ columnType: "PgText";
+ data: string;
+ driverParam: string;
+ notNull: true;
+ hasDefault: true;
+ isPrimaryKey: true;
+ isAutoincrement: false;
+ hasRuntimeDefault: true;
+ enumValues: [string, ...string[]];
+ baseColumn: never;
+ identity: undefined;
+ generated: undefined;
+ }, {}, {}>;
+ entryId: drizzle_orm_pg_core.PgColumn<{
+ name: "entry_id";
+ tableName: "readabilities";
+ dataType: "string";
+ columnType: "PgText";
+ data: string;
+ driverParam: string;
+ notNull: true;
+ hasDefault: false;
+ isPrimaryKey: false;
+ isAutoincrement: false;
+ hasRuntimeDefault: false;
+ enumValues: [string, ...string[]];
+ baseColumn: never;
+ identity: undefined;
+ generated: undefined;
+ }, {}, {}>;
+ content: drizzle_orm_pg_core.PgColumn<{
+ name: "content";
+ tableName: "readabilities";
+ dataType: "string";
+ columnType: "PgText";
+ data: string;
+ driverParam: string;
+ notNull: false;
+ hasDefault: false;
+ isPrimaryKey: false;
+ isAutoincrement: false;
+ hasRuntimeDefault: false;
+ enumValues: [string, ...string[]];
+ baseColumn: never;
+ identity: undefined;
+ generated: undefined;
+ }, {}, {}>;
+ };
+ dialect: "pg";
+}>;
+
declare const rsshub: drizzle_orm_pg_core.PgTableWithColumns<{
name: "rsshub";
schema: undefined;
@@ -7004,38 +7234,27 @@ declare const auth: {
handler: (request: Request) => Promise;
api: better_auth.InferAPI<{
ok: {
- (inputCtx_0?: ({
+ body?: undefined;
} & {
- use: any[];
- }> | undefined)?]>(...inputCtx: C): Promise | undefined;
+ } & {
+ params?: Record;
+ } & {
+ request?: Request;
+ } & {
+ headers?: HeadersInit;
+ } & {
+ asResponse?: boolean;
+ returnHeaders?: boolean;
+ use?: better_call.Middleware[];
+ path?: string;
+ } & {
+ asResponse?: AsResponse | undefined;
+ returnHeaders?: ReturnHeaders | undefined;
+ }) | undefined): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
headers: Headers;
response: {
ok: boolean;
@@ -7058,8 +7277,10 @@ declare const auth: {
properties: {
ok: {
type: string;
+ description: string;
};
};
+ required: string[];
};
};
};
@@ -7074,33 +7295,27 @@ declare const auth: {
path: "/ok";
};
error: {
- (inputCtx_0?: ({
+ body?: undefined;
} & {
- use: any[];
- }> | undefined)?]>(...inputCtx: C): Promise | undefined;
+ } & {
+ params?: Record;
+ } & {
+ request?: Request;
+ } & {
+ headers?: HeadersInit;
+ } & {
+ asResponse?: boolean;
+ returnHeaders?: boolean;
+ use?: better_call.Middleware[];
+ path?: string;
+ } & {
+ asResponse?: AsResponse | undefined;
+ returnHeaders?: ReturnHeaders | undefined;
+ }) | undefined): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
headers: Headers;
response: Response;
} : Response>;
@@ -7116,6 +7331,7 @@ declare const auth: {
"text/html": {
schema: {
type: "string";
+ description: string;
};
};
};
@@ -7130,38 +7346,9 @@ declare const auth: {
path: "/error";
};
signInSocial: {
- ;
- newUserCallbackURL: zod.ZodOptional;
- errorCallbackURL: zod.ZodOptional;
- provider: zod.ZodEnum<["github", ...("apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "vk" | "kick")[]]>;
- disableRedirect: zod.ZodOptional;
- idToken: zod.ZodOptional;
- accessToken: zod.ZodOptional;
- refreshToken: zod.ZodOptional;
- expiresAt: zod.ZodOptional;
- }, "strip", zod.ZodTypeAny, {
- token: string;
- refreshToken?: string | undefined;
- accessToken?: string | undefined;
- expiresAt?: number | undefined;
- nonce?: string | undefined;
- }, {
- token: string;
- refreshToken?: string | undefined;
- accessToken?: string | undefined;
- expiresAt?: number | undefined;
- nonce?: string | undefined;
- }>>;
- scopes: zod.ZodOptional>;
- requestSignUp: zod.ZodOptional;
- loginHint: zod.ZodOptional;
- }, "strip", zod.ZodTypeAny, {
- provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "vk" | "kick";
+ (inputCtx_0: {
+ body: {
+ provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "vk" | "kick" | "zoom";
scopes?: string[] | undefined;
loginHint?: string | undefined;
idToken?: {
@@ -7176,63 +7363,26 @@ declare const auth: {
errorCallbackURL?: string | undefined;
newUserCallbackURL?: string | undefined;
disableRedirect?: boolean | undefined;
- }, {
- provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "vk" | "kick";
- scopes?: string[] | undefined;
- loginHint?: string | undefined;
- idToken?: {
- token: string;
- refreshToken?: string | undefined;
- accessToken?: string | undefined;
- expiresAt?: number | undefined;
- nonce?: string | undefined;
- } | undefined;
- callbackURL?: string | undefined;
- requestSignUp?: boolean | undefined;
- errorCallbackURL?: string | undefined;
- newUserCallbackURL?: string | undefined;
- disableRedirect?: boolean | undefined;
- }>;
- metadata: {
- openapi: {
- description: string;
- responses: {
- "200": {
- description: string;
- content: {
- "application/json": {
- schema: {
- type: "object";
- properties: {
- token: {
- type: string;
- };
- user: {
- type: string;
- ref: string;
- };
- url: {
- type: string;
- };
- redirect: {
- type: string;
- };
- };
- required: string[];
- };
- };
- };
- };
- };
- };
};
} & {
- use: any[];
- }>]>(...inputCtx: C): Promise | undefined;
+ } & {
+ params?: Record;
+ } & {
+ request?: Request;
+ } & {
+ headers?: HeadersInit;
+ } & {
+ asResponse?: boolean;
+ returnHeaders?: boolean;
+ use?: better_call.Middleware[];
+ path?: string;
+ } & {
+ asResponse?: AsResponse | undefined;
+ returnHeaders?: ReturnHeaders | undefined;
+ }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
headers: Headers;
response: {
redirect: boolean;
@@ -7274,7 +7424,7 @@ declare const auth: {
callbackURL: zod.ZodOptional;
newUserCallbackURL: zod.ZodOptional;
errorCallbackURL: zod.ZodOptional;
- provider: zod.ZodEnum<["github", ...("apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "vk" | "kick")[]]>;
+ provider: zod.ZodEnum<["github", ...("apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "vk" | "kick" | "zoom")[]]>;
disableRedirect: zod.ZodOptional;
idToken: zod.ZodOptional;
loginHint: zod.ZodOptional;
}, "strip", zod.ZodTypeAny, {
- provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "vk" | "kick";
+ provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "vk" | "kick" | "zoom";
scopes?: string[] | undefined;
loginHint?: string | undefined;
idToken?: {
@@ -7315,7 +7465,7 @@ declare const auth: {
newUserCallbackURL?: string | undefined;
disableRedirect?: boolean | undefined;
}, {
- provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "vk" | "kick";
+ provider: "apple" | "discord" | "facebook" | "github" | "google" | "microsoft" | "spotify" | "twitch" | "twitter" | "dropbox" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "vk" | "kick" | "zoom";
scopes?: string[] | undefined;
loginHint?: string | undefined;
idToken?: {
@@ -7334,6 +7484,7 @@ declare const auth: {
metadata: {
openapi: {
description: string;
+ operationId: string;
responses: {
"200": {
description: string;
@@ -7341,22 +7492,53 @@ declare const auth: {
"application/json": {
schema: {
type: "object";
+ description: string;
properties: {
- token: {
- type: string;
- };
- user: {
- type: string;
- ref: string;
- };
- url: {
- type: string;
- };
redirect: {
type: string;
+ enum: boolean[];
};
+ token: {
+ type: string;
+ description: string;
+ url: {
+ type: string;
+ nullable: boolean;
+ };
+ user: {
+ type: string;
+ properties: {
+ id: {
+ type: string;
+ };
+ email: {
+ type: string;
+ };
+ name: {
+ type: string;
+ nullable: boolean;
+ };
+ image: {
+ type: string;
+ nullable: boolean;
+ };
+ emailVerified: {
+ type: string;
+ };
+ createdAt: {
+ type: string;
+ format: string;
+ };
+ updatedAt: {
+ type: string;
+ format: string;
+ };
+ };
+ required: string[];
+ };
+ };
+ required: string[];
};
- required: string[];
};
};
};
@@ -7370,56 +7552,43 @@ declare const auth: {
path: "/sign-in/social";
};
callbackOAuth: {
- ;
- error: zod.ZodOptional;
- device_id: zod.ZodOptional;
- error_description: zod.ZodOptional;
- state: zod.ZodOptional;
- }, "strip", zod.ZodTypeAny, {
+ (inputCtx_0: {
+ body?: {
state?: string | undefined;
code?: string | undefined;
device_id?: string | undefined;
error?: string | undefined;
+ user?: string | undefined;
error_description?: string | undefined;
- }, {
+ } | undefined;
+ } & {
+ method: "GET" | "POST";
+ } & {
+ query?: {
state?: string | undefined;
code?: string | undefined;
device_id?: string | undefined;
error?: string | undefined;
+ user?: string | undefined;
error_description?: string | undefined;
- }>>;
- query: zod.ZodOptional;
- error: zod.ZodOptional;
- device_id: zod.ZodOptional;
- error_description: zod.ZodOptional;
- state: zod.ZodOptional;
- }, "strip", zod.ZodTypeAny, {
- state?: string | undefined;
- code?: string | undefined;
- device_id?: string | undefined;
- error?: string | undefined;
- error_description?: string | undefined;
- }, {
- state?: string | undefined;
- code?: string | undefined;
- device_id?: string | undefined;
- error?: string | undefined;
- error_description?: string | undefined;
- }>>;
- metadata: {
- isAction: false;
+ } | undefined;
+ } & {
+ params: {
+ id: string;
};
} & {
- use: any[];
- }>]>(...inputCtx: C): Promise;
@@ -7431,17 +7600,20 @@ declare const auth: {
device_id: zod.ZodOptional;
error_description: zod.ZodOptional;
state: zod.ZodOptional;
+ user: zod.ZodOptional;
}, "strip", zod.ZodTypeAny, {
state?: string | undefined;
code?: string | undefined;
device_id?: string | undefined;
error?: string | undefined;
+ user?: string | undefined;
error_description?: string | undefined;
}, {
state?: string | undefined;
code?: string | undefined;
device_id?: string | undefined;
error?: string | undefined;
+ user?: string | undefined;
error_description?: string | undefined;
}>>;
query: zod.ZodOptional;
error_description: zod.ZodOptional;
state: zod.ZodOptional;
+ user: zod.ZodOptional;
}, "strip", zod.ZodTypeAny, {
state?: string | undefined;
code?: string | undefined;
device_id?: string | undefined;
error?: string | undefined;
+ user?: string | undefined;
error_description?: string | undefined;
}, {
state?: string | undefined;
code?: string | undefined;
device_id?: string | undefined;
error?: string | undefined;
+ user?: string | undefined;
error_description?: string | undefined;
}>>;
metadata: {
@@ -7472,53 +7647,30 @@ declare const auth: {
path: "/callback/:id";
};
getSession: {
- ]>>>;
- disableRefresh: zod.ZodOptional]>>;
- }, "strip", zod.ZodTypeAny, {
- disableCookieCache?: boolean | undefined;
- disableRefresh?: boolean | undefined;
- }, {
+ (inputCtx_0: {
+ body?: undefined;
+ } & {
+ method?: "GET" | undefined;
+ } & {
+ query?: {
disableCookieCache?: string | boolean | undefined;
disableRefresh?: string | boolean | undefined;
- }>>;
- requireHeaders: true;
- metadata: {
- openapi: {
- description: string;
- responses: {
- "200": {
- description: string;
- content: {
- "application/json": {
- schema: {
- type: "object";
- properties: {
- session: {
- type: string;
- $ref: string;
- };
- user: {
- type: string;
- $ref: string;
- };
- };
- };
- };
- };
- };
- };
- };
- };
+ } | undefined;
} & {
- use: any[];
- }>]>(...inputCtx: C): Promise;
+ } & {
+ request?: Request;
+ } & {
+ headers: HeadersInit;
+ } & {
+ asResponse?: boolean;
+ returnHeaders?: boolean;
+ use?: better_call.Middleware[];
+ path?: string;
+ } & {
+ asResponse?: AsResponse | undefined;
+ returnHeaders?: ReturnHeaders | undefined;
+ }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
headers: Headers;
response: {
session: {
@@ -7550,16 +7702,6 @@ declare const auth: {
image?: string | null | undefined | undefined;
twoFactorEnabled: boolean | null | undefined;
handle: string;
- } & {
- id: string;
- name: string;
- email: string;
- emailVerified: boolean;
- createdAt: Date;
- updatedAt: Date;
- image?: string | null | undefined | undefined;
- isAnonymous?: boolean | null | undefined;
- handle: string;
};
} | null;
} : {
@@ -7592,16 +7734,6 @@ declare const auth: {
image?: string | null | undefined | undefined;
twoFactorEnabled: boolean | null | undefined;
handle: string;
- } & {
- id: string;
- name: string;
- email: string;
- emailVerified: boolean;
- createdAt: Date;
- updatedAt: Date;
- image?: string | null | undefined | undefined;
- isAnonymous?: boolean | null | undefined;
- handle: string;
};
} | null>;
options: {
@@ -7629,14 +7761,13 @@ declare const auth: {
type: "object";
properties: {
session: {
- type: string;
$ref: string;
};
user: {
- type: string;
$ref: string;
};
};
+ required: string[];
};
};
};
@@ -7650,38 +7781,27 @@ declare const auth: {
path: "/get-session";
};
signOut: {
- (inputCtx_0: {
+ body?: undefined;
} & {
- use: any[];
- }>]>(...inputCtx: C): Promise | undefined;
+ } & {
+ params?: Record;
+ } & {
+ request?: Request;
+ } & {
+ headers: HeadersInit;
+ } & {
+ asResponse?: boolean;
+ returnHeaders?: boolean;
+ use?: better_call.Middleware[];
+ path?: string;
+ } & {
+ asResponse?: AsResponse | undefined;
+ returnHeaders?: ReturnHeaders | undefined;
+ }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
headers: Headers;
response: {
success: boolean;
@@ -7720,96 +7840,39 @@ declare const auth: {
path: "/sign-out";
};
signUpEmail: {
- ;
- metadata: {
- $Infer: {
- body: ({
- name: string;
- email: string;
- password: string;
- } & ({} | ({} & {}) | ({} & {
- isAnonymous?: boolean | null | undefined;
- }))) & {
- handle: string;
- } & {
- handle?: string | null | undefined;
- };
- };
- openapi: {
- description: string;
- requestBody: {
- content: {
- "application/json": {
- schema: {
- type: "object";
- properties: {
- name: {
- type: string;
- description: string;
- };
- email: {
- type: string;
- description: string;
- };
- password: {
- type: string;
- description: string;
- };
- callbackURL: {
- type: string;
- description: string;
- };
- };
- required: string[];
- };
- };
- };
- };
- responses: {
- "200": {
- description: string;
- content: {
- "application/json": {
- schema: {
- type: "object";
- properties: {
- id: {
- type: string;
- description: string;
- };
- email: {
- type: string;
- description: string;
- };
- name: {
- type: string;
- description: string;
- };
- image: {
- type: string;
- description: string;
- };
- emailVerified: {
- type: string;
- description: string;
- };
- };
- };
- };
- };
- };
- };
- };
+ (inputCtx_0: {
+ body: ({
+ name: string;
+ email: string;
+ password: string;
+ } | ({
+ name: string;
+ email: string;
+ password: string;
+ } & {} & {})) & {
+ handle: string;
+ } & {
+ handle?: string | null | undefined;
};
} & {
- use: any[];
- }>]>(...inputCtx: C): Promise | undefined;
+ } & {
+ params?: Record;
+ } & {
+ request?: Request;
+ } & {
+ headers?: HeadersInit;
+ } & {
+ asResponse?: boolean;
+ returnHeaders?: boolean;
+ use?: better_call.Middleware[];
+ path?: string;
+ } & {
+ asResponse?: AsResponse | undefined;
+ returnHeaders?: ReturnHeaders | undefined;
+ }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
headers: Headers;
response: {
token: null;
@@ -7866,9 +7929,11 @@ declare const auth: {
name: string;
email: string;
password: string;
- } & ({} | ({} & {}) | ({} & {
- isAnonymous?: boolean | null | undefined;
- }))) & {
+ } | ({
+ name: string;
+ email: string;
+ password: string;
+ } & {} & {})) & {
handle: string;
} & {
handle?: string | null | undefined;
@@ -7912,27 +7977,52 @@ declare const auth: {
schema: {
type: "object";
properties: {
- id: {
+ token: {
type: string;
+ nullable: boolean;
description: string;
};
- email: {
+ user: {
type: string;
- description: string;
- };
- name: {
- type: string;
- description: string;
- };
- image: {
- type: string;
- description: string;
- };
- emailVerified: {
- type: string;
- description: string;
+ properties: {
+ id: {
+ type: string;
+ description: string;
+ };
+ email: {
+ type: string;
+ format: string;
+ description: string;
+ };
+ name: {
+ type: string;
+ description: string;
+ };
+ image: {
+ type: string;
+ format: string;
+ nullable: boolean;
+ description: string;
+ };
+ emailVerified: {
+ type: string;
+ description: string;
+ };
+ createdAt: {
+ type: string;
+ format: string;
+ description: string;
+ };
+ updatedAt: {
+ type: string;
+ format: string;
+ description: string;
+ };
+ };
+ required: string[];
};
};
+ required: string[];
};
};
};
@@ -7946,64 +8036,32 @@ declare const auth: {
path: "/sign-up/email";
};
signInEmail: {
- ;
- rememberMe: zod.ZodOptional>;
- }, "strip", zod.ZodTypeAny, {
+ (inputCtx_0: {
+ body: {
password: string;
email: string;
callbackURL?: string | undefined;
rememberMe?: boolean | undefined;
- }, {
- password: string;
- email: string;
- callbackURL?: string | undefined;
- rememberMe?: boolean | undefined;
- }>;
- metadata: {
- openapi: {
- description: string;
- responses: {
- "200": {
- description: string;
- content: {
- "application/json": {
- schema: {
- type: "object";
- properties: {
- token: {
- type: string;
- };
- user: {
- type: string;
- ref: string;
- };
- url: {
- type: string;
- };
- redirect: {
- type: string;
- };
- };
- required: string[];
- };
- };
- };
- };
- };
- };
};
} & {
- use: any[];
- }>]>(...inputCtx: C): Promise | undefined;
+ } & {
+ params?: Record;
+ } & {
+ request?: Request;
+ } & {
+ headers?: HeadersInit;
+ } & {
+ asResponse?: boolean;
+ returnHeaders?: boolean;
+ use?: better_call.Middleware[];
+ path?: string;
+ } & {
+ asResponse?: AsResponse | undefined;
+ returnHeaders?: ReturnHeaders | undefined;
+ }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
headers: Headers;
response: {
redirect: boolean;
@@ -8061,19 +8119,50 @@ declare const auth: {
"application/json": {
schema: {
type: "object";
+ description: string;
properties: {
+ redirect: {
+ type: string;
+ enum: boolean[];
+ };
token: {
type: string;
- };
- user: {
- type: string;
- ref: string;
+ description: string;
};
url: {
type: string;
+ nullable: boolean;
};
- redirect: {
+ user: {
type: string;
+ properties: {
+ id: {
+ type: string;
+ };
+ email: {
+ type: string;
+ };
+ name: {
+ type: string;
+ nullable: boolean;
+ };
+ image: {
+ type: string;
+ nullable: boolean;
+ };
+ emailVerified: {
+ type: string;
+ };
+ createdAt: {
+ type: string;
+ format: string;
+ };
+ updatedAt: {
+ type: string;
+ format: string;
+ };
+ };
+ required: string[];
};
};
required: string[];
@@ -8090,47 +8179,30 @@ declare const auth: {
path: "/sign-in/email";
};
forgetPassword: {
- ;
- }, "strip", zod.ZodTypeAny, {
+ (inputCtx_0: {
+ body: {
email: string;
redirectTo?: string | undefined;
- }, {
- email: string;
- redirectTo?: string | undefined;
- }>;
- metadata: {
- openapi: {
- description: string;
- responses: {
- "200": {
- description: string;
- content: {
- "application/json": {
- schema: {
- type: "object";
- properties: {
- status: {
- type: string;
- };
- };
- };
- };
- };
- };
- };
- };
};
} & {
- use: any[];
- }>]>(...inputCtx: C): Promise | undefined;
+ } & {
+ params?: Record;
+ } & {
+ request?: Request;
+ } & {
+ headers?: HeadersInit;
+ } & {
+ asResponse?: boolean;
+ returnHeaders?: boolean;
+ use?: better_call.Middleware[];
+ path?: string;
+ } & {
+ asResponse?: AsResponse | undefined;
+ returnHeaders?: ReturnHeaders | undefined;
+ }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
headers: Headers;
response: {
status: boolean;
@@ -8178,54 +8250,32 @@ declare const auth: {
path: "/forget-password";
};
resetPassword: {
- ;
- }, "strip", zod.ZodTypeAny, {
- token?: string | undefined;
- }, {
- token?: string | undefined;
- }>>;
- body: zod.ZodObject<{
- newPassword: zod.ZodString;
- token: zod.ZodOptional;
- }, "strip", zod.ZodTypeAny, {
+ (inputCtx_0: {
+ body: {
newPassword: string;
token?: string | undefined;
- }, {
- newPassword: string;
- token?: string | undefined;
- }>;
- metadata: {
- openapi: {
- description: string;
- responses: {
- "200": {
- description: string;
- content: {
- "application/json": {
- schema: {
- type: "object";
- properties: {
- status: {
- type: string;
- };
- };
- };
- };
- };
- };
- };
- };
};
} & {
- use: any[];
- }>]>(...inputCtx: C): Promise;
+ } & {
+ request?: Request;
+ } & {
+ headers?: HeadersInit;
+ } & {
+ asResponse?: boolean;
+ returnHeaders?: boolean;
+ use?: better_call.Middleware[];
+ path?: string;
+ } & {
+ asResponse?: AsResponse | undefined;
+ returnHeaders?: ReturnHeaders | undefined;
+ }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
headers: Headers;
response: {
status: boolean;
@@ -8280,53 +8330,30 @@ declare const auth: {
path: "/reset-password";
};
verifyEmail: {
- ;
- }, "strip", zod.ZodTypeAny, {
+ (inputCtx_0: {
+ body?: undefined;
+ } & {
+ method?: "GET" | undefined;
+ } & {
+ query: {
token: string;
callbackURL?: string | undefined;
- }, {
- token: string;
- callbackURL?: string | undefined;
- }>;
- use: ((inputContext: better_call.MiddlewareInputContext