diff --git a/apps/desktop/changelog/0.6.0.md b/apps/desktop/changelog/0.6.0.md new file mode 100644 index 000000000..b2d04149c --- /dev/null +++ b/apps/desktop/changelog/0.6.0.md @@ -0,0 +1,33 @@ +# What's New in v0.6.0 + +## Shiny New Things + +- Import and export your Actions (394d00f) +- Add a bio, website, and social links to your profile (507a525) +- Upload a profile picture +- Use video duration as an Action condition + +## Improvements + +- A snazzy new look for your personal profile +- Redesigned the Actions page (1ace5ea) +- Redesigned the RSSHub page (f9aca60) +- Added length limits to certain profile fields +- Simplified default commands in the entry tool (85122fb) +- Enhanced UI labels and descriptions for clarity (2ed9f70) +- Gradually rolling out an experimental unified local database for mobile and desktop (#3897 #3902) +- Polished image-preview styling (cf72753) +- Refined toast notifications (73f8011) + +## No Longer Broken + +- More reliable automatic recovery after database-migration failures (c2e0c3d) +- Fixed unread counts not clearing in the macOS Docker build (70255af) +- Fixed old entries showing during initial load (24ae065) +- Fixed handling of links starting with `.` (de8eac8) +- Fixed text-to-speech not working (82952b0) +- Fixed star/unstar status not syncing across devices (fbd0b3) + +## Thanks + +Special thanks to volunteer contributors @kovsu @huanfe1 @cscnk52 @Olexandr88 @0-o0 @kingsword09 @ericyzhu for their valuable contributions diff --git a/apps/desktop/changelog/next.md b/apps/desktop/changelog/next.md index 71b73d1bd..8f5eac449 100644 --- a/apps/desktop/changelog/next.md +++ b/apps/desktop/changelog/next.md @@ -1,29 +1,11 @@ # What's new in vNEXT_VERSION -## ⚠️ Important - -We’ve made some updates to our `user` database to help keep things running smoothly and securely. Some fields—like your name, email, profile image link, handle, bio, and website—now have maximum character limits: - -- Name: up to 64 characters -- Email: up to 64 characters -- Profile image link: up to 256 characters -- Handle: up to 36 characters -- Bio: up to 256 characters -- Website: up to 256 characters - -If you previously entered information that’s longer than these limits, it will be automatically shortened to fit. Other fields, like your email verification status, two-factor authentication, and social links, are not affected by these changes. - ## Shiny new things -- 🎉 Say hello to a snazzy new look for your personal profile! We've sprinkled in some cool social attribute settings to spice things up. But hold onto your hats—this is just the appetizer for our grand social vision! 🚀 Stay tuned for more! 😎 -- We have redesigned the pages for Discover, RSSHub, and Actions. These pages are now simpler and easier to use, with a more modern UI. - ## Improvements ## No longer broken -🎉 Ta-da! We’ve squashed a bunch of pesky bugs you awesome folks in the community pointed out—high fives all around! 🙌 But if something’s still acting wonky, don’t be shy—holler at us with an issue report, pretty please! 😜 Let’s keep the good vibes rolling! 🚀 - ## Thanks Special thanks to volunteer contributors @ for their valuable contributions diff --git a/apps/desktop/layer/main/preload/index.d.ts b/apps/desktop/layer/main/preload/index.d.ts index d46299e1b..9b776bddd 100644 --- a/apps/desktop/layer/main/preload/index.d.ts +++ b/apps/desktop/layer/main/preload/index.d.ts @@ -5,5 +5,6 @@ declare global { electron?: ElectronAPI api?: { canWindowBlur: boolean } platform: NodeJS.Platform + mas: boolean } } diff --git a/apps/desktop/layer/main/preload/index.ts b/apps/desktop/layer/main/preload/index.ts index d5590bdc4..f1e231445 100644 --- a/apps/desktop/layer/main/preload/index.ts +++ b/apps/desktop/layer/main/preload/index.ts @@ -39,6 +39,7 @@ if (process.contextIsolated) { contextBridge.exposeInMainWorld("electron", electronAPI) contextBridge.exposeInMainWorld("api", api) contextBridge.exposeInMainWorld("platform", process.platform) + contextBridge.exposeInMainWorld("mas", process.mas) } catch (error) { console.error(error) } @@ -49,6 +50,8 @@ if (process.contextIsolated) { window.api = api // @ts-ignore (define in dts) window.platform = process.platform + // @ts-ignore (define in dts) + window.mas = process.mas Object.defineProperty(window.navigator, "clipboard", { get: () => { diff --git a/apps/desktop/layer/renderer/global.d.ts b/apps/desktop/layer/renderer/global.d.ts index 571d94de2..71711fa17 100644 --- a/apps/desktop/layer/renderer/global.d.ts +++ b/apps/desktop/layer/renderer/global.d.ts @@ -5,6 +5,7 @@ declare global { electron?: ElectronAPI api?: { canWindowBlur: boolean } platform: NodeJS.Platform + mas: boolean } export const APP_NAME = "Folo" } diff --git a/apps/desktop/layer/renderer/src/atoms/server-configs.ts b/apps/desktop/layer/renderer/src/atoms/server-configs.ts index 418dc7191..87fc1106a 100644 --- a/apps/desktop/layer/renderer/src/atoms/server-configs.ts +++ b/apps/desktop/layer/renderer/src/atoms/server-configs.ts @@ -10,9 +10,5 @@ export const [, , useServerConfigs, , getServerConfigs, setServerConfigs] = crea export const useIsInMASReview = () => { const serverConfigs = useServerConfigs() - return ( - typeof process !== "undefined" && - process.mas && - serverConfigs?.MAS_IN_REVIEW_VERSION === PKG.version - ) + return window.mas && serverConfigs?.MAS_IN_REVIEW_VERSION === PKG.version } diff --git a/apps/desktop/layer/renderer/src/hooks/biz/useSubscriptionActions.tsx b/apps/desktop/layer/renderer/src/hooks/biz/useSubscriptionActions.tsx index 9de2d49dd..66fdff637 100644 --- a/apps/desktop/layer/renderer/src/hooks/biz/useSubscriptionActions.tsx +++ b/apps/desktop/layer/renderer/src/hooks/biz/useSubscriptionActions.tsx @@ -57,12 +57,12 @@ export const useDeleteSubscription = ({ onSuccess }: { onSuccess?: () => void } toast.dismiss(toastId) } - const toastId = toast("", { + const toastId = toast.warning("", { duration: 3000, description: , action: { label: ( - + {t("words.undo")} $mod+Z @@ -95,13 +95,15 @@ const UnfollowInfo = ({ title, undo }: { title: string; undo: () => any }) => { preventDefault: true, }) return ( - {title}, - }} - /> + + {title}, + }} + /> + ) } diff --git a/apps/desktop/layer/renderer/src/initialize/analytics.ts b/apps/desktop/layer/renderer/src/initialize/analytics.ts index 0e51f334a..3a9fe61a0 100644 --- a/apps/desktop/layer/renderer/src/initialize/analytics.ts +++ b/apps/desktop/layer/renderer/src/initialize/analytics.ts @@ -1,10 +1,9 @@ -import { env } from "@follow/shared/env.desktop" import type { AuthSession } from "@follow/shared/hono" -import { setOpenPanelTracker, setPostHogTracker, tracker } from "@follow/tracker" -import posthog from "posthog-js" +import { setFirebaseTracker, setOpenPanelTracker, tracker } from "@follow/tracker" import { QUERY_PERSIST_KEY } from "~/constants/app" +import { ga4 } from "./ga4" import { op } from "./op" export const initAnalytics = async () => { @@ -15,14 +14,9 @@ export const initAnalytics = async () => { language: navigator.language, }) + setFirebaseTracker(ga4) + setOpenPanelTracker(op) - setPostHogTracker( - posthog.init(env.VITE_POSTHOG_KEY, { - api_host: env.VITE_POSTHOG_HOST, - person_profiles: "always", - defaults: "2025-05-24", - }), - ) let session: AuthSession | undefined try { diff --git a/apps/desktop/layer/renderer/src/initialize/ga4.ts b/apps/desktop/layer/renderer/src/initialize/ga4.ts new file mode 100644 index 000000000..dc227f515 --- /dev/null +++ b/apps/desktop/layer/renderer/src/initialize/ga4.ts @@ -0,0 +1,56 @@ +import { v4 as uuidv4 } from "uuid" + +import { apiClient } from "~/lib/api-fetch" + +class Analytics4 { + private clientID: string + private sessionID: string + private userID: string | null = null + private userProperties: Record | null = null + + constructor(clientID: string = uuidv4(), sessionID = uuidv4()) { + this.clientID = clientID + this.sessionID = sessionID + } + + async setUserId(id: string) { + this.userID = id + } + + async setUserProperties(upValue?: Record) { + const userProperties = Object.entries(upValue || {}).reduce((acc, [key, value]) => { + acc[key] = { + value, + } + return acc + }, {}) + this.userProperties = userProperties + } + + async logEvent(eventName: string, params?: Record): Promise { + delete params?.__code + delete params?.__eventName + + const payload = { + client_id: this.clientID, + user_id: this.userID, + events: [ + { + name: eventName, + params: { + session_id: this.sessionID, + engagement_time_msec: 1000, + ...params, + }, + }, + ], + user_properties: this.userProperties, + } + + return apiClient.data.g.$post({ + json: payload, + }) + } +} + +export const ga4 = new Analytics4() diff --git a/apps/desktop/layer/renderer/src/lib/api-fetch.ts b/apps/desktop/layer/renderer/src/lib/api-fetch.ts index e2f47df07..30a6f95b0 100644 --- a/apps/desktop/layer/renderer/src/lib/api-fetch.ts +++ b/apps/desktop/layer/renderer/src/lib/api-fetch.ts @@ -61,6 +61,7 @@ export const apiFetch = ofetch.create({ { closeButton: true, duration: 10e4, + classNames: { content: tw`w-full`, }, diff --git a/apps/desktop/layer/renderer/src/main.tsx b/apps/desktop/layer/renderer/src/main.tsx index 439da6cbb..283988fef 100644 --- a/apps/desktop/layer/renderer/src/main.tsx +++ b/apps/desktop/layer/renderer/src/main.tsx @@ -3,7 +3,11 @@ import "@follow/components/tailwind" import "./styles/main.css" import { IN_ELECTRON, WEB_BUILD } from "@follow/shared/constants" -import { apiClientSimpleContext, authClientSimpleContext } from "@follow/store/context" +import { + apiClientSimpleContext, + authClientSimpleContext, + queryClientSimpleContext, +} from "@follow/store/context" import { getOS } from "@follow/utils/utils" import * as React from "react" import ReactDOM from "react-dom/client" @@ -16,10 +20,12 @@ import { setAppIsReady } from "./atoms/app" import { ElECTRON_CUSTOM_TITLEBAR_HEIGHT } from "./constants" import { initializeApp } from "./initialize" import { registerAppGlobalShortcuts } from "./initialize/global-shortcuts" +import { queryClient } from "./lib/query-client" import { router } from "./router" apiClientSimpleContext.provide(apiClient) authClientSimpleContext.provide(authClient) +queryClientSimpleContext.provide(queryClient) initializeApp().finally(() => { import("./push-notification").then(({ registerWebPushNotifications }) => { diff --git a/apps/desktop/layer/renderer/src/modules/activation/NeedActivationToast.tsx b/apps/desktop/layer/renderer/src/modules/activation/NeedActivationToast.tsx index 1849a62ec..edce2be1e 100644 --- a/apps/desktop/layer/renderer/src/modules/activation/NeedActivationToast.tsx +++ b/apps/desktop/layer/renderer/src/modules/activation/NeedActivationToast.tsx @@ -1,3 +1,4 @@ +import { toastStyles } from "@follow/components/ui/toast/styles.js" import { stopPropagation } from "@follow/utils/dom" import { useCallback } from "react" import { useTranslation } from "react-i18next" @@ -13,10 +14,8 @@ export const NeedActivationToast = (props: { dimiss: () => void }) => {
{t("activation.description")}
- + - + - - + + {/* Additional Help with fade in */} - +

