From bcf4f78fabec5c22055d4c8e8b904b98dd228110 Mon Sep 17 00:00:00 2001 From: Innei Date: Mon, 23 Jun 2025 22:36:09 +0800 Subject: [PATCH 1/9] release(mobile): release v0.2.2 --- apps/mobile/changelog/0.2.2.md | 26 ++++++++++++++++++++++++++ apps/mobile/changelog/next.md | 19 ------------------- apps/mobile/package.json | 2 +- 3 files changed, 27 insertions(+), 20 deletions(-) create mode 100644 apps/mobile/changelog/0.2.2.md diff --git a/apps/mobile/changelog/0.2.2.md b/apps/mobile/changelog/0.2.2.md new file mode 100644 index 000000000..998a1beb2 --- /dev/null +++ b/apps/mobile/changelog/0.2.2.md @@ -0,0 +1,26 @@ +# What's New in v0.2.2 + +## ⚠️ 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! 😎 + +## Improvements + +- 🌟 Buckle up, folks! Our AI summarization just got a dazzling new animation makeover—smoother, flashier, and ready to impress! Plus, we’ve cranked up the performance so it runs like a dream. Get ready to be wowed! 😎✨ + +## 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! 🚀 diff --git a/apps/mobile/changelog/next.md b/apps/mobile/changelog/next.md index ac3e39f29..4ef2c94b6 100644 --- a/apps/mobile/changelog/next.md +++ b/apps/mobile/changelog/next.md @@ -1,26 +1,7 @@ # 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! 😎 - ## Improvements -- 🌟 Buckle up, folks! Our AI summarization just got a dazzling new animation makeover—smoother, flashier, and ready to impress! Plus, we’ve cranked up the performance so it runs like a dream. Get ready to be wowed! 😎✨ - ## 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! 🚀 diff --git a/apps/mobile/package.json b/apps/mobile/package.json index b38190210..601ccd599 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@follow/mobile", - "version": "0.2.1", + "version": "0.2.2", "private": true, "main": "src/main.tsx", "scripts": { From abbc8ca1c938c7a558c2a725d5878feeeb7c3c79 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Thu, 26 Jun 2025 09:30:59 +0800 Subject: [PATCH 2/9] fix: collection is not up to date (#4009) --- apps/desktop/layer/renderer/src/main.tsx | 8 +- .../src/modules/discover/FeedForm.tsx | 10 +- .../src/modules/discover/ListForm.tsx | 10 +- .../entry-column/hooks/useEntriesByView.ts | 30 +++--- .../subscription-column/FeedCategory.tsx | 7 -- .../layer/renderer/src/queries/entries.ts | 93 +------------------ .../layer/renderer/src/queries/feed.ts | 14 +-- apps/mobile/src/main.tsx | 8 +- .../mobile/src/modules/context-menu/feeds.tsx | 23 ++--- apps/mobile/src/modules/screen/atoms.ts | 19 ++-- .../database/src/services/collection.ts | 12 ++- .../internal/store/src/collection/store.ts | 40 +++++--- packages/internal/store/src/context.ts | 3 + packages/internal/store/src/entry/hooks.ts | 65 ++++++++++--- packages/internal/store/src/entry/store.ts | 11 +-- packages/internal/store/src/entry/types.ts | 4 +- packages/internal/store/src/morph/hono.ts | 2 +- .../internal/store/src/subscription/store.ts | 5 + 18 files changed, 159 insertions(+), 205 deletions(-) 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/discover/FeedForm.tsx b/apps/desktop/layer/renderer/src/modules/discover/FeedForm.tsx index ab8414db5..6232ea55f 100644 --- a/apps/desktop/layer/renderer/src/modules/discover/FeedForm.tsx +++ b/apps/desktop/layer/renderer/src/modules/discover/FeedForm.tsx @@ -15,6 +15,7 @@ import { ScrollArea } from "@follow/components/ui/scroll-area/index.js" import { Switch } from "@follow/components/ui/switch/index.jsx" import { FeedViewType } from "@follow/constants" import type { EntryModelSimple, FeedAnalyticsModel, FeedModel } from "@follow/models/types" +import { invalidateEntriesQuery } from "@follow/store/entry/hooks" import { useFeedByIdOrUrl } from "@follow/store/feed/hooks" import { useCategories, useSubscriptionByFeedId } from "@follow/store/subscription/hooks" import { subscriptionSyncService } from "@follow/store/subscription/store" @@ -36,7 +37,6 @@ import { getRouteParams } from "~/hooks/biz/useRouteParams" import { useI18n } from "~/hooks/common" import { apiClient } from "~/lib/api-fetch" import { toastFetchError } from "~/lib/error-parser" -import { entries as entriesQuery } from "~/queries/entries" import { feed as feedQuery, useFeedQuery } from "~/queries/feed" import { ViewSelectorRadioGroup } from "../shared/ViewSelectorRadioGroup" @@ -250,13 +250,7 @@ const FeedInnerForm = ({ }, onSuccess: (data, variables) => { if (getGeneralSettings().hidePrivateSubscriptionsInTimeline) { - entriesQuery - .entries({ - feedId: "all", - view: Number(variables.view), - excludePrivate: true, - }) - .invalidate({ exact: true }) + invalidateEntriesQuery({ views: [Number(variables.view)] }) } if ("unread" in data) { diff --git a/apps/desktop/layer/renderer/src/modules/discover/ListForm.tsx b/apps/desktop/layer/renderer/src/modules/discover/ListForm.tsx index 94ea6c832..1dc27b409 100644 --- a/apps/desktop/layer/renderer/src/modules/discover/ListForm.tsx +++ b/apps/desktop/layer/renderer/src/modules/discover/ListForm.tsx @@ -13,6 +13,7 @@ import { LoadingCircle } from "@follow/components/ui/loading/index.jsx" import { Switch } from "@follow/components/ui/switch/index.jsx" import { FeedViewType } from "@follow/constants" import type { ListAnalyticsModel } from "@follow/models/types" +import { invalidateEntriesQuery } from "@follow/store/entry/hooks" import { useListById, usePrefetchListById } from "@follow/store/list/hooks" import type { ListModel } from "@follow/store/list/types" import { useSubscriptionByFeedId } from "@follow/store/subscription/hooks" @@ -34,7 +35,6 @@ import { useI18n } from "~/hooks/common" import { apiClient } from "~/lib/api-fetch" import { getFetchErrorMessage, toastFetchError } from "~/lib/error-parser" import { getNewIssueUrl } from "~/lib/issues" -import { entries as entriesQuery } from "~/queries/entries" import { useTOTPModalWrapper } from "../profile/hooks" import { ViewSelectorRadioGroup } from "../shared/ViewSelectorRadioGroup" @@ -224,13 +224,7 @@ const ListInnerForm = ({ }, onSuccess: (data, variables) => { if (getGeneralSettings().hidePrivateSubscriptionsInTimeline) { - entriesQuery - .entries({ - feedId: "all", - view: Number(variables.view), - excludePrivate: true, - }) - .invalidate({ exact: true }) + invalidateEntriesQuery({ views: [Number(variables.view)] }) } if ("unread" in data) { diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/hooks/useEntriesByView.ts b/apps/desktop/layer/renderer/src/modules/entry-column/hooks/useEntriesByView.ts index 2c30f8f42..b373f6479 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/hooks/useEntriesByView.ts +++ b/apps/desktop/layer/renderer/src/modules/entry-column/hooks/useEntriesByView.ts @@ -1,6 +1,7 @@ import { views } from "@follow/constants" import { useCollectionEntryList } from "@follow/store/collection/hooks" import { + useEntriesQuery, useEntryIdsByFeedId, useEntryIdsByFeedIds, useEntryIdsByInboxId, @@ -21,7 +22,7 @@ import { useGeneralSettingKey } from "~/atoms/settings/general" import { ROUTE_FEED_PENDING } from "~/constants/app" import { useRouteParams } from "~/hooks/biz/useRouteParams" import { useAuthQuery } from "~/hooks/common" -import { entries, useEntries } from "~/queries/entries" +import { entries } from "~/queries/entries" import { useIsPreviewFeed } from "./useIsPreviewFeed" @@ -45,8 +46,10 @@ const useRemoteEntries = (): UseEntriesReturn => { inboxId, listId, view, - ...(unreadOnly === true && !isPreview && { read: false }), - ...(hidePrivateSubscriptionsInTimeline === true && { excludePrivate: true }), + ...(unreadOnly === true && !isPreview && { unreadOnly: true }), + ...(hidePrivateSubscriptionsInTimeline === true && { + hidePrivateSubscriptionsInTimeline: true, + }), } if (feedId && listId && isBizId(feedId)) { @@ -64,7 +67,7 @@ const useRemoteEntries = (): UseEntriesReturn => { view, hidePrivateSubscriptionsInTimeline, ]) - const query = useEntries(entriesOptions) + const query = useEntriesQuery(entriesOptions) const [fetchedTime, setFetchedTime] = useState() useEffect(() => { @@ -155,15 +158,16 @@ const useLocalEntries = (): UseEntriesReturn => { const allEntries = useEntryStore( useCallback( (state) => { - const ids = showEntriesByView - ? (entryIdsByView ?? []) - : (getEntryIdsFromMultiplePlace( - entryIdsByCollections, - entryIdsByFeedId, - entryIdsByCategory, - entryIdsByListId, - entryIdsByInboxId, - ) ?? []) + const ids = isCollection + ? entryIdsByCollections + : showEntriesByView + ? (entryIdsByView ?? []) + : (getEntryIdsFromMultiplePlace( + entryIdsByFeedId, + entryIdsByCategory, + entryIdsByListId, + entryIdsByInboxId, + ) ?? []) return ids .map((id) => { diff --git a/apps/desktop/layer/renderer/src/modules/subscription-column/FeedCategory.tsx b/apps/desktop/layer/renderer/src/modules/subscription-column/FeedCategory.tsx index 3b30ed0a8..2eaa1fb9e 100644 --- a/apps/desktop/layer/renderer/src/modules/subscription-column/FeedCategory.tsx +++ b/apps/desktop/layer/renderer/src/modules/subscription-column/FeedCategory.tsx @@ -35,7 +35,6 @@ import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry" import { getRouteParams, useRouteParamsSelector } from "~/hooks/biz/useRouteParams" import { useContextMenu } from "~/hooks/common/useContextMenu" import { createErrorToaster } from "~/lib/error-parser" -import { invalidateEntriesQuery } from "~/queries/entries" import { getPreferredTitle } from "~/store/feed/hooks" import { useModalStack } from "../../components/ui/modal/stacked/hooks" @@ -162,12 +161,6 @@ function FeedCategoryImpl({ data: ids, view, categoryOpenStateData }: FeedCatego newView: nextView, }) }, - - onSuccess(_data, variables) { - invalidateEntriesQuery({ - views: [view, variables], - }) - }, }) const [isCategoryEditing, setIsCategoryEditing] = useState(false) diff --git a/apps/desktop/layer/renderer/src/queries/entries.ts b/apps/desktop/layer/renderer/src/queries/entries.ts index 10a0773e7..50b17f172 100644 --- a/apps/desktop/layer/renderer/src/queries/entries.ts +++ b/apps/desktop/layer/renderer/src/queries/entries.ts @@ -1,61 +1,9 @@ -import type { FeedViewType } from "@follow/constants" -import { useFeedUnreadIsDirty } from "@follow/store/atoms/feed" -import { entrySyncServices } from "@follow/store/entry/store" - -import { useAuthInfiniteQuery, useAuthQuery } from "~/hooks/common" +import { useAuthQuery } from "~/hooks/common" import { apiClient } from "~/lib/api-fetch" import { defineQuery } from "~/lib/defineQuery" -import { queryClient } from "~/lib/query-client" import { getEntriesParams } from "~/lib/utils" -export function invalidateEntriesQuery({ views }: { views: FeedViewType[] }) { - return queryClient.invalidateQueries({ - predicate: (query) => { - const { queryKey } = query - if (Array.isArray(queryKey) && queryKey[0] === "entries") { - const view = queryKey[2] - return views.includes(view as FeedViewType) - } - return false - }, - }) -} - export const entries = { - entries: ({ - feedId, - inboxId, - listId, - view, - read, - excludePrivate, - limit, - }: { - feedId?: string - inboxId?: string - listId?: string - view?: number - read?: boolean - excludePrivate?: boolean - limit?: number - }) => - defineQuery( - ["entries", inboxId || listId || feedId, view, read, excludePrivate, limit], - async ({ pageParam }) => - entrySyncServices.fetchEntries({ - feedId, - inboxId, - listId, - view, - read, - excludePrivate, - limit, - pageParam: pageParam as string, - }), - { - rootKey: ["entries", inboxId || listId || feedId], - }, - ), preview: (id: string) => defineQuery( ["entries-preview", id], @@ -123,45 +71,6 @@ export const entries = { ), } -const defaultStaleTime = 10 * (60 * 1000) // 10 minutes - -export const useEntries = ({ - feedId, - inboxId, - listId, - view, - read, - excludePrivate, -}: { - feedId?: string - inboxId?: string - listId?: string - view?: number - read?: boolean - excludePrivate?: boolean -}) => { - const fetchUnread = read === false - const feedUnreadDirty = useFeedUnreadIsDirty((feedId as string) || "") - - return useAuthInfiniteQuery( - entries.entries({ feedId, inboxId, listId, view, read, excludePrivate }), - { - enabled: feedId !== undefined || inboxId !== undefined || listId !== undefined, - getNextPageParam: (lastPage) => lastPage.data?.at(-1)?.entries.publishedAt, - initialPageParam: undefined, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - // DON'T refetch when the router is pop to previous page - refetchOnMount: fetchUnread && feedUnreadDirty && !history.isPop ? "always" : false, - - staleTime: - // Force refetch unread entries when feed is dirty - // HACK: disable refetch when the router is pop to previous page - history.isPop ? Infinity : fetchUnread && feedUnreadDirty ? 0 : defaultStaleTime, - }, - ) -} - export const useEntriesPreview = ({ id }: { id?: string }) => useAuthQuery(entries.preview(id!), { enabled: !!id, diff --git a/apps/desktop/layer/renderer/src/queries/feed.ts b/apps/desktop/layer/renderer/src/queries/feed.ts index 882574f15..f32ecbed5 100644 --- a/apps/desktop/layer/renderer/src/queries/feed.ts +++ b/apps/desktop/layer/renderer/src/queries/feed.ts @@ -12,8 +12,6 @@ import { apiClient } from "~/lib/api-fetch" import { defineQuery } from "~/lib/defineQuery" import { toastFetchError } from "~/lib/error-parser" -import { entries } from "./entries" - type FeedQueryParams = { id?: string; url?: string } export const feed = { @@ -76,15 +74,6 @@ export const useRefreshFeedMutation = (feedId?: string) => useMutation({ mutationKey: ["refreshFeed", feedId], mutationFn: () => apiClient.feeds.refresh.$get({ query: { id: feedId! } }), - - onSuccess() { - if (!feedId) return - entries - .entries({ - feedId: feedId!, - }) - .invalidateRoot() - }, async onError(err) { toastFetchError(err) }, @@ -99,8 +88,7 @@ export const useResetFeed = () => { toastIDRef.current = toast.loading(t("sidebar.feed_actions.resetting_feed")) await apiClient.feeds.reset.$get({ query: { id: feedId } }) }, - onSuccess: (_, feedId) => { - entries.entries({ feedId }).invalidateRoot() + onSuccess: () => { toast.success( t("sidebar.feed_actions.reset_feed_success"), toastIDRef.current ? { id: toastIDRef.current } : undefined, diff --git a/apps/mobile/src/main.tsx b/apps/mobile/src/main.tsx index 7167ada68..d812e0b82 100644 --- a/apps/mobile/src/main.tsx +++ b/apps/mobile/src/main.tsx @@ -1,6 +1,10 @@ import "./global.css" -import { apiClientSimpleContext, authClientSimpleContext } from "@follow/store/context" +import { + apiClientSimpleContext, + authClientSimpleContext, + queryClientSimpleContext, +} from "@follow/store/context" import { registerRootComponent } from "expo" import { Image } from "expo-image" import { LinearGradient } from "expo-linear-gradient" @@ -20,6 +24,7 @@ import { TabBarPortal } from "./lib/navigation/bottom-tab/TabBarPortal" import { TabRoot } from "./lib/navigation/bottom-tab/TabRoot" import { TabScreen } from "./lib/navigation/bottom-tab/TabScreen" import { RootStackNavigation } from "./lib/navigation/StackNavigation" +import { queryClient } from "./lib/query-client" import { RootProviders } from "./providers" import { IndexTabScreen } from "./screens/(stack)/(tabs)" import { DiscoverTabScreen } from "./screens/(stack)/(tabs)/discover" @@ -33,6 +38,7 @@ global.APP_NAME = "Folo" global.ELECTRON = false apiClientSimpleContext.provide(apiClient) authClientSimpleContext.provide(authClient) +queryClientSimpleContext.provide(queryClient) enableFreeze(true) ;[Image, LinearGradient].forEach((Component) => { diff --git a/apps/mobile/src/modules/context-menu/feeds.tsx b/apps/mobile/src/modules/context-menu/feeds.tsx index 5e0027469..387d2b0d1 100644 --- a/apps/mobile/src/modules/context-menu/feeds.tsx +++ b/apps/mobile/src/modules/context-menu/feeds.tsx @@ -1,9 +1,5 @@ import { FeedViewType } from "@follow/constants" -import { - getInvalidateEntriesQueryPredicate, - useEntriesQuery, - useEntryIdsByFeedId, -} from "@follow/store/entry/hooks" +import { useEntriesQuery, useEntryIdsByFeedId } from "@follow/store/entry/hooks" import { getFeedById } from "@follow/store/feed/getter" import { getSubscriptionById } from "@follow/store/subscription/getter" import { getSubscriptionCategory } from "@follow/store/subscription/hooks" @@ -24,7 +20,6 @@ import { PlatformActivityIndicator } from "@/src/components/ui/loading/PlatformA import { views } from "@/src/constants/views" import { useNavigation } from "@/src/lib/navigation/hooks" import type { Navigation } from "@/src/lib/navigation/Navigation" -import { queryClient } from "@/src/lib/query-client" import { toast } from "@/src/lib/toast" import { FollowScreen } from "@/src/screens/(modal)/FollowScreen" import { FeedScreen } from "@/src/screens/(stack)/feeds/[feedId]/FeedScreen" @@ -315,17 +310,11 @@ export const SubscriptionFeedCategoryContextMenu = ({ key={`SubContent/${view.name}`} value={isSelected} onSelect={() => { - subscriptionSyncService - .changeCategoryView({ - category, - currentView, - newView: view.view, - }) - .then(() => { - queryClient.invalidateQueries({ - predicate: getInvalidateEntriesQueryPredicate([view.view, currentView]), - }) - }) + subscriptionSyncService.changeCategoryView({ + category, + currentView, + newView: view.view, + }) }} > {t(view.name, { ns: "common" })} diff --git a/apps/mobile/src/modules/screen/atoms.ts b/apps/mobile/src/modules/screen/atoms.ts index bf576e804..2a185d890 100644 --- a/apps/mobile/src/modules/screen/atoms.ts +++ b/apps/mobile/src/modules/screen/atoms.ts @@ -234,15 +234,16 @@ function useLocalEntries(props?: UseEntriesProps): UseEntriesReturn { const allEntries = useEntryStore( useCallback( (state) => { - const ids = showEntriesByView - ? (entryIdsByView ?? []) - : (getEntryIdsFromMultiplePlace( - entryIdsByCollections, - entryIdsByFeedId, - entryIdsByCategory, - entryIdsByListId, - entryIdsByInboxId, - ) ?? []) + const ids = isCollection + ? entryIdsByCollections + : showEntriesByView + ? (entryIdsByView ?? []) + : (getEntryIdsFromMultiplePlace( + entryIdsByFeedId, + entryIdsByCategory, + entryIdsByListId, + entryIdsByInboxId, + ) ?? []) return ids .map((id) => { diff --git a/packages/internal/database/src/services/collection.ts b/packages/internal/database/src/services/collection.ts index 7ee0dab39..ea19e3821 100644 --- a/packages/internal/database/src/services/collection.ts +++ b/packages/internal/database/src/services/collection.ts @@ -11,8 +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) @@ -26,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/store/src/collection/store.ts b/packages/internal/store/src/collection/store.ts index b997f32c9..af55bdf8c 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,18 +106,18 @@ 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) { const state = useCollectionStore.getState() const nextCollections: CollectionState["collections"] = { ...state.collections, @@ -133,8 +140,17 @@ class CollectionActions implements Hydratable { 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..830d57a32 100644 --- a/packages/internal/store/src/entry/store.ts +++ b/packages/internal/store/src/entry/store.ts @@ -489,12 +489,11 @@ 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 = honoMorph.toCollections(res.data, view) + await collectionActions.upsertMany(collections, { + reset: params.isCollection && !pageParam, + }) } 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..f82937ebd 100644 --- a/packages/internal/store/src/morph/hono.ts +++ b/packages/internal/store/src/morph/hono.ts @@ -146,7 +146,7 @@ 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[] 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({ From 8836522a0128c04c5de918c3c1b6debf14e55a01 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Thu, 26 Jun 2025 10:15:00 +0800 Subject: [PATCH 3/9] fix: clear collection form entries fetch --- .../internal/store/src/collection/store.ts | 20 ++++++++-- packages/internal/store/src/entry/store.ts | 3 +- packages/internal/store/src/morph/hono.ts | 38 ++++++++++++------- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/packages/internal/store/src/collection/store.ts b/packages/internal/store/src/collection/store.ts index af55bdf8c..f3462486e 100644 --- a/packages/internal/store/src/collection/store.ts +++ b/packages/internal/store/src/collection/store.ts @@ -117,25 +117,37 @@ class CollectionActions implements Hydratable, Resetable { await tx.run() } - 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() } diff --git a/packages/internal/store/src/entry/store.ts b/packages/internal/store/src/entry/store.ts index 830d57a32..555eb57f7 100644 --- a/packages/internal/store/src/entry/store.ts +++ b/packages/internal/store/src/entry/store.ts @@ -490,10 +490,11 @@ class EntrySyncServices { await entryActions.upsertMany(entries) if (typeof view === "number") { - const collections = honoMorph.toCollections(res.data, view) + 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/morph/hono.ts b/packages/internal/store/src/morph/hono.ts index f82937ebd..be9bf2e20 100644 --- a/packages/internal/store/src/morph/hono.ts +++ b/packages/internal/store/src/morph/hono.ts @@ -148,21 +148,31 @@ class Morph { toCollections( 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 { From 2bbe51221dcea72c963a45c8afceee71f785d708 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Thu, 26 Jun 2025 21:03:23 +0800 Subject: [PATCH 4/9] fix: try credentials: "omit" --- apps/mobile/src/lib/api-fetch.ts | 2 +- packages/internal/store/src/entry/store.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/mobile/src/lib/api-fetch.ts b/apps/mobile/src/lib/api-fetch.ts index 0ea57a528..5bb94ddcd 100644 --- a/apps/mobile/src/lib/api-fetch.ts +++ b/apps/mobile/src/lib/api-fetch.ts @@ -12,7 +12,7 @@ import { proxyEnv } from "./proxy-env" export const apiFetch = ofetch.create({ retry: false, - + credentials: "omit", baseURL: proxyEnv.API_URL, onRequest: async (ctx) => { const { options, request } = ctx diff --git a/packages/internal/store/src/entry/store.ts b/packages/internal/store/src/entry/store.ts index 555eb57f7..c079f38e0 100644 --- a/packages/internal/store/src/entry/store.ts +++ b/packages/internal/store/src/entry/store.ts @@ -606,7 +606,7 @@ class EntrySyncServices { cookie: options.cookie, } : undefined, - credentials: options?.cookie ? undefined : "include", + credentials: options?.cookie ? "omit" : "include", body: JSON.stringify({ ids: nextIds, }), From 1d13370367e3d4c578cab0b1ee05427a991adaad Mon Sep 17 00:00:00 2001 From: lawvs <18554747+lawvs@users.noreply.github.com> Date: Tue, 24 Jun 2025 00:03:37 +0800 Subject: [PATCH 5/9] fix: tweak style for proper layout on Android --- apps/mobile/src/components/ui/grouped/GroupedList.tsx | 9 +++++---- apps/mobile/src/modules/settings/routes/General.tsx | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/mobile/src/components/ui/grouped/GroupedList.tsx b/apps/mobile/src/components/ui/grouped/GroupedList.tsx index 92d994c8b..8a0d3f3f0 100644 --- a/apps/mobile/src/components/ui/grouped/GroupedList.tsx +++ b/apps/mobile/src/components/ui/grouped/GroupedList.tsx @@ -29,6 +29,7 @@ interface GroupedInsetListCardProps { } interface BaseCellClassNames { + className?: string leftClassName?: string rightClassName?: string } @@ -146,11 +147,11 @@ export const GroupedInsetListNavigationLink: FC< disabled?: boolean postfix?: React.ReactNode } & BaseCellClassNames -> = ({ label, icon, onPress, disabled, leftClassName, rightClassName, postfix }) => { +> = ({ label, icon, onPress, disabled, className, leftClassName, rightClassName, postfix }) => { const rightIconColor = useColor("tertiaryLabel") return ( - + {({ pressed }) => ( void } & BaseCellClassNames -> = ({ label, description, children, leftClassName, rightClassName, icon, onPress }) => { +> = ({ label, description, children, className, leftClassName, rightClassName, icon, onPress }) => { return ( diff --git a/apps/mobile/src/modules/settings/routes/General.tsx b/apps/mobile/src/modules/settings/routes/General.tsx index 89affac9f..d0518e655 100644 --- a/apps/mobile/src/modules/settings/routes/General.tsx +++ b/apps/mobile/src/modules/settings/routes/General.tsx @@ -131,6 +131,7 @@ export const GeneralScreen: NavigationControllerView = () => { return ( } > {/* Language */} From 1f7a2c89d6408137fe3359698cc38388909368a3 Mon Sep 17 00:00:00 2001 From: lawvs <18554747+lawvs@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:58:03 +0800 Subject: [PATCH 6/9] fix: update content view class name in SafeNavigationScrollView usage --- .../src/components/layouts/views/SafeNavigationScrollView.tsx | 3 ++- apps/mobile/src/modules/settings/routes/General.tsx | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/mobile/src/components/layouts/views/SafeNavigationScrollView.tsx b/apps/mobile/src/components/layouts/views/SafeNavigationScrollView.tsx index ceea9f2ed..e5e8f1c67 100644 --- a/apps/mobile/src/components/layouts/views/SafeNavigationScrollView.tsx +++ b/apps/mobile/src/components/layouts/views/SafeNavigationScrollView.tsx @@ -1,4 +1,5 @@ import { useTypeScriptHappyCallback } from "@follow/hooks" +import { cn } from "@follow/utils" import { useSetAtom, useStore } from "jotai" import type { PropsWithChildren } from "react" import { use, useImperativeHandle, useLayoutEffect, useRef, useState } from "react" @@ -134,7 +135,7 @@ export const SafeNavigationScrollView = ({ {...props} > - + {children} {ScrollViewBottom} diff --git a/apps/mobile/src/modules/settings/routes/General.tsx b/apps/mobile/src/modules/settings/routes/General.tsx index d0518e655..89affac9f 100644 --- a/apps/mobile/src/modules/settings/routes/General.tsx +++ b/apps/mobile/src/modules/settings/routes/General.tsx @@ -131,7 +131,6 @@ export const GeneralScreen: NavigationControllerView = () => { return ( } > {/* Language */} From 36b8cde4ba89e0382479e8ab607b48e1696b0233 Mon Sep 17 00:00:00 2001 From: lawvs <18554747+lawvs@users.noreply.github.com> Date: Tue, 24 Jun 2025 19:17:13 +0800 Subject: [PATCH 7/9] refactor: improve layout in TimelineViewSelector --- .../modules/screen/TimelineViewSelector.tsx | 48 +++++++++---------- .../subscription/items/UnreadCount.tsx | 7 ++- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/apps/mobile/src/modules/screen/TimelineViewSelector.tsx b/apps/mobile/src/modules/screen/TimelineViewSelector.tsx index 2e02c1a9d..c10984fea 100644 --- a/apps/mobile/src/modules/screen/TimelineViewSelector.tsx +++ b/apps/mobile/src/modules/screen/TimelineViewSelector.tsx @@ -1,16 +1,12 @@ import { useViewWithSubscription } from "@follow/store/subscription/hooks" import { useUnreadByView } from "@follow/store/unread/hooks" +import { cn } from "@follow/utils" import * as React from "react" import { useEffect } from "react" import { useTranslation } from "react-i18next" import type { StyleProp, ViewStyle } from "react-native" import { ScrollView, Text, useWindowDimensions, View } from "react-native" -import Animated, { - interpolate, - interpolateColor, - useAnimatedStyle, - useSharedValue, -} from "react-native-reanimated" +import Animated, { interpolate, interpolateColor, useAnimatedStyle } from "react-native-reanimated" import { ReAnimatedPressable } from "@/src/components/common/AnimatedComponents" import { TIMELINE_VIEW_SELECTOR_HEIGHT } from "@/src/constants/ui" @@ -28,7 +24,7 @@ import { TimelineViewSelectorContextMenu } from "./TimelineViewSelectorContextMe const ACTIVE_WIDTH = 180 const INACTIVE_WIDTH = 48 -const ACTIVE_TEXT_WIDTH = 85 +const ACTIVE_TEXT_WIDTH = 95 export function TimelineViewSelector() { const activeViews = useViewWithSubscription() @@ -69,15 +65,16 @@ function ItemWrapper({ index, activeColor, children, - isActive, onPress, style, + className, }: { children: React.ReactNode index: number isActive: boolean activeColor: string onPress: () => void + className?: string style?: Exclude, number> }) { const { width: windowWidth } = useWindowDimensions() @@ -88,13 +85,14 @@ function ItemWrapper({ windowWidth - (INACTIVE_WIDTH + 12) * (activeViews.length - 1) - 8 * 2, ACTIVE_WIDTH, ) - - const textWidth = useSharedValue(0) const bgColor = useColor("gray5") return ( ({ backgroundColor: interpolateColor( @@ -105,22 +103,13 @@ function ItemWrapper({ width: interpolate( dragProgress.get(), [index - 1, index, index + 1], - [INACTIVE_WIDTH, Math.max(activeWidth, textWidth.value + INACTIVE_WIDTH), INACTIVE_WIDTH], + [INACTIVE_WIDTH, Math.max(activeWidth, INACTIVE_WIDTH), INACTIVE_WIDTH], "clamp", ), ...style, }))} > - { - if (isActive) { - textWidth.set(nativeEvent.layout.width) - } - }} - > - {children} - + {children} ) } @@ -192,7 +181,7 @@ function ViewItem({ ({ width: interpolate( dragProgress.get(), @@ -202,21 +191,28 @@ function ViewItem({ ), }))} > - + {t(view.name)} {/* Unread indicator for inactive items */} ({ + left: 30, + top: 10, backgroundColor: textColor, borderColor, display: unreadCount ? "flex" : "none", diff --git a/apps/mobile/src/modules/subscription/items/UnreadCount.tsx b/apps/mobile/src/modules/subscription/items/UnreadCount.tsx index 26e9b6a10..0167a6e4d 100644 --- a/apps/mobile/src/modules/subscription/items/UnreadCount.tsx +++ b/apps/mobile/src/modules/subscription/items/UnreadCount.tsx @@ -22,7 +22,12 @@ export function UnreadCount({ if (!unread) return null return showUnreadCount ? ( - + {unread > max ? `${max}+` : unread} ) : ( From b844df5157256f855bb085fdf6f78f73416e01e8 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Sat, 28 Jun 2025 10:44:00 +0800 Subject: [PATCH 8/9] docs(mobile): update changelog --- apps/mobile/changelog/0.2.2.md | 34 ++++++++++++-------------- apps/mobile/changelog/next.md | 4 +++ apps/mobile/changelog/next.template.md | 4 +++ 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/apps/mobile/changelog/0.2.2.md b/apps/mobile/changelog/0.2.2.md index 998a1beb2..e62727313 100644 --- a/apps/mobile/changelog/0.2.2.md +++ b/apps/mobile/changelog/0.2.2.md @@ -1,26 +1,24 @@ # What's New in v0.2.2 -## ⚠️ Important +## Shiny New Things -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! 😎 +- Copy AI summaries with one tap +- Use video duration as an Action condition ## Improvements -- 🌟 Buckle up, folks! Our AI summarization just got a dazzling new animation makeover—smoother, flashier, and ready to impress! Plus, we’ve cranked up the performance so it runs like a dream. Get ready to be wowed! 😎✨ +- Top indicator now fades smoothly when switching views (fa78bad) +- Gradually rolling out an experimental unified local database for mobile and desktop (#3897 #3902) +- Improved image preview +- Refined Android dropdown-menu styling +- Fixed star/unstar status not syncing across devices (fbd0b3) -## No longer broken +## 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! 🚀 +- Fixed login failures caused by corrupted cache in some cases (2bbe512) +- Fixed inability to follow sources in preview mode (2c04919) +- Fixed profile-picture upload not working + +## Thanks + +Special thanks to volunteer contributors @kovsu @huanfe1 @cscnk52 @Olexandr88 @0-o0 @kingsword09 @ericyzhu for their valuable contributions diff --git a/apps/mobile/changelog/next.md b/apps/mobile/changelog/next.md index 4ef2c94b6..000f858e3 100644 --- a/apps/mobile/changelog/next.md +++ b/apps/mobile/changelog/next.md @@ -5,3 +5,7 @@ ## Improvements ## No longer broken + +## Thanks + +Special thanks to volunteer contributors @ for their valuable contributions diff --git a/apps/mobile/changelog/next.template.md b/apps/mobile/changelog/next.template.md index 4ef2c94b6..000f858e3 100644 --- a/apps/mobile/changelog/next.template.md +++ b/apps/mobile/changelog/next.template.md @@ -5,3 +5,7 @@ ## Improvements ## No longer broken + +## Thanks + +Special thanks to volunteer contributors @ for their valuable contributions From fc6f2a32bb3b302cbb2b0d92da5179398d93cb24 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Sat, 28 Jun 2025 10:46:57 +0800 Subject: [PATCH 9/9] chore(mobile): update version --- apps/mobile/ios/Folo/Info.plist | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/mobile/ios/Folo/Info.plist b/apps/mobile/ios/Folo/Info.plist index 409ca3d33..84f21c52e 100644 --- a/apps/mobile/ios/Folo/Info.plist +++ b/apps/mobile/ios/Folo/Info.plist @@ -32,7 +32,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 0.2.1 + 0.2.2 CFBundleSignature ???? CFBundleURLTypes @@ -52,7 +52,7 @@ CFBundleVersion - 109 + 110 ITSAppUsesNonExemptEncryption LSApplicationCategoryType