If you believe this is an error, please submit a issue on{" "} - { style={{ display: "inline-block" }} > GitHub - +

-
+ {/* Floating particles effect */}
{Array.from({ length: 6 }).map((_, i) => ( - { export const NotFound = () => { return ( - + - + ) } diff --git a/apps/ssr/global.ts b/apps/ssr/global.ts index 52056c3b6..e3d3d94d4 100644 --- a/apps/ssr/global.ts +++ b/apps/ssr/global.ts @@ -1,5 +1,11 @@ -export const defineGlobalConstants = () => { +Object.assign(globalThis, { + APP_NAME: "Folo", +}) + +try { + void __DEV__ +} catch { Object.assign(globalThis, { - APP_NAME: "Folo", + __DEV__: process.env.NODE_ENV === "development", }) } diff --git a/apps/ssr/index.ts b/apps/ssr/index.ts index b14fce283..3b2e3becf 100644 --- a/apps/ssr/index.ts +++ b/apps/ssr/index.ts @@ -1,5 +1,8 @@ +import "./global" import "./src/lib/load-env" +import os from "node:os" + import middie from "@fastify/middie" import { fastifyRequestContext } from "@fastify/request-context" import { env } from "@follow/shared/env.ssr" @@ -8,16 +11,12 @@ import Fastify from "fastify" import { nanoid } from "nanoid" import { FetchError } from "ofetch" -import { isDev } from "~/lib/env" import { MetaError } from "~/meta-handler" import { staticRoute } from "~/router/static" -import { defineGlobalConstants } from "./global" import { globalRoute } from "./src/router/global" import { ogRoute } from "./src/router/og" -defineGlobalConstants() - const isVercel = process.env.VERCEL === "1" declare module "@fastify/request-context" { @@ -64,7 +63,7 @@ export const createApp = async () => { const finalHost = forwardedHost || host const upstreamEnv = finalHost?.includes("dev") ? "dev" : "prod" - if (!isDev) req.requestContext.set("upstreamEnv", upstreamEnv) + if (!__DEV__) req.requestContext.set("upstreamEnv", upstreamEnv) if (upstreamEnv === "prod") { req.requestContext.set("upstreamOrigin", env.VITE_WEB_PROD_URL || env.VITE_WEB_URL) } else { @@ -74,8 +73,8 @@ export const createApp = async () => { done() }) - if (isDev) { - const devVite = require("./src/lib/dev-vite") + if (__DEV__) { + const devVite = await import("./src/lib/dev-vite") await devVite.registerDevViteServer(app) } @@ -97,11 +96,11 @@ if (!isVercel) { } function getIPAddress() { - const interfaces = require("node:os").networkInterfaces() + const interfaces = os.networkInterfaces() for (const devName in interfaces) { const iface = interfaces[devName] - for (const alias of iface) { + for (const alias of iface || []) { if (alias.family === "IPv4" && alias.address !== "127.0.0.1" && !alias.internal) return alias.address } diff --git a/apps/ssr/package.json b/apps/ssr/package.json index 023a74725..9fd6111c3 100644 --- a/apps/ssr/package.json +++ b/apps/ssr/package.json @@ -1,5 +1,6 @@ { "name": "@follow/ssr", + "type": "module", "private": true, "scripts": { "build": "cross-env NODE_ENV=production vite build && tsx scripts/prepare-vercel-build.ts && tsdown && tsx scripts/cleanup-vercel-build.ts", diff --git a/apps/ssr/scripts/cleanup-vercel-build.ts b/apps/ssr/scripts/cleanup-vercel-build.ts index 1d9cc2e2e..9c0eb1113 100644 --- a/apps/ssr/scripts/cleanup-vercel-build.ts +++ b/apps/ssr/scripts/cleanup-vercel-build.ts @@ -1,10 +1,7 @@ -import { rmSync, writeFileSync } from "node:fs" -import { resolve } from "node:path" +import { rmSync } from "node:fs" +import { dirname, resolve } from "node:path" +import { fileURLToPath } from "node:url" +const __dirname = dirname(fileURLToPath(import.meta.url)) rmSync(resolve(__dirname, "../.generated"), { recursive: true, force: true }) // restore env file - -writeFileSync( - resolve(__dirname, "../src/lib/env.ts"), - `export const isDev = process.env.NODE_ENV === "development"\n`, -) diff --git a/apps/ssr/scripts/prepare-vercel-build.ts b/apps/ssr/scripts/prepare-vercel-build.ts index b9a34ec7a..f45eff263 100644 --- a/apps/ssr/scripts/prepare-vercel-build.ts +++ b/apps/ssr/scripts/prepare-vercel-build.ts @@ -1,6 +1,9 @@ import { mkdirSync } from "node:fs" import fs from "node:fs/promises" -import path from "node:path" +import path, { dirname } from "node:path" +import { fileURLToPath } from "node:url" + +const __dirname = dirname(fileURLToPath(import.meta.url)) mkdirSync(path.join(__dirname, "../.generated"), { recursive: true }) @@ -12,21 +15,8 @@ async function generateIndexHtmlData() { ) } -async function replaceEnvFile() { - const envFile = await fs.readFile(path.join(__dirname, "../src/lib/env.ts"), "utf-8") - - await fs.writeFile( - path.join(__dirname, "../src/lib/env.ts"), - // For tree shaking - envFile.replace( - `export const isDev = process.env.NODE_ENV === "development"`, - `export const isDev = ${process.env.NODE_ENV === "development"}`, - ), - ) -} - async function main() { - await Promise.all([generateIndexHtmlData(), replaceEnvFile()]) + await generateIndexHtmlData() } main() diff --git a/apps/ssr/src/lib/api-client.ts b/apps/ssr/src/lib/api-client.ts index c5187f36a..db4472ab0 100644 --- a/apps/ssr/src/lib/api-client.ts +++ b/apps/ssr/src/lib/api-client.ts @@ -7,7 +7,6 @@ import { hc } from "hono/client" import { ofetch } from "ofetch" import PKG from "../../../desktop/package.json" -import { isDev } from "./env" const getBaseURL = () => { const req = requestContext.get("req")! @@ -34,7 +33,7 @@ export const createApiFetch = () => { credentials: "include", retry: false, onRequest(context) { - if (isDev) console.info(`request: ${context.request}`) + if (__DEV__) console.info(`request: ${context.request}`) context.options.headers.set("User-Agent", `Folo External Server Api Client/${PKG.version}`) }, @@ -57,7 +56,7 @@ export const createApiClient = () => { headers() { return { "X-App-Version": PKG.version, - "X-App-Dev": isDev ? "1" : "0", + "X-App-Dev": __DEV__ ? "1" : "0", "User-Agent": `Folo External Server Api Client/${PKG.version}`, Cookie: authSessionToken ? `__Secure-better-auth.session_token=${authSessionToken}` : "", } diff --git a/apps/ssr/src/lib/dev-vite.ts b/apps/ssr/src/lib/dev-vite.ts index 84fa90c83..b961318bd 100644 --- a/apps/ssr/src/lib/dev-vite.ts +++ b/apps/ssr/src/lib/dev-vite.ts @@ -1,8 +1,10 @@ -import { resolve } from "node:path" +import { dirname, resolve } from "node:path" +import { fileURLToPath } from "node:url" import type { FastifyInstance } from "fastify" import type { ViteDevServer } from "vite" +const __dirname = dirname(fileURLToPath(import.meta.url)) const root = resolve(__dirname, "../..") let globalVite: ViteDevServer diff --git a/apps/ssr/src/lib/env.ts b/apps/ssr/src/lib/env.ts deleted file mode 100644 index 2dd9d4a4b..000000000 --- a/apps/ssr/src/lib/env.ts +++ /dev/null @@ -1 +0,0 @@ -export const isDev = process.env.NODE_ENV === "development" diff --git a/apps/ssr/src/lib/og/fonts.ts b/apps/ssr/src/lib/og/fonts.ts index 356eb94a4..4b5440fba 100644 --- a/apps/ssr/src/lib/og/fonts.ts +++ b/apps/ssr/src/lib/og/fonts.ts @@ -1,6 +1,9 @@ import fs from "node:fs" +import { createRequire } from "node:module" import path, { resolve } from "node:path" +const require = createRequire(import.meta.url) + const weights = [ { name: "Thin", diff --git a/apps/ssr/src/router/global.ts b/apps/ssr/src/router/global.ts index dfd0f86de..f93131db3 100644 --- a/apps/ssr/src/router/global.ts +++ b/apps/ssr/src/router/global.ts @@ -1,5 +1,6 @@ import { readFileSync } from "node:fs" -import path, { resolve } from "node:path" +import path, { dirname, resolve } from "node:path" +import { fileURLToPath } from "node:url" import { env } from "@follow/shared/env.ssr" import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify" @@ -8,7 +9,6 @@ import { parseHTML } from "linkedom" import { FetchError } from "ofetch" import xss from "xss" -import { isDev } from "~/lib/env" import { NotFoundError } from "~/lib/not-found" import { buildSeoMetaTags } from "~/lib/seo" @@ -17,10 +17,11 @@ import { injectMetaHandler, MetaError } from "../meta-handler" const devHandler = (app: FastifyInstance) => { app.get("*", async (req, reply) => { const url = req.originalUrl + const __dirname = dirname(fileURLToPath(import.meta.url)) const root = resolve(__dirname, "../..") - const vite = require("../lib/dev-vite").getViteServer() + const vite = await import("../lib/dev-vite").then((m) => m.getViteServer()) try { let template = readFileSync(path.resolve(root, vite.config.root, "index.html"), "utf-8") template = await vite.transformIndexHtml(url, template) @@ -30,14 +31,15 @@ const devHandler = (app: FastifyInstance) => { reply.type("text/html") reply.send(document.toString()) } catch (e) { - vite.ssrFixStacktrace(e) + vite.ssrFixStacktrace(e as Error) reply.code(500).send(e) } }) } const prodHandler = (app: FastifyInstance) => { app.get("*", async (req, reply) => { - const template = require("../../.generated/index.template").default + // @ts-expect-error + const template = await import("../../.generated/index.template").then((m) => m.default) const { document } = parseHTML(template) await safeInjectMetaToTemplate(document, req, reply) @@ -91,7 +93,7 @@ injectEnv({"VITE_API_URL":"${apiUrl}","VITE_EXTERNAL_API_URL":"${apiUrl}","VITE_ ) }) } -export const globalRoute = isDev ? devHandler : prodHandler +export const globalRoute = __DEV__ ? devHandler : prodHandler async function safeInjectMetaToTemplate( document: Document, diff --git a/apps/ssr/src/router/og/__base.tsx b/apps/ssr/src/router/og/__base.tsx index c91d7f53c..ee58debb6 100644 --- a/apps/ssr/src/router/og/__base.tsx +++ b/apps/ssr/src/router/og/__base.tsx @@ -69,11 +69,7 @@ export const OGCanvas = ({ children, seed }: { children: React.ReactNode; seed: {/* Follow Logo */}
- - Folo - +
{/* AI RSS */} @@ -132,6 +128,35 @@ function FollowIcon() { ) } +function LogoText() { + return ( + + + + + + + ) +} + export async function getImageBase64(image: string | null | undefined) { if (!image) { return null diff --git a/apps/ssr/tsdown.config.ts b/apps/ssr/tsdown.config.ts index d641ac0a8..c110d7434 100644 --- a/apps/ssr/tsdown.config.ts +++ b/apps/ssr/tsdown.config.ts @@ -9,12 +9,14 @@ export default defineConfig({ outDir: "dist/server", clean: true, - format: ["cjs"], + format: ["esm"], external: ["lightningcss", "vite"], treeshake: true, + define: { __DEV__: JSON.stringify(process.env.NODE_ENV === "development"), }, + hooks(hooks) { hooks.hook("build:done", async () => { if (process.env.VERCEL !== "1") return @@ -24,10 +26,11 @@ export default defineConfig({ try { const insertCode = `try { -require.resolve("@fontsource/sn-pro") -require.resolve('kose-font') -require.resolve('kose-font/fonts/KosefontP-JP.ttf') -require.resolve('kose-font/fonts/Kosefont-JP.ttf') +const noop = () => {} +import("@fontsource/sn-pro").then(noop) +import('kose-font').then(noop) +import('kose-font/fonts/KosefontP-JP.ttf').then(noop) +import('kose-font/fonts/Kosefont-JP.ttf').then(noop) ${(() => { const require = createRequire(import.meta.url) const fontDepsPath = require.resolve("@fontsource/sn-pro") diff --git a/apps/ssr/vite.config.mts b/apps/ssr/vite.config.mts index 20b12b18e..37ec79bc7 100644 --- a/apps/ssr/vite.config.mts +++ b/apps/ssr/vite.config.mts @@ -1,4 +1,5 @@ -import { resolve } from "node:path" +import { dirname, resolve } from "node:path" +import { fileURLToPath } from "node:url" import react from "@vitejs/plugin-react" import { codeInspectorPlugin } from "code-inspector-plugin" @@ -13,6 +14,8 @@ const routeBuilderPluginV2 = await tsImport( import.meta.url, ).then((m) => m.default) +const __dirname = dirname(fileURLToPath(import.meta.url)) + export default defineConfig({ resolve: { alias: { diff --git a/locales/external/en.json b/locales/external/en.json index 9183b2895..4bda047f7 100644 --- a/locales/external/en.json +++ b/locales/external/en.json @@ -41,7 +41,7 @@ "login.confirm_password.label": "Confirm Password", "login.continueWith": "Continue with {{provider}}", "login.email": "Email", - "login.enter_token": "If you aren't redirected automatically, copy the token below and paste it in the \"Enter authorization token to continue\" form on the Desktop App.", + "login.enter_token": "If you aren't redirected automatically, copy the token below and paste it in the \"Enter authorization token to continue\" form on the Desktop App (v0.5.0+).", "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", diff --git a/locales/external/zh-CN.json b/locales/external/zh-CN.json index e8e046983..82638ad88 100644 --- a/locales/external/zh-CN.json +++ b/locales/external/zh-CN.json @@ -41,7 +41,7 @@ "login.confirm_password.label": "确认密码", "login.continueWith": "使用 {{provider}} 登录", "login.email": "邮件地址", - "login.enter_token": "如果未自动重定向,请复制下方令牌并粘贴到桌面应用的“输入授权令牌以继续”表单中。", + "login.enter_token": "如果未自动重定向,请复制下方令牌并粘贴到桌面应用(v0.5.0 以上)的“输入授权令牌以继续”表单中。", "login.errors.unknown": "未知错误", "login.forget_password.description": "请输入与你的帐户关联的邮件地址,我们将向你发送一封关于如何重置密码的邮件。", "login.forget_password.email_invalid": "无效的邮箱地址", diff --git a/packages/internal/components/src/common/MotionProvider.tsx b/packages/internal/components/src/common/MotionProvider.tsx index 898c9a31a..d2fb812d6 100644 --- a/packages/internal/components/src/common/MotionProvider.tsx +++ b/packages/internal/components/src/common/MotionProvider.tsx @@ -1,9 +1,8 @@ -import { LazyMotion, MotionConfig } from "motion/react" +import { domMax, LazyMotion, MotionConfig } from "motion/react" -const loadFeatures = () => import("../framer-lazy-feature").then((res) => res.default) export const MotionProvider = ({ children }: { children: React.ReactNode }) => { return ( - + const TOAST_Z_INDEX = 999999999 @@ -16,70 +17,7 @@ export const Toaster = ({ ...props }: ToasterProps) => { gap={12} toastOptions={{ unstyled: true, - classNames: { - toast: tw` - group relative flex w-full items-center justify-between gap-3 rounded-2xl p-4 shadow-lg - backdrop-blur-xl border border-border/50 - bg-material-ultra-thick - transition-all duration-300 ease-out - hover:scale-[1.02] hover:shadow-xl - data-[type=success]:border-green/30 data-[type=success]:bg-green/5 - data-[type=error]:border-red/30 data-[type=error]:bg-red/5 - data-[type=warning]:border-orange/30 data-[type=warning]:bg-orange/5 - data-[type=info]:border-blue/30 data-[type=info]:bg-blue/5 - data-[type=loading]:border-gray/30 data-[type=loading]:bg-gray/5 - max-w-md min-w-[320px] - font-theme - `, - title: tw` - text-sm font-medium text-text - leading-tight - `, - description: tw` - text-xs text-text-secondary - leading-relaxed mt-1 - `, - content: tw` - flex-1 min-w-0 - `, - icon: tw` - flex-shrink-0 mt-0.5 size-5 - [li[data-type="success"]_&]:text-green - [li[data-type="error"]_&]:text-red - [li[data-type="warning"]_&]:text-orange - [li[data-type="info"]_&]:text-blue - [li[data-type="loading"]_&]:text-gray - `, - actionButton: tw` - px-2.5 py-1 text-xs font-medium rounded-md - transition-all duration-200 - focus:outline-none focus:shadow-lg bg-accent - group-data-[type=success]:bg-green group-data-[type=success]:text-white group-data-[type=success]:hover:bg-green/90 group-data-[type=success]:focus:shadow-green/50 - group-data-[type=error]:bg-red group-data-[type=error]:text-white group-data-[type=error]:hover:bg-red/90 group-data-[type=error]:focus:shadow-red/50 - group-data-[type=warning]:bg-orange group-data-[type=warning]:text-white group-data-[type=warning]:hover:bg-orange/90 group-data-[type=warning]:focus:shadow-orange/50 - group-data-[type=info]:bg-blue group-data-[type=info]:text-white group-data-[type=info]:hover:bg-blue/90 group-data-[type=info]:focus:shadow-blue/50 - group-data-[type=loading]:bg-gray group-data-[type=loading]:text-white group-data-[type=loading]:hover:bg-gray/90 group-data-[type=loading]:focus:shadow-gray/50 - hover:shadow-md active:scale-95 - `, - cancelButton: tw` - px-2.5 py-1 text-xs font-medium rounded-md - bg-fill-secondary text-text-secondary - hover:bg-fill-tertiary hover:text-text - transition-colors duration-200 - focus:outline-none focus:ring-2 focus:ring-fill/50 focus:ring-offset-1 - `, - closeButton: tw` - absolute top-2 right-2 w-6 h-6 rounded-full - flex items-center justify-center - bg-fill text-text-tertiary - hover:bg-fill-secondary hover:text-text-secondary - active:bg-fill-tertiary active:text-text - transition-all duration-200 - opacity-0 group-hover:opacity-100 - focus:outline-none focus:ring-2 focus:ring-accent/50 - focus:opacity-100 - `, - }, + classNames: toastStyles, }} icons={{ success: , diff --git a/packages/internal/components/src/ui/toast/styles.ts b/packages/internal/components/src/ui/toast/styles.ts new file mode 100644 index 000000000..d0b4160bc --- /dev/null +++ b/packages/internal/components/src/ui/toast/styles.ts @@ -0,0 +1,66 @@ +export const toastStyles = { + toast: tw` + group relative flex w-full items-center justify-between gap-3 rounded-2xl p-4 shadow-lg + backdrop-blur-background border border-border/50 + bg-material-ultra-thick duration-300 ease-out + data-[type=success]:border-green/30 data-[type=success]:bg-green/20 + data-[type=error]:border-red/30 data-[type=error]:bg-red/20 + data-[type=warning]:border-orange/30 data-[type=warning]:bg-orange/20 + data-[type=info]:border-blue/30 data-[type=info]:bg-blue/20 + data-[type=loading]:border-gray/30 data-[type=loading]:bg-gray/20 + max-w-md min-w-[320px] + font-theme + `, + title: tw` + text-sm font-medium text-text + leading-tight + `, + description: tw` + text-xs text-text-secondary + leading-relaxed mt-1 + `, + content: tw` + flex-1 min-w-0 + `, + icon: tw` + flex-shrink-0 mt-0.5 size-5 + [li[data-type="success"]_&]:text-green + [li[data-type="error"]_&]:text-red + [li[data-type="warning"]_&]:text-orange + [li[data-type="info"]_&]:text-blue + [li[data-type="loading"]_&]:text-gray + `, + actionButton: tw` + shrink-0 + h-6 + px-2.5 text-xs font-medium rounded-md + transition-all duration-200 + focus:outline-none focus:shadow-lg bg-accent + group-data-[type=success]:bg-green group-data-[type=success]:text-white group-data-[type=success]:hover:bg-green/90 group-data-[type=success]:focus:shadow-green/50 + group-data-[type=error]:bg-red group-data-[type=error]:text-white group-data-[type=error]:hover:bg-red/90 group-data-[type=error]:focus:shadow-red/50 + group-data-[type=warning]:bg-orange group-data-[type=warning]:text-white group-data-[type=warning]:hover:bg-orange/90 group-data-[type=warning]:focus:shadow-orange/50 + group-data-[type=info]:bg-blue group-data-[type=info]:text-white group-data-[type=info]:hover:bg-blue/90 group-data-[type=info]:focus:shadow-blue/50 + group-data-[type=loading]:bg-gray group-data-[type=loading]:text-white group-data-[type=loading]:hover:bg-gray/90 group-data-[type=loading]:focus:shadow-gray/50 + hover:shadow-md active:scale-95 + `, + cancelButton: tw` + h-6 + px-2.5 text-xs font-medium rounded-md + bg-fill-secondary text-text-secondary + hover:bg-fill-tertiary hover:text-text + transition-colors duration-200 + focus:outline-none focus:ring-2 focus:ring-fill/50 focus:ring-offset-1 + `, + closeButton: tw` + absolute -top-2 -right-2 w-6 h-6 rounded-full + flex items-center justify-center + text-text + border border-border + backdrop-blur-background + bg-material-ultra-thick + transition-all duration-200 + opacity-0 group-hover:opacity-100 + focus:outline-none focus:ring-2 focus:ring-accent/50 + focus:opacity-100 + `, +} diff --git a/packages/internal/components/src/ui/tooltip/index.tsx b/packages/internal/components/src/ui/tooltip/index.tsx index be4a225e8..1d827ae5d 100644 --- a/packages/internal/components/src/ui/tooltip/index.tsx +++ b/packages/internal/components/src/ui/tooltip/index.tsx @@ -35,7 +35,7 @@ const TooltipContent = ({ {/* https://github.com/radix-ui/primitives/discussions/868 */} diff --git a/packages/internal/constants/package.json b/packages/internal/constants/package.json index f96b544fb..d7996c1fc 100644 --- a/packages/internal/constants/package.json +++ b/packages/internal/constants/package.json @@ -1,5 +1,6 @@ { "name": "@follow/constants", + "type": "module", "private": true, "sideEffects": false, "exports": { diff --git a/packages/internal/database/src/services/collection.ts b/packages/internal/database/src/services/collection.ts index 364bc54b7..ea19e3821 100644 --- a/packages/internal/database/src/services/collection.ts +++ b/packages/internal/database/src/services/collection.ts @@ -11,9 +11,13 @@ class CollectionServiceStatic implements Resetable { await db.delete(collectionsTable).execute() } - async upsertMany(collections: CollectionSchema[]) { + async upsertMany(collections: CollectionSchema[], options?: { reset?: boolean }) { if (collections.length === 0) return + if (options?.reset) { + await db.delete(collectionsTable).execute() + } + await db .insert(collectionsTable) .values(collections) @@ -27,6 +31,11 @@ class CollectionServiceStatic implements Resetable { await db.delete(collectionsTable).where(eq(collectionsTable.entryId, entryId)) } + async deleteMany(entryId: string[]) { + if (entryId.length === 0) return + await db.delete(collectionsTable).where(inArray(collectionsTable.entryId, entryId)) + } + getCollectionMany(entryId: string[]) { return db.query.collectionsTable.findMany({ where: inArray(collectionsTable.entryId, entryId) }) } diff --git a/packages/internal/shared/src/hono.ts b/packages/internal/shared/src/hono.ts index b141ff232..b835feb52 100644 --- a/packages/internal/shared/src/hono.ts +++ b/packages/internal/shared/src/hono.ts @@ -20928,7 +20928,18 @@ declare const _routes: hono_hono_base.HonoBase, "/">; +}, "/trending"> | hono_types.MergeSchemaPath<{ + "/g": { + $post: { + input: { + json: any; + }; + output: Response; + outputFormat: "json"; + status: hono_utils_http_status.StatusCode; + }; + }; +}, "/data">, "/">; type AppType = typeof _routes; export { type ActionItem, type ActionsModel, type AirdropActivity, type AppType, type AttachmentsModel, type AuthSession, type AuthUser, CommonEntryFields, type ConditionItem, type DetailModel, type EntriesModel, type ExtraModel, type FeedModel, type ListModel, type MediaModel, type MessagingData, MessagingType, type SettingsModel, type UrlReadsModel, account, achievements, achievementsOpenAPISchema, actions, actionsItemOpenAPISchema, actionsOpenAPISchema, actionsRelations, activityEnum, airdrops, airdropsOpenAPISchema, attachmentsZodSchema, authPlugins, boosts, captcha, collections, collectionsOpenAPISchema, collectionsRelations, detailModelSchema, entries, entriesOpenAPISchema, entriesRelations, extraZodSchema, feedAnalytics, feedAnalyticsOpenAPISchema, feedAnalyticsRelations, feedPowerTokens, feedPowerTokensOpenAPISchema, feedPowerTokensRelations, feeds, feedsOpenAPISchema, feedsRelations, inboxHandleSchema, inboxes, inboxesEntries, inboxesEntriesInsertOpenAPISchema, type inboxesEntriesModel, inboxesEntriesOpenAPISchema, inboxesEntriesRelations, inboxesOpenAPISchema, inboxesRelations, invitations, invitationsOpenAPISchema, invitationsRelations, languageSchema, levels, levelsOpenAPISchema, levelsRelations, listAnalytics, listAnalyticsOpenAPISchema, listAnalyticsRelations, lists, listsOpenAPISchema, listsRelations, listsSubscriptions, listsSubscriptionsOpenAPISchema, listsSubscriptionsRelations, lower, mediaZodSchema, messaging, messagingOpenAPISchema, messagingRelations, readabilities, rsshub, rsshubAnalytics, rsshubAnalyticsOpenAPISchema, rsshubOpenAPISchema, rsshubPurchase, rsshubUsage, rsshubUsageOpenAPISchema, rsshubUsageRelations, session, settings, subscriptions, subscriptionsOpenAPISchema, subscriptionsRelations, timeline, timelineOpenAPISchema, timelineRelations, transactionType, transactions, transactionsOpenAPISchema, transactionsRelations, trendingFeeds, trendingFeedsOpenAPISchema, trendingFeedsRelations, twoFactor, uploads, urlReads, urlReadsOpenAPISchema, user, users, usersOpenApiSchema, usersRelations, verification, wallets, walletsOpenAPISchema, walletsRelations }; diff --git a/packages/internal/store/src/collection/store.ts b/packages/internal/store/src/collection/store.ts index b997f32c9..f3462486e 100644 --- a/packages/internal/store/src/collection/store.ts +++ b/packages/internal/store/src/collection/store.ts @@ -4,7 +4,8 @@ import { CollectionService } from "@follow/database/services/collection" import { apiClient } from "../context" import { getEntry } from "../entry/getter" -import type { Hydratable } from "../internal/base" +import { invalidateEntriesQuery } from "../entry/hooks" +import type { Hydratable, Resetable } from "../internal/base" import { createTransaction, createZustandStore } from "../internal/helper" interface CollectionState { @@ -52,6 +53,8 @@ class CollectionSyncService { }) await tx.run() + + invalidateEntriesQuery({ collection: true }) } async unstarEntry(entryId: string) { @@ -75,20 +78,24 @@ class CollectionSyncService { }) await tx.run() + + invalidateEntriesQuery({ collection: true }) } } -class CollectionActions implements Hydratable { +class CollectionActions implements Hydratable, Resetable { async hydrate() { const collections = await CollectionService.getCollectionAll() collectionActions.upsertManyInSession(collections) } - upsertManyInSession(collections: CollectionSchema[]) { + upsertManyInSession(collections: CollectionSchema[], options?: { reset?: boolean }) { const state = get() - const nextCollections: CollectionState["collections"] = { - ...state.collections, - } + const nextCollections: CollectionState["collections"] = options?.reset + ? {} + : { + ...state.collections, + } collections.forEach((collection) => { if (!collection.entryId) return nextCollections[collection.entryId] = collection @@ -99,42 +106,63 @@ class CollectionActions implements Hydratable { }) } - async upsertMany(collections: CollectionSchema[]) { + async upsertMany(collections: CollectionSchema[], options?: { reset?: boolean }) { const tx = createTransaction() tx.store(() => { - this.upsertManyInSession(collections) + this.upsertManyInSession(collections, options) }) tx.persist(() => { - return CollectionService.upsertMany(collections) + return CollectionService.upsertMany(collections, options) }) await tx.run() } - async deleteInSession(entryId: string) { + deleteInSession(entryId: string | string[]) { + const normalizedEntryId = Array.isArray(entryId) ? entryId : [entryId] + const state = useCollectionStore.getState() const nextCollections: CollectionState["collections"] = { ...state.collections, } - delete nextCollections[entryId] + + normalizedEntryId.forEach((id) => { + delete nextCollections[id] + }) set({ ...state, collections: nextCollections, }) } - async delete(entryId: string) { + async delete(entryId: string | string[]) { + const entryIdsInCollection = new Set(Object.keys(get().collections)) + const normalizedEntryId = (Array.isArray(entryId) ? entryId : [entryId]).filter((id) => + entryIdsInCollection.has(id), + ) + + if (normalizedEntryId.length === 0) return + const tx = createTransaction() tx.store(() => { this.deleteInSession(entryId) }) tx.persist(() => { - return CollectionService.delete(entryId) + return CollectionService.deleteMany(normalizedEntryId) }) tx.run() } - reset() { - set(defaultState) + async reset() { + const tx = createTransaction() + tx.store(() => { + set(defaultState) + }) + + tx.persist(() => { + return CollectionService.reset() + }) + + await tx.run() } } diff --git a/packages/internal/store/src/context.ts b/packages/internal/store/src/context.ts index 403f0d526..720efea67 100644 --- a/packages/internal/store/src/context.ts +++ b/packages/internal/store/src/context.ts @@ -1,4 +1,5 @@ import type { AuthClient } from "@follow/shared/auth" +import type { QueryClient } from "@tanstack/react-query" import type { APIClient } from "./types" @@ -27,5 +28,7 @@ function createSimpleContext() { export const apiClientSimpleContext = createSimpleContext() export const authClientSimpleContext = createSimpleContext() +export const queryClientSimpleContext = createSimpleContext() export const apiClient = apiClientSimpleContext.consumer export const authClient = authClientSimpleContext.consumer +export const queryClient = queryClientSimpleContext.consumer diff --git a/packages/internal/store/src/entry/hooks.ts b/packages/internal/store/src/entry/hooks.ts index 14e830d59..dca313b5d 100644 --- a/packages/internal/store/src/entry/hooks.ts +++ b/packages/internal/store/src/entry/hooks.ts @@ -1,26 +1,48 @@ import type { FeedViewType } from "@follow/constants" -import type { Query } from "@tanstack/react-query" import { useInfiniteQuery, useQuery } from "@tanstack/react-query" import { useCallback } from "react" +import { useFeedUnreadIsDirty } from "../atoms/feed" +import { FEED_COLLECTION_LIST } from "../constants/app" +import { queryClient } from "../context" import { getSubscriptionByEntryId } from "../subscription/getter" import { getEntry } from "./getter" import { entrySyncServices, useEntryStore } from "./store" import type { EntryModel, FetchEntriesProps, FetchEntriesPropsSettings } from "./types" -export const getInvalidateEntriesQueryPredicate = (views: FeedViewType[]) => { - return (query: Query) => { - const { queryKey } = query - if (Array.isArray(queryKey) && queryKey[0] === "entries") { - const view = queryKey[4] - return views.includes(view as FeedViewType) - } - return false - } +export const invalidateEntriesQuery = ({ + views, + collection, +}: { + views?: FeedViewType[] + collection?: true +}) => { + return queryClient().invalidateQueries({ + predicate: (query) => { + const { queryKey } = query + if (Array.isArray(queryKey) && queryKey[0] === "entries") { + const feedId = queryKey[1] + const view = queryKey[4] + + const isCollection = queryKey[7] + if (views) { + return views.includes(view as FeedViewType) + } + + if (collection) { + return isCollection === true || feedId === FEED_COLLECTION_LIST + } + } + return false + }, + }) } +const defaultStaleTime = 10 * (60 * 1000) // 10 minutes + export const useEntriesQuery = ( - props?: Omit & FetchEntriesPropsSettings, + props?: Omit & + FetchEntriesPropsSettings, ) => { const { feedId, @@ -29,10 +51,17 @@ export const useEntriesQuery = ( view, limit, feedIdList, + isCollection, unreadOnly, hidePrivateSubscriptionsInTimeline, } = props || {} + const fetchUnread = unreadOnly + const feedUnreadDirty = useFeedUnreadIsDirty((feedId as string) || "") + + const isPop = + "history" in globalThis && "isPop" in globalThis.history && !!globalThis.history.isPop + return useInfiniteQuery({ queryKey: [ "entries", @@ -40,9 +69,10 @@ export const useEntriesQuery = ( inboxId, listId, view, - unreadOnly, limit, feedIdList, + isCollection, + unreadOnly, hidePrivateSubscriptionsInTimeline, ], queryFn: ({ pageParam }) => @@ -52,11 +82,18 @@ export const useEntriesQuery = ( read: unreadOnly ? false : undefined, excludePrivate: hidePrivateSubscriptionsInTimeline, }), - staleTime: 3 * 60 * 1000, + getNextPageParam: (lastPage) => lastPage.data?.at(-1)?.entries.publishedAt, initialPageParam: undefined as undefined | string, refetchOnWindowFocus: false, refetchOnReconnect: false, + // DON'T refetch when the router is pop to previous page + refetchOnMount: fetchUnread && feedUnreadDirty && !isPop ? "always" : false, + + staleTime: + // Force refetch unread entries when feed is dirty + // HACK: disable refetch when the router is pop to previous page + isPop ? Infinity : fetchUnread && feedUnreadDirty ? 0 : defaultStaleTime, enabled: !!props, }) } @@ -104,7 +141,7 @@ function sortEntryIdsByPublishDate(a: string, b: string) { return entryB.publishedAt.getTime() - entryA.publishedAt.getTime() } -export const useEntryIdsByView = (view: FeedViewType, excludePrivate: boolean) => { +export const useEntryIdsByView = (view: FeedViewType, excludePrivate: boolean | undefined) => { return useEntryStore( useCallback( (state) => { diff --git a/packages/internal/store/src/entry/store.ts b/packages/internal/store/src/entry/store.ts index 79a07adb1..555eb57f7 100644 --- a/packages/internal/store/src/entry/store.ts +++ b/packages/internal/store/src/entry/store.ts @@ -489,12 +489,12 @@ class EntrySyncServices { await entryActions.upsertMany(entries) - if (isCollection && res.data) { - if (view === undefined) { - console.error("view is required for collection") - } - const collections = honoMorph.toCollections(res.data, view ?? 0) - await collectionActions.upsertMany(collections) + if (typeof view === "number") { + const { collections, entryIdsNotInCollections } = honoMorph.toCollections(res.data, view) + await collectionActions.upsertMany(collections, { + reset: params.isCollection && !pageParam, + }) + await collectionActions.delete(entryIdsNotInCollections) } const dataFeeds = res.data?.map((e) => e.feeds).filter((f) => f.type === "feed") diff --git a/packages/internal/store/src/entry/types.ts b/packages/internal/store/src/entry/types.ts index 80fdd74d0..7a60b5a5e 100644 --- a/packages/internal/store/src/entry/types.ts +++ b/packages/internal/store/src/entry/types.ts @@ -16,8 +16,8 @@ export type FetchEntriesProps = { } export type FetchEntriesPropsSettings = { - hidePrivateSubscriptionsInTimeline: boolean - unreadOnly: boolean + hidePrivateSubscriptionsInTimeline?: boolean + unreadOnly?: boolean } export type UseEntriesProps = { diff --git a/packages/internal/store/src/morph/hono.ts b/packages/internal/store/src/morph/hono.ts index 49f7d9b8d..be9bf2e20 100644 --- a/packages/internal/store/src/morph/hono.ts +++ b/packages/internal/store/src/morph/hono.ts @@ -146,23 +146,33 @@ class Morph { } toCollections( - data: HonoApiClient.Entry_Post | HonoApiClient.Entry_Inbox_Post, + data: HonoApiClient.Entry_Post | HonoApiClient.Entry_Inbox_Post | undefined, view: FeedViewType, - ): CollectionModel[] { - if (!data) return [] satisfies CollectionModel[] - return data - .map((item) => { - if (!item.collections) { - return null - } - return { - createdAt: item.collections.createdAt, - entryId: item.entries.id, - feedId: item.feeds.id, - view, - } satisfies CollectionModel + ): { + collections: CollectionModel[] + entryIdsNotInCollections: string[] + } { + if (!data) return { collections: [], entryIdsNotInCollections: [] } + + const collections: CollectionModel[] = [] + const entryIdsNotInCollections: string[] = [] + for (const item of data) { + if (!item.collections) { + entryIdsNotInCollections.push(item.entries.id) + continue + } + collections.push({ + createdAt: item.collections.createdAt, + entryId: item.entries.id, + feedId: item.feeds.id, + view, }) - .filter((i) => i !== null) + } + + return { + collections, + entryIdsNotInCollections, + } } toEntry(data?: HonoApiClient.Entry_Get | HonoApiClient.Entry_Inbox_Get): EntryModel | null { diff --git a/packages/internal/store/src/subscription/store.ts b/packages/internal/store/src/subscription/store.ts index 1c2ec7d7b..aa93a25e4 100644 --- a/packages/internal/store/src/subscription/store.ts +++ b/packages/internal/store/src/subscription/store.ts @@ -4,6 +4,7 @@ import { tracker } from "@follow/tracker" import { omit } from "es-toolkit" import { apiClient } from "../context" +import { invalidateEntriesQuery } from "../entry/hooks" import { getFeedById } from "../feed/getter" import { feedActions } from "../feed/store" import { inboxActions } from "../inbox/store" @@ -585,6 +586,10 @@ class SubscriptionSyncService { feedIds: folderFeedIds, view: newView, }) + + invalidateEntriesQuery({ + views: [currentView, newView], + }) } async renameCategory({ diff --git a/patches/@sentry__electron.patch b/patches/@sentry__electron.patch index 2aa4921df..d7f241ca2 100644 --- a/patches/@sentry__electron.patch +++ b/patches/@sentry__electron.patch @@ -1,8 +1,8 @@ diff --git a/esm/main/ipc.js b/esm/main/ipc.js -index 8edfaf4660734f9120a6d5f5f806688c25a3a026..a692b1c16470b0dbed7c53e1c9aeff8a456f1c5a 100644 +index b5d4478b20cac94e1e1e54900d616286aa9f7733..3639b802c2f4012e943ed2f3d0454fbf5eeda3e3 100644 --- a/esm/main/ipc.js +++ b/esm/main/ipc.js -@@ -111,14 +111,6 @@ function configureProtocol(client, options) { +@@ -134,14 +134,6 @@ function configureProtocol(client, options) { if (app.isReady()) { throw new Error("Sentry SDK should be initialized before the Electron app 'ready' event is fired"); } @@ -18,10 +18,10 @@ index 8edfaf4660734f9120a6d5f5f806688c25a3a026..a692b1c16470b0dbed7c53e1c9aeff8a app .whenReady() diff --git a/main/ipc.js b/main/ipc.js -index 01fc75bdf031b62195504cc0bf7055ebbf15b641..0519242ba5f391f9f618229044948feefeae1de8 100644 +index 6866654fb79361ec90cc9b175a624d80c1e614b4..74494413854aba2acf777d641e356d0200b5a6d1 100644 --- a/main/ipc.js +++ b/main/ipc.js -@@ -111,14 +111,6 @@ function configureProtocol(client, options) { +@@ -134,14 +134,6 @@ function configureProtocol(client, options) { if (electron.app.isReady()) { throw new Error("Sentry SDK should be initialized before the Electron app 'ready' event is fired"); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b88062eea..faf20fb81 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,7 +31,7 @@ patchedDependencies: hash: 5b5ab1ba36e8c0d7ffee912ebf29c1a18bc101c9c661ceb1bb0bda3deaf4c667 path: patches/@pengx17__electron-forge-maker-appimage.patch '@sentry/electron': - hash: b5efa039abfa14f7833b762e8a7c1c3ae147fda22954d860ffa3387db9acf8eb + hash: a5a19cbba4427bc1e6f675a47170c6e03adc0fcec259258bfa07288185b5a979 path: patches/@sentry__electron.patch daisyui@4.12.24: hash: d393ab1cbfbfcff21dce0796a59c2d8a37e2c6dd634a8ab476cbc67e47b93d9c @@ -356,7 +356,7 @@ importers: version: 1.0.1 '@sentry/electron': specifier: 6.8.0 - version: 6.8.0(patch_hash=b5efa039abfa14f7833b762e8a7c1c3ae147fda22954d860ffa3387db9acf8eb) + version: 6.8.0(patch_hash=a5a19cbba4427bc1e6f675a47170c6e03adc0fcec259258bfa07288185b5a979) builder-util-runtime: specifier: 9.3.1 version: 9.3.1 @@ -21940,7 +21940,7 @@ snapshots: '@sentry/core@9.30.0': {} - '@sentry/electron@6.8.0(patch_hash=b5efa039abfa14f7833b762e8a7c1c3ae147fda22954d860ffa3387db9acf8eb)': + '@sentry/electron@6.8.0(patch_hash=a5a19cbba4427bc1e6f675a47170c6e03adc0fcec259258bfa07288185b5a979)': dependencies: '@sentry/browser': 9.26.0 '@sentry/core': 9.26.0