From 0b3306cea47b7de2435be44a7db042d8f37fa4a9 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Fri, 10 Jan 2025 17:16:36 +0800 Subject: [PATCH 01/93] chore: disable ssl by default --- apps/renderer/package.json | 2 +- vite.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/renderer/package.json b/apps/renderer/package.json index 054822739e..a889bafab9 100644 --- a/apps/renderer/package.json +++ b/apps/renderer/package.json @@ -6,7 +6,7 @@ "scripts": { "build:web": "cd ../.. && pnpm build:web", "dev": "cd ../.. && pnpm dev:web", - "dev:no-ssl": "cd ../.. && NO_SSL=true pnpm dev:web", + "dev:ssl": "cd ../.. && SSL=true pnpm dev:web", "generate-pwa-assets": "pwa-assets-generator public/icon.svg", "test": "vitest --typecheck", "typecheck": "tsc --noEmit" diff --git a/vite.config.ts b/vite.config.ts index a0e38a095c..6873d414fe 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -208,7 +208,7 @@ export default ({ mode }) => { ], }), htmlInjectPlugin(typedEnv), - process.env.NO_SSL ? false : mkcert(), + process.env.SSL ? mkcert() : false, devPrint(), createDependencyChunksPlugin([ // React framework From fe87c528e26063c980f9426b03dd18510b95ad84 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Fri, 10 Jan 2025 17:21:50 +0800 Subject: [PATCH 02/93] fix: do not skipValidation, close #2430 --- packages/shared/src/env.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/shared/src/env.ts b/packages/shared/src/env.ts index 3f324db4c5..c4cc20cb89 100644 --- a/packages/shared/src/env.ts +++ b/packages/shared/src/env.ts @@ -27,8 +27,6 @@ export const env = createEnv({ emptyStringAsUndefined: true, runtimeEnv: getRuntimeEnv() as any, - - skipValidation: !isDev, }) function metaEnvIsEmpty() { From fb00346ec8dc6224904afe4104f64b38cbbbfa6d Mon Sep 17 00:00:00 2001 From: DIYgod Date: Fri, 10 Jan 2025 17:33:16 +0800 Subject: [PATCH 03/93] chore: update script name --- README.md | 4 ++-- package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 24c3bb2ac2..fbeccd48ba 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ pnpm install ### Develop in the browser ```sh -pnpm run dev:web +pnpm run dev ``` Then the browser opens `https://app.follow.is/__debug_proxy`,you can access the online API environment to development and debugging. @@ -120,7 +120,7 @@ cp .env.example .env Then set `VITE_API_URL` to `https://api.follow.is` and run: ```sh -pnpm run dev +pnpm run dev:electron ``` Since it is not very convenient to develop in Electron, the first way to develop and contribute is recommended at this stage. diff --git a/package.json b/package.json index 748545d952..c12154fab4 100644 --- a/package.json +++ b/package.json @@ -23,12 +23,12 @@ "bump": "vv", "dedupe:locales": "eslint --fix locales/**/*.json", "depcheck": "npx depcheck --quiet", - "dev": "electron-vite dev", + "dev": "turbo run @follow/web#dev @follow/server#dev", "dev:debug": "export DEBUG=true && vite --debug", + "dev:electron": "electron-vite dev", "dev:expo": "pnpm --filter=mobile start", "dev:server": "pnpm run --filter=server dev", "dev:web": " WEB_BUILD=1 vite", - "dev:web-with-server": "turbo run @follow/web#dev @follow/server#dev", "format": "prettier --write .", "format:check": "prettier --check .", "generator:i18n-template": "tsx scripts/generate-i18n-locale.ts", From 61d3ed49b817997b8e7876efda2c0e29b08f9c4b Mon Sep 17 00:00:00 2001 From: DIYgod Date: Fri, 10 Jan 2025 17:59:51 +0800 Subject: [PATCH 04/93] feat: use non https debug host --- apps/renderer/debug_proxy.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/renderer/debug_proxy.html b/apps/renderer/debug_proxy.html index efc372c2e9..88dbafd7c8 100644 --- a/apps/renderer/debug_proxy.html +++ b/apps/renderer/debug_proxy.html @@ -20,7 +20,7 @@ const debugHostInSessionStorage = sessionStorage.getItem("debug-host") - const host = debugHost || debugHostInSessionStorage || "https://localhost:2233" + const host = debugHost || debugHostInSessionStorage || "http://localhost:2233" if (debugHost) { sessionStorage.setItem("debug-host", debugHost) } From f747882565fd474c0bf9cbab7b4a08a033e6386f Mon Sep 17 00:00:00 2001 From: Innei Date: Fri, 10 Jan 2025 20:03:21 +0800 Subject: [PATCH 05/93] feat(rn-search): impl search feeds and list view Signed-off-by: Innei --- .../src/components/ui/form/TextField.tsx | 3 + .../discover/search-tabs/SearchFeed.tsx | 129 +++++++++++++++++- .../modules/discover/search-tabs/__base.tsx | 17 ++- apps/mobile/src/modules/discover/search.tsx | 9 +- apps/mobile/src/modules/login/email.tsx | 10 +- apps/mobile/src/screens/(modal)/add.tsx | 3 +- apps/mobile/src/screens/(modal)/follow.tsx | 33 ++++- 7 files changed, 189 insertions(+), 15 deletions(-) diff --git a/apps/mobile/src/components/ui/form/TextField.tsx b/apps/mobile/src/components/ui/form/TextField.tsx index 4895a8cad7..0e9e7f5ac8 100644 --- a/apps/mobile/src/components/ui/form/TextField.tsx +++ b/apps/mobile/src/components/ui/form/TextField.tsx @@ -3,6 +3,8 @@ import { forwardRef } from "react" import type { StyleProp, TextInputProps, ViewStyle } from "react-native" import { StyleSheet, Text, TextInput, View } from "react-native" +import { accentColor } from "@/src/theme/colors" + import { FormLabel } from "./Label" interface TextFieldProps { @@ -35,6 +37,7 @@ export const TextField = forwardRef( style={wrapperStyle} > { const { searchValueAtom } = useSearchPageContext() const searchValue = useAtomValue(searchValueAtom) - const { data, isLoading } = useQuery({ + const { data, isLoading, refetch } = useQuery({ queryKey: ["searchFeed", searchValue], queryFn: () => { return apiClient.discover.$post({ @@ -38,6 +46,8 @@ export const SearchFeed = () => { return ( } data={data?.data} @@ -52,5 +62,118 @@ const renderItem = ({ item }: { item: SearchResultItem }) => ( ) const SearchFeedItem = memo(({ item }: { item: SearchResultItem }) => { - return {item.feed?.title} + const isSubscribed = useSubscriptionByFeedId(item.feed?.id ?? "") + return ( + + { + if (item.feed?.id) { + router.push(`/follow?id=${item.feed.id}`) + } + }} + > + {/* Headline */} + + + + + + + {item.feed?.title} + + {!!item.feed?.description && ( + + {item.feed?.description} + + )} + + {/* Subscribe */} + {isSubscribed && ( + + + Subscribed + + + )} + + + {/* Preview */} + + + {item.entries?.map((entry) => ( + // + // {entry.title} + // + + ))} + + + + + ) }) +const formatter = new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "short", + day: "numeric", +}) +const PreviewItem = ({ entry }: { entry: NonNullable[number] }) => { + const { width } = useWindowDimensions() + const firstMedia = entry.media?.[0] + + return ( + + {/* Left */} + + + {entry.title} + + + {formatter.format(new Date(entry.publishedAt))} + + + + {/* Right */} + {!!firstMedia && ( + + + + )} + + ) +} diff --git a/apps/mobile/src/modules/discover/search-tabs/__base.tsx b/apps/mobile/src/modules/discover/search-tabs/__base.tsx index e482077e60..cf9a5d0baf 100644 --- a/apps/mobile/src/modules/discover/search-tabs/__base.tsx +++ b/apps/mobile/src/modules/discover/search-tabs/__base.tsx @@ -1,6 +1,6 @@ import { forwardRef } from "react" import type { ScrollViewProps } from "react-native" -import { ScrollView, useWindowDimensions, View } from "react-native" +import { RefreshControl, ScrollView, useWindowDimensions, View } from "react-native" import type { FlatListPropsWithLayout } from "react-native-reanimated" import Animated, { LinearTransition } from "react-native-reanimated" import { useSafeAreaInsets } from "react-native-safe-area-context" @@ -41,7 +41,11 @@ export const BaseSearchPageRootView = ({ children }: { children: React.ReactNode ) } -export function BaseSearchPageFlatList({ ...props }: FlatListPropsWithLayout) { +export function BaseSearchPageFlatList({ + refreshing, + onRefresh, + ...props +}: FlatListPropsWithLayout & { refreshing: boolean; onRefresh: () => void }) { const insets = useSafeAreaInsets() const searchBarHeight = useSearchBarHeight() const offsetTop = searchBarHeight - insets.top @@ -51,10 +55,17 @@ export function BaseSearchPageFlatList({ ...props }: FlatListPropsWithLayout< itemLayoutAnimation={LinearTransition} className="flex-1" style={{ width: windowWidth }} - contentContainerStyle={{ paddingTop: offsetTop }} + contentContainerStyle={{ paddingTop: offsetTop + 8 }} scrollIndicatorInsets={{ bottom: insets.bottom, top: offsetTop }} automaticallyAdjustContentInsets contentInsetAdjustmentBehavior="always" + refreshControl={ + + } {...props} /> ) diff --git a/apps/mobile/src/modules/discover/search.tsx b/apps/mobile/src/modules/discover/search.tsx index 04f6da96f4..3039f3545e 100644 --- a/apps/mobile/src/modules/discover/search.tsx +++ b/apps/mobile/src/modules/discover/search.tsx @@ -56,7 +56,10 @@ const DiscoverHeaderImpl = () => { const headerHeight = getDefaultHeaderHeight(frame, false, insets.top) return ( - + @@ -70,7 +73,7 @@ const PlaceholerSearchBar = () => { return ( { router.push("/search") }} @@ -183,7 +186,7 @@ const SearchInput = () => { }, [isFocused]) return ( - + {focusOrHasValue && ( + return ( + + ) } export function EmailLogin() { diff --git a/apps/mobile/src/screens/(modal)/add.tsx b/apps/mobile/src/screens/(modal)/add.tsx index 28ed250575..ee30f6ea47 100644 --- a/apps/mobile/src/screens/(modal)/add.tsx +++ b/apps/mobile/src/screens/(modal)/add.tsx @@ -4,7 +4,7 @@ import { Text, TextInput, TouchableOpacity, View } from "react-native" import { ModalHeaderCloseButton } from "@/src/components/common/ModalSharedComponents" import { Search2CuteReIcon } from "@/src/icons/search_2_cute_re" -import { useColor } from "@/src/theme/colors" +import { accentColor, useColor } from "@/src/theme/colors" export default function Add() { const [url, setUrl] = useState("") @@ -33,6 +33,7 @@ export default function Add() { Feed URL feedSyncServices.fetchFeedById({ id: id as string }), + enabled: !feed, + }) + + if (isLoading) { + return ( + + + + ) + } + + return +} +function FollowImpl() { + const { id } = useLocalSearchParams() const feed = useFeed(id as string) - // const hasSub = useSubscriptionByFeedId(feed?.id || "") - // const isSubscribed = !!feedQuery.data?.subscription || hasSub + const isSubscribed = useSubscriptionByFeedId(feed?.id || "") const form = useForm>({ resolver: zodResolver(formSchema), @@ -73,11 +94,15 @@ export default function Follow() { const { isValid, isDirty } = form.formState + if (!feed?.id) { + return Feed ({id}) not found + } + return ( ( From 0663ab545bfa969bee1e4daa9465d79ec88f4bc8 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Fri, 10 Jan 2025 21:22:38 +0800 Subject: [PATCH 06/93] chore: update hono type --- packages/shared/src/hono.ts | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/packages/shared/src/hono.ts b/packages/shared/src/hono.ts index 14f5f0edc6..f916b5dfdf 100644 --- a/packages/shared/src/hono.ts +++ b/packages/shared/src/hono.ts @@ -11520,6 +11520,7 @@ declare const auth: { } & { image: string | null; handle: string | null; + twoFactorEnabled: boolean | null; }; session: { id: string; @@ -13472,6 +13473,7 @@ declare const auth: { } & { image: string | null; handle: string | null; + twoFactorEnabled: boolean | null; }; session: { id: string; @@ -16529,21 +16531,6 @@ declare const _routes: hono_hono_base.HonoBase | hono_types.MergeSchemaPath<{ "/": { $post: { From 6746b54bdac79b75480a428050f39af2ccbb24c5 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Fri, 10 Jan 2025 21:38:04 +0800 Subject: [PATCH 07/93] chore: skipValidation for vitest --- packages/shared/src/env.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/shared/src/env.ts b/packages/shared/src/env.ts index c4cc20cb89..00273b4fcc 100644 --- a/packages/shared/src/env.ts +++ b/packages/shared/src/env.ts @@ -27,6 +27,8 @@ export const env = createEnv({ emptyStringAsUndefined: true, runtimeEnv: getRuntimeEnv() as any, + + skipValidation: process.env.VITEST === "true", }) function metaEnvIsEmpty() { From 01011390a629de9e82d5206ff63231047f18899e Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Fri, 10 Jan 2025 21:43:08 +0800 Subject: [PATCH 08/93] chore: update --- packages/shared/src/env.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/env.ts b/packages/shared/src/env.ts index 00273b4fcc..bf75197d6e 100644 --- a/packages/shared/src/env.ts +++ b/packages/shared/src/env.ts @@ -28,7 +28,7 @@ export const env = createEnv({ emptyStringAsUndefined: true, runtimeEnv: getRuntimeEnv() as any, - skipValidation: process.env.VITEST === "true", + skipValidation: "process" in globalThis ? process.env.VITEST === "true" : false, }) function metaEnvIsEmpty() { From 4bae36ae70e5ce7c60eec385873f943976e798f1 Mon Sep 17 00:00:00 2001 From: Innei Date: Fri, 10 Jan 2025 22:38:19 +0800 Subject: [PATCH 09/93] feat(rn-component): refactor context menu for ios Signed-off-by: Innei --- apps/mobile/package.json | 2 + .../ui/context-menu/index.android.tsx | 2 +- .../components/ui/context-menu/index.ios.tsx | 92 ++++++- .../src/components/ui/context-menu/index.ts | 1 - .../src/components/ui/context-menu/index.tsx | 1 + .../src/components/ui/context-menu/types.ts | 37 +++ .../mobile/src/modules/context-menu/feeds.tsx | 243 +++++++++++------- .../mobile/src/modules/context-menu/lists.tsx | 58 +++-- .../discover/RecommendationListItem.tsx | 37 ++- .../discover/search-tabs/SearchFeed.tsx | 7 +- apps/mobile/src/modules/discover/search.tsx | 5 +- .../modules/subscription/CategoryGrouped.tsx | 10 +- .../src/modules/subscription/ViewTab.tsx | 9 +- .../modules/subscription/header-actions.tsx | 2 +- pnpm-lock.yaml | 36 +++ 15 files changed, 389 insertions(+), 153 deletions(-) delete mode 100644 apps/mobile/src/components/ui/context-menu/index.ts create mode 100644 apps/mobile/src/components/ui/context-menu/index.tsx create mode 100644 apps/mobile/src/components/ui/context-menu/types.ts diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 790d8dfa54..57d59b066e 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -67,6 +67,8 @@ "react-native": "0.76.5", "react-native-context-menu-view": "1.16.0", "react-native-gesture-handler": "~2.20.2", + "react-native-ios-context-menu": "3.1.0", + "react-native-ios-utilities": "5.1.0", "react-native-keyboard-controller": "^1.15.0", "react-native-pager-view": "6.6.1", "react-native-reanimated": "~3.16.5", diff --git a/apps/mobile/src/components/ui/context-menu/index.android.tsx b/apps/mobile/src/components/ui/context-menu/index.android.tsx index f36d0b7e6f..4af2883f27 100644 --- a/apps/mobile/src/components/ui/context-menu/index.android.tsx +++ b/apps/mobile/src/components/ui/context-menu/index.android.tsx @@ -1,6 +1,6 @@ import type { FC } from "react" -import type { ContextMenuProps } from "./index.ios" +import type { ContextMenuProps } from "./types" export const ContextMenu: FC = () => { throw new Error("ContextMenu not implemented on Android") diff --git a/apps/mobile/src/components/ui/context-menu/index.ios.tsx b/apps/mobile/src/components/ui/context-menu/index.ios.tsx index 5a5237d1ab..5e504ee9a9 100644 --- a/apps/mobile/src/components/ui/context-menu/index.ios.tsx +++ b/apps/mobile/src/components/ui/context-menu/index.ios.tsx @@ -1 +1,91 @@ -export { default as ContextMenu, type ContextMenuProps } from "react-native-context-menu-view" +// export { default as ContextMenu, type ContextMenuProps } from "react-native-context-menu-view" + +import type { FC, PropsWithChildren } from "react" +import { useMemo, useState } from "react" +import { View } from "react-native" +import type { MenuAttributes, MenuConfig, MenuElementConfig } from "react-native-ios-context-menu" +import { ContextMenuView } from "react-native-ios-context-menu" + +import { useColor } from "@/src/theme/colors" + +import type { ContextMenuProps, IContextMenuItemConfig } from "./types" + +export const ContextMenu: FC = ({ + config, + onPressMenuItem, + children, + renderPreview, + ...props +}) => { + const [actionKeyMap] = useState(() => new Map()) + const menuViewConfig = useMemo((): MenuConfig => { + const createMenuItems = (items: typeof config.items): MenuElementConfig[] => { + return items + .filter((item) => !!item) + .map((item) => { + actionKeyMap.set(item.actionKey, item) + + const menuAttributes: MenuAttributes[] = [] + if (item.destructive) { + menuAttributes.push("destructive") + } + if (item.disabled) { + menuAttributes.push("disabled") + } + if (item.hidden) { + menuAttributes.push("hidden") + } + + const menuItem: MenuElementConfig = { + actionTitle: item.title, + actionKey: item.actionKey, + icon: item.systemIcon ? { iconType: "SYSTEM", iconValue: item.systemIcon } : undefined, + menuAttributes, + } + + if (item.subMenu) { + const subMenuConfig = menuItem as any as MenuConfig + subMenuConfig.menuTitle = item.subMenu.title || item.title || "" + subMenuConfig.menuSubtitle = item.subMenu.subTitle || "" + subMenuConfig.menuItems = createMenuItems(item.subMenu.items) + } + + if (item.checked) { + menuItem.menuState = "on" + } + + return menuItem + }) + } + + return { + menuTitle: config.title || "", + menuSubtitle: config.subTitle || "", + menuItems: createMenuItems(config.items), + } + }, [config, actionKeyMap]) + + const backgroundColor = useColor("systemBackground") + return ( + + { + onPressMenuItem(actionKeyMap.get(e.nativeEvent.actionKey)!) + }} + > + {children} + + + ) +} diff --git a/apps/mobile/src/components/ui/context-menu/index.ts b/apps/mobile/src/components/ui/context-menu/index.ts deleted file mode 100644 index df697997b9..0000000000 --- a/apps/mobile/src/components/ui/context-menu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./index.ios" diff --git a/apps/mobile/src/components/ui/context-menu/index.tsx b/apps/mobile/src/components/ui/context-menu/index.tsx new file mode 100644 index 0000000000..6e32c14733 --- /dev/null +++ b/apps/mobile/src/components/ui/context-menu/index.tsx @@ -0,0 +1 @@ +export { ContextMenu } from "./index.ios" diff --git a/apps/mobile/src/components/ui/context-menu/types.ts b/apps/mobile/src/components/ui/context-menu/types.ts new file mode 100644 index 0000000000..a8c605b2fb --- /dev/null +++ b/apps/mobile/src/components/ui/context-menu/types.ts @@ -0,0 +1,37 @@ +import type { ViewProps } from "react-native" +import type { RenderItem } from "react-native-ios-context-menu" + +export interface IContextMenuConfig { + title?: string + subTitle?: string + items: NullableContextMenuItemConfig[] +} + +export type NullableContextMenuItemConfig = IContextMenuItemConfig | false | null | undefined +export interface IContextMenuItemConfig { + title: string + // icon?: IconConfig | ImageItemConfig + /** + * @note only available on iOS + */ + systemIcon?: string + + subMenu?: IContextMenuConfig + + destructive?: boolean + disabled?: boolean + hidden?: boolean + checked?: boolean + + actionKey: string +} + +export interface ContextMenuProps extends ViewProps { + config: IContextMenuConfig + onPressMenuItem: (item: IContextMenuItemConfig) => void + + /** + * @note only available on iOS + */ + renderPreview?: RenderItem +} diff --git a/apps/mobile/src/modules/context-menu/feeds.tsx b/apps/mobile/src/modules/context-menu/feeds.tsx index c8abae623b..9574ee24a1 100644 --- a/apps/mobile/src/modules/context-menu/feeds.tsx +++ b/apps/mobile/src/modules/context-menu/feeds.tsx @@ -1,15 +1,10 @@ -import type { FeedViewType } from "@follow/constants" +import { FeedViewType } from "@follow/constants" import type { FC, PropsWithChildren } from "react" -import { useCallback, useMemo } from "react" -import type { NativeSyntheticEvent } from "react-native" +import { useMemo } from "react" import { Alert, Clipboard } from "react-native" -import type { - ContextMenuAction, - ContextMenuOnPressNativeEvent, -} from "react-native-context-menu-view" -import { useEventCallback } from "usehooks-ts" import { ContextMenu } from "@/src/components/ui/context-menu" +import type { IContextMenuItemConfig } from "@/src/components/ui/context-menu/types" import { views } from "@/src/constants/views" import { getFeed } from "@/src/store/feed/getter" import { getSubscription } from "@/src/store/subscription/getter" @@ -17,44 +12,89 @@ import { useListSubscriptionCategory } from "@/src/store/subscription/hooks" import { subscriptionSyncService } from "@/src/store/subscription/store" import { unreadSyncService } from "@/src/store/unread/store" +enum FeedItemActionKey { + MARK_ALL_AS_READ = "markAllAsRead", + CLAIM = "claim", + BOOST = "boost", + ADD_TO_CATEGORY = "addToCategory", + CREATE_NEW_CATEGORY = "createNewCategory", + EDIT = "edit", + COPY_LINK = "copyLink", + UNSUBSCRIBE = "unsubscribe", + + CHANGE_TO_OTHER_VIEW = "changeToOtherView", + + DELETE = "delete", +} + +enum ChangeToOtherViewActionKey { + ARTICLE = "article", + SOCIAL = "social", + PICTURE = "picture", + VIDEO = "video", + NOTIFICATION = "notification", + AUDIO = "audio", +} +const changeViewActionKeyMapping: Record = { + [FeedViewType.Articles]: ChangeToOtherViewActionKey.ARTICLE, + [FeedViewType.SocialMedia]: ChangeToOtherViewActionKey.SOCIAL, + [FeedViewType.Pictures]: ChangeToOtherViewActionKey.PICTURE, + [FeedViewType.Videos]: ChangeToOtherViewActionKey.VIDEO, + [FeedViewType.Notifications]: ChangeToOtherViewActionKey.NOTIFICATION, + [FeedViewType.Audios]: ChangeToOtherViewActionKey.AUDIO, +} as const type Options = { categories: string[] } -const createFeedItemActions: (options: Options) => ContextMenuAction[] = (options) => { +const createFeedItemActions: (options: Options) => IContextMenuItemConfig[] = (options) => { return [ { title: "Mark All As Read", + actionKey: FeedItemActionKey.MARK_ALL_AS_READ, }, { title: "Claim", + actionKey: FeedItemActionKey.CLAIM, }, { title: "Boost", + actionKey: FeedItemActionKey.BOOST, }, { title: "Add To Category", - actions: [ - ...options.categories.map((category) => ({ - title: category, - })), - { - title: "Create New Category", - systemIcon: "plus", - }, - ], + actionKey: FeedItemActionKey.ADD_TO_CATEGORY, + subMenu: { + title: "Add To Category", + items: [ + ...options.categories.map((category) => ({ + title: category, + actionKey: `${FeedItemActionKey.ADD_TO_CATEGORY}:${category}`, + })), + { + title: "Create New Category", + actionKey: FeedItemActionKey.CREATE_NEW_CATEGORY, + + systemIcon: "plus", + }, + ], + }, }, { title: "Edit", + actionKey: FeedItemActionKey.EDIT, }, { title: "Copy Link", + actionKey: FeedItemActionKey.COPY_LINK, }, { title: "Unsubscribe", + actionKey: FeedItemActionKey.UNSUBSCRIBE, destructive: true, }, ] } + export const SubscriptionFeedItemContextMenu: FC< PropsWithChildren & { id: string @@ -69,55 +109,36 @@ export const SubscriptionFeedItemContextMenu: FC< return ( ) => { - const [first, second] = e.nativeEvent.indexPath - - switch (first) { - case 0: { + config={{ + items: actions, + }} + onPressMenuItem={(item) => { + switch (item.actionKey) { + case FeedItemActionKey.MARK_ALL_AS_READ: { unreadSyncService.markAsRead(id) break } - case 1: { - // TODO: implement logic - break - } - case 2: { - // TODO: implement logic - break - } - case 3: { - // add to category - - if (!actions[3].actions) break - const newCategory = second === actions[3].actions.length - 1 + case FeedItemActionKey.CREATE_NEW_CATEGORY: { + // create new category const subscription = getSubscription(id) - if (!subscription) return - if (newCategory) { - // create new category - Alert.prompt("Create New Category", "Enter the name of the new category", (text) => { - subscriptionSyncService.edit({ - ...subscription, - category: text, - }) - }) - } else { - // add to category + Alert.prompt("Create New Category", "Enter the name of the new category", (text) => { subscriptionSyncService.edit({ ...subscription, - category: actions[3].actions[second].title, + category: text, }) - } - + }) break } - case 4: { - // edit + case FeedItemActionKey.CLAIM: { + // TODO: implement logic break } - case 5: { - // copy link + case FeedItemActionKey.BOOST: { + // TODO: implement logic + break + } + case FeedItemActionKey.COPY_LINK: { const subscription = getSubscription(id) if (!subscription) return @@ -132,10 +153,9 @@ export const SubscriptionFeedItemContextMenu: FC< } break } - - case 6: { + case FeedItemActionKey.UNSUBSCRIBE: { // unsubscribe - Alert.alert("Unsubscribe?", "This will remove the feed from your list", [ + Alert.alert("Unsubscribe?", "This will remove the feed from your subscriptions", [ { text: "Cancel", style: "cancel", @@ -148,10 +168,26 @@ export const SubscriptionFeedItemContextMenu: FC< }, }, ]) + break } } - })} + + if (item.actionKey.startsWith(FeedItemActionKey.ADD_TO_CATEGORY)) { + const category = item.actionKey.split(":")[1] + if (!category) return + + const subscription = getSubscription(id) + + if (!subscription) return + + // add to category + subscriptionSyncService.edit({ + ...subscription, + category, + }) + } + }} > {children} @@ -162,59 +198,80 @@ export const SubscriptionFeedCategoryContextMenu: FC< { category: string feedIds: string[] + view: FeedViewType } & PropsWithChildren -> = ({ category: _, feedIds, children }) => { +> = ({ category: _, feedIds, view: currentView, children }) => { return ( [ + config={{ + items: [ { title: "Mark All As Read", + actionKey: FeedItemActionKey.MARK_ALL_AS_READ, }, { title: "Change To Other View", - actions: views.map((view) => ({ - title: view.name, - })), + actionKey: FeedItemActionKey.CHANGE_TO_OTHER_VIEW, + subMenu: { + title: "Change To Other View", + items: views.map((view) => ({ + title: view.name, + actionKey: changeViewActionKeyMapping[view.view], + checked: view.view === currentView, + })), + }, }, { title: "Edit Category", + actionKey: FeedItemActionKey.EDIT, }, { title: "Delete Category", + actionKey: FeedItemActionKey.DELETE, destructive: true, }, ], - [], - )} - onPress={useCallback( - (e: NativeSyntheticEvent) => { - const { name } = e.nativeEvent - const [first, second] = e.nativeEvent.indexPath - - if (first === 1) { - void second - // TODO change to other view - return + }} + onPressMenuItem={(item) => { + switch (item.actionKey) { + case FeedItemActionKey.MARK_ALL_AS_READ: { + unreadSyncService.markAsReadMany(feedIds) + break } - switch (name) { - case "Mark All As Read": { - unreadSyncService.markAsReadMany(feedIds) - break - } - case "Change To Other View": { - // TODO: implement logic - break - } - - case "Delete Category": { - // TODO: implement logic - break - } + case ChangeToOtherViewActionKey.ARTICLE: { + // TODO: change to article view + break + } + case ChangeToOtherViewActionKey.SOCIAL: { + // TODO: change to social view + break + } + case ChangeToOtherViewActionKey.PICTURE: { + // TODO: change to picture view + break + } + case ChangeToOtherViewActionKey.VIDEO: { + // TODO: change to video view + break + } + case ChangeToOtherViewActionKey.NOTIFICATION: { + // TODO: change to notification view + break } - }, - [feedIds], - )} + case ChangeToOtherViewActionKey.AUDIO: { + // TODO: change to audio view + break + } + case FeedItemActionKey.EDIT: { + // TODO: edit category + break + } + case FeedItemActionKey.DELETE: { + // TODO: delete category + break + } + } + }} > {children} diff --git a/apps/mobile/src/modules/context-menu/lists.tsx b/apps/mobile/src/modules/context-menu/lists.tsx index f3efd9f101..06a0ff7677 100644 --- a/apps/mobile/src/modules/context-menu/lists.tsx +++ b/apps/mobile/src/modules/context-menu/lists.tsx @@ -1,19 +1,20 @@ import type { FC, PropsWithChildren } from "react" import { useMemo } from "react" -import type { NativeSyntheticEvent } from "react-native" import { Alert, Clipboard } from "react-native" -import type { - ContextMenuAction, - ContextMenuOnPressNativeEvent, -} from "react-native-context-menu-view" -import { useEventCallback } from "usehooks-ts" import { ContextMenu } from "@/src/components/ui/context-menu" +import type { NullableContextMenuItemConfig } from "@/src/components/ui/context-menu/types" import { getWebUrl } from "@/src/lib/env" +import { toast } from "@/src/lib/toast" import { getList } from "@/src/store/list/getters" import { useIsOwnList } from "@/src/store/list/hooks" import { subscriptionSyncService } from "@/src/store/subscription/store" +enum ListItemActionKey { + EDIT = "edit", + COPY_LINK = "copyLink", + UNSUBSCRIBE = "unsubscribe", +} export const SubscriptionListItemContextMenu: FC< PropsWithChildren & { id: string @@ -21,39 +22,40 @@ export const SubscriptionListItemContextMenu: FC< > = ({ id, children }) => { const isOwnList = useIsOwnList(id) const actions = useMemo( - () => - [ - isOwnList && { - title: "Edit", - }, - { - title: "Copy Link", - }, - { - title: "Unsubscribe", - destructive: true, - }, - ].filter(Boolean) as ContextMenuAction[], + (): NullableContextMenuItemConfig[] => [ + isOwnList && { + title: "Edit", + actionKey: ListItemActionKey.EDIT, + }, + { + title: "Copy Link", + actionKey: ListItemActionKey.COPY_LINK, + }, + { + title: "Unsubscribe", + actionKey: ListItemActionKey.UNSUBSCRIBE, + destructive: true, + }, + ], [isOwnList], ) return ( ) => { - const { name } = e.nativeEvent - - switch (name) { - case "Edit": { + config={{ items: actions }} + onPressMenuItem={(item) => { + switch (item.actionKey) { + case ListItemActionKey.EDIT: { // TODO: implement break } - case "Copy Link": { + case ListItemActionKey.COPY_LINK: { const list = getList(id) if (!list) return + toast.info("Link copied to clipboard") Clipboard.setString(`${getWebUrl()}/share/lists/${list.id}`) break } - case "Unsubscribe": { + case ListItemActionKey.UNSUBSCRIBE: { Alert.alert("Unsubscribe", "Are you sure you want to unsubscribe?", [ { text: "Cancel", @@ -70,7 +72,7 @@ export const SubscriptionListItemContextMenu: FC< break } } - })} + }} > {children} diff --git a/apps/mobile/src/modules/discover/RecommendationListItem.tsx b/apps/mobile/src/modules/discover/RecommendationListItem.tsx index cc82bd7d31..a959368f59 100644 --- a/apps/mobile/src/modules/discover/RecommendationListItem.tsx +++ b/apps/mobile/src/modules/discover/RecommendationListItem.tsx @@ -4,6 +4,7 @@ import { router } from "expo-router" import type { FC } from "react" import { memo, useMemo } from "react" import { Clipboard, Linking, Text, TouchableOpacity, View } from "react-native" +import WebView from "react-native-webview" import { ContextMenu } from "@/src/components/ui/context-menu" import { Grid } from "@/src/components/ui/grid" @@ -11,6 +12,11 @@ import { FeedIcon } from "@/src/components/ui/icon/feed-icon" import { RSSHubCategoryCopyMap } from "./copy" +enum RecommendationListItemActionKey { + COPY_MAINTAINER_NAME = "copyMaintainerName", + OPEN_MAINTAINER_PROFILE = "openMaintainerProfile", +} + export const RecommendationListItem: FC<{ data: RSSHubRouteDeclaration routePrefix: string @@ -46,21 +52,28 @@ export const RecommendationListItem: FC<{ {maintainers.map((m) => ( { - switch (e.nativeEvent.index) { - case 0: { + renderPreview={() => { + return + }} + config={{ + items: [ + { + title: "Copy Maintainer Name", + actionKey: RecommendationListItemActionKey.COPY_MAINTAINER_NAME, + }, + { + title: "Open Maintainer's Profile", + actionKey: RecommendationListItemActionKey.OPEN_MAINTAINER_PROFILE, + }, + ], + }} + onPressMenuItem={(e) => { + switch (e.actionKey) { + case RecommendationListItemActionKey.COPY_MAINTAINER_NAME: { Clipboard.setString(m) break } - case 1: { + case RecommendationListItemActionKey.OPEN_MAINTAINER_PROFILE: { Linking.openURL(`https://github.com/${m}`) break } diff --git a/apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx b/apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx index b524fc3747..64596fc084 100644 --- a/apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx +++ b/apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx @@ -124,12 +124,7 @@ const SearchFeedItem = memo(({ item }: { item: SearchResultItem }) => { showsHorizontalScrollIndicator={false} contentContainerClassName="flex flex-row gap-4 pl-14 pr-2" > - {item.entries?.map((entry) => ( - // - // {entry.title} - // - - ))} + {item.entries?.map((entry) => )} diff --git a/apps/mobile/src/modules/discover/search.tsx b/apps/mobile/src/modules/discover/search.tsx index 3039f3545e..6f13898ac0 100644 --- a/apps/mobile/src/modules/discover/search.tsx +++ b/apps/mobile/src/modules/discover/search.tsx @@ -56,10 +56,7 @@ const DiscoverHeaderImpl = () => { const headerHeight = getDefaultHeaderHeight(frame, false, insets.top) return ( - + diff --git a/apps/mobile/src/modules/subscription/CategoryGrouped.tsx b/apps/mobile/src/modules/subscription/CategoryGrouped.tsx index c3b715e1e9..17e0c84b34 100644 --- a/apps/mobile/src/modules/subscription/CategoryGrouped.tsx +++ b/apps/mobile/src/modules/subscription/CategoryGrouped.tsx @@ -7,7 +7,7 @@ import { MingcuteRightLine } from "@/src/icons/mingcute_right_line" import { useUnreadCounts } from "@/src/store/unread/hooks" import { SubscriptionFeedCategoryContextMenu } from "../context-menu/feeds" -import { GroupedContext } from "./ctx" +import { GroupedContext, useViewPageCurrentView } from "./ctx" import { UnGroupedList } from "./UnGroupedList" // const CategoryList: FC<{ @@ -28,9 +28,15 @@ export const CategoryGrouped = memo( transform: [{ rotate: `${rotateSharedValue.value}deg` }], } }, [rotateSharedValue]) + const view = useViewPageCurrentView() + return ( <> - + { // TODO navigate to category diff --git a/apps/mobile/src/modules/subscription/ViewTab.tsx b/apps/mobile/src/modules/subscription/ViewTab.tsx index dab6189f6b..dcbc606544 100644 --- a/apps/mobile/src/modules/subscription/ViewTab.tsx +++ b/apps/mobile/src/modules/subscription/ViewTab.tsx @@ -119,10 +119,11 @@ const TabItem = memo( const unreadCount = useUnreadCountByView(view.view) return ( { - switch (e.nativeEvent.index) { - case 0: { + // actions={[{ title: "Mark all as read" }]} + config={{ items: [{ title: "Mark all as read", actionKey: "markAllAsRead" }] }} + onPressMenuItem={(e) => { + switch (e.actionKey) { + case "markAllAsRead": { unreadSyncService.markViewAsRead(view.view) break } diff --git a/apps/mobile/src/modules/subscription/header-actions.tsx b/apps/mobile/src/modules/subscription/header-actions.tsx index 7971649a8d..ff8c091f35 100644 --- a/apps/mobile/src/modules/subscription/header-actions.tsx +++ b/apps/mobile/src/modules/subscription/header-actions.tsx @@ -1,7 +1,7 @@ import { TouchableOpacity } from "react-native" import type { ContextMenuAction } from "react-native-context-menu-view" +import ContextMenu from "react-native-context-menu-view" -import { ContextMenu } from "@/src/components/ui/context-menu" import { AZSortAscendingLettersCuteReIcon } from "@/src/icons/AZ_sort_ascending_letters_cute_re" import { AZSortDescendingLettersCuteReIcon } from "@/src/icons/AZ_sort_descending_letters_cute_re" import { Numbers90SortAscendingCuteReIcon } from "@/src/icons/numbers_90_sort_ascending_cute_re" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1500a748ed..6937dd42ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -556,6 +556,12 @@ importers: react-native-gesture-handler: specifier: ~2.20.2 version: 2.20.2(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + react-native-ios-context-menu: + specifier: 3.1.0 + version: 3.1.0(react-native-ios-utilities@5.1.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + react-native-ios-utilities: + specifier: 5.1.0 + version: 5.1.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) react-native-keyboard-controller: specifier: ^1.15.0 version: 1.15.0(react-native-reanimated@3.16.5(@babel/core@7.26.0)(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) @@ -2270,6 +2276,9 @@ packages: peerDependencies: react: '>=16.8.0' + '@dominicstop/ts-event-emitter@1.1.0': + resolution: {integrity: sha512-CcxmJIvUb1vsFheuGGVSQf4KdPZC44XolpUT34+vlal+LyQoBUOn31pjFET5M9ctOxEpt8xa0M3/2M7uUiAoJw==} + '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} @@ -12345,6 +12354,19 @@ packages: peerDependencies: react: ^16.6.0 || ^17.0.0 || ^18.0.0 + react-native-ios-context-menu@3.1.0: + resolution: {integrity: sha512-qdPSXMKUp5lDgmZeUPdv5sgBFhkFrIqma+zsnqJQYOvekb6Qs17yJy1Rqhrj0bJrwuduHzZX0aYbaA8whxqpDw==} + peerDependencies: + react: '*' + react-native: '*' + react-native-ios-utilities: '*' + + react-native-ios-utilities@5.1.0: + resolution: {integrity: sha512-25QqdENHyMdlT48Xkr5hEke1yfGbbGk6Wy+hM5jFPlfXDmGgF/QicGN4SiQXIbSLWuMqFHVVoiJ2vkQ8ARP5mg==} + peerDependencies: + react: '*' + react-native: '*' + react-native-is-edge-to-edge@1.1.6: resolution: {integrity: sha512-1pHnFTlBahins6UAajXUqeCOHew9l9C2C8tErnpGC3IyLJzvxD+TpYAixnCbrVS52f7+NvMttbiSI290XfwN0w==} peerDependencies: @@ -15766,6 +15788,8 @@ snapshots: react: 18.3.1 tslib: 2.8.1 + '@dominicstop/ts-event-emitter@1.1.0': {} + '@drizzle-team/brocli@0.10.2': {} '@edge-runtime/format@2.2.1': {} @@ -28354,6 +28378,18 @@ snapshots: react-fast-compare: 3.2.2 shallowequal: 1.1.0 + react-native-ios-context-menu@3.1.0(react-native-ios-utilities@5.1.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1): + dependencies: + '@dominicstop/ts-event-emitter': 1.1.0 + react: 18.3.1 + react-native: 0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1) + react-native-ios-utilities: 5.1.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + + react-native-ios-utilities@5.1.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-native: 0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1) + react-native-is-edge-to-edge@1.1.6(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 From 0227427d295cf8f7e8a5b04ff396929b3d18e052 Mon Sep 17 00:00:00 2001 From: Innei Date: Fri, 10 Jan 2025 22:48:54 +0800 Subject: [PATCH 10/93] feat(search): enhance search components with new styles and functionality Signed-off-by: Innei --- .../discover/search-tabs/SearchFeed.tsx | 20 +++++++++++++++++-- .../modules/discover/search-tabs/__base.tsx | 15 ++++++++++---- apps/mobile/src/modules/discover/search.tsx | 2 +- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx b/apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx index 64596fc084..6f9c430631 100644 --- a/apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx +++ b/apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx @@ -1,4 +1,5 @@ import { FeedViewType } from "@follow/constants" +import { withOpacity } from "@follow/utils" import { useQuery } from "@tanstack/react-query" import { Image } from "expo-image" import { router } from "expo-router" @@ -11,8 +12,10 @@ import Animated, { FadeInUp } from "react-native-reanimated" import { FeedIcon } from "@/src/components/ui/icon/feed-icon" import { LoadingIndicator } from "@/src/components/ui/loading" import { ItemPressable } from "@/src/components/ui/pressable/item-pressable" +import { SadCuteReIcon } from "@/src/icons/sad_cute_re" import { apiClient } from "@/src/lib/api-fetch" import { useSubscriptionByFeedId } from "@/src/store/subscription/hooks" +import { useColor } from "@/src/theme/colors" import { useSearchPageContext } from "../ctx" import { BaseSearchPageFlatList, BaseSearchPageRootView, BaseSearchPageScrollView } from "./__base" @@ -36,10 +39,23 @@ export const SearchFeed = () => { enabled: !!searchValue, }) + const textColor = useColor("text") + if (isLoading) { return ( - - + + + + + ) + } + + if (data?.data.length === 0) { + return ( + + + + No results found ) } diff --git a/apps/mobile/src/modules/discover/search-tabs/__base.tsx b/apps/mobile/src/modules/discover/search-tabs/__base.tsx index cf9a5d0baf..e1b9d52d86 100644 --- a/apps/mobile/src/modules/discover/search-tabs/__base.tsx +++ b/apps/mobile/src/modules/discover/search-tabs/__base.tsx @@ -1,3 +1,4 @@ +import { cn } from "@follow/utils/src/utils" import { forwardRef } from "react" import type { ScrollViewProps } from "react-native" import { RefreshControl, ScrollView, useWindowDimensions, View } from "react-native" @@ -29,13 +30,19 @@ export const BaseSearchPageScrollView = forwardRef( }, ) -export const BaseSearchPageRootView = ({ children }: { children: React.ReactNode }) => { +export const BaseSearchPageRootView = ({ + children, + className, +}: { + children: React.ReactNode + className?: string +}) => { const windowWidth = useWindowDimensions().width - const insets = useSafeAreaInsets() + const searchBarHeight = useSearchBarHeight() - const offsetTop = searchBarHeight - insets.top + const offsetTop = searchBarHeight return ( - + {children} ) diff --git a/apps/mobile/src/modules/discover/search.tsx b/apps/mobile/src/modules/discover/search.tsx index 6f13898ac0..2d29d0d127 100644 --- a/apps/mobile/src/modules/discover/search.tsx +++ b/apps/mobile/src/modules/discover/search.tsx @@ -38,7 +38,7 @@ export const SearchHeader: FC<{ className="relative" onLayout={onLayout} > - + {/* */} From d77ed3cb37424e97490bbf4883f9326acc446d54 Mon Sep 17 00:00:00 2001 From: Wilson Date: Sat, 11 Jan 2025 12:18:18 +0800 Subject: [PATCH 11/93] fix(lists): fix duplicate addition issue in manage feeds functionality (#2544) --- apps/renderer/src/modules/settings/tabs/lists/modals.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/renderer/src/modules/settings/tabs/lists/modals.tsx b/apps/renderer/src/modules/settings/tabs/lists/modals.tsx index fa54f6ab1e..04496a42b9 100644 --- a/apps/renderer/src/modules/settings/tabs/lists/modals.tsx +++ b/apps/renderer/src/modules/settings/tabs/lists/modals.tsx @@ -214,9 +214,11 @@ export const ListFeedsModalContent = ({ id }: { id: string }) => { const { t } = useTranslation("settings") const [feedSearchFor, setFeedSearchFor] = useState("") + const selectedFeedIdRef = useRef() const addMutation = useAddFeedToFeedList({ onSuccess: () => { setFeedSearchFor("") + selectedFeedIdRef.current = null }, }) @@ -230,7 +232,6 @@ export const ListFeedsModalContent = ({ id }: { id: string }) => { })) }, [allFeeds, list?.feedIds]) - const selectedFeedIdRef = useRef() if (!list) return null return ( <> From 0a644730bef2165ef75fb9d2eca2ee9c20ab66bd Mon Sep 17 00:00:00 2001 From: Whitewater Date: Sun, 12 Jan 2025 00:16:11 +0800 Subject: [PATCH 12/93] refactor: rename sticky check functions for clarity (#2542) --- .../src/modules/entry-column/list.tsx | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/apps/renderer/src/modules/entry-column/list.tsx b/apps/renderer/src/modules/entry-column/list.tsx index 94c95ba76a..3b61d4b764 100644 --- a/apps/renderer/src/modules/entry-column/list.tsx +++ b/apps/renderer/src/modules/entry-column/list.tsx @@ -1,6 +1,6 @@ import { EmptyIcon } from "@follow/components/icons/empty.jsx" import { useScrollViewElement } from "@follow/components/ui/scroll-area/hooks.js" -import { FeedViewType } from "@follow/constants" +import type { FeedViewType } from "@follow/constants" import { useTypeScriptHappyCallback } from "@follow/hooks" import { LRUCache } from "@follow/utils/lru-cache" import type { Range, VirtualItem, Virtualizer } from "@tanstack/react-virtual" @@ -161,8 +161,8 @@ export const EntryList: FC = memo( }) const activeStickyIndexRef = useRef(0) - const isActiveSticky = (index: number) => activeStickyIndexRef.current === index - const isSticky = (index: number) => stickyIndexes.includes(index) + const checkIsActiveSticky = (index: number) => activeStickyIndexRef.current === index + const checkIsStickyItem = (index: number) => stickyIndexes.includes(index) const virtualItems = rowVirtualizer.getVirtualItems() useEffect(() => { @@ -242,20 +242,20 @@ export const EntryList: FC = memo( ) } - const activeSticky = isActiveSticky(virtualRow.index) - const sticky = isSticky(virtualRow.index) + const isStickyItem = checkIsStickyItem(virtualRow.index) + const isActiveStickyItem = !isScrollTop && checkIsActiveSticky(virtualRow.index) return ( - {sticky && ( + {isStickyItem && (
= memo( >
)} @@ -272,11 +272,7 @@ export const EntryList: FC = memo( className="absolute left-0 top-0 w-full will-change-transform" style={{ transform, - paddingTop: sticky - ? view === FeedViewType.SocialMedia - ? "3.5rem" - : "1.5rem" - : undefined, + paddingTop: isStickyItem ? "1.75rem" : undefined, }} ref={rowVirtualizer.measureElement} data-index={virtualRow.index} From e625adaf162cdd2962900d1104541f9cc6365af5 Mon Sep 17 00:00:00 2001 From: Innei Date: Sun, 12 Jan 2025 12:40:49 +0800 Subject: [PATCH 13/93] fix(rn-tabbar): calc tab bar indicator position Signed-off-by: Innei --- apps/mobile/src/components/ui/tabview/TabBar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/mobile/src/components/ui/tabview/TabBar.tsx b/apps/mobile/src/components/ui/tabview/TabBar.tsx index e2b9faf70a..41d1e59466 100644 --- a/apps/mobile/src/components/ui/tabview/TabBar.tsx +++ b/apps/mobile/src/components/ui/tabview/TabBar.tsx @@ -117,7 +117,7 @@ export const TabBar = forwardRef( }, [currentTab, indicatorPosition, sharedPagerOffsetX.value, tabPositions, tabWidths]) const indicatorStyle = useAnimatedStyle(() => { - const scrollProgress = sharedPagerOffsetX.value / tabBarWidth + const scrollProgress = Math.max(sharedPagerOffsetX.value / tabBarWidth, 0) const currentIndex = Math.floor(scrollProgress) const nextIndex = Math.min(currentIndex + 1, tabs.length - 1) @@ -133,7 +133,7 @@ export const TabBar = forwardRef( tabWidths[currentIndex] + (tabWidths[nextIndex] - tabWidths[currentIndex]) * progress return { - transform: [{ translateX: xPosition }], + transform: [{ translateX: Math.max(xPosition, 0) }], width, backgroundColor: tabs[currentTab].activeColor || accentColor, } From 6abb442f031b9881fa4177390cf5c3406e27dd12 Mon Sep 17 00:00:00 2001 From: Cesaryuan <35998162+cesaryuan@users.noreply.github.com> Date: Mon, 13 Jan 2025 14:04:38 +0800 Subject: [PATCH 14/93] feat: add proxy support for native fetch using undici (#2537) --- apps/main/src/lib/proxy.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/main/src/lib/proxy.ts b/apps/main/src/lib/proxy.ts index f9e00c8a38..bc3aaf28ba 100644 --- a/apps/main/src/lib/proxy.ts +++ b/apps/main/src/lib/proxy.ts @@ -1,4 +1,5 @@ import { session } from "electron" +import { ProxyAgent, setGlobalDispatcher } from "undici" import { logger } from "../logger" import { store } from "./store" @@ -77,4 +78,8 @@ export const updateProxy = () => { proxyRules, proxyBypassRules: BYPASS_RULES, }) + // Currently, Session.setProxy is not working for native fetch, which is used by readability. + // So we need to set proxy for native fetch manually, refer to https://stackoverflow.com/a/76503362/14676508 + const dispatcher = new ProxyAgent({ uri: new URL(proxyUri).toString() }) + setGlobalDispatcher(dispatcher) } From 89126ed83ecc410cc36f568ba3e161dde141bf77 Mon Sep 17 00:00:00 2001 From: Innei Date: Mon, 13 Jan 2025 15:33:55 +0800 Subject: [PATCH 15/93] perf(rn): memo and optimize list Signed-off-by: Innei --- .../src/components/ui/tabview/TabBar.tsx | 79 +++++++++++++------ .../src/components/ui/tabview/TabView.tsx | 24 +++++- .../src/modules/discover/Recommendations.tsx | 11 ++- 3 files changed, 83 insertions(+), 31 deletions(-) diff --git a/apps/mobile/src/components/ui/tabview/TabBar.tsx b/apps/mobile/src/components/ui/tabview/TabBar.tsx index 41d1e59466..046cdc39cf 100644 --- a/apps/mobile/src/components/ui/tabview/TabBar.tsx +++ b/apps/mobile/src/components/ui/tabview/TabBar.tsx @@ -1,9 +1,18 @@ import { cn } from "@follow/utils" import { debounce } from "es-toolkit/compat" import type { FC } from "react" -import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react" +import { + forwardRef, + memo, + useCallback, + useEffect, + useImperativeHandle, + useRef, + useState, +} from "react" import type { Animated as AnimatedNative, + LayoutChangeEvent, StyleProp, TouchableOpacityProps, ViewStyle, @@ -115,7 +124,19 @@ export const TabBar = forwardRef( } } }, [currentTab, indicatorPosition, sharedPagerOffsetX.value, tabPositions, tabWidths]) - + const handleTabItemLayout = useCallback((event: LayoutChangeEvent, index: number) => { + const { width, x } = event.nativeEvent.layout + setTabWidths((prev) => { + const newWidths = [...prev] + newWidths[index] = width + return newWidths + }) + setTabPositions((prev) => { + const newPositions = [...prev] + newPositions[index] = x + return newPositions + }) + }, []) const indicatorStyle = useAnimatedStyle(() => { const scrollProgress = Math.max(sharedPagerOffsetX.value / tabBarWidth, 0) @@ -155,29 +176,15 @@ export const TabBar = forwardRef( style={[styles.root, tabbarStyle]} > {tabs.map((tab, index) => ( - { - handleChangeTabIndex(index) - }} + { - const { width, x } = event.nativeEvent.layout - setTabWidths((prev) => { - const newWidths = [...prev] - newWidths[index] = width - return newWidths - }) - setTabPositions((prev) => { - const newPositions = [...prev] - newPositions[index] = x - return newPositions - }) - }} + index={index} + onTabItemPress={handleChangeTabIndex} + onLayout={handleTabItemLayout} + isSelected={currentTab === index} tab={tab} - > - - + /> ))} @@ -209,3 +216,29 @@ const TabItemInner = ({ tab, isSelected }: { tab: Tab; isSelected: boolean }) =>
) } + +const TarBarItem: FC<{ + TabItem: FC<{ isSelected: boolean; tab: Tab } & Pick> + onTabItemPress: (index: number) => void + isSelected: boolean + tab: Tab + + index: number + onLayout: (event: LayoutChangeEvent, index: number) => void +}> = memo(({ TabItem = Pressable, onTabItemPress, isSelected, tab, onLayout, index }) => { + return ( + { + onTabItemPress(index) + }} + key={tab.value} + isSelected={isSelected} + onLayout={(event) => { + onLayout(event, index) + }} + tab={tab} + > + + + ) +}) diff --git a/apps/mobile/src/components/ui/tabview/TabView.tsx b/apps/mobile/src/components/ui/tabview/TabView.tsx index b2e8dfc9fe..f38e5ab6d5 100644 --- a/apps/mobile/src/components/ui/tabview/TabView.tsx +++ b/apps/mobile/src/components/ui/tabview/TabView.tsx @@ -1,6 +1,6 @@ import { cn } from "@follow/utils" import type { FC } from "react" -import { useCallback, useEffect, useRef, useState } from "react" +import { memo, useCallback, useEffect, useRef, useState } from "react" import type { ScrollView, StyleProp, TouchableOpacityProps, ViewStyle } from "react-native" import { Animated as RnAnimated, @@ -117,10 +117,30 @@ export const TabView: FC = ({ > {tabs.map((tab, index) => ( - {shouldRenderCurrentTab(index) && } + {shouldRenderCurrentTab(index) && ( + + )} ))} ) } + +const TabComponent: FC<{ + tab: Tab + isSelected: boolean + Tab: TabComponent +}> = memo(({ tab, isSelected, Tab = View }) => { + const { width: windowWidth } = useWindowDimensions() + return ( + + + + ) +}) diff --git a/apps/mobile/src/modules/discover/Recommendations.tsx b/apps/mobile/src/modules/discover/Recommendations.tsx index 2eae69b2eb..53ea8bf95e 100644 --- a/apps/mobile/src/modules/discover/Recommendations.tsx +++ b/apps/mobile/src/modules/discover/Recommendations.tsx @@ -6,7 +6,7 @@ import { useHeaderHeight } from "@react-navigation/elements" import { FlashList } from "@shopify/flash-list" import { useQuery } from "@tanstack/react-query" import type { FC } from "react" -import { useCallback, useMemo, useRef } from "react" +import { memo, useCallback, useMemo, useRef } from "react" import { Text, TouchableOpacity, View } from "react-native" import type { PanGestureHandlerGestureEvent } from "react-native-gesture-handler" import { PanGestureHandler } from "react-native-gesture-handler" @@ -63,7 +63,7 @@ const fetchRsshubPopular = (category: DiscoverCategories, lang: Language) => { }) } -const Tab: TabComponent = ({ tab }) => { +const Tab: TabComponent = ({ tab, ...rest }) => { const tabHeight = useBottomTabBarHeight() const { data, isLoading } = useQuery({ @@ -150,7 +150,7 @@ const Tab: TabComponent = ({ tab }) => { } return ( - + { contentContainerStyle={{ paddingBottom: tabHeight }} removeClippedSubviews /> - {/* Right Sidebar */} @@ -194,7 +193,7 @@ const ItemRenderer = ({ const NavigationSidebar: FC<{ alphabetGroups: (string | { key: string; data: RSSHubRouteDeclaration })[] listRef: React.RefObject> -}> = ({ alphabetGroups, listRef }) => { +}> = memo(({ alphabetGroups, listRef }) => { const scrollToLetter = useCallback( (letter: string, animated = true) => { const index = alphabetGroups.findIndex((group) => { @@ -264,4 +263,4 @@ const NavigationSidebar: FC<{ ) -} +}) From 6828606cc42445f46df9fddb9b547fd1e05e68f6 Mon Sep 17 00:00:00 2001 From: Jerry Wong Date: Mon, 13 Jan 2025 17:09:45 +0800 Subject: [PATCH 16/93] feat(locales): enhance zh-HK locale with new user prompts (#2556) * feat: update zh-HK file * feat: add translation for "show more" feat: Improve the zh-hk file * feat: update zh-HK * feat: improve zh-HK --- locales/app/zh-HK.json | 17 ++++++++++++++ locales/common/zh-HK.json | 1 + locales/errors/zh-HK.json | 9 +++++++- locales/external/zh-HK.json | 3 ++- locales/settings/zh-HK.json | 46 +++++++++++++++++++++++++++++++++++-- 5 files changed, 72 insertions(+), 4 deletions(-) diff --git a/locales/app/zh-HK.json b/locales/app/zh-HK.json index 1722ee81b3..d8ebfcb493 100644 --- a/locales/app/zh-HK.json +++ b/locales/app/zh-HK.json @@ -228,6 +228,14 @@ "feed_view_type.pictures": "圖片", "feed_view_type.social_media": "社交媒體", "feed_view_type.videos": "影片", + "login.confirm_password.label": "確認密碼", + "login.continueWith": "繼續使用 {{provider}}", + "login.email": "電子郵件", + "login.forget_password.note": "忘記密碼?", + "login.password": "密碼", + "login.signUp": "使用電子郵件註冊", + "login.submit": "提交", + "login.with_email.title": "使用電子郵件登入", "mark_all_read_button.auto_confirm_info": "將在 {{countdown}} 秒後自動確認", "mark_all_read_button.confirm": "確認", "mark_all_read_button.confirm_mark_all": "確定將 標記為已讀?", @@ -271,6 +279,7 @@ "notify.update_info": "{{app_name}} 已準備好更新!", "notify.update_info_1": "點擊以重新啟動", "notify.update_info_2": "點擊以重新載入頁面", + "notify.update_info_3": "觸碰以重新載入頁面", "player.back_10s": "倒退 10 秒", "player.close": "關閉", "player.download": "下載", @@ -286,6 +295,13 @@ "player.volume": "音量", "quick_add.placeholder": "快速追隨訂閱源,請在此輸入訂閱源網址...", "quick_add.title": "快速追隨", + "register.confirm_password": "確認密碼", + "register.email": "電子郵件", + "register.label": "建立 {{app_name}} 帳戶", + "register.login": "登入", + "register.note": "已經有帳戶?", + "register.password": "密碼", + "register.submit": "建立帳戶", "resize.tooltip.double_click_to_collapse": "雙擊以摺疊", "resize.tooltip.drag_to_resize": "拖曳以調整大小", "search.empty.no_results": "未找到結果", @@ -392,6 +408,7 @@ "words.browser": "瀏覽器", "words.confirm": "確認", "words.discover": "探索", + "words.email": "電子郵件", "words.feeds": "訂閱源", "words.import": "匯入", "words.inbox": "收件匣", diff --git a/locales/common/zh-HK.json b/locales/common/zh-HK.json index 5f3b5240ac..7b94fd68ed 100644 --- a/locales/common/zh-HK.json +++ b/locales/common/zh-HK.json @@ -36,6 +36,7 @@ "words.result": "結果", "words.result_one": "結果", "words.result_other": "結果", + "words.rsshub": "RSSHub", "words.save": "儲存", "words.submit": "提交", "words.update": "更新", diff --git a/locales/errors/zh-HK.json b/locales/errors/zh-HK.json index e483085376..7df781e481 100644 --- a/locales/errors/zh-HK.json +++ b/locales/errors/zh-HK.json @@ -51,5 +51,12 @@ "10001": "收件箱已存在", "10002": "超出收件箱限制", "10003": "收件箱無權限", - "12000": "操作次數超出限制" + "12000": "操作次數超出限制", + "13000": "未找到 RSSHub 路由", + "13001": "您不是此 RSSHub 實例的擁有者", + "13002": "RSSHub 正在使用中", + "13003": "未找到 RSSHub", + "13004": "RSSHub 使用者數量已超過限制", + "13005": "未找到 RSSHub 購買記錄", + "13006": "RSSHub 配置無效" } diff --git a/locales/external/zh-HK.json b/locales/external/zh-HK.json index 98959550d7..ed47c4220b 100644 --- a/locales/external/zh-HK.json +++ b/locales/external/zh-HK.json @@ -66,5 +66,6 @@ "register.login": "登入", "register.note": "已經有帳號了嗎?", "register.password": "密碼", - "register.submit": "建立帳號" + "register.submit": "建立帳號", + "words.email": "電子郵件" } diff --git a/locales/settings/zh-HK.json b/locales/settings/zh-HK.json index d52f6dd7a6..12a3f6f874 100644 --- a/locales/settings/zh-HK.json +++ b/locales/settings/zh-HK.json @@ -2,6 +2,7 @@ "about.changelog": "更新日誌", "about.feedbackInfo": "{{appName}}({{commitSha}})仍處於開發初期階段。如果你有任何意見或建議,歡迎到我們的 GitHub 提交問題 。", "about.iconLibrary": "所使用的圖示庫受 版權保護,不能轉發。", + "about.licenseInfo": "版權所有 © 2024 {{appName}}。保留一切權利。", "about.sidebar_title": "關於", "about.socialMedia": "社交媒體", "actions.actionName": "動作 {{number}}", @@ -96,6 +97,7 @@ "appearance.zen_mode.description": "Zen 模式是一種不受打擾的閱讀模式,讓您專注於內容而不會有任何干擾。啟用 Zen 模式後,側邊欄將被隱藏。", "appearance.zen_mode.label": "Zen 模式", "common.give_star": "喜歡我們的產品嗎? 在 GitHub 上給我們 star 吧!", + "customizeToolbar.title": "自訂工具列", "data_control.app_cache_limit.description": "應用程式快取的最大大小。當快取達到此大小時,最舊的項目將被刪除以釋放空間。", "data_control.app_cache_limit.label": "應用程式快取限制", "data_control.clean_cache.button": "清除快取", @@ -118,7 +120,13 @@ "general.data_persist.label": "離線使用時保留數據", "general.export.button": "匯出", "general.export.description": "匯出你的訂閱到 OPML 文件", - "general.export.label": "匯出訂閱", + "general.export.folder_mode.description": "決定如何組織匯出資料夾。", + "general.export.folder_mode.label": "資料夾模式", + "general.export.folder_mode.option.category": "分類", + "general.export.folder_mode.option.view": "檢視", + "general.export.label": "匯出訂閱源", + "general.export.rsshub_url.description": "RSSHub 路由的預設基底網址,留空將使用 https://rsshub.app。", + "general.export.rsshub_url.label": "RSSHub 網址", "general.export_database.button": "匯出", "general.export_database.description": "將您的資料庫匯出為 JSON 檔案", "general.export_database.label": "匯出資料庫", @@ -259,6 +267,9 @@ "profile.change_password.label": "更改密碼", "profile.confirm_password.label": "確認密碼", "profile.current_password.label": "目前密碼", + "profile.email.change": "更改電子郵件", + "profile.email.changed": "電子郵件已更改", + "profile.email.changed_verification_sent": "用於驗證新電子郵件的郵件已發送", "profile.email.label": "電子郵件", "profile.email.send_verification": "發送驗證電子郵件", "profile.email.unverified": "未經驗證", @@ -266,15 +277,45 @@ "profile.email.verified": "已驗證", "profile.handle.description": "你的唯一識別符", "profile.handle.label": "識別符", + "profile.link_social.authentication": "身份驗證", + "profile.link_social.description": "目前只能連結使用相同電子郵件的社交帳戶", + "profile.link_social.link": "連結", + "profile.link_social.unlink.success": "社交帳戶已解除連結", "profile.name.description": "你的公開顯示名稱", "profile.name.label": "顯示名稱", "profile.new_password.label": "新密碼", + "profile.password.label": "密碼", "profile.reset_password_mail_sent": "重設密碼郵件已發送", "profile.sidebar_title": "個人資料", "profile.submit": "提交", "profile.title": "個人資料設置", "profile.updateSuccess": "個人資料已更新", "profile.update_password_success": "密碼已更新", + "rsshub.addModal.access_key_label": "存取金鑰(選填)", + "rsshub.addModal.add": "新增", + "rsshub.addModal.base_url_label": "基本網址", + "rsshub.addModal.description": "要在 Follow 中使用您自己的實例,您必須將以下環境變數新增到它。", + "rsshub.add_new_instance": "新增實例", + "rsshub.description": "RSSHub 是一個由社群驅動的開源 RSS 網絡。Follow 提供內建的專用實例,並使用它來支援成千上萬的訂閱內容,您也可以透過使用您自己的或第三方實例來實現更穩定的內容獲取。", + "rsshub.public_instances": "可用實例", + "rsshub.table.description": "描述", + "rsshub.table.edit": "編輯", + "rsshub.table.inuse": "使用中", + "rsshub.table.official": "官方", + "rsshub.table.owner": "擁有者", + "rsshub.table.price": "每月價格", + "rsshub.table.private": "私人", + "rsshub.table.unlimited": "無限", + "rsshub.table.use": "使用", + "rsshub.table.userCount": "使用者數量", + "rsshub.table.userLimit": "使用者限制", + "rsshub.table.yours": "您的", + "rsshub.useModal.about": "關於此實例", + "rsshub.useModal.month": "月", + "rsshub.useModal.months_label": "您要購買的月份數量", + "rsshub.useModal.purchase_expires_at": "您已購買此實例,您的購買將於以下時間到期", + "rsshub.useModal.title": "RSSHub 實例", + "rsshub.useModal.useWith": "使用 {{amount}} ", "titles.about": "關於", "titles.actions": "動作", "titles.appearance": "外觀", @@ -342,5 +383,6 @@ "wallet.withdraw.error": "提取失敗:{{error}}", "wallet.withdraw.modalTitle": "提取 Power", "wallet.withdraw.submitButton": "提交", - "wallet.withdraw.success": "提取成功!" + "wallet.withdraw.success": "提取成功!", + "wallet.withdraw.toRss3Label": "提取為 RSS3" } From 91f53404071c609a77b8ff809ea2cbb7748cb1c5 Mon Sep 17 00:00:00 2001 From: Innei Date: Mon, 13 Jan 2025 20:13:17 +0800 Subject: [PATCH 17/93] fix(rn-baseline): sync with uikit color and update list Signed-off-by: Innei --- .../components/common/AnimatedComponents.tsx | 5 +- apps/mobile/src/components/ui/form/Select.tsx | 14 ++--- apps/mobile/src/components/ui/form/Switch.tsx | 2 +- .../src/components/ui/form/TextField.tsx | 10 ++-- .../ui/pressable/item-pressable.tsx | 49 +++++++++++++-- .../src/modules/discover/Recommendations.tsx | 4 +- apps/mobile/src/modules/discover/search.tsx | 16 ++--- .../modules/subscription/CategoryGrouped.tsx | 16 ++--- .../modules/subscription/ItemSeparator.tsx | 8 +++ .../subscription/SubscriptionLists.tsx | 31 +++++++--- .../modules/subscription/UnGroupedList.tsx | 21 +++---- .../src/modules/subscription/ViewTab.tsx | 3 +- .../modules/subscription/items/InboxItem.tsx | 2 +- .../items/ListSubscriptionItem.tsx | 2 +- .../subscription/items/SubscriptionItem.tsx | 3 +- apps/mobile/src/screens/(modal)/follow.tsx | 8 +-- .../src/screens/(modal)/rsshub-form.tsx | 14 +++-- apps/mobile/src/theme/colors.ts | 59 ++++++++++--------- apps/mobile/tailwind.config.ts | 28 +++++---- packages/utils/src/color.ts | 4 +- 20 files changed, 183 insertions(+), 116 deletions(-) create mode 100644 apps/mobile/src/modules/subscription/ItemSeparator.tsx diff --git a/apps/mobile/src/components/common/AnimatedComponents.tsx b/apps/mobile/src/components/common/AnimatedComponents.tsx index b2f444054b..2224c8f2f5 100644 --- a/apps/mobile/src/components/common/AnimatedComponents.tsx +++ b/apps/mobile/src/components/common/AnimatedComponents.tsx @@ -1,5 +1,8 @@ -import { Animated, FlatList, ScrollView, TouchableOpacity } from "react-native" +import { Animated, FlatList, Pressable, ScrollView, TouchableOpacity } from "react-native" +import Reanimated from "react-native-reanimated" export const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView) export const AnimatedFlatList = Animated.createAnimatedComponent(FlatList) export const AnimatedTouchableOpacity = Animated.createAnimatedComponent(TouchableOpacity) + +export const ReAnimatedPressable = Reanimated.createAnimatedComponent(Pressable) diff --git a/apps/mobile/src/components/ui/form/Select.tsx b/apps/mobile/src/components/ui/form/Select.tsx index 0f2798fd08..d2810a1ec7 100644 --- a/apps/mobile/src/components/ui/form/Select.tsx +++ b/apps/mobile/src/components/ui/form/Select.tsx @@ -6,7 +6,7 @@ import ContextMenu from "react-native-context-menu-view" import { useEventCallback } from "usehooks-ts" import { MingcuteDownLineIcon } from "@/src/icons/mingcute_down_line" -import { useColor } from "@/src/theme/colors" +import { accentColor } from "@/src/theme/colors" import { FormLabel } from "./Label" @@ -52,11 +52,9 @@ export function Select({ onValueChange(currentValue) }, []) - const systemFill = useColor("text") - return ( - {!!label && } + {!!label && } {/* Trigger */} ({ > - {valueToLabelMap.get(currentValue)} - - + {valueToLabelMap.get(currentValue)} + + diff --git a/apps/mobile/src/components/ui/form/Switch.tsx b/apps/mobile/src/components/ui/form/Switch.tsx index bfb706ca12..7d179faefb 100644 --- a/apps/mobile/src/components/ui/form/Switch.tsx +++ b/apps/mobile/src/components/ui/form/Switch.tsx @@ -21,7 +21,7 @@ export const FormSwitch = forwardRef( {!!label && } {!!description && ( - {description} + {description} )} diff --git a/apps/mobile/src/components/ui/form/TextField.tsx b/apps/mobile/src/components/ui/form/TextField.tsx index 0e9e7f5ac8..00ef621236 100644 --- a/apps/mobile/src/components/ui/form/TextField.tsx +++ b/apps/mobile/src/components/ui/form/TextField.tsx @@ -23,15 +23,13 @@ export const TextField = forwardRef( ) => { return ( <> - {!!label && } + {!!label && } {!!description && ( - - {description} - + {description} )} ( diff --git a/apps/mobile/src/components/ui/pressable/item-pressable.tsx b/apps/mobile/src/components/ui/pressable/item-pressable.tsx index e614596ee3..e507c28464 100644 --- a/apps/mobile/src/components/ui/pressable/item-pressable.tsx +++ b/apps/mobile/src/components/ui/pressable/item-pressable.tsx @@ -1,22 +1,61 @@ import { cn, composeEventHandlers } from "@follow/utils" -import { memo, useState } from "react" -import { Pressable } from "react-native" +import { memo, useEffect, useState } from "react" +import type { Pressable } from "react-native" +import { StyleSheet } from "react-native" +import { + Easing, + interpolateColor, + useAnimatedStyle, + useSharedValue, + withTiming, +} from "react-native-reanimated" + +import { useColor } from "@/src/theme/colors" + +import { ReAnimatedPressable } from "../../common/AnimatedComponents" export const ItemPressable: typeof Pressable = memo(({ children, ...props }) => { const [isPressing, setIsPressing] = useState(false) + + const secondarySystemGroupedBackground = useColor("secondarySystemGroupedBackground") + + const systemFill = useColor("systemFill") + const pressed = useSharedValue(0) + + useEffect(() => { + if (isPressing) { + pressed.value = 1 + } else { + pressed.value = withTiming(0, { + duration: 300, + easing: Easing.ease, + }) + } + }, [isPressing, pressed]) + + const colorStyle = useAnimatedStyle(() => { + return { + backgroundColor: interpolateColor( + pressed.value, + [0, 1], + [secondarySystemGroupedBackground, systemFill], + ), + } + }) return ( - setIsPressing(true))} onPressOut={composeEventHandlers(props.onPressOut, () => setIsPressing(false))} onHoverIn={composeEventHandlers(props.onHoverIn, () => setIsPressing(true))} onHoverOut={composeEventHandlers(props.onHoverOut, () => setIsPressing(false))} className={cn( - isPressing ? "bg-tertiary-system-background" : "bg-system-background", + // isPressing ? "bg-system-fill" : "bg-secondary-system-grouped-background", props.className, )} + style={StyleSheet.flatten([colorStyle, props.style])} > {children} - + ) }) diff --git a/apps/mobile/src/modules/discover/Recommendations.tsx b/apps/mobile/src/modules/discover/Recommendations.tsx index 53ea8bf95e..c10629018e 100644 --- a/apps/mobile/src/modules/discover/Recommendations.tsx +++ b/apps/mobile/src/modules/discover/Recommendations.tsx @@ -176,7 +176,7 @@ const ItemRenderer = ({ if (typeof item === "string") { // Rendering header return ( - + {item} ) @@ -256,7 +256,7 @@ const NavigationSidebar: FC<{ scrollToLetter(title) }} > - {title} + {title} ))} diff --git a/apps/mobile/src/modules/discover/search.tsx b/apps/mobile/src/modules/discover/search.tsx index 2d29d0d127..4aa7e0b523 100644 --- a/apps/mobile/src/modules/discover/search.tsx +++ b/apps/mobile/src/modules/discover/search.tsx @@ -66,11 +66,11 @@ const DiscoverHeaderImpl = () => { } const PlaceholerSearchBar = () => { - const placeholderTextColor = useColor("placeholderText") + const labelColor = useColor("secondaryLabel") return ( { router.push("/search") }} @@ -79,8 +79,8 @@ const PlaceholerSearchBar = () => { className="absolute inset-0 flex flex-row items-center justify-center" pointerEvents="none" > - - + + Search @@ -116,7 +116,7 @@ const ComposeSearchBar = () => { const SearchInput = () => { const { searchFocusedAtom, searchValueAtom } = useSearchPageContext() const [isFocused, setIsFocused] = useAtom(searchFocusedAtom) - const placeholderTextColor = useColor("placeholderText") + const placeholderTextColor = useColor("secondaryLabel") const searchValue = useAtomValue(searchValueAtom) const setSearchValue = useSetAtom(searchValueAtom) const inputRef = useRef(null) @@ -183,7 +183,7 @@ const SearchInput = () => { }, [isFocused]) return ( - + {focusOrHasValue && ( { > {!searchValue && !tempSearchValue && ( - + Search )} @@ -228,7 +228,7 @@ const SearchInput = () => { pointerEvents="none" > - + Search diff --git a/apps/mobile/src/modules/subscription/CategoryGrouped.tsx b/apps/mobile/src/modules/subscription/CategoryGrouped.tsx index 17e0c84b34..007326276f 100644 --- a/apps/mobile/src/modules/subscription/CategoryGrouped.tsx +++ b/apps/mobile/src/modules/subscription/CategoryGrouped.tsx @@ -1,13 +1,15 @@ import { memo, useState } from "react" -import { Text, TouchableOpacity, View } from "react-native" +import { Text, TouchableOpacity } from "react-native" import Animated, { useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated" import { ItemPressable } from "@/src/components/ui/pressable/item-pressable" import { MingcuteRightLine } from "@/src/icons/mingcute_right_line" import { useUnreadCounts } from "@/src/store/unread/hooks" +import { useColor } from "@/src/theme/colors" import { SubscriptionFeedCategoryContextMenu } from "../context-menu/feeds" import { GroupedContext, useViewPageCurrentView } from "./ctx" +import { ItemSeparator } from "./ItemSeparator" import { UnGroupedList } from "./UnGroupedList" // const CategoryList: FC<{ @@ -30,6 +32,7 @@ export const CategoryGrouped = memo( }, [rotateSharedValue]) const view = useViewPageCurrentView() + const tertiaryLabelColor = useColor("tertiaryLabel") return ( <> { // TODO navigate to category }} - className="border-item-pressed h-12 flex-row items-center border-b px-3" + className="h-12 flex-row items-center px-3" > - + {category} {!!unreadCounts && ( - {unreadCounts} + {unreadCounts} )}
{expanded && ( - - - + + )} diff --git a/apps/mobile/src/modules/subscription/ItemSeparator.tsx b/apps/mobile/src/modules/subscription/ItemSeparator.tsx new file mode 100644 index 0000000000..d96b44a8c5 --- /dev/null +++ b/apps/mobile/src/modules/subscription/ItemSeparator.tsx @@ -0,0 +1,8 @@ +import { StyleSheet, View } from "react-native" + +const el = ( + +) +export const ItemSeparator = () => { + return el +} diff --git a/apps/mobile/src/modules/subscription/SubscriptionLists.tsx b/apps/mobile/src/modules/subscription/SubscriptionLists.tsx index a353c8620d..467f82bfac 100644 --- a/apps/mobile/src/modules/subscription/SubscriptionLists.tsx +++ b/apps/mobile/src/modules/subscription/SubscriptionLists.tsx @@ -3,7 +3,7 @@ import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs" import { useHeaderHeight } from "@react-navigation/elements" import { useAtom } from "jotai" import { memo, useEffect, useMemo, useRef, useState } from "react" -import { RefreshControl, StyleSheet, Text, View } from "react-native" +import { FlatList, RefreshControl, StyleSheet, Text, View } from "react-native" import PagerView from "react-native-pager-view" import ReAnimated, { LinearTransition } from "react-native-reanimated" import { useSafeAreaInsets } from "react-native-safe-area-context" @@ -29,6 +29,7 @@ import { useViewPageCurrentView, ViewPageCurrentViewProvider } from "./ctx" import { InboxItem } from "./items/InboxItem" import { ListSubscriptionItem } from "./items/ListSubscriptionItem" import { SubscriptionItem } from "./items/SubscriptionItem" +import { ItemSeparator } from "./ItemSeparator" export const SubscriptionLists = memo(() => { const [currentView, setCurrentView] = useAtom(viewAtom) @@ -112,6 +113,7 @@ const SubscriptionList = ({ view }: { view: FeedViewType }) => { refreshing={refreshing} /> } + className={"bg-system-grouped-background"} contentInsetAdjustmentBehavior="automatic" scrollIndicatorInsets={{ bottom: tabHeight - insets.bottom, @@ -121,6 +123,7 @@ const SubscriptionList = ({ view }: { view: FeedViewType }) => { paddingTop: offsetTop, paddingBottom: tabHeight, }} + ItemSeparatorComponent={ItemSeparator} data={data} ListHeaderComponent={ListHeaderComponent} renderItem={ItemRender} @@ -165,7 +168,7 @@ const ListHeaderComponent = () => { {view === FeedViewType.Articles && } - Feeds + Feeds ) } @@ -175,13 +178,17 @@ const InboxList = () => { if (inboxes.length === 0) return null return ( - Inboxes - {inboxes.map((id) => { - return - })} + Inboxes + + ) } +const renderInboxItems = ({ item }: { item: string }) => const StarItem = () => { return ( @@ -204,13 +211,19 @@ const ListList = () => { if (sortedListIds.length === 0) return null return ( - Lists - {sortedListIds.map((id) => { + Lists + {/* {sortedListIds.map((id) => { return - })} + })} */} + ) } +const renderListItems = ({ item }: { item: string }) => const style = StyleSheet.create({ flex: { diff --git a/apps/mobile/src/modules/subscription/UnGroupedList.tsx b/apps/mobile/src/modules/subscription/UnGroupedList.tsx index 129a7f2949..861101ca8e 100644 --- a/apps/mobile/src/modules/subscription/UnGroupedList.tsx +++ b/apps/mobile/src/modules/subscription/UnGroupedList.tsx @@ -1,9 +1,11 @@ import type { FC } from "react" +import { FlatList } from "react-native" import { useSortedUngroupedSubscription } from "@/src/store/subscription/hooks" import { useFeedListSortMethod, useFeedListSortOrder } from "./atoms" import { SubscriptionItem } from "./items/SubscriptionItem" +import { ItemSeparator } from "./ItemSeparator" export const UnGroupedList: FC<{ subscriptionIds: string[] @@ -11,15 +13,14 @@ export const UnGroupedList: FC<{ const sortBy = useFeedListSortMethod() const sortOrder = useFeedListSortOrder() const sortedSubscriptionIds = useSortedUngroupedSubscription(subscriptionIds, sortBy, sortOrder) - const lastSubscriptionId = sortedSubscriptionIds.at(-1) - return sortedSubscriptionIds.map((id) => { - return ( - - ) - }) + return ( + + ) } + +const renderSubscriptionItems = ({ item }: { item: string }) => diff --git a/apps/mobile/src/modules/subscription/ViewTab.tsx b/apps/mobile/src/modules/subscription/ViewTab.tsx index dcbc606544..cafd5fc86e 100644 --- a/apps/mobile/src/modules/subscription/ViewTab.tsx +++ b/apps/mobile/src/modules/subscription/ViewTab.tsx @@ -66,7 +66,7 @@ export const ViewTab = () => { return ( { scrollOffsetX.current = event.nativeEvent.contentOffset.x }} showsHorizontalScrollIndicator={false} - className="border-tertiary-system-background" horizontal ref={tabRef} contentContainerStyle={styles.tabScroller} diff --git a/apps/mobile/src/modules/subscription/items/InboxItem.tsx b/apps/mobile/src/modules/subscription/items/InboxItem.tsx index 625ad1b1eb..e8edbb4fb3 100644 --- a/apps/mobile/src/modules/subscription/items/InboxItem.tsx +++ b/apps/mobile/src/modules/subscription/items/InboxItem.tsx @@ -16,7 +16,7 @@ export const InboxItem = memo(({ id }: { id: string }) => { if (!subscription) return null return ( - + - + {!!list.image && ( diff --git a/apps/mobile/src/modules/subscription/items/SubscriptionItem.tsx b/apps/mobile/src/modules/subscription/items/SubscriptionItem.tsx index fba7c030dc..7de86fc870 100644 --- a/apps/mobile/src/modules/subscription/items/SubscriptionItem.tsx +++ b/apps/mobile/src/modules/subscription/items/SubscriptionItem.tsx @@ -48,6 +48,7 @@ export const SubscriptionItem = memo(({ id, className }: { id: string; className const feed = useFeed(id) const inGrouped = !!useContext(GroupedContext) const view = useViewPageCurrentView() + // const swipeableRef: SwipeableRef = useRef(null) if (!subscription || !feed) return null @@ -73,7 +74,7 @@ export const SubscriptionItem = memo(({ id, className }: { id: string; className className={cn( "flex h-12 flex-row items-center", inGrouped ? "pl-8 pr-4" : "px-4", - "border-item-pressed border-b", + className, )} onPress={() => { diff --git a/apps/mobile/src/screens/(modal)/follow.tsx b/apps/mobile/src/screens/(modal)/follow.tsx index 3b7144700e..2f6c0f2619 100644 --- a/apps/mobile/src/screens/(modal)/follow.tsx +++ b/apps/mobile/src/screens/(modal)/follow.tsx @@ -116,21 +116,19 @@ function FollowImpl() { /> {/* Group 1 */} - + {feed?.title} - - {feed?.description} - + {feed?.description} {/* Group 2 */} - + - + {keys.map((keyItem) => { const parameters = normalizeRSSHubParameters(route.parameters[keyItem.name]) @@ -172,7 +172,9 @@ function FormImpl({ route, routePrefix, name }: RsshubFormParams) { )} {!!parameters && ( - {parameters.description} + + {parameters.description} + )} ) @@ -200,11 +202,13 @@ const Maintainers = ({ maintainers }: { maintainers?: string[] }) => { } return ( - - This feed is provided by RSSHub, with credit to + + + This feed is provided by RSSHub, with credit to{" "} + {maintainers.map((m) => ( Linking.openURL(`https://github.com/${m}`)}> - @{m} + @{m} ))} diff --git a/apps/mobile/src/theme/colors.ts b/apps/mobile/src/theme/colors.ts index 4ef1c7bad7..5f71e6e8d7 100644 --- a/apps/mobile/src/theme/colors.ts +++ b/apps/mobile/src/theme/colors.ts @@ -64,33 +64,34 @@ export const palette = { export const lightVariants = { // UIKit Colors - label: "0 0 0", - secondaryLabel: "122 122 122", - tertiaryLabel: "172 172 178", - quaternaryLabel: "199 199 204", + placeholderText: "199 199 204", - separator: "209 209 214", - opaqueSeparator: "229 229 234", + separator: "84 84 86 0.34", + opaqueSeparator: "84 84 86 0.34", + nonOpaqueSeparator: "198 198 200", link: "0 122 255", + systemBackground: "255 255 255", secondarySystemBackground: "242 242 247", - tertiarySystemBackground: "229 229 234", + tertiarySystemBackground: "255 255 255", // Grouped systemGroupedBackground: "242 242 247", - systemGroupedBackground2: "255 255 255", + secondarySystemGroupedBackground: "255 255 255", + tertiarySystemGroupedBackground: "242 242 247", // System Colors - systemFill: "209 213 219", - secondarySystemFill: "209 213 219", - tertiarySystemFill: "209 213 219", - quaternarySystemFill: "209 213 219", + systemFill: "120 120 128 0.2", + secondarySystemFill: "120 120 128 0.16", + tertiarySystemFill: "120 120 128 0.12", + quaternarySystemFill: "120 120 128 0.08", // Text Colors + label: "0 0 0", text: "0 0 0", - secondaryText: "142 142 147", - tertiaryText: "99 99 102", - quaternaryText: "72 72 74", + secondaryLabel: "60 60 67 0.6", + tertiaryLabel: "60 60 67 0.3", + quaternaryLabel: "60 60 67 0.18", // Extended colors disabled: "235 235 228", @@ -98,13 +99,11 @@ export const lightVariants = { } export const darkVariants = { // UIKit Colors - label: "255 255 255", - secondaryLabel: "172 172 178", - tertiaryLabel: "122 122 122", - quaternaryLabel: "99 99 102", + placeholderText: "122 122 122", - separator: "72 72 74", - opaqueSeparator: "58 58 60", + separator: "56 56 58 0.6", + opaqueSeparator: "56 56 58 0.6", + nonOpaqueSeparator: "84 84 86", link: "10 132 255", systemBackground: "0 0 0", secondarySystemBackground: "28 28 30", @@ -112,19 +111,21 @@ export const darkVariants = { // Grouped systemGroupedBackground: "0 0 0", - systemGroupedBackground2: "28 28 30", + secondarySystemGroupedBackground: "28 28 30", + tertiarySystemGroupedBackground: "44 44 46", // System Colors - systemFill: "72 72 74", - secondarySystemFill: "99 99 102", - tertiarySystemFill: "122 122 122", - quaternarySystemFill: "142 142 147", + systemFill: "120 120 128 36", + secondarySystemFill: "120 120 128 0.32", + tertiarySystemFill: "120 120 128 0.24", + quaternarySystemFill: "120 120 128 0.19", // Text Colors + label: "255 255 255", text: "255 255 255", - secondaryText: "172 172 178", - tertiaryText: "122 122 122", - quaternaryText: "99 99 102", + secondaryLabel: "235 235 245 0.6", + tertiaryLabel: "235 235 245 0.3", + quaternaryLabel: "235 235 245 0.18", // Extended colors disabled: "85 85 85", diff --git a/apps/mobile/tailwind.config.ts b/apps/mobile/tailwind.config.ts index 885c83d673..f6a5c0a6c5 100644 --- a/apps/mobile/tailwind.config.ts +++ b/apps/mobile/tailwind.config.ts @@ -38,13 +38,11 @@ export default resolveConfig({ }, // System colors - label: "rgb(var(--color-label) / )", - "secondary-label": "rgb(var(--color-secondaryLabel) / )", - "tertiary-label": "rgb(var(--color-tertiaryLabel) / )", - "quaternary-label": "rgb(var(--color-quaternaryLabel) / )", + "placeholder-text": "rgb(var(--color-placeholderText) / )", separator: "rgb(var(--color-separator) / )", - "opaque-separator": "rgb(var(--color-opaqueSeparator) / )", + "opaque-separator": "rgba(var(--color-opaqueSeparator))", + "non-opaque-separator": "rgba(var(--color-nonOpaqueSeparator))", link: "rgb(var(--color-link) / )", // Backgrounds @@ -53,18 +51,22 @@ export default resolveConfig({ "rgb(var(--color-secondarySystemBackground) / )", "tertiary-system-background": "rgb(var(--color-tertiarySystemBackground) / )", "system-grouped-background": "rgb(var(--color-systemGroupedBackground) / )", - "system-grouped-background-2": "rgb(var(--color-systemGroupedBackground2) / )", + "secondary-system-grouped-background": + "rgb(var(--color-secondarySystemGroupedBackground) / )", + "tertiary-system-grouped-background": + "rgb(var(--color-tertiarySystemGroupedBackground) / )", // System fills - "system-fill": "rgb(var(--color-systemFill) / )", - "secondary-system-fill": "rgb(var(--color-secondarySystemFill) / )", - "tertiary-system-fill": "rgb(var(--color-tertiarySystemFill) / )", - "quaternary-system-fill": "rgb(var(--color-quaternarySystemFill) / )", + "system-fill": "rgba(var(--color-systemFill))", + "secondary-system-fill": "rgba(var(--color-secondarySystemFill))", + "tertiary-system-fill": "rgba(var(--color-tertiarySystemFill))", + "quaternary-system-fill": "rgba(var(--color-quaternarySystemFill))", // Text colors + label: "rgb(var(--color-text) / )", text: "rgb(var(--color-text) / )", - "secondary-text": "rgb(var(--color-secondaryText) / )", - "tertiary-text": "rgb(var(--color-tertiaryText) / )", - "quaternary-text": "rgb(var(--color-quaternaryText) / )", + "secondary-label": "rgba(var(--color-secondaryLabel))", + "tertiary-label": "rgba(var(--color-tertiaryLabel))", + "quaternary-label": "rgba(var(--color-quaternaryLabel))", // Extended colors disabled: "rgb(var(--color-disabled) / )", diff --git a/packages/utils/src/color.ts b/packages/utils/src/color.ts index 291a91b007..53c0db5f42 100644 --- a/packages/utils/src/color.ts +++ b/packages/utils/src/color.ts @@ -162,6 +162,6 @@ export const withOpacity = (color: string, opacity: number) => { } } export const rgbStringToRgb = (hex: string) => { - const [r, g, b] = hex.split(" ").map((s) => Number.parseInt(s)) - return `rgb(${r}, ${g}, ${b})` + const [r, g, b, a] = hex.split(" ").map((s) => Number.parseFloat(s)) + return `rgba(${r}, ${g}, ${b}, ${a || 1})` } From 01122c9480a56b5bd3a90d347d787f080ff6d754 Mon Sep 17 00:00:00 2001 From: Innei Date: Mon, 13 Jan 2025 22:08:17 +0800 Subject: [PATCH 18/93] feat(rn): implement search list Signed-off-by: Innei --- .../src/components/ui/icon/fallback-icon.tsx | 30 ++- apps/mobile/src/modules/discover/constants.ts | 2 - .../discover/search-tabs/SearchFeed.tsx | 43 ++-- .../discover/search-tabs/SearchList.tsx | 105 ++++++++- .../discover/search-tabs/SearchRSSHub.tsx | 11 - .../modules/discover/search-tabs/__base.tsx | 12 +- .../modules/discover/search-tabs/hooks.tsx | 35 +++ apps/mobile/src/modules/discover/search.tsx | 2 +- apps/mobile/src/modules/feed/FollowFeed.tsx | 195 +++++++++++++++++ .../mobile/src/modules/feed/view-selector.tsx | 20 +- apps/mobile/src/modules/list/FollowList.tsx | 172 +++++++++++++++ apps/mobile/src/morph/hono.ts | 14 ++ apps/mobile/src/morph/types.ts | 1 + apps/mobile/src/screens/(headless)/search.tsx | 2 - apps/mobile/src/screens/(modal)/follow.tsx | 199 ++---------------- .../src/screens/(stack)/(tabs)/_layout.tsx | 10 +- apps/mobile/src/store/list/store.ts | 14 ++ apps/mobile/src/store/subscription/hooks.ts | 3 + 18 files changed, 622 insertions(+), 248 deletions(-) delete mode 100644 apps/mobile/src/modules/discover/search-tabs/SearchRSSHub.tsx create mode 100644 apps/mobile/src/modules/discover/search-tabs/hooks.tsx create mode 100644 apps/mobile/src/modules/feed/FollowFeed.tsx create mode 100644 apps/mobile/src/modules/list/FollowList.tsx diff --git a/apps/mobile/src/components/ui/icon/fallback-icon.tsx b/apps/mobile/src/components/ui/icon/fallback-icon.tsx index 1989071514..ab4400b6bf 100644 --- a/apps/mobile/src/components/ui/icon/fallback-icon.tsx +++ b/apps/mobile/src/components/ui/icon/fallback-icon.tsx @@ -1,8 +1,9 @@ import { getBackgroundGradient, isCJKChar } from "@follow/utils" +import { Image } from "expo-image" import { LinearGradient } from "expo-linear-gradient" -import { useMemo } from "react" +import { useMemo, useState } from "react" import type { StyleProp, ViewStyle } from "react-native" -import { StyleSheet, Text } from "react-native" +import { StyleSheet, Text, View } from "react-native" export const FallbackIcon = ({ title, @@ -39,6 +40,31 @@ export const FallbackIcon = ({ ) } +export const IconWithFallback = (props: { + url?: string | undefined | null + size: number + title?: string + className?: string + style?: StyleProp +}) => { + const { url, size, title = "", className, style } = props + const [hasError, setHasError] = useState(false) + + if (!url || hasError) { + return + } + + return ( + + setHasError(true)} + /> + + ) +} + const styles = StyleSheet.create({ container: { display: "flex", diff --git a/apps/mobile/src/modules/discover/constants.ts b/apps/mobile/src/modules/discover/constants.ts index 0c2f22c960..96e0092484 100644 --- a/apps/mobile/src/modules/discover/constants.ts +++ b/apps/mobile/src/modules/discover/constants.ts @@ -2,12 +2,10 @@ export enum SearchType { Feed = "feed", List = "list", User = "user", - RSSHub = "rsshub", } export const SearchTabs = [ { name: "Feed", value: SearchType.Feed }, { name: "List", value: SearchType.List }, { name: "User", value: SearchType.User }, - { name: "RSSHub", value: SearchType.RSSHub }, ] diff --git a/apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx b/apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx index 6f9c430631..6ff35362e1 100644 --- a/apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx +++ b/apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx @@ -1,24 +1,21 @@ import { FeedViewType } from "@follow/constants" -import { withOpacity } from "@follow/utils" import { useQuery } from "@tanstack/react-query" import { Image } from "expo-image" import { router } from "expo-router" import { useAtomValue } from "jotai" -import { memo } from "react" +import type { ListRenderItem } from "react-native" import { Text, useWindowDimensions, View } from "react-native" import { ScrollView } from "react-native-gesture-handler" import Animated, { FadeInUp } from "react-native-reanimated" import { FeedIcon } from "@/src/components/ui/icon/feed-icon" -import { LoadingIndicator } from "@/src/components/ui/loading" import { ItemPressable } from "@/src/components/ui/pressable/item-pressable" -import { SadCuteReIcon } from "@/src/icons/sad_cute_re" import { apiClient } from "@/src/lib/api-fetch" import { useSubscriptionByFeedId } from "@/src/store/subscription/hooks" -import { useColor } from "@/src/theme/colors" import { useSearchPageContext } from "../ctx" -import { BaseSearchPageFlatList, BaseSearchPageRootView, BaseSearchPageScrollView } from "./__base" +import { BaseSearchPageFlatList, ItemSeparator, RenderScrollComponent } from "./__base" +import { useDataSkeleton } from "./hooks" type SearchResultItem = Awaited>["data"][number] @@ -39,50 +36,34 @@ export const SearchFeed = () => { enabled: !!searchValue, }) - const textColor = useColor("text") - - if (isLoading) { - return ( - - - - - ) - } - - if (data?.data.length === 0) { - return ( - - - - No results found - - ) - } + const skeleton = useDataSkeleton(isLoading, data) + if (skeleton) return skeleton return ( } + contentContainerClassName={"-mt-4"} + renderScrollComponent={RenderScrollComponent} data={data?.data} renderItem={renderItem} + ItemSeparatorComponent={ItemSeparator} /> ) } const keyExtractor = (item: SearchResultItem) => item.feed?.id ?? Math.random().toString() -const renderItem = ({ item }: { item: SearchResultItem }) => ( +const renderItem: ListRenderItem = ({ item }) => ( ) -const SearchFeedItem = memo(({ item }: { item: SearchResultItem }) => { +const SearchFeedItem = ({ item }: { item: SearchResultItem }) => { const isSubscribed = useSubscriptionByFeedId(item.feed?.id ?? "") return ( { if (item.feed?.id) { router.push(`/follow?id=${item.feed.id}`) @@ -146,7 +127,7 @@ const SearchFeedItem = memo(({ item }: { item: SearchResultItem }) => { ) -}) +} const formatter = new Intl.DateTimeFormat("en-US", { year: "numeric", month: "short", diff --git a/apps/mobile/src/modules/discover/search-tabs/SearchList.tsx b/apps/mobile/src/modules/discover/search-tabs/SearchList.tsx index 244064eb60..90da4e7d82 100644 --- a/apps/mobile/src/modules/discover/search-tabs/SearchList.tsx +++ b/apps/mobile/src/modules/discover/search-tabs/SearchList.tsx @@ -1,16 +1,111 @@ +import { useQuery } from "@tanstack/react-query" +import { Image } from "expo-image" +import { router } from "expo-router" import { useAtomValue } from "jotai" -import { Text } from "react-native" +import { memo } from "react" +import { Text, View } from "react-native" +import Animated, { FadeInUp } from "react-native-reanimated" + +import { FallbackIcon } from "@/src/components/ui/icon/fallback-icon" +import { ItemPressable } from "@/src/components/ui/pressable/item-pressable" +import { apiClient } from "@/src/lib/api-fetch" +import { useSubscriptionByListId } from "@/src/store/subscription/hooks" import { useSearchPageContext } from "../ctx" -import { BaseSearchPageScrollView } from "./__base" +import { BaseSearchPageFlatList, ItemSeparator, RenderScrollComponent } from "./__base" +import { useDataSkeleton } from "./hooks" + +type SearchResultItem = Awaited>["data"][number] export const SearchList = () => { const { searchValueAtom } = useSearchPageContext() const searchValue = useAtomValue(searchValueAtom) + const { data, isLoading, refetch } = useQuery({ + queryKey: ["searchFeed", searchValue], + queryFn: () => { + return apiClient.discover.$post({ + json: { + keyword: searchValue, + target: "lists", + }, + }) + }, + enabled: !!searchValue, + }) + + const skeleton = useDataSkeleton(isLoading, data) + if (skeleton) return skeleton + return ( - - {searchValue} - + ) } + +const keyExtractor = (item: SearchResultItem) => item.list?.id ?? Math.random().toString() + +const renderItem = ({ item }: { item: SearchResultItem }) => ( + +) + +const SearchListCard = memo(({ item }: { item: SearchResultItem }) => { + const isSubscribed = useSubscriptionByListId(item.list?.id ?? "") + return ( + + { + if (item.list?.id) { + router.push(`/follow?id=${item.list.id}&type=list`) + } + }} + > + {/* Headline */} + + + {item.list?.image ? ( + + ) : ( + !!item.list?.title && + )} + + + + {item.list?.title} + + {!!item.list?.description && ( + + {item.list?.description} + + )} + + {/* Subscribe */} + {isSubscribed && ( + + + Subscribed + + + )} + + + + ) +}) diff --git a/apps/mobile/src/modules/discover/search-tabs/SearchRSSHub.tsx b/apps/mobile/src/modules/discover/search-tabs/SearchRSSHub.tsx deleted file mode 100644 index 7083fa8aea..0000000000 --- a/apps/mobile/src/modules/discover/search-tabs/SearchRSSHub.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Text } from "react-native" - -import { BaseSearchPageScrollView } from "./__base" - -export const SearchRSSHub = () => { - return ( - - RSSHub - - ) -} diff --git a/apps/mobile/src/modules/discover/search-tabs/__base.tsx b/apps/mobile/src/modules/discover/search-tabs/__base.tsx index e1b9d52d86..fc45c623eb 100644 --- a/apps/mobile/src/modules/discover/search-tabs/__base.tsx +++ b/apps/mobile/src/modules/discover/search-tabs/__base.tsx @@ -1,7 +1,7 @@ import { cn } from "@follow/utils/src/utils" import { forwardRef } from "react" import type { ScrollViewProps } from "react-native" -import { RefreshControl, ScrollView, useWindowDimensions, View } from "react-native" +import { RefreshControl, ScrollView, StyleSheet, useWindowDimensions, View } from "react-native" import type { FlatListPropsWithLayout } from "react-native-reanimated" import Animated, { LinearTransition } from "react-native-reanimated" import { useSafeAreaInsets } from "react-native-safe-area-context" @@ -70,10 +70,18 @@ export function BaseSearchPageFlatList({ } {...props} /> ) } +const itemSeparator = ( + +) +export const ItemSeparator = () => itemSeparator + +export const RenderScrollComponent = (props: ScrollViewProps) => ( + +) diff --git a/apps/mobile/src/modules/discover/search-tabs/hooks.tsx b/apps/mobile/src/modules/discover/search-tabs/hooks.tsx new file mode 100644 index 0000000000..0c3e72ab19 --- /dev/null +++ b/apps/mobile/src/modules/discover/search-tabs/hooks.tsx @@ -0,0 +1,35 @@ +import { withOpacity } from "@follow/utils" +import { useMemo } from "react" +import { Text, View } from "react-native" + +import { LoadingIndicator } from "@/src/components/ui/loading" +import { SadCuteReIcon } from "@/src/icons/sad_cute_re" +import { useColor } from "@/src/theme/colors" + +import { BaseSearchPageRootView } from "./__base" + +export const useDataSkeleton = (isLoading: boolean, data: any) => { + const textColor = useColor("text") + return useMemo(() => { + if (isLoading) { + return ( + + + + + ) + } + + if (data?.data.length === 0) { + return ( + + + + No results found + + ) + } + + return null + }, [isLoading, data, textColor]) +} diff --git a/apps/mobile/src/modules/discover/search.tsx b/apps/mobile/src/modules/discover/search.tsx index 4aa7e0b523..6b1347489b 100644 --- a/apps/mobile/src/modules/discover/search.tsx +++ b/apps/mobile/src/modules/discover/search.tsx @@ -38,7 +38,7 @@ export const SearchHeader: FC<{ className="relative" onLayout={onLayout} > - {/* */} + diff --git a/apps/mobile/src/modules/feed/FollowFeed.tsx b/apps/mobile/src/modules/feed/FollowFeed.tsx new file mode 100644 index 0000000000..af613aaae6 --- /dev/null +++ b/apps/mobile/src/modules/feed/FollowFeed.tsx @@ -0,0 +1,195 @@ +import { FeedViewType } from "@follow/constants" +import { zodResolver } from "@hookform/resolvers/zod" +import { StackActions } from "@react-navigation/native" +import { useQuery } from "@tanstack/react-query" +import { router, Stack, useLocalSearchParams, useNavigation } from "expo-router" +import { useState } from "react" +import { Controller, useForm } from "react-hook-form" +import { ScrollView, Text, View } from "react-native" +import { z } from "zod" + +import { + ModalHeaderCloseButton, + ModalHeaderShubmitButton, +} from "@/src/components/common/ModalSharedComponents" +import { FormProvider } from "@/src/components/ui/form/FormProvider" +import { FormLabel } from "@/src/components/ui/form/Label" +import { FormSwitch } from "@/src/components/ui/form/Switch" +import { TextField } from "@/src/components/ui/form/TextField" +import { FeedIcon } from "@/src/components/ui/icon/feed-icon" +import { LoadingIndicator } from "@/src/components/ui/loading" +import { useIsRouteOnlyOne } from "@/src/hooks/useIsRouteOnlyOne" +import { FeedViewSelector } from "@/src/modules/feed/view-selector" +import { useFeed } from "@/src/store/feed/hooks" +import { feedSyncServices } from "@/src/store/feed/store" +import { useSubscriptionByFeedId } from "@/src/store/subscription/hooks" +import { subscriptionSyncService } from "@/src/store/subscription/store" +import type { SubscriptionForm } from "@/src/store/subscription/types" + +const formSchema = z.object({ + view: z.string(), + category: z.string().nullable().optional(), + isPrivate: z.boolean().optional(), + title: z.string().optional(), +}) +const defaultValues = { view: FeedViewType.Articles.toString() } +export function FollowFeed(props: { id: string }) { + const { id } = props + const feed = useFeed(id as string) + const { isLoading } = useQuery({ + queryKey: ["feed", id], + queryFn: () => feedSyncServices.fetchFeedById({ id: id as string }), + enabled: !feed, + }) + + if (isLoading) { + return ( + + + + ) + } + + return +} +function FollowImpl() { + const { id } = useLocalSearchParams() + + const feed = useFeed(id as string) + const isSubscribed = useSubscriptionByFeedId(feed?.id || "") + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues, + }) + + const [isLoading, setIsLoading] = useState(false) + const routeOnlyOne = useIsRouteOnlyOne() + const navigate = useNavigation() + const parentRoute = navigate.getParent() + const submit = async () => { + setIsLoading(true) + const values = form.getValues() + const body: SubscriptionForm = { + url: feed.url, + view: Number.parseInt(values.view), + category: values.category ?? "", + isPrivate: values.isPrivate ?? false, + title: values.title ?? "", + feedId: feed.id, + } + + await subscriptionSyncService.subscribe(body).finally(() => { + setIsLoading(false) + }) + + if (router.canDismiss()) { + router.dismissAll() + + if (!routeOnlyOne) { + parentRoute?.dispatch(StackActions.popToTop()) + } + } + } + + const { isValid, isDirty } = form.formState + + if (!feed?.id) { + return Feed ({id}) not found + } + + return ( + + ( + + ), + }} + /> + + {/* Group 1 */} + + + + + + + {feed?.title} + {feed?.description} + + + + {/* Group 2 */} + + + + ( + + )} + /> + + + + ( + + )} + /> + + + + ( + + )} + /> + + + + + + ( + + )} + /> + + + + + ) +} diff --git a/apps/mobile/src/modules/feed/view-selector.tsx b/apps/mobile/src/modules/feed/view-selector.tsx index 7c5f6858e3..7ba834e446 100644 --- a/apps/mobile/src/modules/feed/view-selector.tsx +++ b/apps/mobile/src/modules/feed/view-selector.tsx @@ -4,23 +4,35 @@ import { Text, TouchableOpacity, View } from "react-native" import { Grid } from "@/src/components/ui/grid" import { views } from "@/src/constants/views" +import { useColor } from "@/src/theme/colors" interface Props { value: FeedViewType - onChange: (value: FeedViewType) => void + onChange?: (value: FeedViewType) => void className?: string + readOnly?: boolean } -export const FeedViewSelector = ({ value, onChange, className }: Props) => { +export const FeedViewSelector = ({ value, onChange, className, readOnly }: Props) => { + const secondaryLabelColor = useColor("secondaryLabel") return ( {views.map((view) => { const isSelected = +value === +view.view return ( - onChange(view.view)}> + onChange?.(view.view)} + disabled={readOnly} + className={readOnly ? "opacity-50" : undefined} + > - + { + const { id } = props + const list = useList(id as string) + const { isLoading } = useQuery({ + queryKey: ["list", id], + queryFn: () => listSyncServices.fetchListById({ id: id as string }), + enabled: !list, + }) + + if (isLoading) { + return ( + + + + ) + } + + return +} + +const formSchema = z.object({ + view: z.string(), + isPrivate: z.boolean().optional(), + title: z.string().optional(), +}) +const defaultValues = { view: FeedViewType.Articles.toString() } + +const Impl = (props: { id: string }) => { + const { id } = props + const list = useList(id as string)! + + const isSubscribed = useSubscriptionByListId(id as string) + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues, + }) + const { isValid, isDirty } = form.formState + + const submit = async () => { + const payload = form.getValues() + // console.log("submit", payload) + void payload + + if (list.fee && !isSubscribed) { + Alert.alert( + `To follow this list, you must pay a fee to the list creator. Press OK to pay ${list.fee} power to follow this list.`, + "OK", + [ + { + text: "Cancel", + style: "cancel", + }, + { + text: "OK", + onPress: () => {}, + }, + ], + ) + } + } + + const isLoading = false + + return ( + + ( + + ), + }} + /> + + + + + + + + {list?.title} + {list?.description} + + + + + + + + + + + + + + ( + + )} + /> + + + + ( + + )} + /> + + + {!!list.fee && ( + + + + {list.fee} + + + To follow this list, you must pay a fee to the list creator. + + + )} + + + + ) +} diff --git a/apps/mobile/src/morph/hono.ts b/apps/mobile/src/morph/hono.ts index daba26a2dc..c881ca0e3e 100644 --- a/apps/mobile/src/morph/hono.ts +++ b/apps/mobile/src/morph/hono.ts @@ -79,6 +79,20 @@ class Morph { } return { subscriptions, ...collections } } + + toList({ list: data }: HonoApiClient.List_Get): ListModel { + return { + id: data.id, + title: data.title!, + userId: data.ownerUserId!, + description: data.description!, + view: data.view, + image: data.image!, + ownerUserId: data.ownerUserId!, + feedIds: data.feedIds!, + fee: data.fee!, + } + } } export const honoMorph = new Morph() diff --git a/apps/mobile/src/morph/types.ts b/apps/mobile/src/morph/types.ts index 7836b0b6b0..4f06217f3c 100644 --- a/apps/mobile/src/morph/types.ts +++ b/apps/mobile/src/morph/types.ts @@ -7,4 +7,5 @@ type ExtractData any> = export namespace HonoApiClient { export type Subscription_Get = ExtractData + export type List_Get = ExtractData } diff --git a/apps/mobile/src/screens/(headless)/search.tsx b/apps/mobile/src/screens/(headless)/search.tsx index f8d8989c97..fc8a46dbdc 100644 --- a/apps/mobile/src/screens/(headless)/search.tsx +++ b/apps/mobile/src/screens/(headless)/search.tsx @@ -19,7 +19,6 @@ import { import { SearchHeader } from "@/src/modules/discover/search" import { SearchFeed } from "@/src/modules/discover/search-tabs/SearchFeed" import { SearchList } from "@/src/modules/discover/search-tabs/SearchList" -import { SearchRSSHub } from "@/src/modules/discover/search-tabs/SearchRSSHub" import { SearchUser } from "@/src/modules/discover/search-tabs/SearchUser" const Search = () => { @@ -41,7 +40,6 @@ const SearchType2RenderContent: Record = { [SearchType.Feed]: SearchFeed, [SearchType.List]: SearchList, [SearchType.User]: SearchUser, - [SearchType.RSSHub]: SearchRSSHub, } const PlaceholderLazyView = () => { const windowWidth = Dimensions.get("window").width diff --git a/apps/mobile/src/screens/(modal)/follow.tsx b/apps/mobile/src/screens/(modal)/follow.tsx index 2f6c0f2619..6a4d549c01 100644 --- a/apps/mobile/src/screens/(modal)/follow.tsx +++ b/apps/mobile/src/screens/(modal)/follow.tsx @@ -1,195 +1,20 @@ -import { FeedViewType } from "@follow/constants" -import { zodResolver } from "@hookform/resolvers/zod" -import { StackActions } from "@react-navigation/native" -import { useQuery } from "@tanstack/react-query" -import { router, Stack, useLocalSearchParams, useNavigation } from "expo-router" -import { useState } from "react" -import { Controller, useForm } from "react-hook-form" -import { ScrollView, Text, View } from "react-native" -import { z } from "zod" +import { useLocalSearchParams } from "expo-router" -import { - ModalHeaderCloseButton, - ModalHeaderShubmitButton, -} from "@/src/components/common/ModalSharedComponents" -import { FormProvider } from "@/src/components/ui/form/FormProvider" -import { FormLabel } from "@/src/components/ui/form/Label" -import { FormSwitch } from "@/src/components/ui/form/Switch" -import { TextField } from "@/src/components/ui/form/TextField" -import { FeedIcon } from "@/src/components/ui/icon/feed-icon" -import { LoadingIndicator } from "@/src/components/ui/loading" -import { useIsRouteOnlyOne } from "@/src/hooks/useIsRouteOnlyOne" -import { FeedViewSelector } from "@/src/modules/feed/view-selector" -import { useFeed } from "@/src/store/feed/hooks" -import { feedSyncServices } from "@/src/store/feed/store" -import { useSubscriptionByFeedId } from "@/src/store/subscription/hooks" -import { subscriptionSyncService } from "@/src/store/subscription/store" -import type { SubscriptionForm } from "@/src/store/subscription/types" +import { FollowFeed } from "@/src/modules/feed/FollowFeed" +import { FollowList } from "@/src/modules/list/FollowList" -const formSchema = z.object({ - view: z.string(), - category: z.string().nullable().optional(), - isPrivate: z.boolean().optional(), - title: z.string().optional(), -}) -const defaultValues = { view: FeedViewType.Articles.toString() } export default function Follow() { - const { id } = useLocalSearchParams() - const feed = useFeed(id as string) - const { isLoading } = useQuery({ - queryKey: ["feed", id], - queryFn: () => feedSyncServices.fetchFeedById({ id: id as string }), - enabled: !feed, - }) + const { id, type = "feed" } = useLocalSearchParams() - if (isLoading) { - return ( - - - - ) - } - - return -} -function FollowImpl() { - const { id } = useLocalSearchParams() - - const feed = useFeed(id as string) - const isSubscribed = useSubscriptionByFeedId(feed?.id || "") - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues, - }) - - const [isLoading, setIsLoading] = useState(false) - const routeOnlyOne = useIsRouteOnlyOne() - const navigate = useNavigation() - const parentRoute = navigate.getParent() - const submit = async () => { - setIsLoading(true) - const values = form.getValues() - const body: SubscriptionForm = { - url: feed.url, - view: Number.parseInt(values.view), - category: values.category ?? "", - isPrivate: values.isPrivate ?? false, - title: values.title ?? "", - feedId: feed.id, + switch (type) { + case "feed": { + return } - - await subscriptionSyncService.subscribe(body).finally(() => { - setIsLoading(false) - }) - - if (router.canDismiss()) { - router.dismissAll() - - if (!routeOnlyOne) { - parentRoute?.dispatch(StackActions.popToTop()) - } + case "list": { + return + } + default: { + return } } - - const { isValid, isDirty } = form.formState - - if (!feed?.id) { - return Feed ({id}) not found - } - - return ( - - ( - - ), - }} - /> - - {/* Group 1 */} - - - - - - - {feed?.title} - {feed?.description} - - - - {/* Group 2 */} - - - - ( - - )} - /> - - - - ( - - )} - /> - - - - ( - - )} - /> - - - - - - ( - - )} - /> - - - - - ) } diff --git a/apps/mobile/src/screens/(stack)/(tabs)/_layout.tsx b/apps/mobile/src/screens/(stack)/(tabs)/_layout.tsx index 021e956ea7..78a6528186 100644 --- a/apps/mobile/src/screens/(stack)/(tabs)/_layout.tsx +++ b/apps/mobile/src/screens/(stack)/(tabs)/_layout.tsx @@ -1,7 +1,7 @@ import { FeedViewType } from "@follow/constants" import { PlatformPressable } from "@react-navigation/elements/src/PlatformPressable" import { router, Tabs } from "expo-router" -import { View } from "react-native" +import { Easing, View } from "react-native" import { Gesture, GestureDetector } from "react-native-gesture-handler" import { runOnJS } from "react-native-reanimated" @@ -33,6 +33,14 @@ export default function TabLayout() { tabBarStyle: { position: "absolute", }, + animation: "fade", + transitionSpec: { + animation: "timing", + config: { + duration: 50, + easing: Easing.ease, + }, + }, }} > { export const useSubscriptionByFeedId = (feedId: string) => useSubscriptionStore(useCallback((state) => state.data[feedId] || null, [feedId])) + +export const useSubscriptionByListId = (listId: string) => + useSubscriptionStore(useCallback((state) => state.data[listId] || null, [listId])) From 284a510217bde660d79b8bfa834f304b7640284d Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 14 Jan 2025 17:49:48 +0800 Subject: [PATCH 19/93] feat(rn): support follow list Signed-off-by: Innei --- .../src/components/ui/form/TextField.tsx | 2 +- .../src/components/ui/icon/fallback-icon.tsx | 29 ++++-- .../src/components/ui/toast/CenteredToast.tsx | 2 + .../discover/search-tabs/SearchFeed.tsx | 13 +-- .../discover/search-tabs/SearchList.tsx | 89 +++++++++---------- .../modules/discover/search-tabs/__base.tsx | 9 +- apps/mobile/src/modules/discover/search.tsx | 2 +- apps/mobile/src/modules/list/FollowList.tsx | 58 +++++++++--- 8 files changed, 131 insertions(+), 73 deletions(-) diff --git a/apps/mobile/src/components/ui/form/TextField.tsx b/apps/mobile/src/components/ui/form/TextField.tsx index 00ef621236..2ee4ca30bf 100644 --- a/apps/mobile/src/components/ui/form/TextField.tsx +++ b/apps/mobile/src/components/ui/form/TextField.tsx @@ -23,7 +23,7 @@ export const TextField = forwardRef( ) => { return ( <> - {!!label && } + {!!label && } {!!description && ( {description} )} diff --git a/apps/mobile/src/components/ui/icon/fallback-icon.tsx b/apps/mobile/src/components/ui/icon/fallback-icon.tsx index ab4400b6bf..50f303550f 100644 --- a/apps/mobile/src/components/ui/icon/fallback-icon.tsx +++ b/apps/mobile/src/components/ui/icon/fallback-icon.tsx @@ -2,7 +2,7 @@ import { getBackgroundGradient, isCJKChar } from "@follow/utils" import { Image } from "expo-image" import { LinearGradient } from "expo-linear-gradient" import { useMemo, useState } from "react" -import type { StyleProp, ViewStyle } from "react-native" +import type { StyleProp, TextStyle, ViewStyle } from "react-native" import { StyleSheet, Text, View } from "react-native" export const FallbackIcon = ({ @@ -11,12 +11,16 @@ export const FallbackIcon = ({ size, className, style, + textClassName, + textStyle, }: { title: string url?: string size: number className?: string style?: StyleProp + textClassName?: string + textStyle?: StyleProp }) => { const colors = useMemo(() => getBackgroundGradient(title || url || ""), [title, url]) const sizeStyle = useMemo(() => ({ width: size, height: size }), [size]) @@ -25,8 +29,12 @@ export const FallbackIcon = ({ const renderedText = useMemo(() => { const isCJK = isCJKChar(title[0]) - return {isCJK ? title[0] : title.slice(0, 2)} - }, [title]) + return ( + + {isCJK ? title[0] : title.slice(0, 2)} + + ) + }, [title, textStyle, textClassName]) return ( + textClassName?: string + textStyle?: StyleProp }) => { - const { url, size, title = "", className, style } = props + const { url, size, title = "", className, style, textClassName, textStyle } = props const [hasError, setHasError] = useState(false) if (!url || hasError) { - return + return ( + + ) } return ( diff --git a/apps/mobile/src/components/ui/toast/CenteredToast.tsx b/apps/mobile/src/components/ui/toast/CenteredToast.tsx index b64321776e..9eb9ae6e62 100644 --- a/apps/mobile/src/components/ui/toast/CenteredToast.tsx +++ b/apps/mobile/src/components/ui/toast/CenteredToast.tsx @@ -58,6 +58,8 @@ const styles = StyleSheet.create({ flexDirection: "row", paddingHorizontal: 16, paddingVertical: 12, + alignItems: "center", + borderColor: withOpacity("#ffffff", 0.3), backgroundColor: withOpacity("#000000", 0.9), }, diff --git a/apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx b/apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx index 6ff35362e1..fc54d42e2b 100644 --- a/apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx +++ b/apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx @@ -3,7 +3,8 @@ import { useQuery } from "@tanstack/react-query" import { Image } from "expo-image" import { router } from "expo-router" import { useAtomValue } from "jotai" -import type { ListRenderItem } from "react-native" +import type { FC } from "react" +import type { ListRenderItem, ListRenderItemInfo } from "react-native" import { Text, useWindowDimensions, View } from "react-native" import { ScrollView } from "react-native-gesture-handler" import Animated, { FadeInUp } from "react-native-reanimated" @@ -52,13 +53,15 @@ export const SearchFeed = () => { /> ) } -const keyExtractor = (item: SearchResultItem) => item.feed?.id ?? Math.random().toString() +const keyExtractor = (item: SearchResultItem) => { + return item.feed?.id || Math.random().toString() +} -const renderItem: ListRenderItem = ({ item }) => ( - +const renderItem: ListRenderItem = (props) => ( + ) -const SearchFeedItem = ({ item }: { item: SearchResultItem }) => { +const SearchFeedItem: FC> = ({ item }) => { const isSubscribed = useSubscriptionByFeedId(item.feed?.id ?? "") return ( diff --git a/apps/mobile/src/modules/discover/search-tabs/SearchList.tsx b/apps/mobile/src/modules/discover/search-tabs/SearchList.tsx index 90da4e7d82..ed418e10f6 100644 --- a/apps/mobile/src/modules/discover/search-tabs/SearchList.tsx +++ b/apps/mobile/src/modules/discover/search-tabs/SearchList.tsx @@ -4,7 +4,6 @@ import { router } from "expo-router" import { useAtomValue } from "jotai" import { memo } from "react" import { Text, View } from "react-native" -import Animated, { FadeInUp } from "react-native-reanimated" import { FallbackIcon } from "@/src/components/ui/icon/fallback-icon" import { ItemPressable } from "@/src/components/ui/pressable/item-pressable" @@ -59,53 +58,51 @@ const renderItem = ({ item }: { item: SearchResultItem }) => ( const SearchListCard = memo(({ item }: { item: SearchResultItem }) => { const isSubscribed = useSubscriptionByListId(item.list?.id ?? "") return ( - - { - if (item.list?.id) { - router.push(`/follow?id=${item.list.id}&type=list`) - } - }} - > - {/* Headline */} - - - {item.list?.image ? ( - - ) : ( - !!item.list?.title && - )} - - - - {item.list?.title} + { + if (item.list?.id) { + router.push(`/follow?id=${item.list.id}&type=list`) + } + }} + > + {/* Headline */} + + + {item.list?.image ? ( + + ) : ( + !!item.list?.title && + )} + + + + {item.list?.title} + + {!!item.list?.description && ( + + {item.list?.description} - {!!item.list?.description && ( - - {item.list?.description} - - )} - - {/* Subscribe */} - {isSubscribed && ( - - - Subscribed - - )} - - + {/* Subscribe */} + {isSubscribed && ( + + + Subscribed + + + )} + + ) }) diff --git a/apps/mobile/src/modules/discover/search-tabs/__base.tsx b/apps/mobile/src/modules/discover/search-tabs/__base.tsx index fc45c623eb..a318ec10e2 100644 --- a/apps/mobile/src/modules/discover/search-tabs/__base.tsx +++ b/apps/mobile/src/modules/discover/search-tabs/__base.tsx @@ -1,7 +1,7 @@ import { cn } from "@follow/utils/src/utils" import { forwardRef } from "react" import type { ScrollViewProps } from "react-native" -import { RefreshControl, ScrollView, StyleSheet, useWindowDimensions, View } from "react-native" +import { RefreshControl, ScrollView, useWindowDimensions, View } from "react-native" import type { FlatListPropsWithLayout } from "react-native-reanimated" import Animated, { LinearTransition } from "react-native-reanimated" import { useSafeAreaInsets } from "react-native-safe-area-context" @@ -57,6 +57,7 @@ export function BaseSearchPageFlatList({ const searchBarHeight = useSearchBarHeight() const offsetTop = searchBarHeight - insets.top const windowWidth = useWindowDimensions().width + return ( ({ } {...props} /> ) } -const itemSeparator = ( - -) +const itemSeparator = export const ItemSeparator = () => itemSeparator export const RenderScrollComponent = (props: ScrollViewProps) => ( diff --git a/apps/mobile/src/modules/discover/search.tsx b/apps/mobile/src/modules/discover/search.tsx index 6b1347489b..4aa7e0b523 100644 --- a/apps/mobile/src/modules/discover/search.tsx +++ b/apps/mobile/src/modules/discover/search.tsx @@ -38,7 +38,7 @@ export const SearchHeader: FC<{ className="relative" onLayout={onLayout} > - + {/* */} diff --git a/apps/mobile/src/modules/list/FollowList.tsx b/apps/mobile/src/modules/list/FollowList.tsx index c4d45cb381..139b45c40f 100644 --- a/apps/mobile/src/modules/list/FollowList.tsx +++ b/apps/mobile/src/modules/list/FollowList.tsx @@ -1,9 +1,9 @@ import { FeedViewType } from "@follow/constants" import { zodResolver } from "@hookform/resolvers/zod" import { useQuery } from "@tanstack/react-query" -import { Stack } from "expo-router" +import { router, Stack } from "expo-router" import { Controller, useForm } from "react-hook-form" -import { Alert, ScrollView, Text, View } from "react-native" +import { Alert, ScrollView, StyleSheet, Text, View } from "react-native" import { z } from "zod" import { @@ -16,9 +16,13 @@ import { FormSwitch } from "@/src/components/ui/form/Switch" import { TextField } from "@/src/components/ui/form/TextField" import { IconWithFallback } from "@/src/components/ui/icon/fallback-icon" import { LoadingIndicator } from "@/src/components/ui/loading" +import { PowerIcon } from "@/src/icons/power" +import { apiClient } from "@/src/lib/api-fetch" +import { toast } from "@/src/lib/toast" import { useList } from "@/src/store/list/hooks" import { listSyncServices } from "@/src/store/list/store" import { useSubscriptionByListId } from "@/src/store/subscription/hooks" +import { accentColor } from "@/src/theme/colors" import { FeedViewSelector } from "../feed/view-selector" @@ -63,13 +67,27 @@ const Impl = (props: { id: string }) => { const submit = async () => { const payload = form.getValues() - // console.log("submit", payload) - void payload + const subscribeOrUpdate = async () => { + const body = { + listId: list.id, + view: list.view, + + isPrivate: payload.isPrivate, + title: payload.title, + } + const $method = isSubscribed ? apiClient.subscriptions.$patch : apiClient.subscriptions.$post + + await $method({ + json: body, + }) + router.dismiss() + toast.success(isSubscribed ? "List updated" : "List followed") + } if (list.fee && !isSubscribed) { Alert.alert( + `Follow List - ${list.title}`, `To follow this list, you must pay a fee to the list creator. Press OK to pay ${list.fee} power to follow this list.`, - "OK", [ { text: "Cancel", @@ -77,10 +95,15 @@ const Impl = (props: { id: string }) => { }, { text: "OK", - onPress: () => {}, + onPress: () => { + subscribeOrUpdate() + }, + isPreferred: true, }, ], ) + } else { + subscribeOrUpdate() } } @@ -106,7 +129,13 @@ const Impl = (props: { id: string }) => { - + {list?.title} @@ -120,7 +149,7 @@ const Impl = (props: { id: string }) => { - + @@ -155,10 +184,13 @@ const Impl = (props: { id: string }) => { {!!list.fee && ( - + - {list.fee} + + + {list.fee} + To follow this list, you must pay a fee to the list creator. @@ -170,3 +202,9 @@ const Impl = (props: { id: string }) => { ) } + +const styles = StyleSheet.create({ + title: { + fontSize: 24, + }, +}) From 3dbf7c7ffaded0c276eb3b7a8f4aaf8ad50dc8a1 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Tue, 14 Jan 2025 18:12:40 +0800 Subject: [PATCH 20/93] fix: fast scroll mark read --- .../src/modules/entry-column/index.tsx | 4 + packages/shared/src/hono.ts | 122 +++++++++++++++++- 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/apps/renderer/src/modules/entry-column/index.tsx b/apps/renderer/src/modules/entry-column/index.tsx index 72ac94e572..1368fa364b 100644 --- a/apps/renderer/src/modules/entry-column/index.tsx +++ b/apps/renderer/src/modules/entry-column/index.tsx @@ -130,6 +130,10 @@ function EntryColumnImpl() { const renderAsRead = useGeneralSettingKey("renderMarkUnread") const handleRangeChange = useCallback( (e: Range) => { + const [_, second] = rangeQueueRef.current + if (second?.startIndex === e.startIndex) { + return + } rangeQueueRef.current.push(e) if (rangeQueueRef.current.length > 2) { rangeQueueRef.current.shift() diff --git a/packages/shared/src/hono.ts b/packages/shared/src/hono.ts index f916b5dfdf..f7a011ab5f 100644 --- a/packages/shared/src/hono.ts +++ b/packages/shared/src/hono.ts @@ -2409,6 +2409,104 @@ declare const entryReadHistoriesOpenAPISchema: z.ZodObject<{ userIds: string[]; readCount: number; }>; +declare const urlReads: drizzle_orm_pg_core.PgTableWithColumns<{ + name: "urlReads"; + schema: undefined; + columns: { + url: drizzle_orm_pg_core.PgColumn<{ + name: "url"; + tableName: "urlReads"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: true; + hasDefault: false; + isPrimaryKey: true; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + identity: undefined; + generated: undefined; + }, {}, {}>; + userIds: drizzle_orm_pg_core.PgColumn<{ + name: "user_ids"; + tableName: "urlReads"; + dataType: "array"; + columnType: "PgArray"; + data: string[]; + driverParam: string | string[]; + notNull: true; + hasDefault: true; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: drizzle_orm.Column<{ + name: "user_ids"; + tableName: "urlReads"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + identity: undefined; + generated: undefined; + }, {}, {}>; + identity: undefined; + generated: undefined; + }, {}, { + baseBuilder: drizzle_orm_pg_core.PgColumnBuilder<{ + name: "user_ids"; + dataType: "string"; + columnType: "PgText"; + data: string; + enumValues: [string, ...string[]]; + driverParam: string; + }, {}, {}, drizzle_orm.ColumnBuilderExtraConfig>; + size: undefined; + }>; + count: drizzle_orm_pg_core.PgColumn<{ + name: "count"; + tableName: "urlReads"; + dataType: "number"; + columnType: "PgInteger"; + data: number; + driverParam: string | number; + notNull: true; + hasDefault: true; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + identity: undefined; + generated: undefined; + }, {}, {}>; + }; + dialect: "pg"; +}>; +type UrlReadsModel = InferInsertModel; +declare const urlReadsOpenAPISchema: z.ZodObject<{ + url: z.ZodString; + userIds: z.ZodArray; + count: z.ZodNumber; +}, z.UnknownKeysParam, z.ZodTypeAny, { + url: string; + userIds: string[]; + count: number; +}, { + url: string; + userIds: string[]; + count: number; +}>; declare const feeds: drizzle_orm_pg_core.PgTableWithColumns<{ name: "feeds"; @@ -2652,6 +2750,23 @@ declare const feeds: drizzle_orm_pg_core.PgTableWithColumns<{ identity: undefined; generated: undefined; }, {}, {}>; + migrateTo: drizzle_orm_pg_core.PgColumn<{ + name: "migrate_to"; + tableName: "feeds"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + identity: undefined; + generated: undefined; + }, {}, {}>; }; dialect: "pg"; }>; @@ -2670,6 +2785,7 @@ declare const feedsOpenAPISchema: zod.ZodObject<{ errorAt: zod.ZodNullable; ownerUserId: zod.ZodNullable; language: zod.ZodNullable; + migrateTo: zod.ZodNullable; }, zod.UnknownKeysParam, zod.ZodTypeAny, { description: string | null; title: string | null; @@ -2685,6 +2801,7 @@ declare const feedsOpenAPISchema: zod.ZodObject<{ errorAt: string | null; ownerUserId: string | null; language: string | null; + migrateTo: string | null; }, { description: string | null; title: string | null; @@ -2700,11 +2817,13 @@ declare const feedsOpenAPISchema: zod.ZodObject<{ errorAt: string | null; ownerUserId: string | null; language: string | null; + migrateTo: string | null; }>; declare const feedsRelations: drizzle_orm.Relations<"feeds", { subscriptions: drizzle_orm.Many<"subscriptions">; entries: drizzle_orm.Many<"entries">; owner: drizzle_orm.One<"user", false>; + migrateTo: drizzle_orm.One<"feeds", false>; }>; type FeedModel = InferInsertModel; @@ -15333,6 +15452,7 @@ declare const _routes: hono_hono_base.HonoBase, "/">; type AppType = typeof _routes; -export { type ActionsModel, type AirdropActivity, type AppType, type AttachmentsModel, type AuthSession, type AuthUser, CommonEntryFields, type ConditionItem, type DetailModel, type EntriesModel, type EntryReadHistoriesModel, type ExtraModel, type FeedModel, type MediaModel, type MessagingData, MessagingType, type SettingsModel, account, achievements, achievementsOpenAPISchema, actions, actionsItemOpenAPISchema, actionsOpenAPISchema, actionsRelations, activityEnum, airdrops, airdropsOpenAPISchema, attachmentsZodSchema, authPlugins, boosts, collections, collectionsOpenAPISchema, collectionsRelations, detailModelSchema, entries, entriesOpenAPISchema, entriesRelations, entryReadHistories, entryReadHistoriesOpenAPISchema, entryReadHistoriesRelations, extraZodSchema, feedPowerTokens, feedPowerTokensOpenAPISchema, feedPowerTokensRelations, feeds, feedsOpenAPISchema, feedsRelations, inboxHandleSchema, inboxes, inboxesEntries, inboxesEntriesInsertOpenAPISchema, type inboxesEntriesModel, inboxesEntriesOpenAPISchema, inboxesEntriesRelations, inboxesOpenAPISchema, inboxesRelations, invitations, invitationsOpenAPISchema, invitationsRelations, languageSchema, levels, levelsOpenAPISchema, levelsRelations, lists, listsOpenAPISchema, listsRelations, listsSubscriptions, listsSubscriptionsOpenAPISchema, listsSubscriptionsRelations, listsTimeline, listsTimelineOpenAPISchema, listsTimelineRelations, lower, mediaZodSchema, messaging, messagingOpenAPISchema, messagingRelations, rsshub, rsshubOpenAPISchema, rsshubPurchase, rsshubUsage, rsshubUsageOpenAPISchema, rsshubUsageRelations, session, settings, subscriptions, subscriptionsOpenAPISchema, subscriptionsRelations, timeline, timelineOpenAPISchema, timelineRelations, transactionType, transactions, transactionsOpenAPISchema, transactionsRelations, twoFactor, user, users, usersOpenApiSchema, usersRelations, verification, wallets, walletsOpenAPISchema, walletsRelations }; +export { type ActionsModel, type AirdropActivity, type AppType, type AttachmentsModel, type AuthSession, type AuthUser, CommonEntryFields, type ConditionItem, type DetailModel, type EntriesModel, type EntryReadHistoriesModel, type ExtraModel, type FeedModel, type MediaModel, type MessagingData, MessagingType, type SettingsModel, type UrlReadsModel, account, achievements, achievementsOpenAPISchema, actions, actionsItemOpenAPISchema, actionsOpenAPISchema, actionsRelations, activityEnum, airdrops, airdropsOpenAPISchema, attachmentsZodSchema, authPlugins, boosts, collections, collectionsOpenAPISchema, collectionsRelations, detailModelSchema, entries, entriesOpenAPISchema, entriesRelations, entryReadHistories, entryReadHistoriesOpenAPISchema, entryReadHistoriesRelations, extraZodSchema, feedPowerTokens, feedPowerTokensOpenAPISchema, feedPowerTokensRelations, feeds, feedsOpenAPISchema, feedsRelations, inboxHandleSchema, inboxes, inboxesEntries, inboxesEntriesInsertOpenAPISchema, type inboxesEntriesModel, inboxesEntriesOpenAPISchema, inboxesEntriesRelations, inboxesOpenAPISchema, inboxesRelations, invitations, invitationsOpenAPISchema, invitationsRelations, languageSchema, levels, levelsOpenAPISchema, levelsRelations, lists, listsOpenAPISchema, listsRelations, listsSubscriptions, listsSubscriptionsOpenAPISchema, listsSubscriptionsRelations, listsTimeline, listsTimelineOpenAPISchema, listsTimelineRelations, lower, mediaZodSchema, messaging, messagingOpenAPISchema, messagingRelations, rsshub, rsshubOpenAPISchema, rsshubPurchase, rsshubUsage, rsshubUsageOpenAPISchema, rsshubUsageRelations, session, settings, subscriptions, subscriptionsOpenAPISchema, subscriptionsRelations, timeline, timelineOpenAPISchema, timelineRelations, transactionType, transactions, transactionsOpenAPISchema, transactionsRelations, twoFactor, urlReads, urlReadsOpenAPISchema, user, users, usersOpenApiSchema, usersRelations, verification, wallets, walletsOpenAPISchema, walletsRelations }; From 398e02c1e9c9206dd789ac820e60631177977148 Mon Sep 17 00:00:00 2001 From: Konv Suu <2583695112@qq.com> Date: Tue, 14 Jan 2025 19:35:39 +0800 Subject: [PATCH 21/93] chore(profile): unify with other pages (#2563) --- .../src/pages/settings/(settings)/profile.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/renderer/src/pages/settings/(settings)/profile.tsx b/apps/renderer/src/pages/settings/(settings)/profile.tsx index 69c903673b..077e7393b3 100644 --- a/apps/renderer/src/pages/settings/(settings)/profile.tsx +++ b/apps/renderer/src/pages/settings/(settings)/profile.tsx @@ -19,13 +19,15 @@ export function Component() { return ( <> - - +
+ + - + - - + + +
) } From 5bccea21fd41aad4a590d0a203dd14951cc18367 Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 14 Jan 2025 19:45:26 +0800 Subject: [PATCH 22/93] chore: update pr template Signed-off-by: Innei --- .github/PULL_REQUEST_TEMPLATE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 076ed77a13..631fd3a46b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -24,6 +24,10 @@ Before submitting the PR, please make sure you do the following: - [ ] Hotfix - [ ] Other (please describe): +### Screenshots (if UI change) + +### Demo Video (if new feature) + ### Linked Issues ### Additional context From b581393c9aeccb15f1c3e7265273538eb055d140 Mon Sep 17 00:00:00 2001 From: Whitewater Date: Tue, 14 Jan 2025 19:47:38 +0800 Subject: [PATCH 23/93] feat(rn): implement feed drawer (#2443) * feat: add drawer state management hooks and providers * feat: add useCurrentViewDefinition hook for view definition retrieval * feat: implement feed drawer with collection and feed panels * feat: integrate feed drawer into tab layout and manage drawer state * chore: add @react-navigation/drawer dependency for drawer navigation support * chore: simplify color * refactor: migrate drawer state management to Jotai atoms * feat: enhance collect select state * chore: update api * feat: implement list view * refactor: extract usePrefetchFeed * feat: add loading indicator and separate header components for list and view * fix: merge --- apps/mobile/package.json | 1 + apps/mobile/src/modules/feed-drawer/atoms.ts | 65 +++++ .../modules/feed-drawer/collection-panel.tsx | 90 ++++++ .../mobile/src/modules/feed-drawer/drawer.tsx | 71 +++++ .../src/modules/feed-drawer/feed-panel.tsx | 274 ++++++++++++++++++ .../mobile/src/modules/feed-drawer/header.tsx | 35 +++ apps/mobile/src/modules/feed/FollowFeed.tsx | 10 +- apps/mobile/src/modules/subscription/atoms.ts | 11 + .../src/screens/(stack)/(tabs)/_layout.tsx | 129 +++++---- .../screens/(stack)/(tabs)/subscription.tsx | 3 + apps/mobile/src/store/feed/hooks.ts | 12 +- apps/mobile/src/store/subscription/hooks.ts | 8 + pnpm-lock.yaml | 109 +++++-- 13 files changed, 716 insertions(+), 102 deletions(-) create mode 100644 apps/mobile/src/modules/feed-drawer/atoms.ts create mode 100644 apps/mobile/src/modules/feed-drawer/collection-panel.tsx create mode 100644 apps/mobile/src/modules/feed-drawer/drawer.tsx create mode 100644 apps/mobile/src/modules/feed-drawer/feed-panel.tsx create mode 100644 apps/mobile/src/modules/feed-drawer/header.tsx diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 57d59b066e..16f1b685db 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -27,6 +27,7 @@ "@react-native-cookies/cookies": "^6.2.1", "@react-native-picker/picker": "2.9.0", "@react-navigation/bottom-tabs": "^7.0.0", + "@react-navigation/drawer": "^7.1.1", "@react-navigation/native": "^7.0.0", "@shopify/flash-list": "1.7.1", "@tanstack/react-query": "5.62.3", diff --git a/apps/mobile/src/modules/feed-drawer/atoms.ts b/apps/mobile/src/modules/feed-drawer/atoms.ts new file mode 100644 index 0000000000..57fa8b3a5f --- /dev/null +++ b/apps/mobile/src/modules/feed-drawer/atoms.ts @@ -0,0 +1,65 @@ +import { FeedViewType } from "@follow/constants" +import { jotaiStore } from "@follow/utils" +import { atom, useAtom, useAtomValue, useSetAtom } from "jotai" +import { useCallback, useMemo } from "react" + +import { views } from "@/src/constants/views" + +// drawer open state + +const drawerOpenAtom = atom(false) + +export function useFeedDrawer() { + const [state, setState] = useAtom(drawerOpenAtom) + + return { + isDrawerOpen: state, + openDrawer: useCallback(() => setState(true), [setState]), + closeDrawer: useCallback(() => setState(false), [setState]), + toggleDrawer: useCallback(() => setState(!state), [setState, state]), + } +} + +// is drawer swipe disabled + +const isDrawerSwipeDisabledAtom = atom(false) + +export function useIsDrawerSwipeDisabled() { + return useAtomValue(isDrawerSwipeDisabledAtom) +} + +export function useSetDrawerSwipeDisabled() { + return useSetAtom(isDrawerSwipeDisabledAtom) +} + +// collection panel selected state + +type CollectionPanelState = + | { + type: "view" + viewId: FeedViewType + } + | { + type: "list" + listId: string + } + +const collectionPanelStateAtom = atom({ + type: "view", + viewId: FeedViewType.Articles, +}) + +export function useSelectedCollection() { + return useAtomValue(collectionPanelStateAtom) +} +export const selectCollection = (state: CollectionPanelState) => { + jotaiStore.set(collectionPanelStateAtom, state) +} + +export const useViewDefinition = (view: FeedViewType) => { + const viewDef = useMemo(() => views.find((v) => v.view === view), [view]) + if (!viewDef) { + throw new Error(`View ${view} not found`) + } + return viewDef +} diff --git a/apps/mobile/src/modules/feed-drawer/collection-panel.tsx b/apps/mobile/src/modules/feed-drawer/collection-panel.tsx new file mode 100644 index 0000000000..78fdfd115d --- /dev/null +++ b/apps/mobile/src/modules/feed-drawer/collection-panel.tsx @@ -0,0 +1,90 @@ +import { cn } from "@follow/utils" +import { + Image, + SafeAreaView, + ScrollView, + TouchableOpacity, + useWindowDimensions, + View, +} from "react-native" + +import { FallbackIcon } from "@/src/components/ui/icon/fallback-icon" +import type { ViewDefinition } from "@/src/constants/views" +import { views } from "@/src/constants/views" +import { useList } from "@/src/store/list/hooks" +import { useAllListSubscription } from "@/src/store/subscription/hooks" + +import { selectCollection, useSelectedCollection } from "./atoms" + +export const CollectionPanel = () => { + const winDim = useWindowDimensions() + const lists = useAllListSubscription() + + return ( + + + {views.map((viewDef) => ( + + ))} + {lists.map((listId) => ( + + ))} + + + ) +} + +const ViewButton = ({ viewDef }: { viewDef: ViewDefinition }) => { + const selectedCollection = useSelectedCollection() + const isActive = selectedCollection.type === "view" && selectedCollection.viewId === viewDef.view + + return ( + + selectCollection({ + type: "view", + viewId: viewDef.view, + }) + } + > + + + ) +} + +const ListButton = ({ listId }: { listId: string }) => { + const list = useList(listId) + const selectedCollection = useSelectedCollection() + const isActive = selectedCollection.type === "list" && selectedCollection.listId === listId + if (!list) return null + + return ( + + selectCollection({ + type: "list", + listId, + }) + } + > + + {list.image ? ( + + ) : ( + + )} + + + ) +} diff --git a/apps/mobile/src/modules/feed-drawer/drawer.tsx b/apps/mobile/src/modules/feed-drawer/drawer.tsx new file mode 100644 index 0000000000..9ae8ae7320 --- /dev/null +++ b/apps/mobile/src/modules/feed-drawer/drawer.tsx @@ -0,0 +1,71 @@ +import type { PropsWithChildren } from "react" +import { useCallback } from "react" +import { useWindowDimensions, View } from "react-native" +import { Drawer } from "react-native-drawer-layout" +import type { PanGestureType } from "react-native-gesture-handler/lib/typescript/handlers/gestures/panGesture" + +import { isIOS } from "@/src/lib/platform" + +import { useFeedDrawer, useIsDrawerSwipeDisabled } from "./atoms" +import { CollectionPanel } from "./collection-panel" +import { FeedPanel } from "./feed-panel" + +export const FeedDrawer = ({ children }: PropsWithChildren) => { + const { isDrawerOpen, openDrawer, closeDrawer } = useFeedDrawer() + const winDim = useWindowDimensions() + const isDrawerSwipeDisabled = useIsDrawerSwipeDisabled() + + const renderDrawerContent = useCallback(() => , []) + const configureGestureHandler = useCallback( + (handler: PanGestureType) => { + const swipeEnabled = !isDrawerSwipeDisabled + if (swipeEnabled) { + if (isDrawerOpen) { + return handler.activeOffsetX([-1, 1]) + } else { + return ( + handler + // Any movement to the left is a pager swipe + // so fail the drawer gesture immediately. + .failOffsetX(-1) + // Don't rush declaring that a movement to the right + // is a drawer swipe. It could be a vertical scroll. + .activeOffsetX(5) + ) + } + } else { + // Fail the gesture immediately. + // This seems more reliable than the `swipeEnabled` prop. + // With `swipeEnabled` alone, the gesture may freeze after toggling off/on. + return handler.failOffsetX([0, 0]).failOffsetY([0, 0]) + } + }, + [isDrawerOpen, isDrawerSwipeDisabled], + ) + + return ( + + {children} + + ) +} + +const DrawerContent = () => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/modules/feed-drawer/feed-panel.tsx b/apps/mobile/src/modules/feed-drawer/feed-panel.tsx new file mode 100644 index 0000000000..b2896fff9e --- /dev/null +++ b/apps/mobile/src/modules/feed-drawer/feed-panel.tsx @@ -0,0 +1,274 @@ +import type { FeedViewType } from "@follow/constants" +import { cn } from "@follow/utils" +import { router } from "expo-router" +import type { FC } from "react" +import { createContext, memo, useContext, useMemo } from "react" +import { + Animated, + Easing, + SafeAreaView, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + useAnimatedValue, + View, +} from "react-native" +import { useSharedValue } from "react-native-reanimated" + +import { AccordionItem } from "@/src/components/ui/accordion/AccordionItem" +import { FeedIcon } from "@/src/components/ui/icon/feed-icon" +import { LoadingIndicator } from "@/src/components/ui/loading" +import { ItemPressable } from "@/src/components/ui/pressable/item-pressable" +import { MingcuteRightLine } from "@/src/icons/mingcute_right_line" +import { useFeed, usePrefetchFeed } from "@/src/store/feed/hooks" +import { useList } from "@/src/store/list/hooks" +import { + useGroupedSubscription, + usePrefetchSubscription, + useSortedGroupedSubscription, + useSortedUngroupedSubscription, + useSubscription, +} from "@/src/store/subscription/hooks" +import { useUnreadCount, useUnreadCounts } from "@/src/store/unread/hooks" + +import { + SubscriptionFeedCategoryContextMenu, + SubscriptionFeedItemContextMenu, +} from "../context-menu/feeds" +import { useCurrentView, useFeedListSortMethod, useFeedListSortOrder } from "../subscription/atoms" +import { useSelectedCollection } from "./atoms" +import { ListHeaderComponent, ViewHeaderComponent } from "./header" + +const useSortedSubscription = (view: FeedViewType) => { + usePrefetchSubscription(view) + const { grouped, unGrouped } = useGroupedSubscription(view) + + const sortBy = useFeedListSortMethod() + const sortOrder = useFeedListSortOrder() + const sortedGrouped = useSortedGroupedSubscription(grouped, sortBy, sortOrder) + const sortedUnGrouped = useSortedUngroupedSubscription(unGrouped, sortBy, sortOrder) + const data = useMemo( + () => [...sortedGrouped, ...sortedUnGrouped], + [sortedGrouped, sortedUnGrouped], + ) + return data +} + +export const FeedPanel = () => { + const selectedCollection = useSelectedCollection() + if (selectedCollection.type === "view") { + return ( + + + + + ) + } + + return ( + + + + + ) +} + +const ListView = ({ listId }: { listId: string }) => { + const list = useList(listId) + if (!list) { + console.warn("list not found:", listId) + return null + } + const { feedIds } = list + + return ( + + {feedIds.map((item, index) => ( + + ))} + {/* Just a placeholder */} + + + ) +} + +const FeedListView = ({ view }: { view: FeedViewType }) => { + const data = useSortedSubscription(view) + return ( + + {data.map((item, index) => ( + + ))} + {/* Just a placeholder */} + + + ) +} + +const ItemRender = ({ + item, +}: { + item: string | { category: string; subscriptionIds: string[] } + index: number + extraData?: { + total: number + } +}) => { + if (typeof item === "string") { + return + } + const { category, subscriptionIds } = item + + return +} + +const UnGroupedList: FC<{ + subscriptionIds: string[] +}> = ({ subscriptionIds }) => { + const sortBy = useFeedListSortMethod() + const sortOrder = useFeedListSortOrder() + const sortedSubscriptionIds = useSortedUngroupedSubscription(subscriptionIds, sortBy, sortOrder) + const lastSubscriptionId = sortedSubscriptionIds.at(-1) + + return sortedSubscriptionIds.map((id) => { + return ( + + ) + }) +} + +const GroupedContext = createContext(null) + +const AnimatedTouchableOpacity = Animated.createAnimatedComponent(TouchableOpacity) + +const CategoryGrouped = memo( + ({ category, subscriptionIds }: { category: string; subscriptionIds: string[] }) => { + const unreadCounts = useUnreadCounts(subscriptionIds) + const isExpanded = useSharedValue(false) + const rotateValue = useAnimatedValue(1) + const selectedCollection = useSelectedCollection() + const view = selectedCollection.type === "view" ? selectedCollection.viewId : undefined + if (view === undefined) { + console.warn("view is undefined", selectedCollection) + return null + } + return ( + + { + // TODO navigate to category + }} + className="h-12 flex-row items-center px-3" + > + { + Animated.timing(rotateValue, { + toValue: isExpanded.value ? 1 : 0, + easing: Easing.linear, + + useNativeDriver: true, + }).start() + isExpanded.value = !isExpanded.value + }} + style={[ + { + transform: [ + { + rotate: rotateValue.interpolate({ + inputRange: [0, 1], + outputRange: ["90deg", "0deg"], + }), + }, + ], + }, + style.accordionIcon, + ]} + > + + + {category} + {!!unreadCounts && ( + {unreadCounts} + )} + + + + + + + + ) + }, +) + +const SubscriptionItem = memo(({ id, className }: { id: string; className?: string }) => { + const subscription = useSubscription(id) + const unreadCount = useUnreadCount(id) + const feed = useFeed(id) + const inGrouped = !!useContext(GroupedContext) + const view = useCurrentView() + const { isLoading } = usePrefetchFeed(id, { enabled: !feed }) + + if (isLoading) { + return ( + + + + ) + } + + if (!subscription && !feed) return null + + return ( + + { + router.push({ + pathname: `/feeds/[feedId]`, + params: { + feedId: id, + }, + }) + }} + > + + + + + {subscription?.title || feed.title} + + {!!unreadCount && ( + {unreadCount} + )} + + + ) +}) + +const style = StyleSheet.create({ + accordionIcon: { + height: 20, + width: 20, + alignItems: "center", + justifyContent: "center", + }, +}) diff --git a/apps/mobile/src/modules/feed-drawer/header.tsx b/apps/mobile/src/modules/feed-drawer/header.tsx new file mode 100644 index 0000000000..2afabd0e54 --- /dev/null +++ b/apps/mobile/src/modules/feed-drawer/header.tsx @@ -0,0 +1,35 @@ +import type { FeedViewType } from "@follow/constants" +import { Text, View } from "react-native" + +import { useList } from "@/src/store/list/hooks" + +import { SortActionButton } from "../subscription/header-actions" +import { useViewDefinition } from "./atoms" + +export const ViewHeaderComponent = ({ view }: { view: FeedViewType }) => { + const viewDef = useViewDefinition(view) + return ( + + {viewDef.name} + + + ) +} + +export const ListHeaderComponent = ({ listId }: { listId: string }) => { + const list = useList(listId) + if (!list) { + console.warn("list not found:", listId) + return null + } + return ( + + + {list.title} + + {list.description && ( + {list.description} + )} + + ) +} diff --git a/apps/mobile/src/modules/feed/FollowFeed.tsx b/apps/mobile/src/modules/feed/FollowFeed.tsx index af613aaae6..d5a418bfb2 100644 --- a/apps/mobile/src/modules/feed/FollowFeed.tsx +++ b/apps/mobile/src/modules/feed/FollowFeed.tsx @@ -1,7 +1,6 @@ import { FeedViewType } from "@follow/constants" import { zodResolver } from "@hookform/resolvers/zod" import { StackActions } from "@react-navigation/native" -import { useQuery } from "@tanstack/react-query" import { router, Stack, useLocalSearchParams, useNavigation } from "expo-router" import { useState } from "react" import { Controller, useForm } from "react-hook-form" @@ -20,8 +19,7 @@ import { FeedIcon } from "@/src/components/ui/icon/feed-icon" import { LoadingIndicator } from "@/src/components/ui/loading" import { useIsRouteOnlyOne } from "@/src/hooks/useIsRouteOnlyOne" import { FeedViewSelector } from "@/src/modules/feed/view-selector" -import { useFeed } from "@/src/store/feed/hooks" -import { feedSyncServices } from "@/src/store/feed/store" +import { useFeed, usePrefetchFeed } from "@/src/store/feed/hooks" import { useSubscriptionByFeedId } from "@/src/store/subscription/hooks" import { subscriptionSyncService } from "@/src/store/subscription/store" import type { SubscriptionForm } from "@/src/store/subscription/types" @@ -36,11 +34,7 @@ const defaultValues = { view: FeedViewType.Articles.toString() } export function FollowFeed(props: { id: string }) { const { id } = props const feed = useFeed(id as string) - const { isLoading } = useQuery({ - queryKey: ["feed", id], - queryFn: () => feedSyncServices.fetchFeedById({ id: id as string }), - enabled: !feed, - }) + const { isLoading } = usePrefetchFeed(id as string, { enabled: !feed }) if (isLoading) { return ( diff --git a/apps/mobile/src/modules/subscription/atoms.ts b/apps/mobile/src/modules/subscription/atoms.ts index d6d51c780c..a9fdd80a60 100644 --- a/apps/mobile/src/modules/subscription/atoms.ts +++ b/apps/mobile/src/modules/subscription/atoms.ts @@ -2,7 +2,9 @@ import { FeedViewType } from "@follow/constants" import { createAtomHooks, jotaiStore } from "@follow/utils" import { atom, useAtomValue } from "jotai" import { atomWithStorage } from "jotai/utils" +import { useMemo } from "react" +import { views } from "@/src/constants/views" import { JotaiPersistSyncStorage } from "@/src/lib/jotai" export const viewAtom = atom(FeedViewType.Articles) @@ -11,6 +13,15 @@ export const useCurrentView = () => { return useAtomValue(viewAtom) } +export const useCurrentViewDefinition = () => { + const view = useCurrentView() + const viewDef = useMemo(() => views.find((v) => v.view === view), [view]) + if (!viewDef) { + throw new Error(`View ${view} not found`) + } + return viewDef +} + export const offsetAtom = atom(0) export const setCurrentView = (view: FeedViewType) => { diff --git a/apps/mobile/src/screens/(stack)/(tabs)/_layout.tsx b/apps/mobile/src/screens/(stack)/(tabs)/_layout.tsx index 78a6528186..b86f34a492 100644 --- a/apps/mobile/src/screens/(stack)/(tabs)/_layout.tsx +++ b/apps/mobile/src/screens/(stack)/(tabs)/_layout.tsx @@ -11,6 +11,7 @@ import { SafariCuteFi } from "@/src/icons/safari_cute_fi" import { SafariCuteIcon } from "@/src/icons/safari_cute-re" import { Setting7CuteFi } from "@/src/icons/setting_7_cute_fi" import { Settings7CuteReIcon } from "@/src/icons/settings_7_cute_re" +import { FeedDrawer } from "@/src/modules/feed-drawer/drawer" import { setCurrentView } from "@/src/modules/subscription/atoms" const doubleTap = Gesture.Tap() @@ -27,71 +28,75 @@ const fifthTap = Gesture.Tap() export default function TabLayout() { return ( - + - , - tabBarButton(props) { - return ( - - - - - - ) + animation: "fade", + transitionSpec: { + animation: "timing", + config: { + duration: 50, + easing: Easing.ease, + }, }, }} - /> - { - const Icon = !focused ? SafariCuteIcon : SafariCuteFi - return - }, - }} - /> + > + ( + + ), + tabBarButton(props) { + return ( + + + + + + ) + }, + }} + /> + { + const Icon = !focused ? SafariCuteIcon : SafariCuteFi + return + }, + }} + /> - - - - - - ) - }, - tabBarIcon: ({ color, focused }) => { - const Icon = !focused ? Settings7CuteReIcon : Setting7CuteFi - return - }, - }} - /> - + + + + + + ) + }, + tabBarIcon: ({ color, focused }) => { + const Icon = !focused ? Settings7CuteReIcon : Setting7CuteFi + return + }, + }} + /> + + ) } diff --git a/apps/mobile/src/screens/(stack)/(tabs)/subscription.tsx b/apps/mobile/src/screens/(stack)/(tabs)/subscription.tsx index 7ce324b78d..a7384bfe24 100644 --- a/apps/mobile/src/screens/(stack)/(tabs)/subscription.tsx +++ b/apps/mobile/src/screens/(stack)/(tabs)/subscription.tsx @@ -4,6 +4,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context" import { views } from "@/src/constants/views" import { AddCuteReIcon } from "@/src/icons/add_cute_re" +import { useFeedDrawer } from "@/src/modules/feed-drawer/atoms" import { useCurrentView } from "@/src/modules/subscription/atoms" import { SortActionButton } from "@/src/modules/subscription/header-actions" import { SubscriptionLists } from "@/src/modules/subscription/SubscriptionLists" @@ -40,7 +41,9 @@ const useActionPadding = () => { } function LeftAction() { + const { openDrawer } = useFeedDrawer() const handleEdit = () => { + openDrawer() //TODO } diff --git a/apps/mobile/src/store/feed/hooks.ts b/apps/mobile/src/store/feed/hooks.ts index 663cf548c6..e985d7e4bf 100644 --- a/apps/mobile/src/store/feed/hooks.ts +++ b/apps/mobile/src/store/feed/hooks.ts @@ -1,7 +1,17 @@ -import { useFeedStore } from "./store" +import { useQuery } from "@tanstack/react-query" + +import { feedSyncServices, useFeedStore } from "./store" export const useFeed = (id: string) => { return useFeedStore((state) => { return state.feeds[id] }) } + +export const usePrefetchFeed = (id: string, options?: { enabled?: boolean }) => { + return useQuery({ + queryKey: ["feed", id], + queryFn: () => feedSyncServices.fetchFeedById({ id }), + ...options, + }) +} diff --git a/apps/mobile/src/store/subscription/hooks.ts b/apps/mobile/src/store/subscription/hooks.ts index 01604a5a88..a882805eac 100644 --- a/apps/mobile/src/store/subscription/hooks.ts +++ b/apps/mobile/src/store/subscription/hooks.ts @@ -144,6 +144,14 @@ export const useSubscription = (id: string) => { }) } +export const useAllListSubscription = () => { + return useSubscriptionStore( + useCallback((state) => { + return Object.values(state.listIdByView).flatMap((list) => Array.from(list)) + }, []), + ) +} + export const useListSubscription = (view: FeedViewType) => { return useSubscriptionStore( useCallback( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6937dd42ec..cd5245309e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -161,7 +161,7 @@ importers: version: 16.4.7 drizzle-orm: specifier: 0.37.0 - version: 0.37.0(@opentelemetry/api@1.9.0)(@types/pg@8.11.10)(@types/react@18.3.14)(expo-sqlite@15.0.3(qc23ies2iywwyrf5w5kjvn5iki))(kysely@0.27.5)(react@18.3.1) + version: 0.37.0(@opentelemetry/api@1.9.0)(@types/pg@8.11.10)(@types/react@18.3.14)(expo-sqlite@15.0.3(q65rno7mdzia6zdespm3xhq57e))(kysely@0.27.5)(react@18.3.1) electron: specifier: 33.2.0 version: 33.2.0 @@ -436,6 +436,9 @@ importers: '@react-navigation/bottom-tabs': specifier: ^7.0.0 version: 7.2.0(36el4dfrgnbt7wqu67o6leoq5q) + '@react-navigation/drawer': + specifier: ^7.1.1 + version: 7.1.1(vwskucj4jxsojke6dhlsncyura) '@react-navigation/native': specifier: ^7.0.0 version: 7.0.14(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) @@ -495,7 +498,7 @@ importers: version: 7.0.3(vc5zx7mqwgzirpvpjamp5nboge) expo-router: specifier: 4.0.11 - version: 4.0.11(ilcj5ynoywkepfq4gmlis7vsci) + version: 4.0.11(4kc7a2oyapueyif6rxtpvh4774) expo-sharing: specifier: ~13.0.0 version: 13.0.0(expo@52.0.18(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)) @@ -1386,7 +1389,7 @@ importers: version: 1.1.9-beta.1(encoding@0.1.13) drizzle-orm: specifier: 0.37.0 - version: 0.37.0(@opentelemetry/api@1.9.0)(@types/pg@8.11.10)(@types/react@18.3.14)(expo-sqlite@15.0.3(qc23ies2iywwyrf5w5kjvn5iki))(kysely@0.27.5)(react@18.3.1) + version: 0.37.0(@opentelemetry/api@1.9.0)(@types/pg@8.11.10)(@types/react@18.3.14)(expo-sqlite@15.0.3(q65rno7mdzia6zdespm3xhq57e))(kysely@0.27.5)(react@18.3.1) hono: specifier: 4.6.13 version: 4.6.13(patch_hash=qptujxncoai6tukc4qaqsrqk24) @@ -3333,8 +3336,8 @@ packages: resolution: {integrity: sha512-9lgXmcIePvZ7Wef63XtvuN3HfCUevF4E4tQPdEbH9/dUWwpOvvwQ3KT4OJ9jdh8JJ3nTdO9eDQ/8k8xr1aQ5Kg==} hasBin: true - '@expo/fingerprint@0.11.4': - resolution: {integrity: sha512-FfcvHjrWjOJ17wiMfr1iQ1YDyjlj8qfxG+GDce0khrjNSkzRjVdCOIFsMvfVSBPnOPX5NuZlgMRvMkcPUtGClA==} + '@expo/fingerprint@0.11.6': + resolution: {integrity: sha512-hlVIfMEJYZIqIFMjeGRN5GhK/h6vJ3M4QVc1ZD8F0Bh7gMeI+jZkEyZdL5XT29jergQrksP638e2qFwgrGTw/w==} hasBin: true '@expo/image-utils@0.6.3': @@ -4996,6 +4999,17 @@ packages: peerDependencies: react: '>= 18.2.0' + '@react-navigation/drawer@7.1.1': + resolution: {integrity: sha512-34UqRS5OLFaNXPs5ocz3Du9c7em0P7fFMPYCZn/MxadDzQ4Mn/74pmJczmiyvyvz8vcWsNRbZ3Qswm0Dv6z60w==} + peerDependencies: + '@react-navigation/native': ^7.0.14 + react: '>= 18.2.0' + react-native: '*' + react-native-gesture-handler: '>= 2.0.0' + react-native-reanimated: '>= 2.0.0' + react-native-safe-area-context: '>= 4.0.0' + react-native-screens: '>= 4.0.0' + '@react-navigation/elements@2.2.5': resolution: {integrity: sha512-sDhE+W14P7MNWLMxXg1MEVXwkLUpMZJGflE6nQNzLmolJQIHgcia0Mrm8uRa3bQovhxYu1UzEojLZ+caoZt7Fg==} peerDependencies: @@ -8655,8 +8669,8 @@ packages: react-native-webview: optional: true - expo@52.0.20: - resolution: {integrity: sha512-fyjKhd3o4MCSaGUIqZCO8bRSLpv+mR3hR1Nkq4AYs5avmfLOq8TEdLtjKT/gWjdRCDS93cXnpl3paekvIvFXSA==} + expo@52.0.23: + resolution: {integrity: sha512-DR36Vkpz/ZLPci4fxDBG/pLk26nGK63vcZ+X4RZJfNBzi14DXZ939loP8YzWGV78Qp23qdPINczpo2727tqLxg==} hasBin: true peerDependencies: '@expo/dom-webview': '*' @@ -12343,6 +12357,14 @@ packages: react-native-svg: optional: true + react-native-drawer-layout@4.1.1: + resolution: {integrity: sha512-ob6O3ph7PZ3A2FpdlsSxHuMpHDXREZPR8A6S3q0dSxV7i6d+8Z6CPCTbegfN2QZyizSow9NLrKyXP93tlqZ3dA==} + peerDependencies: + react: '>= 18.2.0' + react-native: '*' + react-native-gesture-handler: '>= 2.0.0' + react-native-reanimated: '>= 2.0.0' + react-native-gesture-handler@2.20.2: resolution: {integrity: sha512-HqzFpFczV4qCnwKlvSAvpzEXisL+Z9fsR08YV5LfJDkzuArMhBu2sOoSPUF/K62PCoAb+ObGlTC83TKHfUd0vg==} peerDependencies: @@ -17082,7 +17104,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/fingerprint@0.11.4': + '@expo/fingerprint@0.11.6': dependencies: '@expo/spawn-async': 1.7.2 arg: 5.0.2 @@ -19458,6 +19480,22 @@ snapshots: use-latest-callback: 0.2.3(react@18.3.1) use-sync-external-store: 1.4.0(react@18.3.1) + '@react-navigation/drawer@7.1.1(vwskucj4jxsojke6dhlsncyura)': + dependencies: + '@react-navigation/elements': 2.2.5(@react-navigation/native@7.0.14(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + '@react-navigation/native': 7.0.14(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + color: 4.2.3 + react: 18.3.1 + react-native: 0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1) + react-native-drawer-layout: 4.1.1(react-native-gesture-handler@2.20.2(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native-reanimated@3.16.5(@babel/core@7.26.0)(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + react-native-gesture-handler: 2.20.2(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + react-native-reanimated: 3.16.5(@babel/core@7.26.0)(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + react-native-safe-area-context: 4.12.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + react-native-screens: 4.1.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + use-latest-callback: 0.2.3(react@18.3.1) + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + '@react-navigation/elements@2.2.5(@react-navigation/native@7.0.14(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)': dependencies: '@react-navigation/native': 7.0.14(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) @@ -22656,12 +22694,12 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.37.0(@opentelemetry/api@1.9.0)(@types/pg@8.11.10)(@types/react@18.3.14)(expo-sqlite@15.0.3(qc23ies2iywwyrf5w5kjvn5iki))(kysely@0.27.5)(react@18.3.1): + drizzle-orm@0.37.0(@opentelemetry/api@1.9.0)(@types/pg@8.11.10)(@types/react@18.3.14)(expo-sqlite@15.0.3(q65rno7mdzia6zdespm3xhq57e))(kysely@0.27.5)(react@18.3.1): optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/pg': 8.11.10 '@types/react': 18.3.14 - expo-sqlite: 15.0.3(qc23ies2iywwyrf5w5kjvn5iki) + expo-sqlite: 15.0.3(q65rno7mdzia6zdespm3xhq57e) kysely: 0.27.5 react: 18.3.1 @@ -23806,11 +23844,11 @@ snapshots: expo: 52.0.18(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) react-native: 0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1) - expo-asset@11.0.1(qc23ies2iywwyrf5w5kjvn5iki): + expo-asset@11.0.1(q65rno7mdzia6zdespm3xhq57e): dependencies: '@expo/image-utils': 0.6.3 - expo: 52.0.20(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) - expo-constants: 17.0.3(vbbboaie7wu7c2ong6rrw33wna) + expo: 52.0.23(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + expo-constants: 17.0.3(4zmahxl5ymv4iqnmz7u7srsqim) invariant: 2.2.4 md5-file: 3.2.3 react: 18.3.1 @@ -23849,11 +23887,11 @@ snapshots: react: 18.3.1 react-native: 0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1) - expo-constants@17.0.3(vbbboaie7wu7c2ong6rrw33wna): + expo-constants@17.0.3(4zmahxl5ymv4iqnmz7u7srsqim): dependencies: '@expo/config': 10.0.6 '@expo/env': 0.4.0 - expo: 52.0.20(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + expo: 52.0.23(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) react-native: 0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1) transitivePeerDependencies: - supports-color @@ -23878,9 +23916,9 @@ snapshots: expo-eas-client@0.13.1: {} - expo-file-system@18.0.6(vbbboaie7wu7c2ong6rrw33wna): + expo-file-system@18.0.6(4zmahxl5ymv4iqnmz7u7srsqim): dependencies: - expo: 52.0.20(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + expo: 52.0.23(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) react-native: 0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1) web-streams-polyfill: 3.3.3 optional: true @@ -23897,9 +23935,9 @@ snapshots: fontfaceobserver: 2.3.0 react: 18.3.1 - expo-font@13.0.2(expo@52.0.20(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1): + expo-font@13.0.2(expo@52.0.23(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: - expo: 52.0.20(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + expo: 52.0.23(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) fontfaceobserver: 2.3.0 react: 18.3.1 optional: true @@ -23923,9 +23961,9 @@ snapshots: expo: 52.0.18(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) react: 18.3.1 - expo-keep-awake@14.0.1(expo@52.0.20(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1): + expo-keep-awake@14.0.1(expo@52.0.23(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: - expo: 52.0.20(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + expo: 52.0.23(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) react: 18.3.1 optional: true @@ -23973,7 +24011,7 @@ snapshots: invariant: 2.2.4 optional: true - expo-router@4.0.11(ilcj5ynoywkepfq4gmlis7vsci): + expo-router@4.0.11(4kc7a2oyapueyif6rxtpvh4774): dependencies: '@expo/metro-runtime': 4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)) '@expo/server': 0.5.0(typescript@5.7.2) @@ -23994,6 +24032,7 @@ snapshots: semver: 7.6.3 server-only: 0.0.1 optionalDependencies: + '@react-navigation/drawer': 7.1.1(vwskucj4jxsojke6dhlsncyura) react-native-reanimated: 3.16.5(@babel/core@7.26.0)(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) transitivePeerDependencies: - '@react-native-masked-view/masked-view' @@ -24014,9 +24053,9 @@ snapshots: transitivePeerDependencies: - supports-color - expo-sqlite@15.0.3(qc23ies2iywwyrf5w5kjvn5iki): + expo-sqlite@15.0.3(q65rno7mdzia6zdespm3xhq57e): dependencies: - expo: 52.0.20(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + expo: 52.0.23(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) react: 18.3.1 react-native: 0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1) optional: true @@ -24117,21 +24156,21 @@ snapshots: - supports-color - utf-8-validate - expo@52.0.20(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1): + expo@52.0.23(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.26.0 '@expo/cli': 0.22.7(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1) '@expo/config': 10.0.6 '@expo/config-plugins': 9.0.12 - '@expo/fingerprint': 0.11.4 + '@expo/fingerprint': 0.11.6 '@expo/metro-config': 0.19.8 '@expo/vector-icons': 14.0.4 babel-preset-expo: 12.0.4(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) - expo-asset: 11.0.1(qc23ies2iywwyrf5w5kjvn5iki) - expo-constants: 17.0.3(vbbboaie7wu7c2ong6rrw33wna) - expo-file-system: 18.0.6(vbbboaie7wu7c2ong6rrw33wna) - expo-font: 13.0.2(expo@52.0.20(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1) - expo-keep-awake: 14.0.1(expo@52.0.20(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1) + expo-asset: 11.0.1(q65rno7mdzia6zdespm3xhq57e) + expo-constants: 17.0.3(4zmahxl5ymv4iqnmz7u7srsqim) + expo-file-system: 18.0.6(4zmahxl5ymv4iqnmz7u7srsqim) + expo-font: 13.0.2(expo@52.0.23(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1) + expo-keep-awake: 14.0.1(expo@52.0.23(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1) expo-modules-autolinking: 2.0.4 expo-modules-core: 2.1.2 fbemitter: 3.0.0(encoding@0.1.13) @@ -28362,6 +28401,14 @@ snapshots: transitivePeerDependencies: - supports-color + react-native-drawer-layout@4.1.1(react-native-gesture-handler@2.20.2(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native-reanimated@3.16.5(@babel/core@7.26.0)(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-native: 0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1) + react-native-gesture-handler: 2.20.2(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + react-native-reanimated: 3.16.5(@babel/core@7.26.0)(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + use-latest-callback: 0.2.3(react@18.3.1) + react-native-gesture-handler@2.20.2(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1): dependencies: '@egjs/hammerjs': 2.0.17 From 3c5dba4d7aee870d1594e39a6e185622dff46abe Mon Sep 17 00:00:00 2001 From: DIYgod Date: Tue, 14 Jan 2025 18:49:50 +0800 Subject: [PATCH 24/93] feat: disable more line clamp for translation --- .../entry-column/templates/list-item-template.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/apps/renderer/src/modules/entry-column/templates/list-item-template.tsx b/apps/renderer/src/modules/entry-column/templates/list-item-template.tsx index 937b15c3c5..f721377888 100644 --- a/apps/renderer/src/modules/entry-column/templates/list-item-template.tsx +++ b/apps/renderer/src/modules/entry-column/templates/list-item-template.tsx @@ -58,14 +58,8 @@ export function ListItem({ const lineClamp = useMemo(() => { const envIsSafari = isSafari() - let lineClampTitle = settingWideMode ? 1 : 2 - let lineClampDescription = settingWideMode ? 1 : 2 - if (translation?.title) { - lineClampTitle += settingWideMode ? 1 : 2 - } - if (translation?.description) { - lineClampDescription += settingWideMode ? 1 : 2 - } + const lineClampTitle = settingWideMode ? 1 : 2 + const lineClampDescription = settingWideMode ? 1 : 2 // for tailwind // line-clamp-[1] line-clamp-[2] line-clamp-[3] line-clamp-[4] line-clamp-[5] line-clamp-[6] line-clamp-[7] line-clamp-[8] @@ -76,7 +70,7 @@ export function ListItem({ title: envIsSafari ? `line-clamp-[${lineClampTitle}]` : "", description: envIsSafari ? `line-clamp-[${lineClampDescription}]` : "", } - }, [translation?.title, translation?.description, settingWideMode]) + }, [settingWideMode]) // NOTE: prevent 0 height element, react virtuoso will not stop render any more if (!entry || !(feed || inbox)) return null From f850474d13b88e9f388216e1402f4fa0dfbc2d64 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Tue, 14 Jan 2025 19:05:39 +0800 Subject: [PATCH 25/93] feat: optimize language check --- apps/renderer/src/lib/translate.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/renderer/src/lib/translate.ts b/apps/renderer/src/lib/translate.ts index e3035911c7..db354eb9a9 100644 --- a/apps/renderer/src/lib/translate.ts +++ b/apps/renderer/src/lib/translate.ts @@ -1,3 +1,4 @@ +import { parseHtml } from "@follow/components/ui/markdown/parse-html.js" import { views } from "@follow/constants" import type { SupportedLanguages } from "@follow/models/types" @@ -58,7 +59,12 @@ export async function translate({ fields = fields.filter((field) => { if (language && entry.entries[field]) { - const sourceLanguage = franc(entry.entries[field]) + const content = parseHtml(entry.entries[field]) + .toText() + .replaceAll(/https?:\/\/\S+|www\.\S+/g, " ") + const sourceLanguage = franc(content, { + only: [LanguageMap[language].code], + }) if (sourceLanguage === LanguageMap[language].code) { return false From 75b9135a02fb6e94abb5c0670c9f12f354a88f0e Mon Sep 17 00:00:00 2001 From: DIYgod Date: Tue, 14 Jan 2025 20:18:26 +0800 Subject: [PATCH 26/93] feat: optimize translation display --- .../src/modules/entry-column/translation.tsx | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/apps/renderer/src/modules/entry-column/translation.tsx b/apps/renderer/src/modules/entry-column/translation.tsx index 10db0ddcb1..8d56f6a997 100644 --- a/apps/renderer/src/modules/entry-column/translation.tsx +++ b/apps/renderer/src/modules/entry-column/translation.tsx @@ -16,25 +16,42 @@ export const EntryTranslation: Component<{ return target }, [source, target, showTranslation]) - const content = useMemo(() => source + (nextTarget ? ` ${nextTarget}` : ""), [source, nextTarget]) - if (!source) { return null } return (
- {nextTarget && } {isHTML ? ( - - {content} - + <> + + {source} + + {nextTarget && ( + <> + + + {nextTarget} + + + )} + ) : ( -
{content}
+ <> +
+ {source} + {nextTarget && ( + <> + + {nextTarget} + + )} +
+ )}
) From 902d68180d1cc872cbe4435efbcae108f0d732b5 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Tue, 14 Jan 2025 20:19:29 +0800 Subject: [PATCH 27/93] fix: missing translation in pitcure masonry --- .../entry-column/Items/picture-masonry.tsx | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/renderer/src/modules/entry-column/Items/picture-masonry.tsx b/apps/renderer/src/modules/entry-column/Items/picture-masonry.tsx index 4cef49e98e..7d86295d20 100644 --- a/apps/renderer/src/modules/entry-column/Items/picture-masonry.tsx +++ b/apps/renderer/src/modules/entry-column/Items/picture-masonry.tsx @@ -29,7 +29,9 @@ import { useEventCallback } from "usehooks-ts" import { useGeneralSettingKey } from "~/atoms/settings/general" import { MediaContainerWidthProvider } from "~/components/ui/media" -import { getEntry } from "~/store/entry" +import { useAuthQuery } from "~/hooks/common/useBizQuery" +import { Queries } from "~/queries" +import { getEntry, useEntry } from "~/store/entry" import { imageActions } from "~/store/image" import { getMasonryColumnValue, setMasonryColumnValue, useMasonryColumnValue } from "../atoms" @@ -245,6 +247,23 @@ const MasonryRender: React.ComponentType< }> > = ({ data, index }) => { const firstScreenReady = useContext(FirstScreenReadyContext) + const entry = useEntry(data.entryId) + const translation = useAuthQuery( + Queries.ai.translation({ + entry: entry!, + view: entry?.view, + language: entry?.settings?.translation, + }), + { + enabled: !!entry?.settings?.translation, + refetchOnMount: false, + refetchOnWindowFocus: false, + meta: { + persist: true, + }, + }, + ) + if (data.entryId.startsWith("placeholder")) { return } @@ -257,6 +276,7 @@ const MasonryRender: React.ComponentType< )} entryId={data.entryId} index={index} + translation={translation.data} /> ) } From fe44c38b4985a1316cda2df9d3c03182397a0733 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Tue, 14 Jan 2025 20:23:21 +0800 Subject: [PATCH 28/93] feat: one more line clamp for translated title --- .../modules/entry-column/templates/list-item-template.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/renderer/src/modules/entry-column/templates/list-item-template.tsx b/apps/renderer/src/modules/entry-column/templates/list-item-template.tsx index f721377888..bac629e3ec 100644 --- a/apps/renderer/src/modules/entry-column/templates/list-item-template.tsx +++ b/apps/renderer/src/modules/entry-column/templates/list-item-template.tsx @@ -58,9 +58,13 @@ export function ListItem({ const lineClamp = useMemo(() => { const envIsSafari = isSafari() - const lineClampTitle = settingWideMode ? 1 : 2 + let lineClampTitle = settingWideMode ? 1 : 2 const lineClampDescription = settingWideMode ? 1 : 2 + if (translation?.title) { + lineClampTitle += 1 + } + // for tailwind // line-clamp-[1] line-clamp-[2] line-clamp-[3] line-clamp-[4] line-clamp-[5] line-clamp-[6] line-clamp-[7] line-clamp-[8] From 2edebdfcdb7af277b19454551729e30f5cfa46ce Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 14 Jan 2025 20:25:18 +0800 Subject: [PATCH 29/93] refactor(rn): subscription drawer layout Signed-off-by: Innei --- apps/mobile/src/lib/platform.ts | 3 + apps/mobile/src/modules/discover/constants.ts | 4 +- .../modules/feed-drawer/collection-panel.tsx | 22 ++++-- .../mobile/src/modules/feed-drawer/drawer.tsx | 2 +- .../src/modules/feed-drawer/feed-panel.tsx | 77 +++++++------------ .../mobile/src/modules/feed-drawer/header.tsx | 30 ++++++-- .../modules/subscription/ItemSeparator.tsx | 2 +- .../subscription/SubscriptionLists.tsx | 14 +++- apps/mobile/src/screens/(headless)/search.tsx | 3 +- 9 files changed, 89 insertions(+), 68 deletions(-) create mode 100644 apps/mobile/src/lib/platform.ts diff --git a/apps/mobile/src/lib/platform.ts b/apps/mobile/src/lib/platform.ts new file mode 100644 index 0000000000..b0b492f46f --- /dev/null +++ b/apps/mobile/src/lib/platform.ts @@ -0,0 +1,3 @@ +import { Platform } from "react-native" + +export const isIOS = Platform.OS === "ios" diff --git a/apps/mobile/src/modules/discover/constants.ts b/apps/mobile/src/modules/discover/constants.ts index 96e0092484..39dcfb8911 100644 --- a/apps/mobile/src/modules/discover/constants.ts +++ b/apps/mobile/src/modules/discover/constants.ts @@ -1,11 +1,11 @@ export enum SearchType { Feed = "feed", List = "list", - User = "user", + // User = "user", } export const SearchTabs = [ { name: "Feed", value: SearchType.Feed }, { name: "List", value: SearchType.List }, - { name: "User", value: SearchType.User }, + // { name: "User", value: SearchType.User }, ] diff --git a/apps/mobile/src/modules/feed-drawer/collection-panel.tsx b/apps/mobile/src/modules/feed-drawer/collection-panel.tsx index 78fdfd115d..1b59baf8dc 100644 --- a/apps/mobile/src/modules/feed-drawer/collection-panel.tsx +++ b/apps/mobile/src/modules/feed-drawer/collection-panel.tsx @@ -1,12 +1,13 @@ import { cn } from "@follow/utils" import { Image, - SafeAreaView, ScrollView, + StyleSheet, TouchableOpacity, useWindowDimensions, View, } from "react-native" +import { useSafeAreaInsets } from "react-native-safe-area-context" import { FallbackIcon } from "@/src/components/ui/icon/fallback-icon" import type { ViewDefinition } from "@/src/constants/views" @@ -20,23 +21,34 @@ export const CollectionPanel = () => { const winDim = useWindowDimensions() const lists = useAllListSubscription() + const insets = useSafeAreaInsets() return ( - - + {views.map((viewDef) => ( ))} + {lists.map((listId) => ( ))} - +
) } +const styles = StyleSheet.create({ + hairline: { + height: StyleSheet.hairlineWidth, + }, +}) + const ViewButton = ({ viewDef }: { viewDef: ViewDefinition }) => { const selectedCollection = useSelectedCollection() const isActive = selectedCollection.type === "view" && selectedCollection.viewId === viewDef.view @@ -45,7 +57,7 @@ const ViewButton = ({ viewDef }: { viewDef: ViewDefinition }) => { selectCollection({ diff --git a/apps/mobile/src/modules/feed-drawer/drawer.tsx b/apps/mobile/src/modules/feed-drawer/drawer.tsx index 9ae8ae7320..7ecb4ef229 100644 --- a/apps/mobile/src/modules/feed-drawer/drawer.tsx +++ b/apps/mobile/src/modules/feed-drawer/drawer.tsx @@ -63,7 +63,7 @@ export const FeedDrawer = ({ children }: PropsWithChildren) => { const DrawerContent = () => { return ( - + diff --git a/apps/mobile/src/modules/feed-drawer/feed-panel.tsx b/apps/mobile/src/modules/feed-drawer/feed-panel.tsx index b2896fff9e..e58a9c61eb 100644 --- a/apps/mobile/src/modules/feed-drawer/feed-panel.tsx +++ b/apps/mobile/src/modules/feed-drawer/feed-panel.tsx @@ -1,12 +1,12 @@ -import type { FeedViewType } from "@follow/constants" import { cn } from "@follow/utils" +import { BottomTabBarHeightContext } from "@react-navigation/bottom-tabs" +import { HeaderHeightContext } from "@react-navigation/elements" import { router } from "expo-router" import type { FC } from "react" -import { createContext, memo, useContext, useMemo } from "react" +import { createContext, memo, useContext, useState } from "react" import { Animated, Easing, - SafeAreaView, ScrollView, StyleSheet, Text, @@ -23,13 +23,7 @@ import { ItemPressable } from "@/src/components/ui/pressable/item-pressable" import { MingcuteRightLine } from "@/src/icons/mingcute_right_line" import { useFeed, usePrefetchFeed } from "@/src/store/feed/hooks" import { useList } from "@/src/store/list/hooks" -import { - useGroupedSubscription, - usePrefetchSubscription, - useSortedGroupedSubscription, - useSortedUngroupedSubscription, - useSubscription, -} from "@/src/store/subscription/hooks" +import { useSortedUngroupedSubscription, useSubscription } from "@/src/store/subscription/hooks" import { useUnreadCount, useUnreadCounts } from "@/src/store/unread/hooks" import { @@ -37,40 +31,44 @@ import { SubscriptionFeedItemContextMenu, } from "../context-menu/feeds" import { useCurrentView, useFeedListSortMethod, useFeedListSortOrder } from "../subscription/atoms" +import { ViewPageCurrentViewProvider } from "../subscription/ctx" +import { SubscriptionList } from "../subscription/SubscriptionLists" import { useSelectedCollection } from "./atoms" import { ListHeaderComponent, ViewHeaderComponent } from "./header" -const useSortedSubscription = (view: FeedViewType) => { - usePrefetchSubscription(view) - const { grouped, unGrouped } = useGroupedSubscription(view) - - const sortBy = useFeedListSortMethod() - const sortOrder = useFeedListSortOrder() - const sortedGrouped = useSortedGroupedSubscription(grouped, sortBy, sortOrder) - const sortedUnGrouped = useSortedUngroupedSubscription(unGrouped, sortBy, sortOrder) - const data = useMemo( - () => [...sortedGrouped, ...sortedUnGrouped], - [sortedGrouped, sortedUnGrouped], - ) - return data -} - export const FeedPanel = () => { const selectedCollection = useSelectedCollection() + const [headerHeight, setHeaderHeight] = useState(0) + if (selectedCollection.type === "view") { return ( - - - - + + { + setHeaderHeight(e.nativeEvent.layout.height) + }} + /> + + + + + + + + + ) } return ( - + - + ) } @@ -93,23 +91,6 @@ const ListView = ({ listId }: { listId: string }) => { ) } -const FeedListView = ({ view }: { view: FeedViewType }) => { - const data = useSortedSubscription(view) - return ( - - {data.map((item, index) => ( - - ))} - {/* Just a placeholder */} - - - ) -} - const ItemRender = ({ item, }: { diff --git a/apps/mobile/src/modules/feed-drawer/header.tsx b/apps/mobile/src/modules/feed-drawer/header.tsx index 2afabd0e54..67471adc9e 100644 --- a/apps/mobile/src/modules/feed-drawer/header.tsx +++ b/apps/mobile/src/modules/feed-drawer/header.tsx @@ -1,16 +1,32 @@ import type { FeedViewType } from "@follow/constants" +import type { LayoutChangeEvent } from "react-native" import { Text, View } from "react-native" +import { useSafeAreaInsets } from "react-native-safe-area-context" +import { BlurEffect } from "@/src/components/common/HeaderBlur" import { useList } from "@/src/store/list/hooks" import { SortActionButton } from "../subscription/header-actions" import { useViewDefinition } from "./atoms" -export const ViewHeaderComponent = ({ view }: { view: FeedViewType }) => { +export const ViewHeaderComponent = ({ + view, + onLayout, +}: { + view: FeedViewType + onLayout?: (event: LayoutChangeEvent) => void +}) => { const viewDef = useViewDefinition(view) + const insets = useSafeAreaInsets() + return ( - - {viewDef.name} + + + {viewDef.name} ) @@ -18,17 +34,21 @@ export const ViewHeaderComponent = ({ view }: { view: FeedViewType }) => { export const ListHeaderComponent = ({ listId }: { listId: string }) => { const list = useList(listId) + const insets = useSafeAreaInsets() if (!list) { console.warn("list not found:", listId) return null } return ( - + {list.title} {list.description && ( - {list.description} + {list.description} )} ) diff --git a/apps/mobile/src/modules/subscription/ItemSeparator.tsx b/apps/mobile/src/modules/subscription/ItemSeparator.tsx index d96b44a8c5..b96f3061ce 100644 --- a/apps/mobile/src/modules/subscription/ItemSeparator.tsx +++ b/apps/mobile/src/modules/subscription/ItemSeparator.tsx @@ -1,7 +1,7 @@ import { StyleSheet, View } from "react-native" const el = ( - + ) export const ItemSeparator = () => { return el diff --git a/apps/mobile/src/modules/subscription/SubscriptionLists.tsx b/apps/mobile/src/modules/subscription/SubscriptionLists.tsx index 467f82bfac..8d886f0197 100644 --- a/apps/mobile/src/modules/subscription/SubscriptionLists.tsx +++ b/apps/mobile/src/modules/subscription/SubscriptionLists.tsx @@ -62,7 +62,7 @@ export const SubscriptionLists = memo(() => { ].map((view) => { return ( - + ) })} @@ -75,7 +75,13 @@ const keyExtractor = (item: string | { category: string; subscriptionIds: string } return item.category } -const SubscriptionList = ({ view }: { view: FeedViewType }) => { +export const SubscriptionList = ({ + view, + additionalOffsetTop, +}: { + view: FeedViewType + additionalOffsetTop?: number +}) => { const headerHeight = useHeaderHeight() const insets = useSafeAreaInsets() const tabHeight = useBottomTabBarHeight() @@ -97,13 +103,13 @@ const SubscriptionList = ({ view }: { view: FeedViewType }) => { return subscriptionSyncService.fetch(view) }) - const offsetTop = headerHeight - insets.top + ViewTabHeight * 2 + 23 + const offsetTop = headerHeight - insets.top + (additionalOffsetTop || 0) return ( { setRefreshing(true) onRefresh().finally(() => { diff --git a/apps/mobile/src/screens/(headless)/search.tsx b/apps/mobile/src/screens/(headless)/search.tsx index fc8a46dbdc..a3b11185db 100644 --- a/apps/mobile/src/screens/(headless)/search.tsx +++ b/apps/mobile/src/screens/(headless)/search.tsx @@ -19,7 +19,6 @@ import { import { SearchHeader } from "@/src/modules/discover/search" import { SearchFeed } from "@/src/modules/discover/search-tabs/SearchFeed" import { SearchList } from "@/src/modules/discover/search-tabs/SearchList" -import { SearchUser } from "@/src/modules/discover/search-tabs/SearchUser" const Search = () => { return ( @@ -39,7 +38,7 @@ const Search = () => { const SearchType2RenderContent: Record = { [SearchType.Feed]: SearchFeed, [SearchType.List]: SearchList, - [SearchType.User]: SearchUser, + // [SearchType.User]: SearchUser, } const PlaceholderLazyView = () => { const windowWidth = Dimensions.get("window").width From 225e470db84649bd23657d82b3c6a71b94cccc5f Mon Sep 17 00:00:00 2001 From: DIYgod Date: Tue, 14 Jan 2025 20:51:17 +0800 Subject: [PATCH 30/93] feat: checkLanguage utils --- apps/renderer/src/lib/immersive-translate.ts | 16 +++++--- apps/renderer/src/lib/translate.ts | 39 +++++++++++++------ .../modules/entry-content/index.desktop.tsx | 18 ++++++--- .../modules/entry-content/index.mobile.tsx | 18 ++++++--- 4 files changed, 61 insertions(+), 30 deletions(-) diff --git a/apps/renderer/src/lib/immersive-translate.ts b/apps/renderer/src/lib/immersive-translate.ts index f1be28d339..4bf92ee3b5 100644 --- a/apps/renderer/src/lib/immersive-translate.ts +++ b/apps/renderer/src/lib/immersive-translate.ts @@ -1,9 +1,8 @@ import type { SupportedLanguages } from "@follow/models/types" -import { franc } from "franc-min" import type { FlatEntryModel } from "~/store/entry" -import { LanguageMap, translate } from "./translate" +import { checkLanguage, translate } from "./translate" function textNodesUnder(el: Node) { const children: Node[] = el.nodeType === Node.TEXT_NODE ? [el] : [] @@ -17,7 +16,7 @@ function textNodesUnder(el: Node) { return children } -const tagsToDuplicate = ["h1", "h2", "h3", "h4", "h5", "h6", "p", "li", "blockquote"] +const tagsToDuplicate = ["h1", "h2", "h3", "h4", "h5", "h6", "p", "li", "blockquote", "article"] export function immersiveTranslate({ html, @@ -94,9 +93,14 @@ export function immersiveTranslate({ }) as HTMLElement[] for (const tag of tags) { - const sourceLanguage = franc(tag.textContent ?? "") - if (sourceLanguage === LanguageMap[translation].code) { - return + if (tag.textContent) { + const isLanguageMatch = checkLanguage({ + content: tag.textContent, + language: translation, + }) + if (isLanguageMatch) { + continue + } } const children = Array.from(tag.childNodes) diff --git a/apps/renderer/src/lib/translate.ts b/apps/renderer/src/lib/translate.ts index db354eb9a9..be83987ebf 100644 --- a/apps/renderer/src/lib/translate.ts +++ b/apps/renderer/src/lib/translate.ts @@ -1,6 +1,7 @@ import { parseHtml } from "@follow/components/ui/markdown/parse-html.js" import { views } from "@follow/constants" import type { SupportedLanguages } from "@follow/models/types" +import { franc } from "franc-min" import type { FlatEntryModel } from "~/store/entry" @@ -35,6 +36,28 @@ export const LanguageMap: Record< code: "cmn", }, } + +export const checkLanguage = ({ + content, + language, +}: { + content: string + language: SupportedLanguages +}) => { + if (!content) return true + const pureContent = parseHtml(content) + .toText() + .replaceAll(/https?:\/\/\S+|www\.\S+/g, " ") + const sourceLanguage = franc(pureContent, { + only: [LanguageMap[language].code], + }) + if (sourceLanguage === LanguageMap[language].code) { + return true + } else { + return false + } +} + export async function translate({ entry, view, @@ -55,22 +78,14 @@ export async function translate({ if (extraFields) { fields = [...fields, ...extraFields] } - const { franc } = await import("franc-min") fields = fields.filter((field) => { if (language && entry.entries[field]) { - const content = parseHtml(entry.entries[field]) - .toText() - .replaceAll(/https?:\/\/\S+|www\.\S+/g, " ") - const sourceLanguage = franc(content, { - only: [LanguageMap[language].code], + const isLanguageMatch = checkLanguage({ + content: entry.entries[field], + language, }) - - if (sourceLanguage === LanguageMap[language].code) { - return false - } else { - return true - } + return !isLanguageMatch } else { return false } diff --git a/apps/renderer/src/modules/entry-content/index.desktop.tsx b/apps/renderer/src/modules/entry-content/index.desktop.tsx index be5c275c57..4c41c298f5 100644 --- a/apps/renderer/src/modules/entry-content/index.desktop.tsx +++ b/apps/renderer/src/modules/entry-content/index.desktop.tsx @@ -16,7 +16,7 @@ import { ShadowDOM } from "~/components/common/ShadowDOM" import { useInPeekModal } from "~/components/ui/modal/inspire/PeekModal" import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams" import { useAuthQuery } from "~/hooks/common" -import { LanguageMap } from "~/lib/translate" +import { checkLanguage } from "~/lib/translate" import { WrappedElementProvider } from "~/providers/wrapped-element-provider" import { Queries } from "~/queries" import { useEntry } from "~/store/entry" @@ -108,7 +108,9 @@ export const EntryContent: Component = ({ ) const customCSS = useUISettingKey("customCSS") const showAITranslation = useShowAITranslation() - const translationLanguage = useGeneralSettingSelector((s) => s.translationLanguage) + const translationLanguage = useGeneralSettingSelector( + (s) => s.translationLanguage, + ) as SupportedLanguages if (!entry) return null @@ -120,13 +122,17 @@ export const EntryContent: Component = ({ const fullText = html.textContent ?? "" if (!fullText) return - const { franc } = await import("franc-min") const translation = entry.settings?.translation ?? (showAITranslation ? translationLanguage : undefined) - const sourceLanguage = franc(fullText) - if (translation && sourceLanguage === LanguageMap[translation].code) { - return + if (translation) { + const isLanguageMatch = checkLanguage({ + content: fullText, + language: translation, + }) + if (isLanguageMatch) { + return + } } const { immersiveTranslate } = await import("~/lib/immersive-translate") diff --git a/apps/renderer/src/modules/entry-content/index.mobile.tsx b/apps/renderer/src/modules/entry-content/index.mobile.tsx index 35e414570a..dc252dc68a 100644 --- a/apps/renderer/src/modules/entry-content/index.mobile.tsx +++ b/apps/renderer/src/modules/entry-content/index.mobile.tsx @@ -14,7 +14,7 @@ import { ShadowDOM } from "~/components/common/ShadowDOM" import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry" import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams" import { useAuthQuery, usePreventOverscrollBounce } from "~/hooks/common" -import { LanguageMap } from "~/lib/translate" +import { checkLanguage } from "~/lib/translate" import { WrappedElementProvider } from "~/providers/wrapped-element-provider" import { Queries } from "~/queries" import { useEntry } from "~/store/entry" @@ -102,7 +102,9 @@ export const EntryContent: Component<{ const [scrollElement, setScrollElement] = useState(null) const showAITranslation = useShowAITranslation() - const translationLanguage = useGeneralSettingSelector((s) => s.translationLanguage) + const translationLanguage = useGeneralSettingSelector( + (s) => s.translationLanguage, + ) as SupportedLanguages if (!entry) return null @@ -114,13 +116,17 @@ export const EntryContent: Component<{ const fullText = html.textContent ?? "" if (!fullText) return - const { franc } = await import("franc-min") const translation = entry.settings?.translation ?? (showAITranslation ? translationLanguage : undefined) - const sourceLanguage = franc(fullText) - if (translation && sourceLanguage === LanguageMap[translation].code) { - return + if (translation) { + const isLanguageMatch = checkLanguage({ + content: fullText, + language: translation, + }) + if (isLanguageMatch) { + return + } } const { immersiveTranslate } = await import("~/lib/immersive-translate") From 5f44d245fd07ec36da16837ed6134538340b114f Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 14 Jan 2025 22:00:37 +0800 Subject: [PATCH 31/93] feat(rn): init setting page Signed-off-by: Innei --- apps/mobile/package.json | 2 + .../src/components/common/ThemedBlurView.tsx | 7 +- .../src/components/ui/grouped/GroupedList.tsx | 61 ++++++ .../src/contexts/TabBarBackgroundContext.tsx | 10 + .../src/modules/settings/SettingsList.tsx | 25 +++ .../src/modules/settings/UserHeaderBanner.tsx | 28 +++ apps/mobile/src/modules/settings/hooks.ts | 10 + .../src/modules/settings/routes/Account.tsx | 9 + .../src/modules/settings/routes/index.tsx | 7 + .../src/screens/(stack)/(tabs)/_layout.tsx | 162 ++++++++------ .../src/screens/(stack)/(tabs)/settings.tsx | 69 +++++- apps/mobile/src/store/user/hooks.ts | 6 +- pnpm-lock.yaml | 203 ++++++++++++++++++ 13 files changed, 525 insertions(+), 74 deletions(-) create mode 100644 apps/mobile/src/components/ui/grouped/GroupedList.tsx create mode 100644 apps/mobile/src/contexts/TabBarBackgroundContext.tsx create mode 100644 apps/mobile/src/modules/settings/SettingsList.tsx create mode 100644 apps/mobile/src/modules/settings/UserHeaderBanner.tsx create mode 100644 apps/mobile/src/modules/settings/hooks.ts create mode 100644 apps/mobile/src/modules/settings/routes/Account.tsx create mode 100644 apps/mobile/src/modules/settings/routes/index.tsx diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 16f1b685db..17f7dd487b 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -24,6 +24,8 @@ "@follow/utils": "workspace:*", "@gorhom/portal": "1.0.14", "@hookform/resolvers": "3.9.1", + "@infinite-list/data-model": "2.2.10", + "@infinite-list/react-native": "2.2.10", "@react-native-cookies/cookies": "^6.2.1", "@react-native-picker/picker": "2.9.0", "@react-navigation/bottom-tabs": "^7.0.0", diff --git a/apps/mobile/src/components/common/ThemedBlurView.tsx b/apps/mobile/src/components/common/ThemedBlurView.tsx index 9a7843ab07..806c3bdf83 100644 --- a/apps/mobile/src/components/common/ThemedBlurView.tsx +++ b/apps/mobile/src/components/common/ThemedBlurView.tsx @@ -1,14 +1,15 @@ import type { BlurViewProps } from "expo-blur" import { BlurView } from "expo-blur" import { useColorScheme } from "nativewind" -import type { FC } from "react" +import { forwardRef } from "react" -export const ThemedBlurView: FC = ({ tint, ...rest }) => { +export const ThemedBlurView = forwardRef(({ tint, ...rest }, ref) => { const { colorScheme } = useColorScheme() return ( ) -} +}) diff --git a/apps/mobile/src/components/ui/grouped/GroupedList.tsx b/apps/mobile/src/components/ui/grouped/GroupedList.tsx new file mode 100644 index 0000000000..0e2e3aa3b6 --- /dev/null +++ b/apps/mobile/src/components/ui/grouped/GroupedList.tsx @@ -0,0 +1,61 @@ +import { cn } from "@follow/utils" +import type { FC, PropsWithChildren } from "react" +import type { ViewProps } from "react-native" +import { Pressable, Text, View } from "react-native" + +import { RightCuteReIcon } from "@/src/icons/right_cute_re" +import { useColor } from "@/src/theme/colors" + +export const GroupedInsetListCard: FC = ({ children }) => { + return ( + + {children} + + ) +} + +export const GroupedInsetListSectionHeader: FC<{ + label: string +}> = ({ label }) => { + return ( + + + {label} + + + ) +} + +export const GroupedInsetListItem: FC = ({ children, ...props }) => { + return ( + + {children} + + ) +} + +export const GroupedInsetListNavigationLink: FC<{ + label: string + icon?: React.ReactNode + onPress: () => void +}> = ({ label, icon, onPress }) => { + const tertiaryLabelColor = useColor("tertiaryLabel") + + return ( + + {({ pressed }) => ( + + + + {icon} + {label} + + + + + + + )} + + ) +} diff --git a/apps/mobile/src/contexts/TabBarBackgroundContext.tsx b/apps/mobile/src/contexts/TabBarBackgroundContext.tsx new file mode 100644 index 0000000000..ffab78f021 --- /dev/null +++ b/apps/mobile/src/contexts/TabBarBackgroundContext.tsx @@ -0,0 +1,10 @@ +import { createContext } from "react" +import type { SharedValue } from "react-native-reanimated" + +interface TabBarBackgroundContextType { + opacity: SharedValue +} + +export const TabBarBackgroundContext = createContext({ + opacity: null!, +}) diff --git a/apps/mobile/src/modules/settings/SettingsList.tsx b/apps/mobile/src/modules/settings/SettingsList.tsx new file mode 100644 index 0000000000..e609bc1aba --- /dev/null +++ b/apps/mobile/src/modules/settings/SettingsList.tsx @@ -0,0 +1,25 @@ +import { View } from "react-native" + +import { + GroupedInsetListCard, + GroupedInsetListNavigationLink, +} from "@/src/components/ui/grouped/GroupedList" + +import { useSettingsNavigation } from "./hooks" + +export const SettingsList = () => { + const navigation = useSettingsNavigation() + + return ( + + + { + navigation.navigate("Account") + }} + /> + + + ) +} diff --git a/apps/mobile/src/modules/settings/UserHeaderBanner.tsx b/apps/mobile/src/modules/settings/UserHeaderBanner.tsx new file mode 100644 index 0000000000..b3e7b3e0fe --- /dev/null +++ b/apps/mobile/src/modules/settings/UserHeaderBanner.tsx @@ -0,0 +1,28 @@ +import { Image, Text, View } from "react-native" +import { useSafeAreaInsets } from "react-native-safe-area-context" + +import { useWhoami } from "@/src/store/user/hooks" + +export const UserHeaderBanner = () => { + const whoami = useWhoami() + const insets = useSafeAreaInsets() + + if (!whoami) return null + return ( + + + {!!whoami.image && ( + + )} + + + + {whoami.name} + {!!whoami.handle && @{whoami.handle}} + + + ) +} diff --git a/apps/mobile/src/modules/settings/hooks.ts b/apps/mobile/src/modules/settings/hooks.ts new file mode 100644 index 0000000000..52c8bdc66a --- /dev/null +++ b/apps/mobile/src/modules/settings/hooks.ts @@ -0,0 +1,10 @@ +import { useNavigation } from "@react-navigation/native" +import type { NativeStackNavigationProp } from "@react-navigation/native-stack" + +type RootStackParamList = { + Account: undefined +} + +export const useSettingsNavigation = () => { + return useNavigation>() +} diff --git a/apps/mobile/src/modules/settings/routes/Account.tsx b/apps/mobile/src/modules/settings/routes/Account.tsx new file mode 100644 index 0000000000..c6e706ab06 --- /dev/null +++ b/apps/mobile/src/modules/settings/routes/Account.tsx @@ -0,0 +1,9 @@ +import { Text, View } from "react-native" + +export const AccountScreen = () => { + return ( + + Account + + ) +} diff --git a/apps/mobile/src/modules/settings/routes/index.tsx b/apps/mobile/src/modules/settings/routes/index.tsx new file mode 100644 index 0000000000..4d4aad7315 --- /dev/null +++ b/apps/mobile/src/modules/settings/routes/index.tsx @@ -0,0 +1,7 @@ +import type { createNativeStackNavigator } from "@react-navigation/native-stack" + +import { AccountScreen } from "./Account" + +export const SettingRoutes = (Stack: ReturnType) => { + return [] +} diff --git a/apps/mobile/src/screens/(stack)/(tabs)/_layout.tsx b/apps/mobile/src/screens/(stack)/(tabs)/_layout.tsx index b86f34a492..2d05bf6c67 100644 --- a/apps/mobile/src/screens/(stack)/(tabs)/_layout.tsx +++ b/apps/mobile/src/screens/(stack)/(tabs)/_layout.tsx @@ -1,18 +1,21 @@ import { FeedViewType } from "@follow/constants" import { PlatformPressable } from "@react-navigation/elements/src/PlatformPressable" import { router, Tabs } from "expo-router" -import { Easing, View } from "react-native" +import { useContext, useMemo } from "react" +import { Easing, StyleSheet, View } from "react-native" import { Gesture, GestureDetector } from "react-native-gesture-handler" -import { runOnJS } from "react-native-reanimated" +import Animated, { runOnJS, useAnimatedStyle, useSharedValue } from "react-native-reanimated" -import { BlurEffect } from "@/src/components/common/HeaderBlur" +import { ThemedBlurView } from "@/src/components/common/ThemedBlurView" import { FollowIcon } from "@/src/components/ui/logo" +import { TabBarBackgroundContext } from "@/src/contexts/TabBarBackgroundContext" import { SafariCuteFi } from "@/src/icons/safari_cute_fi" import { SafariCuteIcon } from "@/src/icons/safari_cute-re" import { Setting7CuteFi } from "@/src/icons/setting_7_cute_fi" import { Settings7CuteReIcon } from "@/src/icons/settings_7_cute_re" import { FeedDrawer } from "@/src/modules/feed-drawer/drawer" import { setCurrentView } from "@/src/modules/subscription/atoms" +import { useColor } from "@/src/theme/colors" const doubleTap = Gesture.Tap() .numberOfTaps(2) @@ -27,76 +30,101 @@ const fifthTap = Gesture.Tap() }) export default function TabLayout() { + const opacity = useSharedValue(1) return ( - ({ opacity }), [opacity])}> + - ( - - ), - tabBarButton(props) { - return ( - - - - - - ) + animation: "fade", + transitionSpec: { + animation: "timing", + config: { + duration: 50, + easing: Easing.ease, + }, }, }} - /> - { - const Icon = !focused ? SafariCuteIcon : SafariCuteFi - return - }, - }} - /> + > + ( + + ), + tabBarButton(props) { + return ( + + + + + + ) + }, + }} + /> + { + const Icon = !focused ? SafariCuteIcon : SafariCuteFi + return + }, + }} + /> - - - - - - ) - }, - tabBarIcon: ({ color, focused }) => { - const Icon = !focused ? Settings7CuteReIcon : Setting7CuteFi - return - }, - }} - /> - + + + + + + ) + }, + tabBarIcon: ({ color, focused }) => { + const Icon = !focused ? Settings7CuteReIcon : Setting7CuteFi + return + }, + }} + /> + + ) } + +const AnimatedThemedBlurView = Animated.createAnimatedComponent(ThemedBlurView) +const TabBarBackground = () => { + const { opacity } = useContext(TabBarBackgroundContext) + + const animatedStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + ...styles.blurEffect, + })) + const borderColor = useColor("opaqueSeparator") + return +} + +const styles = StyleSheet.create({ + blurEffect: { + ...StyleSheet.absoluteFillObject, + overflow: "hidden", + backgroundColor: "transparent", + borderTopWidth: StyleSheet.hairlineWidth, + }, +}) diff --git a/apps/mobile/src/screens/(stack)/(tabs)/settings.tsx b/apps/mobile/src/screens/(stack)/(tabs)/settings.tsx index 481e2ce9ff..6d89b47c42 100644 --- a/apps/mobile/src/screens/(stack)/(tabs)/settings.tsx +++ b/apps/mobile/src/screens/(stack)/(tabs)/settings.tsx @@ -1,5 +1,68 @@ -import { Text } from "react-native" +import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs" +import { createNativeStackNavigator } from "@react-navigation/native-stack" +import { useContext, useEffect } from "react" +import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native" +import { ScrollView, useAnimatedValue } from "react-native" +import { withTiming } from "react-native-reanimated" +import { useSafeAreaInsets } from "react-native-safe-area-context" +import { useEventCallback } from "usehooks-ts" -export default function Settings() { - return Settings +import { TabBarBackgroundContext } from "@/src/contexts/TabBarBackgroundContext" +import { SettingRoutes } from "@/src/modules/settings/routes" +import { SettingsList } from "@/src/modules/settings/SettingsList" +import { UserHeaderBanner } from "@/src/modules/settings/UserHeaderBanner" + +const Stack = createNativeStackNavigator() +export default function SettingsX() { + return ( + + + {SettingRoutes(Stack)} + + ) +} + +function Settings() { + const insets = useSafeAreaInsets() + const { opacity } = useContext(TabBarBackgroundContext) + const tabBarHeight = useBottomTabBarHeight() + useEffect(() => { + opacity.value = 1 + return () => { + opacity.value = 1 + } + }, [opacity]) + + const animatedScrollY = useAnimatedValue(0) + const handleScroll = useEventCallback( + ({ nativeEvent }: NativeSyntheticEvent) => { + const { contentOffset, contentSize, layoutMeasurement } = nativeEvent + + const distanceFromBottom = contentSize.height - layoutMeasurement.height - contentOffset.y + + const fadeThreshold = 50 + + if (distanceFromBottom <= fadeThreshold) { + const newOpacity = Math.max(0, distanceFromBottom / fadeThreshold) + opacity.value = withTiming(newOpacity, { duration: 150 }) + } else { + opacity.value = withTiming(1, { duration: 150 }) + } + animatedScrollY.setValue(nativeEvent.contentOffset.y) + }, + ) + return ( + + + + + + ) } diff --git a/apps/mobile/src/store/user/hooks.ts b/apps/mobile/src/store/user/hooks.ts index 0a003dfaa2..c1f12e6871 100644 --- a/apps/mobile/src/store/user/hooks.ts +++ b/apps/mobile/src/store/user/hooks.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query" -import { userSyncService } from "./store" +import { userSyncService, useUserStore } from "./store" export const usePrefetchSessionUser = () => { useQuery({ @@ -8,3 +8,7 @@ export const usePrefetchSessionUser = () => { queryFn: () => userSyncService.whoami(), }) } + +export const useWhoami = () => { + return useUserStore((state) => state.whoami) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd5245309e..9e1e5365ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -427,6 +427,12 @@ importers: '@hookform/resolvers': specifier: 3.9.1 version: 3.9.1(react-hook-form@7.54.0(react@18.3.1)) + '@infinite-list/data-model': + specifier: 2.2.10 + version: 2.2.10 + '@infinite-list/react-native': + specifier: 2.2.10 + version: 2.2.10(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) '@react-native-cookies/cookies': specifier: ^6.2.1 version: 6.2.1(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)) @@ -3784,6 +3790,15 @@ packages: '@iconify/utils@2.1.33': resolution: {integrity: sha512-jP9h6v/g0BIZx0p7XGJJVtkVnydtbgTgt9mVNcGDYwaa7UhdHdI9dvoq+gKj9sijMSJKxUPEG2JyjsgXjxL7Kw==} + '@infinite-list/data-model@2.2.10': + resolution: {integrity: sha512-ubpinQohjcTZvZm5PbHzJ0PkZb5SkCuvcnBkR2etdA0O1MqrDLa0iFr6Vu/hxF7e8lqBdKscub2WTXvUW2/9AA==} + + '@infinite-list/react-native@2.2.10': + resolution: {integrity: sha512-aUum1QHtf+A5XaOYEjysc0xojtrtwMzG+Y4T5naNp+Yysvq+NpLFKFM7n7eWd3Kqc3KhcN5fcODVh7bGYBaA1A==} + peerDependencies: + react: 18.2.0 + react-native: 0.74.1 + '@innei/react-resizable-layout@0.7.3-fork.1': resolution: {integrity: sha512-xBgY0+prk58+Hq235dmdi9AI60xYxqGEYxP44t2+3IqZoa/MUNC3FbGz1ICFGhaQurY/hUPQGOawZNVmcw8tNw==} peerDependencies: @@ -5970,6 +5985,87 @@ packages: peerDependencies: react: ^18 + '@x-oasis/batchinate-last@0.1.35': + resolution: {integrity: sha512-+YELtwndkTu3mtL8pKKJvJXa0b6PlPpWh6lbwtiDvMSlzz2lTC57AAt9HDq6cSlIFzEG7AUcPagSKt9EwyFF2Q==} + + '@x-oasis/batchinator@0.1.35': + resolution: {integrity: sha512-2FPn56U+zzV+G0Fpuut+DF8WSROBy19Rw1YuWuyrwGyL/v29ujvdh+AUwyywcITSJk/OC15ghfPMy1oLw/y6/w==} + + '@x-oasis/capitalize@0.1.35': + resolution: {integrity: sha512-UcFOothNKAYkyAvpTkzmrRWIrZdwC1atyKXH03RyJdoCSJUN88CNUHnxn2h2+K+opOiZxBmMGgACwUeKOVJayg==} + + '@x-oasis/default-boolean-value@0.1.35': + resolution: {integrity: sha512-XAauacjZlfYgGcm0yl3JmAsVqaC7eYnDuT9DkZktlkYaMhlwWWEPfwEjETTRjNpqRPWSNnw8WvjTNcGuo4aRQw==} + + '@x-oasis/default-value@0.1.35': + resolution: {integrity: sha512-SmdgeZ1hAeK7naG+ut26OhOIZBAE+v2z5EdfooJskr/nAaLL81ZJZYDCMBvmnpS613Ud/9Q2+JEQnfDCJJbZmA==} + + '@x-oasis/find-last-index@0.1.35': + resolution: {integrity: sha512-jGG84OPhKEw4K0ECkgZbutnt1WPRecxgh3O2oJwX2algDmMQAWjRfol+6tg5kjpiml0QTFi9nN/Teakmr3eQ0w==} + + '@x-oasis/get-map-key-by-value@0.1.35': + resolution: {integrity: sha512-CAyHbBVJqlJLDu/dXgrRXFl3jfLwBeiSc/j/zUE1SXDoWF4BqWZlz5MgSUllFfbnafjUW2jMlZdaA+VJpJp9Kg==} + + '@x-oasis/heap@0.1.35': + resolution: {integrity: sha512-KyHhox7rnQNsydBAxNYQltbEFqVKODTupfTCI9qth4dPIwWh6tLtuGL6PBGFXzAPrB/4NMh186E6aE+zoeXBBg==} + + '@x-oasis/integer-buffer-set@0.1.36': + resolution: {integrity: sha512-kpaImsAq1c7+aPS735A7XSO6EujLNw+Fzjp8GY072LRmNIuVuP4r65DySAEu3JVxmL0x2et3WOM3XZiSL7HcWw==} + + '@x-oasis/invariant@0.1.35': + resolution: {integrity: sha512-E2VNNgMXAfmmHWqA29zkap0mpUi4tc3Kq21hq5UYS0IQcweZJvO+KeVCIf9ADAre8cW7Sx5yBLEP6aTlSm1dnw==} + + '@x-oasis/is-clamped@0.1.35': + resolution: {integrity: sha512-9NKBqYefUnCpJrA4hc/2//jCjYLVCLrcAIraPH750+88fKjQg6ddXEImvV/+xhz050OJcm4oBq7KaBQo0RWxaw==} + + '@x-oasis/is-object@0.1.35': + resolution: {integrity: sha512-Dqb8kPyTHQNtKiXRpxJSHRbSxar6iKihn88NRGflss/Xu1CnFNbyShUH6i2IH5UJCO4zEQHiemAHWySnA4Jetg==} + + '@x-oasis/is-ref@0.1.35': + resolution: {integrity: sha512-yfnDNUGuZlYZNToj2eynYu4iTGmhRLJ+LpreksJcYW0eoJnh3PLm2fEIw4Vloq72j79OXwwxEOaRbeZkH1aDFQ==} + + '@x-oasis/is@0.1.39': + resolution: {integrity: sha512-va/6+C33k3/Au24WJwJwuS1oy5JA+Sd7hb0ohb0jjRoTzEA3kb5TQGl6gnhsD84ZDEf5jMaNvTz6KCizAP6VVQ==} + + '@x-oasis/layout-equal@0.1.35': + resolution: {integrity: sha512-FjzliFGkzsoi3K3aThxKi9zZOVsgw40dgRxADeE4UlhUxhnqkTi2cXxvrExurR1jJNZkz3vwOE7Xu1ssXOIBwQ==} + + '@x-oasis/noop@0.1.35': + resolution: {integrity: sha512-HOzkGw1JVLWThPWGQYGCmcT8bf+3DbiXIkjCpS4lv0kR3hjTCF3z2Fl966ph3Qtny0rW9RQeBtMknchWUnXcNw==} + + '@x-oasis/omit@0.1.35': + resolution: {integrity: sha512-pG3R9b1OSVIDKZnApclMKiJmVHuuRQd10zB6Y6xPzW9rDjO+kuk0mPz7eYMv5lK+Ko3ZImBYJV+pvC6441aVYw==} + + '@x-oasis/prefix-interval-tree@0.2.4': + resolution: {integrity: sha512-oE4dasiGw78tmh/YFh9ezsbukO+4+9wuiuxR9jTgnSC6qhVZaSmJmXOFZyKHsz2ddsqh/CcoBgEAUeQc3wpchw==} + + '@x-oasis/recycler@0.1.38': + resolution: {integrity: sha512-IOUbHqXEFY/wM+SN9bwChmSFOd1JM5+4WfVoV9tAmDhEcK+xHZ/SY3FbiWAkFWRD34+RbT1UMaadKKIBIG4gzA==} + + '@x-oasis/resolve-changed@0.1.35': + resolution: {integrity: sha512-wgGsN6Bth81LlcsjB4jX3jxaluRHYiS9pTRaHw+m8D9WGwSnwNuL4feN/9JdEr3S0bOGiAf7ISN+t5rEQF5Urg==} + + '@x-oasis/return-hook@0.1.35': + resolution: {integrity: sha512-HDg0r86+/gBVkqtDgqrsvCSb8LykNhSpqb5dhEDdgP/ZRdT5FOeZz8XfXGa6Fze1XhbPgS0Dq6Ej2zJaS/FGlA==} + + '@x-oasis/select-value@0.1.35': + resolution: {integrity: sha512-iB14Ja6JWado+Ndj0vkqn6qBysvpSQjOol9NCsKOiqiWqvx4FNV7LSWkCXdYFTBtNxkVyOeTfZNFVfvKBmf9yg==} + + '@x-oasis/shallow-array-equal@0.1.35': + resolution: {integrity: sha512-RLcfd2YxvBcYIyOO1XnRu35obK61Dow894eQxUcWWP3XRTaK5lMbMwhDXfjAR/MeoC0+zKQruOhGgS24u1zGew==} + + '@x-oasis/shallow-equal@0.1.38': + resolution: {integrity: sha512-TXHqtJ7TXn/QORGH38sOmJ1bgvJKSUrRDKVvkcUyrjdJLd29P4szwYmPZ7E9HWMBj9FRBFTVWOJSagESgyO3Ew==} + + '@x-oasis/throttle@0.1.35': + resolution: {integrity: sha512-L/uiT2TR4MLdP2ucEE6eh4Tan94ePJ6/fA2AGkEwFc4geV7+OWzVydl89JJtVsG3N7/OV/NYp/9vfyZqtbt4gA==} + + '@x-oasis/to-string@0.1.35': + resolution: {integrity: sha512-v6pNsaZ7+UGz995stUEe7YrDRbVIAUcybD70NePXaZiEqhe0TErTWFQ55K8I9ii3UAGfxP4/7twT+1iSacaxNw==} + + '@x-oasis/unique-array-object@0.1.35': + resolution: {integrity: sha512-MxSexUwtrLDwDPF/qHrtNmHoxtE61Kac1J4Me1l2uwiv6bgdII4ZFGCVClsxIFtry1+2x7kS+pRnRHP/k37gJw==} + '@xmldom/xmldom@0.7.13': resolution: {integrity: sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==} engines: {node: '>=10.0.0'} @@ -17852,6 +17948,42 @@ snapshots: transitivePeerDependencies: - supports-color + '@infinite-list/data-model@2.2.10': + dependencies: + '@x-oasis/batchinate-last': 0.1.35 + '@x-oasis/batchinator': 0.1.35 + '@x-oasis/capitalize': 0.1.35 + '@x-oasis/default-boolean-value': 0.1.35 + '@x-oasis/default-value': 0.1.35 + '@x-oasis/find-last-index': 0.1.35 + '@x-oasis/get-map-key-by-value': 0.1.35 + '@x-oasis/is-clamped': 0.1.35 + '@x-oasis/is-object': 0.1.35 + '@x-oasis/layout-equal': 0.1.35 + '@x-oasis/noop': 0.1.35 + '@x-oasis/omit': 0.1.35 + '@x-oasis/prefix-interval-tree': 0.2.4 + '@x-oasis/recycler': 0.1.38 + '@x-oasis/resolve-changed': 0.1.35 + '@x-oasis/select-value': 0.1.35 + '@x-oasis/shallow-array-equal': 0.1.35 + '@x-oasis/shallow-equal': 0.1.38 + '@x-oasis/unique-array-object': 0.1.35 + memoize-one: 6.0.0 + tslib: 2.8.1 + + '@infinite-list/react-native@2.2.10(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)': + dependencies: + '@infinite-list/data-model': 2.2.10 + '@x-oasis/batchinator': 0.1.35 + '@x-oasis/is-ref': 0.1.35 + '@x-oasis/noop': 0.1.35 + '@x-oasis/select-value': 0.1.35 + '@x-oasis/shallow-equal': 0.1.38 + '@x-oasis/throttle': 0.1.35 + react: 18.3.1 + react-native: 0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1) + '@innei/react-resizable-layout@0.7.3-fork.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: react: 18.3.1 @@ -20669,6 +20801,77 @@ snapshots: lodash: 4.17.21 react: 18.3.1 + '@x-oasis/batchinate-last@0.1.35': {} + + '@x-oasis/batchinator@0.1.35': + dependencies: + '@x-oasis/default-boolean-value': 0.1.35 + + '@x-oasis/capitalize@0.1.35': {} + + '@x-oasis/default-boolean-value@0.1.35': + dependencies: + '@x-oasis/default-value': 0.1.35 + + '@x-oasis/default-value@0.1.35': {} + + '@x-oasis/find-last-index@0.1.35': {} + + '@x-oasis/get-map-key-by-value@0.1.35': {} + + '@x-oasis/heap@0.1.35': {} + + '@x-oasis/integer-buffer-set@0.1.36': + dependencies: + '@x-oasis/heap': 0.1.35 + '@x-oasis/invariant': 0.1.35 + '@x-oasis/is-clamped': 0.1.35 + '@x-oasis/return-hook': 0.1.35 + + '@x-oasis/invariant@0.1.35': {} + + '@x-oasis/is-clamped@0.1.35': {} + + '@x-oasis/is-object@0.1.35': + dependencies: + '@x-oasis/to-string': 0.1.35 + + '@x-oasis/is-ref@0.1.35': + dependencies: + '@x-oasis/is-object': 0.1.35 + + '@x-oasis/is@0.1.39': {} + + '@x-oasis/layout-equal@0.1.35': {} + + '@x-oasis/noop@0.1.35': {} + + '@x-oasis/omit@0.1.35': {} + + '@x-oasis/prefix-interval-tree@0.2.4': {} + + '@x-oasis/recycler@0.1.38': + dependencies: + '@x-oasis/integer-buffer-set': 0.1.36 + + '@x-oasis/resolve-changed@0.1.35': {} + + '@x-oasis/return-hook@0.1.35': {} + + '@x-oasis/select-value@0.1.35': {} + + '@x-oasis/shallow-array-equal@0.1.35': {} + + '@x-oasis/shallow-equal@0.1.38': + dependencies: + '@x-oasis/is': 0.1.39 + + '@x-oasis/throttle@0.1.35': {} + + '@x-oasis/to-string@0.1.35': {} + + '@x-oasis/unique-array-object@0.1.35': {} + '@xmldom/xmldom@0.7.13': {} '@xmldom/xmldom@0.8.10': {} From 8ab6835141d4bec3543e6006d9ec92a2f03619db Mon Sep 17 00:00:00 2001 From: Eric Zhu Date: Tue, 14 Jan 2025 22:06:32 +0800 Subject: [PATCH 32/93] chore: apply custom css to mobile (#2533) --- apps/renderer/src/modules/entry-content/index.mobile.tsx | 5 +++++ locales/settings/zh-CN.json | 1 + 2 files changed, 6 insertions(+) diff --git a/apps/renderer/src/modules/entry-content/index.mobile.tsx b/apps/renderer/src/modules/entry-content/index.mobile.tsx index dc252dc68a..df07339dd8 100644 --- a/apps/renderer/src/modules/entry-content/index.mobile.tsx +++ b/apps/renderer/src/modules/entry-content/index.mobile.tsx @@ -1,3 +1,4 @@ +import { MemoedDangerousHTMLStyle } from "@follow/components/common/MemoedDangerousHTMLStyle.js" import { ScrollElementContext } from "@follow/components/ui/scroll-area/ctx.js" import { useTitle } from "@follow/hooks" import type { FeedModel, InboxModel, SupportedLanguages } from "@follow/models/types" @@ -101,6 +102,7 @@ export const EntryContent: Component<{ usePreventOverscrollBounce() const [scrollElement, setScrollElement] = useState(null) + const customCSS = useUISettingKey("customCSS") const showAITranslation = useShowAITranslation() const translationLanguage = useGeneralSettingSelector( (s) => s.translationLanguage, @@ -192,6 +194,9 @@ export const EntryContent: Component<{ + {!!customCSS && ( + {customCSS} + )} Date: Tue, 14 Jan 2025 22:28:23 +0800 Subject: [PATCH 33/93] feat: enhance tab layout and settings screen functionality - Added screen listeners for tab press and transition start to manage opacity in TabLayout. - Introduced a context provider for tracking focus state in SettingsX. - Refactored opacity handling in Settings component to improve scroll behavior and UI responsiveness. This update improves user experience by ensuring the tab bar opacity responds correctly to user interactions and screen focus. Signed-off-by: Innei --- .../src/screens/(stack)/(tabs)/_layout.tsx | 8 +++ .../src/screens/(stack)/(tabs)/settings.tsx | 62 +++++++++++++------ 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/apps/mobile/src/screens/(stack)/(tabs)/_layout.tsx b/apps/mobile/src/screens/(stack)/(tabs)/_layout.tsx index 2d05bf6c67..9b493bed53 100644 --- a/apps/mobile/src/screens/(stack)/(tabs)/_layout.tsx +++ b/apps/mobile/src/screens/(stack)/(tabs)/_layout.tsx @@ -35,6 +35,14 @@ export default function TabLayout() { ({ opacity }), [opacity])}> { + opacity.value = 1 + }, + transitionStart: () => { + opacity.value = 1 + }, + }} screenOptions={{ tabBarBackground: TabBarBackground, tabBarStyle: { diff --git a/apps/mobile/src/screens/(stack)/(tabs)/settings.tsx b/apps/mobile/src/screens/(stack)/(tabs)/settings.tsx index 6d89b47c42..544fead742 100644 --- a/apps/mobile/src/screens/(stack)/(tabs)/settings.tsx +++ b/apps/mobile/src/screens/(stack)/(tabs)/settings.tsx @@ -1,8 +1,9 @@ import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs" +import { useIsFocused } from "@react-navigation/native" import { createNativeStackNavigator } from "@react-navigation/native-stack" -import { useContext, useEffect } from "react" +import { createContext, useCallback, useContext, useEffect, useRef } from "react" import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native" -import { ScrollView, useAnimatedValue } from "react-native" +import { findNodeHandle, ScrollView, UIManager, useAnimatedValue } from "react-native" import { withTiming } from "react-native-reanimated" import { useSafeAreaInsets } from "react-native-safe-area-context" import { useEventCallback } from "usehooks-ts" @@ -13,33 +14,29 @@ import { SettingsList } from "@/src/modules/settings/SettingsList" import { UserHeaderBanner } from "@/src/modules/settings/UserHeaderBanner" const Stack = createNativeStackNavigator() +const OutIsFocused = createContext(false) export default function SettingsX() { + const isFocused = useIsFocused() + return ( - - - {SettingRoutes(Stack)} - + + + + {SettingRoutes(Stack)} + + ) } function Settings() { const insets = useSafeAreaInsets() + const isFocused = useContext(OutIsFocused) const { opacity } = useContext(TabBarBackgroundContext) const tabBarHeight = useBottomTabBarHeight() - useEffect(() => { - opacity.value = 1 - return () => { - opacity.value = 1 - } - }, [opacity]) - - const animatedScrollY = useAnimatedValue(0) - const handleScroll = useEventCallback( - ({ nativeEvent }: NativeSyntheticEvent) => { - const { contentOffset, contentSize, layoutMeasurement } = nativeEvent - - const distanceFromBottom = contentSize.height - layoutMeasurement.height - contentOffset.y + const calculateOpacity = useCallback( + (contentHeight: number, viewportHeight: number, scrollY: number) => { + const distanceFromBottom = contentHeight - viewportHeight - scrollY const fadeThreshold = 50 if (distanceFromBottom <= fadeThreshold) { @@ -48,13 +45,38 @@ function Settings() { } else { opacity.value = withTiming(1, { duration: 150 }) } - animatedScrollY.setValue(nativeEvent.contentOffset.y) }, + [opacity], ) + + useEffect(() => { + if (!isFocused) return + const scrollView = scrollRef.current + if (scrollView) { + const node = findNodeHandle(scrollView) + if (node) { + UIManager.measure(node, (x, y, width, height) => { + calculateOpacity(height, height, 0) + }) + } + } + }, [opacity, isFocused, calculateOpacity]) + + const animatedScrollY = useAnimatedValue(0) + const handleScroll = useEventCallback( + ({ nativeEvent }: NativeSyntheticEvent) => { + const { contentOffset, contentSize, layoutMeasurement } = nativeEvent + calculateOpacity(contentSize.height, layoutMeasurement.height, contentOffset.y) + animatedScrollY.setValue(contentOffset.y) + }, + ) + + const scrollRef = useRef(null) return ( Date: Wed, 15 Jan 2025 13:24:10 +0800 Subject: [PATCH 34/93] feat: enhance input fields for numeric entry - Added `inputMode="numeric"` and `pattern="[0-9]*"` attributes to number input fields in WithdrawModalContent, SetModalContent, and ListCreationModalContent components. - These changes improve user experience by ensuring that numeric keyboards are displayed on mobile devices, facilitating easier input of numerical values. Signed-off-by: Innei --- .../src/modules/power/my-wallet-section/withdraw.tsx | 2 ++ apps/renderer/src/modules/rsshub/set-modal-content.tsx | 9 ++++++++- apps/renderer/src/modules/settings/tabs/lists/modals.tsx | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/renderer/src/modules/power/my-wallet-section/withdraw.tsx b/apps/renderer/src/modules/power/my-wallet-section/withdraw.tsx index 1dfdfb7341..33298c795b 100644 --- a/apps/renderer/src/modules/power/my-wallet-section/withdraw.tsx +++ b/apps/renderer/src/modules/power/my-wallet-section/withdraw.tsx @@ -148,6 +148,8 @@ const WithdrawModalContent = ({ dismiss }: { dismiss: () => void }) => { field.onChange(value.target.valueAsNumber)} /> diff --git a/apps/renderer/src/modules/rsshub/set-modal-content.tsx b/apps/renderer/src/modules/rsshub/set-modal-content.tsx index 8b8e3cfee3..1c8f233853 100644 --- a/apps/renderer/src/modules/rsshub/set-modal-content.tsx +++ b/apps/renderer/src/modules/rsshub/set-modal-content.tsx @@ -122,7 +122,14 @@ export function SetModalContent({
- + {t("rsshub.useModal.month")} diff --git a/apps/renderer/src/modules/settings/tabs/lists/modals.tsx b/apps/renderer/src/modules/settings/tabs/lists/modals.tsx index 04496a42b9..90d1820123 100644 --- a/apps/renderer/src/modules/settings/tabs/lists/modals.tsx +++ b/apps/renderer/src/modules/settings/tabs/lists/modals.tsx @@ -189,6 +189,8 @@ export const ListCreationModalContent = ({ id }: { id?: string }) => { field.onChange(value.target.valueAsNumber)} /> From aeb5965db7ffd936bbfba8e597f24f722b03c0ea Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 15 Jan 2025 13:57:02 +0800 Subject: [PATCH 35/93] chore: remove default fillout error issue Signed-off-by: Innei --- .../src/components/common/ErrorElement.tsx | 15 +++++------ apps/renderer/src/lib/issues.ts | 6 ++++- .../src/modules/discover/list-form.tsx | 2 ++ .../modules/entry-content/index.shared.tsx | 27 ++++++++++--------- .../src/modules/settings/tabs/about.tsx | 2 +- 5 files changed, 28 insertions(+), 24 deletions(-) diff --git a/apps/renderer/src/components/common/ErrorElement.tsx b/apps/renderer/src/components/common/ErrorElement.tsx index 30ec8b5b01..d607b4861b 100644 --- a/apps/renderer/src/components/common/ErrorElement.tsx +++ b/apps/renderer/src/components/common/ErrorElement.tsx @@ -91,11 +91,7 @@ export function ErrorElement() { ) } -export const FeedbackIssue = ({ - message, - stack, - error, -}: { +export const FeedbackIssue = (_props: { message: string stack: string | null | undefined error?: unknown @@ -105,10 +101,11 @@ export const FeedbackIssue = ({ = {}) => { + // @see https://docs.github.com/en/enterprise-cloud@latest/issues/tracking-your-work-with-issues/using-issues/creating-an-issue const baseUrl = target === "discussion" ? `${repository.url}/discussions/new` : `${repository.url}/issues/new` @@ -25,7 +28,7 @@ export const getNewIssueUrl = ({ if (category) searchParams.set("category", category) let nextBody = [body || "", "", ...getCurrentEnvironment()].join("\n") - if (label) searchParams.set("label", label) + if (label) searchParams.set("labels", label) if (title) searchParams.set("title", title) if (error && "traceId" in error && error.traceId) { @@ -33,5 +36,6 @@ export const getNewIssueUrl = ({ } searchParams.set("body", nextBody) + if (template) searchParams.set("template", template) return `${baseUrl}?${searchParams.toString()}` } diff --git a/apps/renderer/src/modules/discover/list-form.tsx b/apps/renderer/src/modules/discover/list-form.tsx index aa27293988..696f7d0504 100644 --- a/apps/renderer/src/modules/discover/list-form.tsx +++ b/apps/renderer/src/modules/discover/list-form.tsx @@ -124,6 +124,8 @@ export const ListForm: Component<{ onClick={() => { window.open( getNewIssueUrl({ + target: "discussion", + category: "list-expired", body: [ "### Info:", "", diff --git a/apps/renderer/src/modules/entry-content/index.shared.tsx b/apps/renderer/src/modules/entry-content/index.shared.tsx index 3e6a2881ba..f0b1490d88 100644 --- a/apps/renderer/src/modules/entry-content/index.shared.tsx +++ b/apps/renderer/src/modules/entry-content/index.shared.tsx @@ -202,19 +202,20 @@ export const RenderError: FallbackRender = ({ error }) => { onClick={() => { window.open( getNewIssueUrl({ - body: [ - "### Error", - "", - nextError.message, - "", - "### Stack", - "", - "```", - nextError.stack, - "```", - ].join("\n"), - label: "bug", - title: "Render error", + // body: [ + // "### Error", + // "", + // nextError.message, + // "", + // "### Stack", + // "", + // "```", + // nextError.stack, + // "```", + // ].join("\n"), + // label: "bug", + // title: "Render error", + template: "bug_report.yml", }), ) }} diff --git a/apps/renderer/src/modules/settings/tabs/about.tsx b/apps/renderer/src/modules/settings/tabs/about.tsx index 0783622fd0..170d064e07 100644 --- a/apps/renderer/src/modules/settings/tabs/about.tsx +++ b/apps/renderer/src/modules/settings/tabs/about.tsx @@ -94,7 +94,7 @@ export const SettingAbout = () => { OpenIssueLink: ( open an issue From a92364f025ad9267712a5d46e51815956f888d45 Mon Sep 17 00:00:00 2001 From: Whitewater Date: Wed, 15 Jan 2025 16:06:22 +0800 Subject: [PATCH 36/93] chore: tweak styles of feed item (#2574) * chore: tweak styles of feed item * chore: update background color of collection panel * chore: adjust padding for unread count --- apps/mobile/src/lib/platform.ts | 4 ++++ .../modules/feed-drawer/collection-panel.tsx | 2 +- .../subscription/items/SubscriptionItem.tsx | 20 +++++++++++++++---- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/apps/mobile/src/lib/platform.ts b/apps/mobile/src/lib/platform.ts index b0b492f46f..e23d31dc54 100644 --- a/apps/mobile/src/lib/platform.ts +++ b/apps/mobile/src/lib/platform.ts @@ -1,3 +1,7 @@ import { Platform } from "react-native" export const isIOS = Platform.OS === "ios" +export const isAndroid = Platform.OS === "android" +export const isNative = isIOS || isAndroid +export const devicePlatform = isIOS ? "ios" : isAndroid ? "android" : "web" +export const isWeb = !isNative diff --git a/apps/mobile/src/modules/feed-drawer/collection-panel.tsx b/apps/mobile/src/modules/feed-drawer/collection-panel.tsx index 1b59baf8dc..9610177583 100644 --- a/apps/mobile/src/modules/feed-drawer/collection-panel.tsx +++ b/apps/mobile/src/modules/feed-drawer/collection-panel.tsx @@ -24,7 +24,7 @@ export const CollectionPanel = () => { const insets = useSafeAreaInsets() return ( + + + ) + } // const swipeableRef: SwipeableRef = useRef(null) - if (!subscription || !feed) return null + if (!subscription && !feed) return null return ( // FIXME: Here leads to very serious performance issues, the frame rate of both the UI and JS threads has dropped @@ -89,9 +99,11 @@ export const SubscriptionItem = memo(({ id, className }: { id: string; className - {subscription.title || feed.title} + + {subscription?.title || feed.title} + {!!unreadCount && ( - {unreadCount} + {unreadCount} )} From 15a14c63b6e0fcdcd83dd7da750e8b00c784b9c4 Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 15 Jan 2025 16:59:08 +0800 Subject: [PATCH 37/93] fix: conditionally render feed preview and items based on entries availability - Updated the rendering logic in the share list component to display the feed preview and items only when there are entries available. This change enhances the user experience by preventing empty states and ensuring that the UI reflects the current data accurately. Signed-off-by: Innei --- .../client/pages/(main)/share/lists/[id]/index.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/server/client/pages/(main)/share/lists/[id]/index.tsx b/apps/server/client/pages/(main)/share/lists/[id]/index.tsx index b8ec3c1ce6..7b6d14e1f8 100644 --- a/apps/server/client/pages/(main)/share/lists/[id]/index.tsx +++ b/apps/server/client/pages/(main)/share/lists/[id]/index.tsx @@ -113,10 +113,14 @@ export function Component() {
)}
-
{t("feed.preview")}
-
- -
+ {!!list.data.entries?.length && ( + <> +
{t("feed.preview")}
+
+ +
+ + )} ) )} From fb2a4589eb4454a60f1ca6911075c5cfc41078fc Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 15 Jan 2025 17:16:01 +0800 Subject: [PATCH 38/93] fix(follow): fix follow modal overflow scrollbar position, fix #1238 Signed-off-by: Innei --- apps/renderer/src/modules/discover/feed-form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/renderer/src/modules/discover/feed-form.tsx b/apps/renderer/src/modules/discover/feed-form.tsx index b7403755cf..33738ead71 100644 --- a/apps/renderer/src/modules/discover/feed-form.tsx +++ b/apps/renderer/src/modules/discover/feed-form.tsx @@ -380,7 +380,7 @@ const FeedInnerForm = ({ )} /> -
+
{isSubscribed && ( diff --git a/apps/renderer/src/modules/boost/modal.tsx b/apps/renderer/src/modules/boost/modal.tsx index 5b4f962f5c..a0f7d7fc4b 100644 --- a/apps/renderer/src/modules/boost/modal.tsx +++ b/apps/renderer/src/modules/boost/modal.tsx @@ -13,6 +13,7 @@ import { useFeedById } from "~/store/feed" import { feedIconSelector } from "~/store/feed/selector" import { FeedIcon } from "../feed/feed-icon" +import { useTOTPModalWrapper } from "../profile/hooks" import { BoostProgress } from "./boost-progress" import { BoostingContributors } from "./boosting-contributors" import { LevelBenefits } from "./level-benefits" @@ -33,11 +34,12 @@ export const BoostModalContent = ({ feedId }: { feedId: string }) => { const { data: boostStatus, isLoading } = useBoostStatusQuery(feedId) const boostFeedMutation = useBoostFeedMutation() const { dismiss } = useCurrentModal() + const present = useTOTPModalWrapper(boostFeedMutation.mutateAsync) const handleBoost = useCallback(() => { if (boostFeedMutation.isPending) return - boostFeedMutation.mutate({ feedId, amount: amountBigInt.toString() }) - }, [amountBigInt, boostFeedMutation, feedId]) + present({ feedId, amount: amountBigInt.toString() }) + }, [amountBigInt, boostFeedMutation.isPending, feedId, present]) const feed = useFeedById(feedId, feedIconSelector) diff --git a/apps/renderer/src/modules/discover/list-form.tsx b/apps/renderer/src/modules/discover/list-form.tsx index 696f7d0504..ccfb346d17 100644 --- a/apps/renderer/src/modules/discover/list-form.tsx +++ b/apps/renderer/src/modules/discover/list-form.tsx @@ -37,6 +37,7 @@ import { useListById } from "~/store/list" import { useSubscriptionByFeedId } from "~/store/subscription" import { feedUnreadActions } from "~/store/unread" +import { useTOTPModalWrapper } from "../profile/hooks" import { ViewSelectorRadioGroup } from "../shared/ViewSelectorRadioGroup" const formSchema = z.object({ @@ -251,8 +252,13 @@ const ListInnerForm = ({ }, }) + const preset = useTOTPModalWrapper(followMutation.mutateAsync) function onSubmit(values: z.infer) { - followMutation.mutate(values) + if (isSubscribed) { + followMutation.mutate(values) + } else { + preset(values) + } } const t = useI18n() diff --git a/apps/renderer/src/modules/power/my-wallet-section/withdraw.tsx b/apps/renderer/src/modules/power/my-wallet-section/withdraw.tsx index 33298c795b..54f1724339 100644 --- a/apps/renderer/src/modules/power/my-wallet-section/withdraw.tsx +++ b/apps/renderer/src/modules/power/my-wallet-section/withdraw.tsx @@ -23,6 +23,7 @@ import { useModalStack } from "~/components/ui/modal/stacked/hooks" import { useAuthQuery } from "~/hooks/common/useBizQuery" import { apiClient } from "~/lib/api-fetch" import { defineQuery } from "~/lib/defineQuery" +import { useTOTPModalWrapper } from "~/modules/profile/hooks" import { Balance } from "~/modules/wallet/balance" import { useWallet, wallet as walletActions } from "~/queries/wallet" @@ -88,9 +89,10 @@ const WithdrawModalContent = ({ dismiss }: { dismiss: () => void }) => { }) }, }) + const present = useTOTPModalWrapper(mutation.mutateAsync, { force: true }) const onSubmit = (values: z.infer) => { - mutation.mutate(values) + present(values) } useEffect(() => { diff --git a/apps/renderer/src/modules/profile/hooks.ts b/apps/renderer/src/modules/profile/hooks.ts index 49ff9ca9cb..7aa87b1dc3 100644 --- a/apps/renderer/src/modules/profile/hooks.ts +++ b/apps/renderer/src/modules/profile/hooks.ts @@ -1,16 +1,22 @@ import { isMobile } from "@follow/components/hooks/useMobile.js" import { capitalizeFirstLetter } from "@follow/utils/utils" import { createElement, lazy, useCallback } from "react" +import { useTranslation } from "react-i18next" +import { toast } from "sonner" import { parse } from "tldts" +import { useWhoami } from "~/atoms/user" import { useAsyncModal } from "~/components/ui/modal/helper/use-async-modal" import { PlainModal } from "~/components/ui/modal/stacked/custom-modal" import { useModalStack } from "~/components/ui/modal/stacked/hooks" import { useAuthQuery } from "~/hooks/common" import { apiClient } from "~/lib/api-fetch" import { defineQuery } from "~/lib/defineQuery" +import { getFetchErrorInfo } from "~/lib/error-parser" import { users } from "~/queries/users" +import { TOTPForm, TwoFactorForm } from "./two-factor" + const LazyUserProfileModalContent = lazy(() => import("./user-profile-modal").then((mod) => ({ default: mod.UserProfileModalContent })), ) @@ -98,3 +104,55 @@ export const usePresentUserProfileModal = (variant: Variant = "dialog") => { [present, presentAsync, variant], ) } + +export function useTOTPModalWrapper( + callback: (input: T) => Promise, + options?: { force?: boolean }, +) { + const { present } = useModalStack() + const { t } = useTranslation("settings") + const user = useWhoami() + return useCallback( + async (input: T) => { + const presentTOTPModal = () => { + if (!user?.twoFactorEnabled) { + toast.error(t("profile.two_factor.enable_notice")) + present({ + title: t("profile.two_factor.enable"), + content: TwoFactorForm, + }) + return + } + + present({ + title: t("profile.totp_code.title"), + content: ({ dismiss }) => { + return createElement(TOTPForm, { + async onSubmitMutationFn(values) { + await callback({ + ...input, + TOTPCode: values.code, + }) + dismiss() + }, + }) + }, + }) + } + + if (options?.force) { + presentTOTPModal() + } + + try { + await callback(input) + } catch (error) { + const { code } = getFetchErrorInfo(error as Error) + if (code === 4008) { + presentTOTPModal() + } + } + }, + [callback, options?.force, present, t, user?.twoFactorEnabled], + ) +} diff --git a/apps/renderer/src/modules/profile/two-factor.tsx b/apps/renderer/src/modules/profile/two-factor.tsx new file mode 100644 index 0000000000..b195dd9021 --- /dev/null +++ b/apps/renderer/src/modules/profile/two-factor.tsx @@ -0,0 +1,279 @@ +import { Button } from "@follow/components/ui/button/index.js" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@follow/components/ui/form/index.jsx" +import { Input, InputOTP, InputOTPGroup, InputOTPSlot } from "@follow/components/ui/input/index.js" +import { Label } from "@follow/components/ui/label/index.js" +import { twoFactor } from "@follow/shared/auth" +import { zodResolver } from "@hookform/resolvers/zod" +import { useMutation } from "@tanstack/react-query" +import { m, useAnimation } from "framer-motion" +import { useState } from "react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import QRCode from "react-qr-code" +import { toast } from "sonner" +import { z } from "zod" + +import { setWhoami, useWhoami } from "~/atoms/user" +import { useCurrentModal, useModalStack } from "~/components/ui/modal/stacked/hooks" +import { getFetchErrorInfo } from "~/lib/error-parser" +import { useHasPassword } from "~/queries/auth" + +import { NoPasswordHint } from "./update-password-form" + +const passwordSchema = z.string().min(8).max(128) +const totpCodeSchema = z.string().length(6).regex(/^\d+$/) + +const passwordFormSchema = z.object({ + password: passwordSchema, +}) +type PasswordFormValues = z.infer + +const totpFormSchema = z.object({ + code: totpCodeSchema, +}) +type TOTPFormValues = z.infer + +type PasswordFormProps = { + onSubmitMutationFn: (values: V) => Promise + message?: { + placeholder?: string + label?: string + } + onSuccess?: () => void +} + +const shakeVariants = { + shake: { + x: [0, -2, 2, -2, 2, 0], + transition: { + duration: 0.5, + }, + }, + reset: { + x: 0, + }, +} + +export function TOTPForm({ + message, + onSubmitMutationFn, + onSuccess, +}: PasswordFormProps) { + const { t } = useTranslation("settings") + const controls = useAnimation() + + const form = useForm({ + resolver: zodResolver(totpFormSchema), + defaultValues: { code: "" }, + }) + + const updateMutation = useMutation({ + mutationFn: onSubmitMutationFn, + onError: (error) => { + const { code } = getFetchErrorInfo(error) + if (error.message === "invalid two factor authentication" || code === 4007) { + form.resetField("code" as any) + form.setError("code" as any, { + type: "manual", + message: t("profile.totp_code.invalid"), + }) + controls.start("shake") + } + }, + onSuccess, + }) + + function onSubmit(values: TOTPFormValues) { + updateMutation.mutate(values) + } + + return ( +
+ + ( + + + {message?.label ?? t("profile.totp_code.label")} + + + + form.handleSubmit(onSubmit)()} + {...field} + > + + + + + + + + + + + + + + )} + /> + + + ) +} + +export function PasswordForm({ + message, + onSubmitMutationFn, + onSuccess, +}: PasswordFormProps) { + const { t } = useTranslation("settings") + + const form = useForm({ + resolver: zodResolver(passwordFormSchema), + defaultValues: { password: "" }, + }) + + const updateMutation = useMutation({ + mutationFn: onSubmitMutationFn, + onError: (error) => { + toast.error(error.message) + }, + onSuccess, + }) + + function onSubmit(values: PasswordFormValues) { + updateMutation.mutate(values) + } + + return ( +
+ + ( + + + {message?.label ?? t("profile.current_password.label")} + + + + + + + )} + /> + +
+ +
+ + + ) +} + +export const TwoFactorForm = () => { + const { t } = useTranslation("settings") + const modal = useCurrentModal() + const user = useWhoami() + const [totpURI, setTotpURI] = useState("") + return totpURI ? ( +
+
+ +
+ { + const res = await twoFactor.verifyTotp({ code: values.code }) + if (res.error) { + throw new Error(res.error.message) + } + toast.success(t("profile.two_factor.enabled")) + modal.dismiss() + setWhoami((prev) => { + if (!prev) return prev + return { ...prev, twoFactorEnabled: true } + }) + }} + /> +
+ ) : ( + { + const res = user?.twoFactorEnabled + ? await twoFactor.disable({ password: values.password }) + : await twoFactor.enable({ password: values.password }) + if (res.error) { + throw new Error(res.error.message) + } + if ("totpURI" in res.data) { + setTotpURI(res.data?.totpURI ?? "") + } else { + toast.success(t("profile.two_factor.disabled")) + modal.dismiss() + setWhoami((prev) => { + if (!prev) return prev + return { ...prev, twoFactorEnabled: false } + }) + } + }} + /> + ) +} + +export function TwoFactor() { + const { t } = useTranslation("settings") + const { present } = useModalStack() + const user = useWhoami() + const actionTitle = user?.twoFactorEnabled + ? t("profile.two_factor.disable") + : t("profile.two_factor.enable") + + const { data: hasPassword, isLoading } = useHasPassword() + + return ( +
+ + {isLoading ? null : hasPassword ? ( + + ) : ( + + )} +
+ ) +} diff --git a/apps/renderer/src/modules/profile/update-password-form.tsx b/apps/renderer/src/modules/profile/update-password-form.tsx index 04ef750929..8405f7919e 100644 --- a/apps/renderer/src/modules/profile/update-password-form.tsx +++ b/apps/renderer/src/modules/profile/update-password-form.tsx @@ -9,10 +9,11 @@ import { import { Input } from "@follow/components/ui/input/index.js" import { Label } from "@follow/components/ui/label/index.js" import { changePassword } from "@follow/shared/auth" +import { env } from "@follow/shared/env" import { zodResolver } from "@hookform/resolvers/zod" import { useMutation } from "@tanstack/react-query" import { useForm } from "react-hook-form" -import { useTranslation } from "react-i18next" +import { Trans, useTranslation } from "react-i18next" import { toast } from "sonner" import { z } from "zod" @@ -133,24 +134,45 @@ export const UpdatePasswordForm = () => { const { t } = useTranslation("settings") const { present } = useModalStack() - if (isLoading || !hasPassword) { - return null - } return ( -
- - +
+ + {isLoading ? null : hasPassword ? ( + + ) : ( + + )}
) } + +export const NoPasswordHint = ({ i18nKey }: { i18nKey: string }) => { + return ( +

+ + ), + }} + /> +

+ ) +} diff --git a/apps/renderer/src/modules/rsshub/set-modal-content.tsx b/apps/renderer/src/modules/rsshub/set-modal-content.tsx index 1c8f233853..5a0fa58813 100644 --- a/apps/renderer/src/modules/rsshub/set-modal-content.tsx +++ b/apps/renderer/src/modules/rsshub/set-modal-content.tsx @@ -22,6 +22,8 @@ import { UserAvatar } from "~/modules/user/UserAvatar" import { Queries } from "~/queries" import { useSetRSSHubMutation } from "~/queries/rsshub" +import { useTOTPModalWrapper } from "../profile/hooks" + export function SetModalContent({ dismiss, instance, @@ -31,6 +33,7 @@ export function SetModalContent({ }) { const { t } = useTranslation("settings") const setRSSHubMutation = useSetRSSHubMutation() + const preset = useTOTPModalWrapper(setRSSHubMutation.mutateAsync) const details = useAuthQuery(Queries.rsshub.get({ id: instance.id })) const hasPurchase = !!details.data?.purchase const price = instance.ownerUserId === whoami()?.id ? 0 : instance.price @@ -52,7 +55,7 @@ export function SetModalContent({ const months = form.watch("months") const onSubmit = (data: z.infer) => { - setRSSHubMutation.mutate({ id: instance.id, durationInMonths: data.months }) + preset({ id: instance.id, durationInMonths: data.months }) } useEffect(() => { diff --git a/apps/renderer/src/modules/settings/tabs/invitations.tsx b/apps/renderer/src/modules/settings/tabs/invitations.tsx index 2f0b92c084..4801708cc4 100644 --- a/apps/renderer/src/modules/settings/tabs/invitations.tsx +++ b/apps/renderer/src/modules/settings/tabs/invitations.tsx @@ -27,7 +27,7 @@ import { useModalStack } from "~/components/ui/modal/stacked/hooks" import { useAuthQuery } from "~/hooks/common" import { apiClient } from "~/lib/api-fetch" import { toastFetchError } from "~/lib/error-parser" -import { usePresentUserProfileModal } from "~/modules/profile/hooks" +import { usePresentUserProfileModal, useTOTPModalWrapper } from "~/modules/profile/hooks" import { UserAvatar } from "~/modules/user/UserAvatar" import { Queries } from "~/queries" @@ -171,7 +171,8 @@ const ConfirmModalContent = ({ dismiss }: { dismiss: () => void }) => { const { t } = useTranslation("settings") const newInvitation = useMutation({ mutationKey: ["newInvitation"], - mutationFn: () => apiClient.invitations.new.$post(), + mutationFn: (values: Parameters[0]["json"]) => + apiClient.invitations.new.$post({ json: values }), onError(err) { toastFetchError(err) }, @@ -182,6 +183,7 @@ const ConfirmModalContent = ({ dismiss }: { dismiss: () => void }) => { dismiss() }, }) + const preset = useTOTPModalWrapper(newInvitation.mutateAsync) const serverConfigs = useServerConfigs() @@ -206,7 +208,7 @@ const ConfirmModalContent = ({ dismiss }: { dismiss: () => void }) => { -
diff --git a/apps/renderer/src/modules/wallet/tip-modal.tsx b/apps/renderer/src/modules/wallet/tip-modal.tsx index 5704178509..fb09e7c9cb 100644 --- a/apps/renderer/src/modules/wallet/tip-modal.tsx +++ b/apps/renderer/src/modules/wallet/tip-modal.tsx @@ -15,6 +15,7 @@ import { UserAvatar } from "~/modules/user/UserAvatar" import { useWallet, useWalletTipMutation } from "~/queries/wallet" import { useFeedClaimModal } from "../claim" +import { useTOTPModalWrapper } from "../profile/hooks" import { Balance } from "./balance" const DEFAULT_RECOMMENDED_TIP = 10 @@ -56,6 +57,7 @@ const TipModalContent_: FC<{ const balanceBigInt = cPowerBigInt + dPowerBigInt const tipMutation = useWalletTipMutation() + const present = useTOTPModalWrapper(tipMutation.mutateAsync) const [amount, setAmount] = useState(DEFAULT_RECOMMENDED_TIP) @@ -182,7 +184,7 @@ const TipModalContent_: FC<{ isLoading={tipMutation.isPending} onClick={() => { if (tipMutation.isPending) return - tipMutation.mutate({ + present({ entryId, amount: amountBigInt.toString() as "1000000000000000000" | "2000000000000000000", }) diff --git a/apps/renderer/src/pages/(main)/(layer)/(subview)/rsshub/index.tsx b/apps/renderer/src/pages/(main)/(layer)/(subview)/rsshub/index.tsx index aceab81ed7..9d5413f87a 100644 --- a/apps/renderer/src/pages/(main)/(layer)/(subview)/rsshub/index.tsx +++ b/apps/renderer/src/pages/(main)/(layer)/(subview)/rsshub/index.tsx @@ -17,6 +17,7 @@ import { whoami } from "~/atoms/user" import { useModalStack } from "~/components/ui/modal/stacked/hooks" import { useAuthQuery } from "~/hooks/common" import { useSubViewTitle } from "~/modules/app-layout/subview/hooks" +import { useTOTPModalWrapper } from "~/modules/profile/hooks" import { AddModalContent } from "~/modules/rsshub/add-modal-content" import { SetModalContent } from "~/modules/rsshub/set-modal-content" import { UserAvatar } from "~/modules/user/UserAvatar" @@ -63,6 +64,7 @@ function List({ data }: { data?: RSSHubModel[] }) { const me = whoami() const status = useAuthQuery(Queries.rsshub.status()) const setRSSHubMutation = useSetRSSHubMutation() + const presetTOTPModal = useTOTPModalWrapper(setRSSHubMutation.mutateAsync) const { present } = useModalStack() return ( @@ -112,9 +114,7 @@ function List({ data }: { data?: RSSHubModel[] }) { )} {!!status?.data?.usage?.rsshubId && ( - + )} diff --git a/apps/renderer/src/pages/settings/(settings)/profile.tsx b/apps/renderer/src/pages/settings/(settings)/profile.tsx index 077e7393b3..496c695273 100644 --- a/apps/renderer/src/pages/settings/(settings)/profile.tsx +++ b/apps/renderer/src/pages/settings/(settings)/profile.tsx @@ -3,6 +3,7 @@ import { Divider } from "@follow/components/ui/divider/Divider.js" import { AccountManagement } from "~/modules/profile/account-management" import { EmailManagement } from "~/modules/profile/email-management" import { ProfileSettingForm } from "~/modules/profile/profile-setting-form" +import { TwoFactor } from "~/modules/profile/two-factor" import { UpdatePasswordForm } from "~/modules/profile/update-password-form" import { SettingsTitle } from "~/modules/settings/title" import { defineSettingPageData } from "~/modules/settings/utils" @@ -25,8 +26,11 @@ export function Component() { - - +
+ + + +
) diff --git a/apps/server/client/modules/login/index.tsx b/apps/server/client/modules/login/index.tsx index c231e94088..1b3d0de3f5 100644 --- a/apps/server/client/modules/login/index.tsx +++ b/apps/server/client/modules/login/index.tsx @@ -16,7 +16,7 @@ import { import { Input } from "@follow/components/ui/input/index.js" import { LoadingCircle } from "@follow/components/ui/loading/index.jsx" import { authProvidersConfig } from "@follow/constants" -import { createSession, loginHandler, signOut } from "@follow/shared/auth" +import { createSession, loginHandler, signOut, twoFactor } from "@follow/shared/auth" import { DEEPLINK_SCHEME } from "@follow/shared/constants" import { cn } from "@follow/utils/utils" import { zodResolver } from "@hookform/resolvers/zod" @@ -221,18 +221,10 @@ export function Login() { const formSchema = z.object({ email: z.string().email(), - password: z.string().max(128), + password: z.string().min(8).max(128), + code: z.string().length(6).regex(/^\d+$/).optional(), }) -async function onSubmit(values: z.infer) { - const res = await loginHandler("credential", "app", values) - if (res?.error) { - toast.error(res.error.message) - return - } - queryClient.invalidateQueries({ queryKey: ["auth", "session"] }) -} - function LoginWithPassword() { const { t } = useTranslation("external") const form = useForm>({ @@ -242,9 +234,37 @@ function LoginWithPassword() { password: "", }, }) - const { isValid } = form.formState + const [needTwoFactor, setNeedTwoFactor] = useState(false) + + const { isValid, isSubmitting } = form.formState const navigate = useNavigate() + async function onSubmit(values: z.infer) { + if (needTwoFactor && values.code) { + const res = await twoFactor.verifyTotp({ code: values.code }) + if (res?.error) { + toast.error(res.error.message) + } else { + queryClient.invalidateQueries({ queryKey: ["auth", "session"] }) + } + return + } + + const res = await loginHandler("credential", "app", values) + if (res?.error) { + toast.error(res.error.message) + return + } + if ((res?.data as any)?.twoFactorRedirect) { + setNeedTwoFactor(true) + form.setValue("code", "") + form.setFocus("code") + return + } else { + queryClient.invalidateQueries({ queryKey: ["auth", "session"] }) + } + } + return (
@@ -277,15 +297,31 @@ function LoginWithPassword() { {t("login.forget_password.note")} + {needTwoFactor && ( + ( + + {t("login.two_factor.code")} + + + + + + )} + /> + )}
diff --git a/locales/settings/en.json b/locales/settings/en.json index 5075d76ef3..1c3d65e553 100644 --- a/locales/settings/en.json +++ b/locales/settings/en.json @@ -97,6 +97,11 @@ "appearance.zen_mode.description": "Zen mode is an undisturbed reading mode that allows you to focus on the content without any interference. Enabling Zen mode will hide the sidebar.", "appearance.zen_mode.label": "Zen mode", "common.give_star": "Love our product? Give us a star on GitHub!", + "customizeToolbar.more_actions.description": "Will be shown in the dropdown menu", + "customizeToolbar.more_actions.title": "More Actions", + "customizeToolbar.quick_actions.description": "Customize and reorder your frequently used actions", + "customizeToolbar.quick_actions.title": "Quick Actions", + "customizeToolbar.reset_layout": "Reset to Default Layout", "customizeToolbar.title": "Customize Toolbar", "data_control.app_cache_limit.description": "The maximum size of the app cache. Once the cache reaches this size, the oldest items will be deleted to free up space.", "data_control.app_cache_limit.label": "App Cache Limit", diff --git a/locales/settings/zh-CN.json b/locales/settings/zh-CN.json index 880a4d659c..25f0f6559b 100644 --- a/locales/settings/zh-CN.json +++ b/locales/settings/zh-CN.json @@ -97,6 +97,11 @@ "appearance.zen_mode.description": "禅定模式是一种不受干扰的阅读模式,让你能够专注于内容而不受任何干扰。启用禅定模式将会隐藏侧边栏。", "appearance.zen_mode.label": "禅定模式", "common.give_star": "喜欢我们的产品吗? 在 GitHub 上给我们「标星」吧!", + "customizeToolbar.more_actions.description": "将显示在下拉菜单中", + "customizeToolbar.more_actions.title": "更多操作", + "customizeToolbar.quick_actions.description": "自定义并重新排列您常用的操作", + "customizeToolbar.quick_actions.title": "快捷操作", + "customizeToolbar.reset_layout": "重置为默认布局", "customizeToolbar.title": "自定义工具栏", "data_control.app_cache_limit.description": "应用缓存大小的上限。一旦缓存达到此上限,最早的项目将被删除以释放空间。", "data_control.app_cache_limit.label": "应用缓存限制", From 61b3371848acc8eb5059c4692a94a71862890566 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Fri, 17 Jan 2025 10:29:07 +0800 Subject: [PATCH 65/93] chore: init entries table --- .../mobile/drizzle/0005_tense_sleepwalker.sql | 18 + apps/mobile/drizzle/0006_exotic_kid_colt.sql | 3 + apps/mobile/drizzle/meta/0005_snapshot.json | 473 +++++++++++++++++ apps/mobile/drizzle/meta/0006_snapshot.json | 494 ++++++++++++++++++ apps/mobile/drizzle/meta/_journal.json | 14 + apps/mobile/drizzle/migrations.js | 4 + apps/mobile/src/database/schemas/index.ts | 26 + apps/mobile/src/database/schemas/types.ts | 28 + 8 files changed, 1060 insertions(+) create mode 100644 apps/mobile/drizzle/0005_tense_sleepwalker.sql create mode 100644 apps/mobile/drizzle/0006_exotic_kid_colt.sql create mode 100644 apps/mobile/drizzle/meta/0005_snapshot.json create mode 100644 apps/mobile/drizzle/meta/0006_snapshot.json diff --git a/apps/mobile/drizzle/0005_tense_sleepwalker.sql b/apps/mobile/drizzle/0005_tense_sleepwalker.sql new file mode 100644 index 0000000000..9a1fb51a38 --- /dev/null +++ b/apps/mobile/drizzle/0005_tense_sleepwalker.sql @@ -0,0 +1,18 @@ +CREATE TABLE `entries` ( + `id` text PRIMARY KEY NOT NULL, + `title` text, + `url` text, + `content` text, + `description` text, + `guid` text NOT NULL, + `author` text, + `author_url` text, + `author_avatar` text, + `inserted_at` integer NOT NULL, + `published_at` integer NOT NULL, + `media` text, + `categories` text, + `attachments` text, + `extra` text, + `language` text +); diff --git a/apps/mobile/drizzle/0006_exotic_kid_colt.sql b/apps/mobile/drizzle/0006_exotic_kid_colt.sql new file mode 100644 index 0000000000..644d2175eb --- /dev/null +++ b/apps/mobile/drizzle/0006_exotic_kid_colt.sql @@ -0,0 +1,3 @@ +ALTER TABLE `entries` ADD `feed_id` text;--> statement-breakpoint +ALTER TABLE `entries` ADD `inbox_handle` text;--> statement-breakpoint +ALTER TABLE `entries` ADD `read` integer; \ No newline at end of file diff --git a/apps/mobile/drizzle/meta/0005_snapshot.json b/apps/mobile/drizzle/meta/0005_snapshot.json new file mode 100644 index 0000000000..a6f38f857b --- /dev/null +++ b/apps/mobile/drizzle/meta/0005_snapshot.json @@ -0,0 +1,473 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "babbc945-5d80-431e-b106-da8cfac9f77d", + "prevId": "fc790c48-4ade-4648-a1b4-0a3e9faf65c5", + "tables": { + "entries": { + "name": "entries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guid": { + "name": "guid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_url": { + "name": "author_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_avatar": { + "name": "author_avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inserted_at": { + "name": "inserted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "media": { + "name": "media", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "categories": { + "name": "categories", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attachments": { + "name": "attachments", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "extra": { + "name": "extra", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "feeds": { + "name": "feeds", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_at": { + "name": "error_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "site_url": { + "name": "site_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inboxes": { + "name": "inboxes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "lists": { + "name": "lists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "feed_ids": { + "name": "feed_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "view": { + "name": "view", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fee": { + "name": "fee", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "subscriptions": { + "name": "subscriptions", + "columns": { + "feed_id": { + "name": "feed_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "list_id": { + "name": "list_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inbox_id": { + "name": "inbox_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "view": { + "name": "view", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_private": { + "name": "is_private", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "unread": { + "name": "unread", + "columns": { + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_me": { + "name": "is_me", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/apps/mobile/drizzle/meta/0006_snapshot.json b/apps/mobile/drizzle/meta/0006_snapshot.json new file mode 100644 index 0000000000..4bc16f7b88 --- /dev/null +++ b/apps/mobile/drizzle/meta/0006_snapshot.json @@ -0,0 +1,494 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "9f943ca5-ddaf-4916-98cd-d3e3f7ead201", + "prevId": "babbc945-5d80-431e-b106-da8cfac9f77d", + "tables": { + "entries": { + "name": "entries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guid": { + "name": "guid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_url": { + "name": "author_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_avatar": { + "name": "author_avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inserted_at": { + "name": "inserted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "media": { + "name": "media", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "categories": { + "name": "categories", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attachments": { + "name": "attachments", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "extra": { + "name": "extra", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "feed_id": { + "name": "feed_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inbox_handle": { + "name": "inbox_handle", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "read": { + "name": "read", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "feeds": { + "name": "feeds", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_at": { + "name": "error_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "site_url": { + "name": "site_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inboxes": { + "name": "inboxes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "lists": { + "name": "lists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "feed_ids": { + "name": "feed_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "view": { + "name": "view", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fee": { + "name": "fee", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "subscriptions": { + "name": "subscriptions", + "columns": { + "feed_id": { + "name": "feed_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "list_id": { + "name": "list_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inbox_id": { + "name": "inbox_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "view": { + "name": "view", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_private": { + "name": "is_private", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "unread": { + "name": "unread", + "columns": { + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_me": { + "name": "is_me", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/apps/mobile/drizzle/meta/_journal.json b/apps/mobile/drizzle/meta/_journal.json index 1a807c5b3d..e6a3090c50 100644 --- a/apps/mobile/drizzle/meta/_journal.json +++ b/apps/mobile/drizzle/meta/_journal.json @@ -36,6 +36,20 @@ "when": 1735137187415, "tag": "0004_majestic_thunderbolt_ross", "breakpoints": true + }, + { + "idx": 5, + "version": "6", + "when": 1737080611316, + "tag": "0005_tense_sleepwalker", + "breakpoints": true + }, + { + "idx": 6, + "version": "6", + "when": 1737080887634, + "tag": "0006_exotic_kid_colt", + "breakpoints": true } ] } diff --git a/apps/mobile/drizzle/migrations.js b/apps/mobile/drizzle/migrations.js index 70c732a848..49ecb4cf9d 100644 --- a/apps/mobile/drizzle/migrations.js +++ b/apps/mobile/drizzle/migrations.js @@ -5,6 +5,8 @@ import m0001 from "./0001_bored_hobgoblin.sql" import m0002 from "./0002_smart_power_man.sql" import m0003 from "./0003_known_roland_deschain.sql" import m0004 from "./0004_majestic_thunderbolt_ross.sql" +import m0005 from "./0005_tense_sleepwalker.sql" +import m0006 from "./0006_exotic_kid_colt.sql" import journal from "./meta/_journal.json" export default { @@ -15,5 +17,7 @@ export default { m0002, m0003, m0004, + m0005, + m0006, }, } diff --git a/apps/mobile/src/database/schemas/index.ts b/apps/mobile/src/database/schemas/index.ts index 600ec76c07..7b65634fd3 100644 --- a/apps/mobile/src/database/schemas/index.ts +++ b/apps/mobile/src/database/schemas/index.ts @@ -1,6 +1,8 @@ import type { FeedViewType } from "@follow/constants" import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" +import type { AttachmentsModel, ExtraModel, MediaModel } from "./types" + export const feedsTable = sqliteTable("feeds", { id: text("id").primaryKey(), title: text("title"), @@ -57,3 +59,27 @@ export const usersTable = sqliteTable("users", { image: text("image"), isMe: integer("is_me").notNull(), }) + +export const entriesTable = sqliteTable("entries", { + id: text("id").primaryKey(), + title: text("title"), + url: text("url"), + content: text("content"), + description: text("description"), + guid: text("guid").notNull(), + author: text("author"), + authorUrl: text("author_url"), + authorAvatar: text("author_avatar"), + insertedAt: integer("inserted_at", { mode: "timestamp" }).notNull(), + publishedAt: integer("published_at", { mode: "timestamp" }).notNull(), + media: text("media", { mode: "json" }).$type(), + categories: text("categories", { mode: "json" }).$type(), + attachments: text("attachments", { mode: "json" }).$type(), + extra: text("extra", { mode: "json" }).$type(), + language: text("language"), + + feedId: text("feed_id"), + + inboxHandle: text("inbox_handle"), + read: integer("read", { mode: "boolean" }), +}) diff --git a/apps/mobile/src/database/schemas/types.ts b/apps/mobile/src/database/schemas/types.ts index 12fefcc6ad..5c51761198 100644 --- a/apps/mobile/src/database/schemas/types.ts +++ b/apps/mobile/src/database/schemas/types.ts @@ -1,4 +1,5 @@ import type { + entriesTable, feedsTable, inboxesTable, listsTable, @@ -18,3 +19,30 @@ export type ListSchema = typeof listsTable.$inferSelect export type UnreadSchema = typeof unreadTable.$inferSelect export type UserSchema = typeof usersTable.$inferSelect + +export type EntrySchema = typeof entriesTable.$inferSelect + +export type MediaModel = { + url: string + type: "photo" | "video" + preview_image_url?: string + width?: number + height?: number + blurhash?: string +} + +export type AttachmentsModel = { + url: string + duration_in_seconds?: number + mime_type?: string + size_in_bytes?: number + title?: string +} + +export type ExtraModel = { + links?: { + url: string + type: string + content_html?: string + }[] +} From 870905d7b7e97238bd2b4c435d78a7f9d019d6d8 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Fri, 17 Jan 2025 13:10:51 +0800 Subject: [PATCH 66/93] feat: sync icons --- apps/mobile/src/icons/arrow_left_cute_re.tsx | 34 ++++++++++++++++++++ icons/mgc/arrow_left_cute_re.svg | 1 + icons/mgc/information_cute_re copy.svg | 1 - icons/mgc/time_cute_re copy.svg | 1 - package.json | 1 + scripts/svg-to-rn.ts | 2 +- 6 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 apps/mobile/src/icons/arrow_left_cute_re.tsx create mode 100644 icons/mgc/arrow_left_cute_re.svg delete mode 100644 icons/mgc/information_cute_re copy.svg delete mode 100644 icons/mgc/time_cute_re copy.svg diff --git a/apps/mobile/src/icons/arrow_left_cute_re.tsx b/apps/mobile/src/icons/arrow_left_cute_re.tsx new file mode 100644 index 0000000000..ea884584ce --- /dev/null +++ b/apps/mobile/src/icons/arrow_left_cute_re.tsx @@ -0,0 +1,34 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface ArrowLeftCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const ArrowLeftCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: ArrowLeftCuteReIconProps) => { + return ( + + + + + + ) +} diff --git a/icons/mgc/arrow_left_cute_re.svg b/icons/mgc/arrow_left_cute_re.svg new file mode 100644 index 0000000000..e47da21a06 --- /dev/null +++ b/icons/mgc/arrow_left_cute_re.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/mgc/information_cute_re copy.svg b/icons/mgc/information_cute_re copy.svg deleted file mode 100644 index 1de4156655..0000000000 --- a/icons/mgc/information_cute_re copy.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/icons/mgc/time_cute_re copy.svg b/icons/mgc/time_cute_re copy.svg deleted file mode 100644 index fcab9f21e4..0000000000 --- a/icons/mgc/time_cute_re copy.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/package.json b/package.json index 64f752596e..286f1e2f45 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "publish": "electron-vite build && electron-forge publish", "start": "electron-vite preview", "sync:ab": "tsx scripts/pull-ab-flags.ts", + "sync:icons": "tsx scripts/svg-to-rn.ts && prettier --write apps/mobile/src/icons/**/*.tsx", "test": "CI=1 pnpm --recursive run test", "typecheck": "turbo typecheck", "update:main-hash": "tsx plugins/vite/generate-main-hash.ts" diff --git a/scripts/svg-to-rn.ts b/scripts/svg-to-rn.ts index 12306ffffa..88538bc1eb 100644 --- a/scripts/svg-to-rn.ts +++ b/scripts/svg-to-rn.ts @@ -59,7 +59,7 @@ interface ${componentName}Props { export const ${componentName} = ({ width = ${width}, height = ${height}, - color = "${DEFAULT_COLOR}", + ${pathElements.includes(`{color}`) ? `color = "${DEFAULT_COLOR}",` : ""} }: ${componentName}Props) => { return ( From 3ef5dea5d190ef81e91ae936f3d6c84898c569ac Mon Sep 17 00:00:00 2001 From: DIYgod Date: Fri, 17 Jan 2025 13:19:38 +0800 Subject: [PATCH 67/93] feat: smaller debug button --- apps/mobile/src/modules/debug/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/mobile/src/modules/debug/index.tsx b/apps/mobile/src/modules/debug/index.tsx index 11724b74ca..83962e184a 100644 --- a/apps/mobile/src/modules/debug/index.tsx +++ b/apps/mobile/src/modules/debug/index.tsx @@ -50,13 +50,13 @@ export const DebugButton = () => { From f5e1d33e2a42f1ce44788ec65d51d54f66056f29 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Fri, 17 Jan 2025 19:00:03 +0800 Subject: [PATCH 68/93] feat: add cookie to api fetch --- apps/mobile/src/lib/api-fetch.ts | 16 +++++++++++----- apps/mobile/src/screens/(headless)/login.tsx | 6 +++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/apps/mobile/src/lib/api-fetch.ts b/apps/mobile/src/lib/api-fetch.ts index 05e5f03bc6..ea80d3a3f7 100644 --- a/apps/mobile/src/lib/api-fetch.ts +++ b/apps/mobile/src/lib/api-fetch.ts @@ -1,7 +1,9 @@ /* eslint-disable no-console */ import type { AppType } from "@follow/shared" +import { router } from "expo-router" import { ofetch } from "ofetch" +import { getSessionToken } from "./cookie" import { getApiUrl } from "./env" const { hc } = require("hono/dist/cjs/client/client") as typeof import("hono/client") @@ -15,10 +17,10 @@ export const apiFetch = ofetch.create({ header.set("x-app-name", "Follow Mobile") - // const sessionToken = await getSessionToken() - // if (sessionToken.value) { - // // header.set("cookie", `better-auth.session_token=${sessionToken.value};`) - // } + const sessionToken = await getSessionToken() + if (sessionToken) { + header.set("cookie", `__Secure-better-auth.session_token=${sessionToken}`) + } if (__DEV__) { // Logger console.log(`---> ${options.method} ${request as string}`) @@ -42,7 +44,11 @@ export const apiFetch = ofetch.create({ if (__DEV__) { console.log(`<--- [Error] ${response.status} ${options.method} ${request as string}`) } - console.error(error) + if (response.status === 401) { + router.replace("/login") + } else { + console.error(error) + } }, }) diff --git a/apps/mobile/src/screens/(headless)/login.tsx b/apps/mobile/src/screens/(headless)/login.tsx index 0800db0570..c80da55523 100644 --- a/apps/mobile/src/screens/(headless)/login.tsx +++ b/apps/mobile/src/screens/(headless)/login.tsx @@ -1,12 +1,12 @@ import { Redirect } from "expo-router" -import { useAuthToken } from "@/src/lib/auth" import { Login } from "@/src/modules/login" +import { useWhoami } from "@/src/store/user/hooks" export default function LoginPage() { - const { data: token } = useAuthToken() + const whoami = useWhoami() - if (token) { + if (whoami?.id) { return } From 0fc90c7c280e8d1eb170a5aca2374d1bd29d3b97 Mon Sep 17 00:00:00 2001 From: Innei Date: Fri, 17 Jan 2025 20:16:58 +0800 Subject: [PATCH 69/93] feat(rn): adjust login page styles Signed-off-by: Innei --- .../components/common/AnimatedComponents.tsx | 1 + apps/mobile/src/modules/login/email.tsx | 74 +++++++++++++++---- apps/mobile/src/modules/login/index.tsx | 5 +- apps/mobile/src/modules/login/social.tsx | 2 +- .../src/modules/settings/routes/Profile.tsx | 38 +++++++++- 5 files changed, 100 insertions(+), 20 deletions(-) diff --git a/apps/mobile/src/components/common/AnimatedComponents.tsx b/apps/mobile/src/components/common/AnimatedComponents.tsx index 2bbd2ef4e6..602341839d 100644 --- a/apps/mobile/src/components/common/AnimatedComponents.tsx +++ b/apps/mobile/src/components/common/AnimatedComponents.tsx @@ -7,3 +7,4 @@ export const AnimatedTouchableOpacity = Animated.createAnimatedComponent(Touchab export const ReAnimatedPressable = Reanimated.createAnimatedComponent(Pressable) export const ReAnimatedScrollView = Reanimated.createAnimatedComponent(ScrollView) +export const ReAnimatedTouchableOpacity = Reanimated.createAnimatedComponent(TouchableOpacity) diff --git a/apps/mobile/src/modules/login/email.tsx b/apps/mobile/src/modules/login/email.tsx index c98ecffaea..f4d04205b2 100644 --- a/apps/mobile/src/modules/login/email.tsx +++ b/apps/mobile/src/modules/login/email.tsx @@ -1,14 +1,27 @@ import { zodResolver } from "@hookform/resolvers/zod" import { useMutation } from "@tanstack/react-query" +import { router } from "expo-router" +import { useEffect } from "react" import type { Control } from "react-hook-form" import { useController, useForm } from "react-hook-form" import type { TextInputProps } from "react-native" -import { ActivityIndicator, TextInput, TouchableOpacity, View } from "react-native" +import { ActivityIndicator, TextInput, View } from "react-native" +import { KeyboardController } from "react-native-keyboard-controller" +import { + interpolate, + interpolateColor, + useAnimatedStyle, + useSharedValue, + withTiming, +} from "react-native-reanimated" import { z } from "zod" +import { ReAnimatedPressable } from "@/src/components/common/AnimatedComponents" import { ThemedText } from "@/src/components/common/ThemedText" import { signIn } from "@/src/lib/auth" -import { accentColor } from "@/src/theme/colors" +import { setSessionToken } from "@/src/lib/cookie" +import { toast } from "@/src/lib/toast" +import { accentColor, useColor } from "@/src/theme/colors" const formSchema = z.object({ email: z.string().email(), @@ -18,10 +31,23 @@ const formSchema = z.object({ type FormValue = z.infer async function onSubmit(values: FormValue) { - await signIn.email({ - email: values.email, - password: values.password, - }) + await signIn + .email({ + email: values.email, + password: values.password, + }) + .then((user) => { + if (!user.data) { + toast.error("Login failed") + return + } + setSessionToken(user.data.token) + router.push("/(stack)/(tabs)") + }) + .catch((error) => { + console.error(error) + toast.error("Login failed") + }) } function Input({ @@ -59,10 +85,22 @@ export function EmailLogin() { mutationFn: onSubmit, }) + const login = handleSubmit((values) => submitMutation.mutate(values)) + + const disableColor = useColor("gray3") + + const canLogin = useSharedValue(0) + useEffect(() => { + canLogin.value = withTiming(submitMutation.isPending || !formState.isValid ? 1 : 0) + }, [submitMutation.isPending, formState.isValid, canLogin]) + const buttonStyle = useAnimatedStyle(() => ({ + opacity: interpolate(canLogin.value, [0, 1], [1, 0.5]), + backgroundColor: interpolateColor(canLogin.value, [1, 0], [disableColor, accentColor]), + })) + return ( - Account - + Password - - submitMutation.mutate(values))} - className="disabled:bg-gray-3 rounded-lg bg-accent p-3" + onPress={login} + className="mt-8 h-10 flex-row items-center justify-center rounded-lg" + style={buttonStyle} > {submitMutation.isPending ? ( ) : ( - Continue + Continue )} - + ) } diff --git a/apps/mobile/src/modules/login/index.tsx b/apps/mobile/src/modules/login/index.tsx index f98a3b6503..c7c087f0b8 100644 --- a/apps/mobile/src/modules/login/index.tsx +++ b/apps/mobile/src/modules/login/index.tsx @@ -16,12 +16,13 @@ export function Login() { }} accessible={false} > - - + + Login to Follow + diff --git a/apps/mobile/src/modules/login/social.tsx b/apps/mobile/src/modules/login/social.tsx index 631a88cd2c..babe586af2 100644 --- a/apps/mobile/src/modules/login/social.tsx +++ b/apps/mobile/src/modules/login/social.tsx @@ -49,7 +49,7 @@ export function SocialLogin() { return ( { if (!data?.[providerInfo.id]) return diff --git a/apps/mobile/src/modules/settings/routes/Profile.tsx b/apps/mobile/src/modules/settings/routes/Profile.tsx index f64faa7161..55782af92a 100644 --- a/apps/mobile/src/modules/settings/routes/Profile.tsx +++ b/apps/mobile/src/modules/settings/routes/Profile.tsx @@ -1,9 +1,41 @@ -import { Text, View } from "react-native" +import { Stack } from "expo-router" +import { Pressable, Text, View } from "react-native" +import { useAnimatedScrollHandler, useSharedValue } from "react-native-reanimated" +import { useSafeAreaInsets } from "react-native-safe-area-context" + +import { ReAnimatedScrollView } from "@/src/components/common/AnimatedComponents" +import { MingcuteLeftLineIcon } from "@/src/icons/mingcute_left_line" + +import { useSettingsNavigation } from "../hooks" +import { UserHeaderBanner } from "../UserHeaderBanner" export const ProfileScreen = () => { + const scrollY = useSharedValue(0) + const scrollHandler = useAnimatedScrollHandler((event) => { + scrollY.value = event.contentOffset.y + }) + + const insets = useSafeAreaInsets() + const settingNavigation = useSettingsNavigation() + // const { subscription } = useSubscription() return ( - - Account + + + + + + Account + + settingNavigation.goBack()} + className="absolute left-4" + style={{ top: insets.top }} + > + + ) } From accbf7fa601cacc6a9c7bed837f16b6e284a128a Mon Sep 17 00:00:00 2001 From: DIYgod Date: Fri, 17 Jan 2025 20:45:40 +0800 Subject: [PATCH 70/93] feat: use expo-secure-store and better-auth buildin getCookie for auth --- apps/mobile/app.config.ts | 1 + apps/mobile/package.json | 1 + apps/mobile/src/lib/api-fetch.ts | 13 ++----- apps/mobile/src/lib/auth.ts | 28 ++------------- apps/mobile/src/lib/cookie.ts | 35 ------------------- apps/mobile/src/screens/(headless)/debug.tsx | 18 +++++++--- .../mobile/src/screens/(headless)/webview.tsx | 35 +++++++++---------- pnpm-lock.yaml | 12 +++++++ 8 files changed, 47 insertions(+), 96 deletions(-) delete mode 100644 apps/mobile/src/lib/cookie.ts diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index ffc3512a26..ea2d31647a 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -91,6 +91,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ "expo-apple-authentication", [require("./scripts/with-follow-assets.js")], [require("./scripts/with-follow-app-delegate.js")], + "expo-secure-store", ], experiments: { typedRoutes: true, diff --git a/apps/mobile/package.json b/apps/mobile/package.json index c3e49948d0..e4a715d549 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -51,6 +51,7 @@ "expo-linear-gradient": "~14.0.1", "expo-linking": "~7.0.3", "expo-router": "4.0.11", + "expo-secure-store": "^14.0.1", "expo-sharing": "~13.0.0", "expo-splash-screen": "~0.29.18", "expo-sqlite": "15.0.3", diff --git a/apps/mobile/src/lib/api-fetch.ts b/apps/mobile/src/lib/api-fetch.ts index ea80d3a3f7..272a6cad3f 100644 --- a/apps/mobile/src/lib/api-fetch.ts +++ b/apps/mobile/src/lib/api-fetch.ts @@ -3,7 +3,7 @@ import type { AppType } from "@follow/shared" import { router } from "expo-router" import { ofetch } from "ofetch" -import { getSessionToken } from "./cookie" +import { getCookie } from "./auth" import { getApiUrl } from "./env" const { hc } = require("hono/dist/cjs/client/client") as typeof import("hono/client") @@ -13,20 +13,10 @@ export const apiFetch = ofetch.create({ baseURL: getApiUrl(), onRequest: async ({ options, request }) => { - const header = new Headers(options.headers) - - header.set("x-app-name", "Follow Mobile") - - const sessionToken = await getSessionToken() - if (sessionToken) { - header.set("cookie", `__Secure-better-auth.session_token=${sessionToken}`) - } if (__DEV__) { // Logger console.log(`---> ${options.method} ${request as string}`) } - - options.headers = header }, onRequestError: ({ error, request, options }) => { if (__DEV__) { @@ -60,6 +50,7 @@ export const apiClient = hc(getApiUrl(), { headers() { return { "X-App-Name": "Follow Mobile", + cookie: getCookie(), } }, }) diff --git a/apps/mobile/src/lib/auth.ts b/apps/mobile/src/lib/auth.ts index af13165539..7ae3e2ff1a 100644 --- a/apps/mobile/src/lib/auth.ts +++ b/apps/mobile/src/lib/auth.ts @@ -2,11 +2,9 @@ import { expoClient } from "@better-auth/expo/client" import { useQuery } from "@tanstack/react-query" import { createAuthClient } from "better-auth/react" import type * as better_call from "better-call" -import { parse } from "cookie-es" +import * as SecureStore from "expo-secure-store" import { getApiUrl } from "./env" -import { kv } from "./kv" -import { queryClient } from "./query-client" const storagePrefix = "follow_auth" export const cookieKey = `${storagePrefix}_cookie` @@ -22,17 +20,7 @@ const authClient = createAuthClient({ expoClient({ scheme: "follow", storagePrefix, - storage: { - setItem(key, value) { - kv.setSync(key, value) - if (key === cookieKey) { - queryClient.invalidateQueries({ queryKey: ["cookie"] }) - } - }, - getItem(key) { - return kv.getSync(key) - }, - }, + storage: SecureStore, }), ], }) @@ -54,18 +42,6 @@ export const useAuthProviders = () => { }) } -export const getSessionTokenFromCookie = () => { - const cookie = getCookie() - return cookie ? parse(cookie)[sessionTokenKey] : null -} - -export const useAuthToken = () => { - return useQuery({ - queryKey: ["cookie"], - queryFn: getSessionTokenFromCookie, - }) -} - // eslint-disable-next-line unused-imports/no-unused-vars declare const authPlugins: { id: "getProviders" diff --git a/apps/mobile/src/lib/cookie.ts b/apps/mobile/src/lib/cookie.ts deleted file mode 100644 index 60ffc2e9eb..0000000000 --- a/apps/mobile/src/lib/cookie.ts +++ /dev/null @@ -1,35 +0,0 @@ -import CookieManager from "@react-native-cookies/cookies" - -import { cookieKey, getSessionTokenFromCookie, sessionTokenKey } from "./auth" -import { getApiUrl } from "./env" -import { kv } from "./kv" - -export const setSessionToken = (token: string) => { - kv.setSync( - cookieKey, - JSON.stringify({ - [sessionTokenKey]: { - value: token, - expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365).toISOString(), - }, - }), - ) - - return CookieManager.set(getApiUrl(), { - name: sessionTokenKey, - value: token, - httpOnly: true, - secure: true, - }) -} - -export const getSessionToken = async () => { - const cookies = await CookieManager.get(getApiUrl()) - - return cookies[sessionTokenKey] || getSessionTokenFromCookie() -} - -export const clearSessionToken = async () => { - await kv.delete(cookieKey) - return CookieManager.clearAll() -} diff --git a/apps/mobile/src/screens/(headless)/debug.tsx b/apps/mobile/src/screens/(headless)/debug.tsx index 13cfca3c14..2538264ba9 100644 --- a/apps/mobile/src/screens/(headless)/debug.tsx +++ b/apps/mobile/src/screens/(headless)/debug.tsx @@ -2,6 +2,7 @@ import { sleep } from "@follow/utils" import * as Clipboard from "expo-clipboard" import * as FileSystem from "expo-file-system" import { Sitemap } from "expo-router/build/views/Sitemap" +import * as SecureStore from "expo-secure-store" import type { FC } from "react" import * as React from "react" import { useRef, useState } from "react" @@ -18,7 +19,7 @@ import { import { useSafeAreaInsets } from "react-native-safe-area-context" import { getDbPath } from "@/src/database" -import { clearSessionToken, getSessionToken, setSessionToken } from "@/src/lib/cookie" +import { cookieKey, getCookie, sessionTokenKey, signOut } from "@/src/lib/auth" import { loading } from "@/src/lib/loading" import { toast } from "@/src/lib/toast" @@ -44,14 +45,14 @@ export default function DebugPanel() { { title: "Get Current Session Token", onPress: async () => { - const token = await getSessionToken() - Alert.alert(`Current Session Token: ${token?.value}`) + const token = getCookie() + Alert.alert(`Current Session Token: ${token}`) }, }, { title: "Clear Session Token", onPress: async () => { - await clearSessionToken() + await signOut() Alert.alert("Session Token Cleared") }, }, @@ -178,7 +179,14 @@ const UserSessionSetting = () => { { - setSessionToken(input) + SecureStore.setItem( + cookieKey, + JSON.stringify({ + [sessionTokenKey]: { + value: input, + }, + }), + ) Alert.alert("Session Token Saved") }} > diff --git a/apps/mobile/src/screens/(headless)/webview.tsx b/apps/mobile/src/screens/(headless)/webview.tsx index 8553007f77..9eba5a925e 100644 --- a/apps/mobile/src/screens/(headless)/webview.tsx +++ b/apps/mobile/src/screens/(headless)/webview.tsx @@ -1,41 +1,38 @@ -import { Redirect } from "expo-router" -import { useEffect, useRef, useState } from "react" +import { useRef } from "react" import { TouchableOpacity, View } from "react-native" import { useSafeAreaInsets } from "react-native-safe-area-context" import type { WebView } from "react-native-webview" -import { FollowWebView } from "@/src/components/common/FollowWebView" import { BugCuteReIcon } from "@/src/icons/bug_cute_re" import { ExitCuteReIcon } from "@/src/icons/exit_cute_re" import { Refresh2CuteReIcon } from "@/src/icons/refresh_2_cute_re" import { World2CuteReIcon } from "@/src/icons/world_2_cute_re" -import { signOut, useAuthToken } from "@/src/lib/auth" -import { setSessionToken } from "@/src/lib/cookie" +import { signOut } from "@/src/lib/auth" export default function Index() { const webViewRef = useRef(null) const insets = useSafeAreaInsets() - const { data: token, isPending } = useAuthToken() + // const { data: token, isPending } = useAuthToken() - const [isCookieReady, setIsCookieReady] = useState(false) - useEffect(() => { - if (!token) { - return - } + // const [isCookieReady, setIsCookieReady] = useState(false) + // useEffect(() => { + // if (!token) { + // return + // } - setSessionToken(token).then(() => { - setIsCookieReady(true) - }) - }, [token]) + // // setSessionToken(token).then(() => { + // // setIsCookieReady(true) + // // }) + // }, [token]) - if (!token && !isPending) { - return - } + // if (!token && !isPending) { + // return + // } return ( - {isCookieReady && } + {/* {isCookieReady && } */} {__DEV__ && ( Date: Fri, 17 Jan 2025 21:00:11 +0800 Subject: [PATCH 71/93] fix(rn): login redirection --- apps/mobile/src/lib/auth.ts | 12 +++++++++++- apps/mobile/src/store/user/hooks.ts | 4 +++- apps/mobile/src/store/user/store.ts | 8 +++----- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/apps/mobile/src/lib/auth.ts b/apps/mobile/src/lib/auth.ts index 7ae3e2ff1a..33a8029acd 100644 --- a/apps/mobile/src/lib/auth.ts +++ b/apps/mobile/src/lib/auth.ts @@ -4,7 +4,9 @@ import { createAuthClient } from "better-auth/react" import type * as better_call from "better-call" import * as SecureStore from "expo-secure-store" +import { whoamiQueryKey } from "../store/user/hooks" import { getApiUrl } from "./env" +import { queryClient } from "./query-client" const storagePrefix = "follow_auth" export const cookieKey = `${storagePrefix}_cookie` @@ -20,7 +22,15 @@ const authClient = createAuthClient({ expoClient({ scheme: "follow", storagePrefix, - storage: SecureStore, + storage: { + setItem(key, value) { + SecureStore.setItem(key, value) + if (key === cookieKey) { + queryClient.invalidateQueries({ queryKey: whoamiQueryKey }) + } + }, + getItem: SecureStore.getItem, + }, }), ], }) diff --git a/apps/mobile/src/store/user/hooks.ts b/apps/mobile/src/store/user/hooks.ts index c1f12e6871..04fc3f41c2 100644 --- a/apps/mobile/src/store/user/hooks.ts +++ b/apps/mobile/src/store/user/hooks.ts @@ -2,9 +2,11 @@ import { useQuery } from "@tanstack/react-query" import { userSyncService, useUserStore } from "./store" +export const whoamiQueryKey = ["user", "whoami"] + export const usePrefetchSessionUser = () => { useQuery({ - queryKey: ["user", "whoami"], + queryKey: whoamiQueryKey, queryFn: () => userSyncService.whoami(), }) } diff --git a/apps/mobile/src/store/user/store.ts b/apps/mobile/src/store/user/store.ts index 971ec1067e..e69c781ebb 100644 --- a/apps/mobile/src/store/user/store.ts +++ b/apps/mobile/src/store/user/store.ts @@ -1,5 +1,5 @@ import type { UserSchema } from "@/src/database/schemas/types" -import { apiFetch } from "@/src/lib/api-fetch" +import { apiClient } from "@/src/lib/api-fetch" import { UserService } from "@/src/services/user" import { createImmerSetter, createTransaction, createZustandStore } from "../internal/helper" @@ -20,11 +20,9 @@ const immerSet = createImmerSetter(useUserStore) class UserSyncService { async whoami() { - const res = await apiFetch<{ + const res = (await (apiClient["better-auth"] as any)["get-session"].$get()) as { user: UserModel - } | null>("/better-auth/get-session", { - method: "GET", - }) + } | null // TODO if (res) { immerSet((state) => { state.whoami = res.user From 7169ff640719d09f126c9f2f1921c3a971d70ecc Mon Sep 17 00:00:00 2001 From: DIYgod Date: Fri, 17 Jan 2025 21:01:05 +0800 Subject: [PATCH 72/93] chore(rn): remove @react-native-cookies/cookies --- apps/mobile/package.json | 1 - pnpm-lock.yaml | 23 ++++++++--------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/apps/mobile/package.json b/apps/mobile/package.json index e4a715d549..d8fc6e879a 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -26,7 +26,6 @@ "@hookform/resolvers": "3.9.1", "@infinite-list/data-model": "2.2.10", "@infinite-list/react-native": "2.2.10", - "@react-native-cookies/cookies": "^6.2.1", "@react-native-picker/picker": "2.9.0", "@react-navigation/bottom-tabs": "^7.0.0", "@react-navigation/drawer": "^7.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a3bfb01e90..050baabd38 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -433,9 +433,6 @@ importers: '@infinite-list/react-native': specifier: 2.2.10 version: 2.2.10(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) - '@react-native-cookies/cookies': - specifier: ^6.2.1 - version: 6.2.1(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)) '@react-native-picker/picker': specifier: 2.9.0 version: 2.9.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) @@ -5006,11 +5003,6 @@ packages: '@react-native-community/cli-tools@14.1.0': resolution: {integrity: sha512-r1KxSu2+OSuhWFoE//1UR7aSTXMLww/UYWQprEw4bSo/kvutGX//4r9ywgXSWp+39udpNN4jQpNTHuWhGZd/Bg==} - '@react-native-cookies/cookies@6.2.1': - resolution: {integrity: sha512-D17wCA0DXJkGJIxkL74Qs9sZ3sA+c+kCoGmXVknW7bVw/W+Vv1m/7mWTNi9DLBZSRddhzYw8SU0aJapIaM/g5w==} - peerDependencies: - react-native: '>= 0.60.2' - '@react-native-picker/picker@2.9.0': resolution: {integrity: sha512-khEhIW/uhfMqq/+tvg4rEAiPGT8GX+Y6QydlP2TSMSmRHoSJK+ShXvXZXSr4Sii4imkj4BwvLunGywwtQDODqg==} peerDependencies: @@ -17345,7 +17337,7 @@ snapshots: undici: 6.19.8 unique-string: 2.0.0 wrap-ansi: 7.0.0 - ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + ws: 8.18.0(bufferutil@4.0.8) transitivePeerDependencies: - bufferutil - encoding @@ -19905,11 +19897,6 @@ snapshots: sudo-prompt: 9.2.1 optional: true - '@react-native-cookies/cookies@6.2.1(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))': - dependencies: - invariant: 2.2.4 - react-native: 0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1) - '@react-native-picker/picker@2.9.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)': dependencies: react: 18.3.1 @@ -22398,7 +22385,7 @@ snapshots: centra@2.7.0: dependencies: - follow-redirects: 1.15.9(debug@4.4.0) + follow-redirects: 1.15.9 transitivePeerDependencies: - debug @@ -25271,6 +25258,8 @@ snapshots: imul: 1.0.1 optional: true + follow-redirects@1.15.9: {} + follow-redirects@1.15.9(debug@4.4.0): optionalDependencies: debug: 4.4.0(supports-color@8.1.1) @@ -32021,6 +32010,10 @@ snapshots: optionalDependencies: bufferutil: 4.0.8 + ws@8.18.0(bufferutil@4.0.8): + optionalDependencies: + bufferutil: 4.0.8 + ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4): optionalDependencies: bufferutil: 4.0.8 From 8a1ff735bd0451e2586b99299cf67fa25ad05c64 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Fri, 17 Jan 2025 21:04:44 +0800 Subject: [PATCH 73/93] fix(rn): login email --- apps/mobile/src/modules/login/email.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/apps/mobile/src/modules/login/email.tsx b/apps/mobile/src/modules/login/email.tsx index f4d04205b2..8ae7f58281 100644 --- a/apps/mobile/src/modules/login/email.tsx +++ b/apps/mobile/src/modules/login/email.tsx @@ -1,6 +1,5 @@ import { zodResolver } from "@hookform/resolvers/zod" import { useMutation } from "@tanstack/react-query" -import { router } from "expo-router" import { useEffect } from "react" import type { Control } from "react-hook-form" import { useController, useForm } from "react-hook-form" @@ -19,7 +18,6 @@ import { z } from "zod" import { ReAnimatedPressable } from "@/src/components/common/AnimatedComponents" import { ThemedText } from "@/src/components/common/ThemedText" import { signIn } from "@/src/lib/auth" -import { setSessionToken } from "@/src/lib/cookie" import { toast } from "@/src/lib/toast" import { accentColor, useColor } from "@/src/theme/colors" @@ -36,14 +34,6 @@ async function onSubmit(values: FormValue) { email: values.email, password: values.password, }) - .then((user) => { - if (!user.data) { - toast.error("Login failed") - return - } - setSessionToken(user.data.token) - router.push("/(stack)/(tabs)") - }) .catch((error) => { console.error(error) toast.error("Login failed") From aef9406defdde43874945fd92ed422f53ed669fc Mon Sep 17 00:00:00 2001 From: Innei Date: Fri, 17 Jan 2025 21:10:51 +0800 Subject: [PATCH 74/93] feat(rn): add login teams Signed-off-by: Innei --- apps/mobile/package.json | 1 + .../src/components/common/ThemedText.tsx | 3 +- .../components/ui/context-menu/index.ios.tsx | 2 + .../src/components/ui/context-menu/types.ts | 4 + .../components/ui/typography/MarkdownWeb.tsx | 10 +- .../mobile/src/contexts/LoginTeamsContext.tsx | 7 + apps/mobile/src/modules/login/email.tsx | 9 +- apps/mobile/src/modules/login/index.tsx | 150 +++++++++++++++--- apps/mobile/src/modules/login/social.tsx | 61 +++---- apps/mobile/src/screens/(headless)/teams.tsx | 103 ++++++++++++ pnpm-lock.yaml | 15 ++ 11 files changed, 312 insertions(+), 53 deletions(-) create mode 100644 apps/mobile/src/contexts/LoginTeamsContext.tsx create mode 100644 apps/mobile/src/screens/(headless)/teams.tsx diff --git a/apps/mobile/package.json b/apps/mobile/package.json index d8fc6e879a..3899f1b20d 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -69,6 +69,7 @@ "react-dom": "^18.3.1", "react-hook-form": "7.54.0", "react-native": "0.76.5", + "react-native-bouncy-checkbox": "4.1.2", "react-native-context-menu-view": "1.16.0", "react-native-gesture-handler": "~2.20.2", "react-native-image-colors": "2.4.0", diff --git a/apps/mobile/src/components/common/ThemedText.tsx b/apps/mobile/src/components/common/ThemedText.tsx index d10df57934..48d7be8db5 100644 --- a/apps/mobile/src/components/common/ThemedText.tsx +++ b/apps/mobile/src/components/common/ThemedText.tsx @@ -1,6 +1,7 @@ +import { cn } from "@follow/utils" import type { TextProps } from "react-native" import { Text } from "react-native" export function ThemedText(props: TextProps) { - return + return } diff --git a/apps/mobile/src/components/ui/context-menu/index.ios.tsx b/apps/mobile/src/components/ui/context-menu/index.ios.tsx index 5e504ee9a9..33d4c2d9af 100644 --- a/apps/mobile/src/components/ui/context-menu/index.ios.tsx +++ b/apps/mobile/src/components/ui/context-menu/index.ios.tsx @@ -15,6 +15,7 @@ export const ContextMenu: FC = ({ onPressMenuItem, children, renderPreview, + onPressPreview, ...props }) => { const [actionKeyMap] = useState(() => new Map()) @@ -80,6 +81,7 @@ export const ContextMenu: FC = ({ } renderPreview={renderPreview} menuConfig={menuViewConfig} + onPressMenuPreview={onPressPreview} onPressMenuItem={(e) => { onPressMenuItem(actionKeyMap.get(e.nativeEvent.actionKey)!) }} diff --git a/apps/mobile/src/components/ui/context-menu/types.ts b/apps/mobile/src/components/ui/context-menu/types.ts index a8c605b2fb..74768cf0d4 100644 --- a/apps/mobile/src/components/ui/context-menu/types.ts +++ b/apps/mobile/src/components/ui/context-menu/types.ts @@ -34,4 +34,8 @@ export interface ContextMenuProps extends ViewProps { * @note only available on iOS */ renderPreview?: RenderItem + /** + * @note only available on iOS + */ + onPressPreview?: () => void } diff --git a/apps/mobile/src/components/ui/typography/MarkdownWeb.tsx b/apps/mobile/src/components/ui/typography/MarkdownWeb.tsx index 47833fad01..f6ad599be8 100644 --- a/apps/mobile/src/components/ui/typography/MarkdownWeb.tsx +++ b/apps/mobile/src/components/ui/typography/MarkdownWeb.tsx @@ -8,12 +8,18 @@ import { useDarkMode } from "usehooks-ts" import { useCSSInjection } from "@/src/theme/web" -const MarkdownWeb: WebComponent<{ value: string }> = ({ value }) => { +const MarkdownWeb: WebComponent<{ value: string; style?: React.CSSProperties }> = ({ + value, + style, +}) => { useCSSInjection() const { isDarkMode } = useDarkMode() return ( -
+
{useMemo(() => parseMarkdown(value).content, [value])}
) diff --git a/apps/mobile/src/contexts/LoginTeamsContext.tsx b/apps/mobile/src/contexts/LoginTeamsContext.tsx new file mode 100644 index 0000000000..f79b2b18b6 --- /dev/null +++ b/apps/mobile/src/contexts/LoginTeamsContext.tsx @@ -0,0 +1,7 @@ +import { createContext } from "react" + +export const LoginTeamsCheckedContext = createContext(__DEV__) + +export const LoginTeamsCheckGuardContext = createContext<((callback: () => void) => void) | null>( + () => {}, +) diff --git a/apps/mobile/src/modules/login/email.tsx b/apps/mobile/src/modules/login/email.tsx index 8ae7f58281..f90a553610 100644 --- a/apps/mobile/src/modules/login/email.tsx +++ b/apps/mobile/src/modules/login/email.tsx @@ -1,6 +1,7 @@ import { zodResolver } from "@hookform/resolvers/zod" import { useMutation } from "@tanstack/react-query" -import { useEffect } from "react" +import { router } from "expo-router" +import { useContext, useEffect } from "react" import type { Control } from "react-hook-form" import { useController, useForm } from "react-hook-form" import type { TextInputProps } from "react-native" @@ -17,6 +18,7 @@ import { z } from "zod" import { ReAnimatedPressable } from "@/src/components/common/AnimatedComponents" import { ThemedText } from "@/src/components/common/ThemedText" +import { LoginTeamsCheckGuardContext } from "@/src/contexts/LoginTeamsContext" import { signIn } from "@/src/lib/auth" import { toast } from "@/src/lib/toast" import { accentColor, useColor } from "@/src/theme/colors" @@ -75,7 +77,10 @@ export function EmailLogin() { mutationFn: onSubmit, }) - const login = handleSubmit((values) => submitMutation.mutate(values)) + const teamsCheckGuard = useContext(LoginTeamsCheckGuardContext) + const login = handleSubmit((values) => { + teamsCheckGuard?.(() => submitMutation.mutate(values)) + }) const disableColor = useColor("gray3") diff --git a/apps/mobile/src/modules/login/index.tsx b/apps/mobile/src/modules/login/index.tsx index c7c087f0b8..a31ffcdeab 100644 --- a/apps/mobile/src/modules/login/index.tsx +++ b/apps/mobile/src/modules/login/index.tsx @@ -1,36 +1,146 @@ +import { noop } from "es-toolkit/compat" +import { router } from "expo-router" +import { forwardRef, useCallback, useImperativeHandle, useRef, useState } from "react" import { TouchableWithoutFeedback, View } from "react-native" +import BouncyCheckbox from "react-native-bouncy-checkbox" import { KeyboardController } from "react-native-keyboard-controller" +import Animated, { + runOnUI, + useAnimatedStyle, + useSharedValue, + withTiming, +} from "react-native-reanimated" import { ThemedText } from "@/src/components/common/ThemedText" +import { ContextMenu } from "@/src/components/ui/context-menu" import { Logo } from "@/src/components/ui/logo" +import { + LoginTeamsCheckedContext, + LoginTeamsCheckGuardContext, +} from "@/src/contexts/LoginTeamsContext" +import { isIOS } from "@/src/lib/platform" +import { toast } from "@/src/lib/toast" +import { TeamsMarkdown } from "@/src/screens/(headless)/teams" import { EmailLogin } from "./email" import { SocialLogin } from "./social" export function Login() { + const [isChecked, setIsChecked] = useState(false) + + const teamsCheckBoxRef = useRef<{ shake: () => void }>(null) return ( - - { - KeyboardController.dismiss() - }} - accessible={false} + + void) => { + if (isChecked) { + callback() + } else { + toast.info("Please accept the Terms of Service and Privacy Policy") + + teamsCheckBoxRef.current?.shake() + } + }, + [isChecked], + )} > - - - Login to Follow - + + { + KeyboardController.dismiss() + }} + accessible={false} + > + + + Login to Follow + + + + + + + + + or + + + + - - - - - - or - + + + ) +} + +const TeamsCheckBox = forwardRef< + { shake: () => void }, + { + isChecked: boolean + setIsChecked: (isChecked: boolean) => void + } +>(({ isChecked, setIsChecked }, ref) => { + const shakeSharedValue = useSharedValue(0) + const shakeStyle = useAnimatedStyle(() => ({ + transform: [{ translateX: shakeSharedValue.value }], + })) + useImperativeHandle(ref, () => ({ + shake: () => { + const animations = [-10, 10, -8, 8, -6, 6, 0] + + runOnUI(() => { + "worklet" + shakeSharedValue.value = 0 + + const runAnimation = (index: number) => { + "worklet" + if (index < animations.length) { + shakeSharedValue.value = withTiming(animations[index], { duration: 100 }, () => { + runAnimation(index + 1) + }) + } + } + + runAnimation(0) + })() + }, + })) + return ( + + } + onLongPress={() => { + if (!isIOS) { + router.push("/teams") + } + }} + /> + + ) +}) + +const TeamsText = () => { + return ( + { + router.push("/teams") + }} + renderPreview={() => ( + + - - - + )} + > + + I agree to the Terms of Service and Privacy Policy + + ) } diff --git a/apps/mobile/src/modules/login/social.tsx b/apps/mobile/src/modules/login/social.tsx index babe586af2..d1dec8a73f 100644 --- a/apps/mobile/src/modules/login/social.tsx +++ b/apps/mobile/src/modules/login/social.tsx @@ -1,7 +1,9 @@ import * as AppleAuthentication from "expo-apple-authentication" import { useColorScheme } from "nativewind" +import { useContext } from "react" import { Platform, TouchableOpacity, View } from "react-native" +import { LoginTeamsCheckGuardContext } from "@/src/contexts/LoginTeamsContext" import { AppleCuteFiIcon } from "@/src/icons/apple_cute_fi" import { GithubCuteFiIcon } from "@/src/icons/github_cute_fi" import { GoogleCuteFiIcon } from "@/src/icons/google_cute_fi" @@ -38,6 +40,7 @@ const provider: Record< export function SocialLogin() { const { data } = useAuthProviders() + const teamsCheckGuard = useContext(LoginTeamsCheckGuardContext) const { colorScheme } = useColorScheme() return ( @@ -50,40 +53,42 @@ export function SocialLogin() { { - if (!data?.[providerInfo.id]) return + onPress={() => + teamsCheckGuard?.(async () => { + if (!data?.[providerInfo.id]) return - if (providerInfo.id === "apple") { - try { - const credential = await AppleAuthentication.signInAsync({ - requestedScopes: [ - AppleAuthentication.AppleAuthenticationScope.FULL_NAME, - AppleAuthentication.AppleAuthenticationScope.EMAIL, - ], - }) - - if (credential.identityToken) { - await signIn.social({ - provider: "apple", - idToken: { - token: credential.identityToken, - }, + if (providerInfo.id === "apple") { + try { + const credential = await AppleAuthentication.signInAsync({ + requestedScopes: [ + AppleAuthentication.AppleAuthenticationScope.FULL_NAME, + AppleAuthentication.AppleAuthenticationScope.EMAIL, + ], }) - } else { - throw new Error("No identityToken.") + + if (credential.identityToken) { + await signIn.social({ + provider: "apple", + idToken: { + token: credential.identityToken, + }, + }) + } else { + throw new Error("No identityToken.") + } + } catch (e) { + console.error(e) + // handle errors } - } catch (e) { - console.error(e) - // handle errors + return } - return - } - signIn.social({ - provider: providerInfo.id as any, - callbackURL: "/", + signIn.social({ + provider: providerInfo.id as any, + callbackURL: "/", + }) }) - }} + } disabled={!data?.[providerInfo.id]} > { + return ( + + ) +} + +export default function Teams() { + return ( + + + + + + ) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 050baabd38..2a5fb16189 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -562,6 +562,9 @@ importers: react-native: specifier: 0.76.5 version: 0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1) + react-native-bouncy-checkbox: + specifier: 4.1.2 + version: 4.1.2 react-native-context-menu-view: specifier: 1.16.0 version: 1.16.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) @@ -3744,6 +3747,9 @@ packages: '@fontsource/sn-pro@5.1.0': resolution: {integrity: sha512-k7cdU1hftD/pyrnrmQg+egKAssbuPORbtgQM/9JG5b76EchEKZdl/juO8xBjN3O/H5BQ16iu8/9vNPgzyExZeg==} + '@freakycoder/react-native-bounceable@1.0.3': + resolution: {integrity: sha512-+iMq2tnqxCwFjitbPUz9nZ+VfJ8OU9waIlDJAAsoq1229QEwCmERCNy5zVtDsz75q3i4FLXX/n7fimdMzmP21A==} + '@gar/promisify@1.1.3': resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} @@ -12688,6 +12694,9 @@ packages: react-merge-refs@1.1.0: resolution: {integrity: sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==} + react-native-bouncy-checkbox@4.1.2: + resolution: {integrity: sha512-hB7YwCGTNoMpTPOPiP+RWyQH35S6vxUbc7IGEW/Rqyp7GonEyhtqtthmxiphneRXnywMh8CZwND7OnvppJZscg==} + react-native-context-menu-view@1.16.0: resolution: {integrity: sha512-zqeOAizM7MVV9o6h/quS0REQikBq3J4BkIRLFygY6RiCjr6rwuzSGkif7JRCHpAQQumSKlLqYl4N2h3AdoIHVg==} peerDependencies: @@ -18229,6 +18238,8 @@ snapshots: '@fontsource/sn-pro@5.1.0': {} + '@freakycoder/react-native-bounceable@1.0.3': {} + '@gar/promisify@1.1.3': {} '@gorhom/portal@1.0.14(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)': @@ -29273,6 +29284,10 @@ snapshots: react-merge-refs@1.1.0: {} + react-native-bouncy-checkbox@4.1.2: + dependencies: + '@freakycoder/react-native-bounceable': 1.0.3 + react-native-context-menu-view@1.16.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 From ff76b5cede5386992a85a76511c149a8df946fdc Mon Sep 17 00:00:00 2001 From: Innei Date: Fri, 17 Jan 2025 21:12:00 +0800 Subject: [PATCH 75/93] chore: update changelog Signed-off-by: Innei --- changelog/next.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 760158db27..2839b2f13a 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -3,7 +3,3 @@ ## New Features - Protect your account logins and wallet operations with two-factor authentication (2FA). - -## Improvements - -## Bug Fixes From 0ea516c9f9186ec7dce4f79b2863ad9ec4591b93 Mon Sep 17 00:00:00 2001 From: Innei Date: Fri, 17 Jan 2025 21:12:17 +0800 Subject: [PATCH 76/93] chore(release): release v0.3.2-beta.0 --- CHANGELOG.md | 67 +++++++++++++++++++++++++++++++++++++++++++++- changelog/0.3.2.md | 5 ++++ changelog/next.md | 4 ++- package.json | 4 +-- 4 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 changelog/0.3.2.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 0798978108..28397a4189 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # CHANGELOG -## [0.3.1-beta.0](https://github.com/RSSNext/follow/compare/v0.2.7-beta.0...v0.3.1-beta.0) (2025-01-09) +## [0.3.2-beta.0](https://github.com/RSSNext/follow/compare/v0.2.7-beta.0...v0.3.2-beta.0) (2025-01-17) ### Bug Fixes @@ -24,7 +24,10 @@ * add turbo build in ci ([7b848cd](https://github.com/RSSNext/follow/commit/7b848cd08aa4f97b1df35666166561d6fc30c5c9)) * add type checking to test script ([#1666](https://github.com/RSSNext/follow/issues/1666)) ([518a32f](https://github.com/RSSNext/follow/commit/518a32f2ce1498f21cff87e0b95d0a4fd0a0f4a0)) * adjust activation toast style ([e81245e](https://github.com/RSSNext/follow/commit/e81245e395ce1829f0c185a5a184c4cba8f8406c)) +* adjust padding based on FeedViewType in EntryList component ([f3f86b7](https://github.com/RSSNext/follow/commit/f3f86b7c0ddc1cae0c26246bc8e91cbb79e64189)) +* adjust ScrollView padding to accommodate safe area insets in FollowFeed component ([de3a625](https://github.com/RSSNext/follow/commit/de3a625db513f559ad2e8d6e86b1af2798b88a37)) * ai daily report layout in mobile ([64c7df1](https://github.com/RSSNext/follow/commit/64c7df1dda3e4a30a6152b24bcea0dd135e12ca9)) +* ai entry actions with user role checks, close [#2591](https://github.com/RSSNext/follow/issues/2591) ([7129db7](https://github.com/RSSNext/follow/commit/7129db766c5ed2e7257f59bc914c1b56a27cc6a3)) * align notification skeleton to left ([fa08d1a](https://github.com/RSSNext/follow/commit/fa08d1a0cb8421ca40e38dddd0d74415389f7a80)), closes [#1672](https://github.com/RSSNext/follow/issues/1672) * allow click when selecting ([c514458](https://github.com/RSSNext/follow/commit/c51445873249ed7340d86bcb7901dbeca98d096e)), closes [#1512](https://github.com/RSSNext/follow/issues/1512) * allow collect entry from list ([706f484](https://github.com/RSSNext/follow/commit/706f4847576fbb0237925434d4dace5c22fdbf93)) @@ -83,12 +86,14 @@ * **cmdk:** change default cmdk search type to feed ([4a8fdea](https://github.com/RSSNext/follow/commit/4a8fdea84a4df90313f8c577d77d3944a34aa149)) * commands in action responsive ([ffca130](https://github.com/RSSNext/follow/commit/ffca1301b4f89a27b995b79b6e43d6b7f581e5c4)) * condition key ([ffb3b94](https://github.com/RSSNext/follow/commit/ffb3b943b0c502cf940d7f3c0d4d506489b65d37)) +* conditionally render feed preview and items based on entries availability ([15a14c6](https://github.com/RSSNext/follow/commit/15a14c63b6e0fcdcd83dd7da750e8b00c784b9c4)) * console error in power page ([#2405](https://github.com/RSSNext/follow/issues/2405)) ([c05b586](https://github.com/RSSNext/follow/commit/c05b5865d1a68fe210d83b034c4cf0739ecf8c76)) * content ([5a30f38](https://github.com/RSSNext/follow/commit/5a30f384082949f17a71f6d1f837b30966368157)) * context menu for multi select ([d68de79](https://github.com/RSSNext/follow/commit/d68de7933235351d64df68e3e2c49623184f4284)) * corner player style on safari ([e740d4d](https://github.com/RSSNext/follow/commit/e740d4da78032547c48014a31a25968fe1ad192f)) * correct visibility logic for entry read command ([bdad091](https://github.com/RSSNext/follow/commit/bdad0915995fd4853db49a257a5bd1f13ad092be)) * correct z-index in toast ([6507744](https://github.com/RSSNext/follow/commit/6507744847b6ceee093adb17076292993d525313)) +* crash when using Google Translate, close [#2121](https://github.com/RSSNext/follow/issues/2121) ([4ae7290](https://github.com/RSSNext/follow/commit/4ae7290de115e6d352d84282b8470bcd15a115c4)) * csrf token singleton ([b4e935f](https://github.com/RSSNext/follow/commit/b4e935fba0b2f874a5c24006d5afdd54f50f3e76)) * csrf token singleton ([#1463](https://github.com/RSSNext/follow/issues/1463)) ([16b5349](https://github.com/RSSNext/follow/commit/16b5349c3d4461356ef124c10df8c117fd3bac81)) * css editor lazy and input composition handler ([5c6004a](https://github.com/RSSNext/follow/commit/5c6004a0905c6207c6119de0d37e9a5baf744dab)) @@ -98,6 +103,7 @@ * debug html ([e9afc79](https://github.com/RSSNext/follow/commit/e9afc7944f4dc42be2ea530b4852e9c2f6207ac8)) * debug proxy html env inejct ([f98ae93](https://github.com/RSSNext/follow/commit/f98ae93ff4d0d25216e0570c44ef657cd36657e0)) * debug proxy page ([#1439](https://github.com/RSSNext/follow/issues/1439)) ([c8d1230](https://github.com/RSSNext/follow/commit/c8d1230be753109bd7eaef45e7d0ad0399d8bd4d)) +* debugger network ([a76814d](https://github.com/RSSNext/follow/commit/a76814ddfc5a019564bfc1980572157e1a30d776)) * **debug:** inject `import.env` ([810516a](https://github.com/RSSNext/follow/commit/810516ae7d9b4196c047249cb0f889de8b012b99)) * delete condition item ([8b9750d](https://github.com/RSSNext/follow/commit/8b9750da1219f3d046b78c70c1966e284b086348)) * delete last condition ([72d894a](https://github.com/RSSNext/follow/commit/72d894a754ba3a0c3b6c4e030b3b274dc98de6b6)) @@ -113,9 +119,11 @@ * discover form button disable state error ([#2113](https://github.com/RSSNext/follow/issues/2113)) ([0ae6ec9](https://github.com/RSSNext/follow/commit/0ae6ec954af3e4c42f224020b67ea294a9c834d6)) * **discover:** card text truncate ([557e912](https://github.com/RSSNext/follow/commit/557e91228af27251663904dfd3211c535b5b5415)) * dismiss non-important modal when click outside, fix [#1468](https://github.com/RSSNext/follow/issues/1468) ([cda2264](https://github.com/RSSNext/follow/commit/cda226420f6c42679d594c02fd7f6fb103bfc236)) +* dispatch I18N_UPDATE event after language load ([#2590](https://github.com/RSSNext/follow/issues/2590)) ([69704f3](https://github.com/RSSNext/follow/commit/69704f303d33d20b99d13d63f88f2e4959d4a035)) * dnd and scroll ([#1528](https://github.com/RSSNext/follow/issues/1528)) ([b1e28be](https://github.com/RSSNext/follow/commit/b1e28be2b0049eae73d51a18cd9180b077a512ca)) * dnd responsive ([1f51abe](https://github.com/RSSNext/follow/commit/1f51abe3ed2fbca66e6d9ab2ab63ef71b7b9eda4)) * do not exclude required path params ([f6cbace](https://github.com/RSSNext/follow/commit/f6cbacec853dd8aa901c93c8c86820bbbf1f8232)), closes [#1623](https://github.com/RSSNext/follow/issues/1623) +* do not skipValidation, close [#2430](https://github.com/RSSNext/follow/issues/2430) ([fe87c52](https://github.com/RSSNext/follow/commit/fe87c528e26063c980f9426b03dd18510b95ad84)) * do not update action setting for archived query ([4adcded](https://github.com/RSSNext/follow/commit/4adcded02179b8d42f1a906b75b10fd403438f8d)) * don't cache user session ([e8d7e6c](https://github.com/RSSNext/follow/commit/e8d7e6ce64d09bb1f2b8f3e26f7aba250c3cca84)) * don't inject pwa into debug html ([5fec56e](https://github.com/RSSNext/follow/commit/5fec56e9b02ec8a3dbed7c99a7328edf51d41035)) @@ -125,7 +133,9 @@ * dynamic import dexie-export-import ([48aea7b](https://github.com/RSSNext/follow/commit/48aea7b88e4d72d20e52ce7a5037ef239d80611d)) * electron sidebar blur effect ([ec7105c](https://github.com/RSSNext/follow/commit/ec7105c9604cc900dd4e6ef218562f47f3c4a507)) * **electron:** fix can eval code in main process, fixed [#2362](https://github.com/RSSNext/follow/issues/2362) ([026b207](https://github.com/RSSNext/follow/commit/026b2075bc291e7c2d9cb1821bb08cbc6202ecd0)) +* enhance shortcut handling for OS compatibility ([#2576](https://github.com/RSSNext/follow/issues/2576)) ([4217086](https://github.com/RSSNext/follow/commit/421708630fe595476dcd16bc02d9b7b2475f51a8)) * enhance tray icon visual effect in Windows platform ([64c891f](https://github.com/RSSNext/follow/commit/64c891f30c27ff1c868a4ba8fb88adf2acda7dfd)) +* enhancing the Visibility of Download Buttons on White Background Images, fixed FOL-1270 ([23152d0](https://github.com/RSSNext/follow/commit/23152d022231c94531bb3e6362f6895e1afc90c8)) * entry action deps ([4eba28b](https://github.com/RSSNext/follow/commit/4eba28b76fc62133ecd0e4746b3bb0b4d341b338)) * entry thumbnail keep origin aspect ratio ([6e04e6e](https://github.com/RSSNext/follow/commit/6e04e6eacd6346bd2869c683ec417932a0d13807)) * entry title bar margin-left in zen mode ([25abd8f](https://github.com/RSSNext/follow/commit/25abd8ff1c42a37b741676e1b3a3312f99decb8a)) @@ -142,6 +152,7 @@ * **external:** set auth config first ([ef46c70](https://github.com/RSSNext/follow/commit/ef46c70a9849c315cc8d827f4ad8a3c004003bac)) * **external:** should hard go back to app home ([6786e20](https://github.com/RSSNext/follow/commit/6786e203031f18be0ca386dcaf1dddf4d05ae95b)) * fallback for code can not render to html ([08acded](https://github.com/RSSNext/follow/commit/08acded2d549bc80d2ed8f180a6d89cc3a3c861a)), closes [#1142](https://github.com/RSSNext/follow/issues/1142) +* fast scroll mark read ([3dbf7c7](https://github.com/RSSNext/follow/commit/3dbf7c7ffaded0c276eb3b7a8f4aaf8ad50dc8a1)) * feed action in selection ([ef9db5c](https://github.com/RSSNext/follow/commit/ef9db5c8b39c8229b1e907c6a0e6e74431f677de)) * feed boost modal title wrap ([a3eb287](https://github.com/RSSNext/follow/commit/a3eb2875e9456e8ec30ed96294b2bad82ccd5a4d)) * feed column flickering ([#2143](https://github.com/RSSNext/follow/issues/2143)) ([ffe535d](https://github.com/RSSNext/follow/commit/ffe535d5e7f2aea0b7a0d1d1a13cdc49559044c5)) @@ -157,8 +168,10 @@ * flickering issue when FAB button disappears ([#2390](https://github.com/RSSNext/follow/issues/2390)) ([2bc8968](https://github.com/RSSNext/follow/commit/2bc89685b5abee504f6c4dc0aad479a5b931266e)) * float sidebar missing background ([b9982e0](https://github.com/RSSNext/follow/commit/b9982e0a6b44f476621daf30f93f8e740a544a02)) * follow external server fetch ua ([072dec0](https://github.com/RSSNext/follow/commit/072dec02b787bcdd8af7f5c0bf25398e2da8c15e)) +* **follow:** fix follow modal overflow scrollbar position, fix [#1238](https://github.com/RSSNext/follow/issues/1238) ([fb2a458](https://github.com/RSSNext/follow/commit/fb2a4589eb4454a60f1ca6911075c5cfc41078fc)) * form button align center ([589c6e4](https://github.com/RSSNext/follow/commit/589c6e469b3fecb1c46f1ec2735494da3367abec)) * form content width ([42d66ce](https://github.com/RSSNext/follow/commit/42d66ce19487678a3fd56059b03496d5b50f3b3d)) +* get-session response null error ([3bdcc90](https://github.com/RSSNext/follow/commit/3bdcc90c704c4a6adadbaaa7d87cb0f5709fb8f4)) * grid virtualizer on mobile can't show any items ([2b93dfd](https://github.com/RSSNext/follow/commit/2b93dfd58f403756955f071703b5bdb6573e916b)) * group date item z index ([8c5c62e](https://github.com/RSSNext/follow/commit/8c5c62e0d698f3a0d72a2ef0595e44986c95320b)) * handle popstate for navigation in mobile entry content ([#2038](https://github.com/RSSNext/follow/issues/2038)) ([ab85406](https://github.com/RSSNext/follow/commit/ab85406d4f7137b6f85c85a528c42d598dbcb573)) @@ -193,6 +206,7 @@ * lint ([dafaf0a](https://github.com/RSSNext/follow/commit/dafaf0a97d40a6df659a7869d7c11f5cf529dfe7)) * list form width ([6487be1](https://github.com/RSSNext/follow/commit/6487be1c9f8c085f6722595d59f85d1176c503c5)) * list unread dot position ([dad33a1](https://github.com/RSSNext/follow/commit/dad33a12db4b270801da6b8c621b3049e6672c27)) +* **lists:** fix duplicate addition issue in manage feeds functionality ([#2544](https://github.com/RSSNext/follow/issues/2544)) ([d77ed3c](https://github.com/RSSNext/follow/commit/d77ed3cb37424e97490bbf4883f9326acc446d54)) * load dynamic render assets ([3499535](https://github.com/RSSNext/follow/commit/3499535b4091e3a82734c416528b0766e70f0b63)) * load instagram image fail ([f8fd58f](https://github.com/RSSNext/follow/commit/f8fd58f78ec11bc221eb9190ac7b6a66dd858f9c)), closes [#1539](https://github.com/RSSNext/follow/issues/1539) * loading style in trending on mobile ([98beb0a](https://github.com/RSSNext/follow/commit/98beb0aee0a092bb696fda5d6d65b86c8ed5fee4)) @@ -204,11 +218,13 @@ * mark as read when navigating ([423777e](https://github.com/RSSNext/follow/commit/423777e1372631b8ea8d06913ce55fbdf97f1f07)) * mark feed unread dirty, refetch unread next time, fixed [#1830](https://github.com/RSSNext/follow/issues/1830) ([58d0e9c](https://github.com/RSSNext/follow/commit/58d0e9c1902157c2e9fb1444bc1c0db0b775485b)) * mark single feed as all read ([a4acd2f](https://github.com/RSSNext/follow/commit/a4acd2fb65335f228875ed5b38f03a9c71b76e1b)) +* markdown mp4 check ([44afb1e](https://github.com/RSSNext/follow/commit/44afb1e888e64737eb4f06b3ecc6b26366487978)) * max width for truncate ([6359c5b](https://github.com/RSSNext/follow/commit/6359c5b6f91e19b3d26f5cd195645e2c4522df53)) * md lint ([c8e556a](https://github.com/RSSNext/follow/commit/c8e556a3ac861cab4a4a7aff1aa2fa51aaca1f42)) * media item dot position on mobile ([c67248f](https://github.com/RSSNext/follow/commit/c67248ffc8d77cbdeac289f5ed398248dff41cb3)) * **media:** adjust inline media size and alignment for better display [#2251](https://github.com/RSSNext/follow/issues/2251) ([#2254](https://github.com/RSSNext/follow/issues/2254)) ([86b5712](https://github.com/RSSNext/follow/commit/86b57120fca3aadcead672fb6e9ea470045b4759)) * missing site url in feed selector ([ea677ac](https://github.com/RSSNext/follow/commit/ea677ac6a3a43489a9508a32ecb22fa38ecf4f27)) +* missing translation in pitcure masonry ([902d681](https://github.com/RSSNext/follow/commit/902d68180d1cc872cbe4435efbcae108f0d732b5)) * mobile need login modal ([0fb16f2](https://github.com/RSSNext/follow/commit/0fb16f2352e9776731522204b838be59f66127a4)) * mobile pop back to entry list will refresh data ([3c18768](https://github.com/RSSNext/follow/commit/3c18768968aeb4635933ae616c52de55b6fad2ab)) * **mobile:** background color ([9f6934f](https://github.com/RSSNext/follow/commit/9f6934f844c995a1dcbb326123420e6ee9b05a96)) @@ -276,14 +292,21 @@ * remove wdyr in prod ([70e3b72](https://github.com/RSSNext/follow/commit/70e3b72b0ae88a722e2ff9e7d6027003f88829e2)) * replace Avatar component with UserAvatar in ranking and transaction sections ([#1759](https://github.com/RSSNext/follow/issues/1759)) ([52c3695](https://github.com/RSSNext/follow/commit/52c3695355ed3962d9ad53e61a612d88c54ab650)) * response error toast style ([7748588](https://github.com/RSSNext/follow/commit/77485886089e9faea54cf80a96a48a0bedd2a39e)) +* restore BlurEffect in SearchHeader and enhance refresh control in search tabs ([180aa66](https://github.com/RSSNext/follow/commit/180aa667d50c18ed978e4b227b5f03347025775a)) * revalidate when switch from subscription startupScreen ([e63c12c](https://github.com/RSSNext/follow/commit/e63c12c27227341a2477eb06fa039e2b0a2a00e5)) * revert electron bump ([#1991](https://github.com/RSSNext/follow/issues/1991)) ([200415c](https://github.com/RSSNext/follow/commit/200415c77f52d11a43c2610c5c58c61fbbfb23f8)) * revert merge chunk ([4a9679b](https://github.com/RSSNext/follow/commit/4a9679bad109a786b80939ca9ead32f1505be186)) +* **rn-baseline:** sync with uikit color and update list ([91f5340](https://github.com/RSSNext/follow/commit/91f53404071c609a77b8ff809ea2cbb7748cb1c5)) +* **rn-tabbar:** calc tab bar indicator position ([e625ada](https://github.com/RSSNext/follow/commit/e625adaf162cdd2962900d1104541f9cc6365af5)) * **rn:** adjust ui colors ([33d9e14](https://github.com/RSSNext/follow/commit/33d9e14d7c377c76dc349c1fc6ae901da2fe30c7)) * **rn:** check tab current index frequency ([fc3a3ce](https://github.com/RSSNext/follow/commit/fc3a3cef23532143853625a1af6cc2bbe3c28a3d)) * **rn:** delete subscribe in db persist ([e2c9459](https://github.com/RSSNext/follow/commit/e2c9459db8cc0137bb63ea83b1cd7f2cb2a3dab4)) * **rn:** discover form ([4f3d990](https://github.com/RSSNext/follow/commit/4f3d9902af372dd849f02a20dc1ae644d309fd73)) +* **rn:** hide tabbar when push to other setting page ([c47905b](https://github.com/RSSNext/follow/commit/c47905b70fb188acdae48f27a93822ccd20a944c)) +* **rn:** login email ([8a1ff73](https://github.com/RSSNext/follow/commit/8a1ff735bd0451e2586b99299cf67fa25ad05c64)) +* **rn:** login redirection ([9efbe79](https://github.com/RSSNext/follow/commit/9efbe79961b3dba138b9553d131b73c94c3a8cdf)) * **rn:** rsshub form styles ([8583210](https://github.com/RSSNext/follow/commit/85832105b4e6a860d34374cfa7e6c01a06cc54a3)) +* rsshub link in mobile ([ac76cde](https://github.com/RSSNext/follow/commit/ac76cde0ca99e3f9ca842be84603f5bd6e5a0ad4)) * **rsshub:** adjust table cell width ([ca8d19d](https://github.com/RSSNext/follow/commit/ca8d19dc394d7f9907a40834abcdd0e14ddc2b47)) * scroll bar z index ([5057999](https://github.com/RSSNext/follow/commit/50579995367b871e84768c5983b3302f75f8e077)), closes [#1233](https://github.com/RSSNext/follow/issues/1233) * scroll out mark read logic ([eaecb25](https://github.com/RSSNext/follow/commit/eaecb252d322b5f2e8e89873523dfbe8d213848c)) @@ -305,6 +328,7 @@ * setting loader type error ([da8aad7](https://github.com/RSSNext/follow/commit/da8aad7327d45cd899f8091cb0315ff2497b2ab9)) * setting loader type error ([#1469](https://github.com/RSSNext/follow/issues/1469)) ([6ef5622](https://github.com/RSSNext/follow/commit/6ef5622a32eaaebfeec32cca1000bf908f742e34)) * **settings:** align submit button in ExportFeedsForm for better layout ([15feb5a](https://github.com/RSSNext/follow/commit/15feb5a35ac6db4ff5c12ff4d1f46c25632d7693)) +* share button in electron ([e628e7a](https://github.com/RSSNext/follow/commit/e628e7a253b4581992150781afd1659b29ebf1fe)) * **share:** normal list item layout ([3a0c039](https://github.com/RSSNext/follow/commit/3a0c0393e01e09ebf86ab1415b3d84bca6d2b882)) * **share:** og image grid width and image align top to description ([14e37da](https://github.com/RSSNext/follow/commit/14e37da0cebbdb1b283c562c21acc2f8ba385bf9)) * sheet stack dismiss transition ([6a30f23](https://github.com/RSSNext/follow/commit/6a30f2363e3d99c95b3b7a5415c4fd898d1c12a4)) @@ -372,8 +396,10 @@ * update i18n-detector ([7969022](https://github.com/RSSNext/follow/commit/7969022657b4fe2b5c9a0a0f93467604f697d796)) * update Japanese translations for user actions and login prompts ([#2188](https://github.com/RSSNext/follow/issues/2188)) ([88458bd](https://github.com/RSSNext/follow/commit/88458bd7a3e32854cd5b7b93f36c72f6cf3abac5)) * update linkedom to version 0.18.6 ([#2185](https://github.com/RSSNext/follow/issues/2185)) ([faebe6c](https://github.com/RSSNext/follow/commit/faebe6c819cd0742470f8aebe9be5f978d3827e6)) +* update query key in SearchList component for improved data fetching ([77d73c2](https://github.com/RSSNext/follow/commit/77d73c2843c7d6e819177b67717da113dc81bac4)) * update SupportCreator component styles for better layout ([351f342](https://github.com/RSSNext/follow/commit/351f34289fab89f9866405f8910e6226cb88ec87)) * update tray icon path for windows and improve tray behavior ([#1511](https://github.com/RSSNext/follow/issues/1511)) ([3706874](https://github.com/RSSNext/follow/commit/3706874e42c215a84df71eae1c135a114be8985a)) +* update type ([5958ec1](https://github.com/RSSNext/follow/commit/5958ec18d81030ae8f9bb579df613a18689ac929)) * update user profile ([aeaf795](https://github.com/RSSNext/follow/commit/aeaf79522be23abb64ae940a4377a92448dfd923)) * update version toast ([25e7cea](https://github.com/RSSNext/follow/commit/25e7cea3c139335960c3de2922cf0ffa9c9c6c39)), closes [#1450](https://github.com/RSSNext/follow/issues/1450) * update zh-CN translations and remove unused setting ([#2132](https://github.com/RSSNext/follow/issues/2132)) ([0dcfde5](https://github.com/RSSNext/follow/commit/0dcfde5ea40d688eaf8acc9f15f7a298421a12f0)) @@ -398,6 +424,7 @@ ### Features +* 2fa ([#2540](https://github.com/RSSNext/follow/issues/2540)) ([fb531b1](https://github.com/RSSNext/follow/commit/fb531b1c9df64c7536f30994c1cc40c2df013d32)) * achievement badge ([aceb4c9](https://github.com/RSSNext/follow/commit/aceb4c964f8d62cd64d26b321021b0b0534677bb)) * action page ([#2006](https://github.com/RSSNext/follow/issues/2006)) ([6d9212e](https://github.com/RSSNext/follow/commit/6d9212ef481cf784f84ab25da3c9a819e6637b63)) * add all language filter in discover ([#1598](https://github.com/RSSNext/follow/issues/1598)) ([cefdd6e](https://github.com/RSSNext/follow/commit/cefdd6e36cdc3c3ebe62df525d98da516290e286)) @@ -406,10 +433,13 @@ * add application tray functionality ([#1416](https://github.com/RSSNext/follow/issues/1416)) ([76a6e38](https://github.com/RSSNext/follow/commit/76a6e38376a01dbeee83161cdce9fe5e205b1523)) * add auto-expand setting for long social media entries and remove placeholder component, fixed [#2064](https://github.com/RSSNext/follow/issues/2064) ([639b922](https://github.com/RSSNext/follow/commit/639b922ec88ac431f14f9fe5bbb7f4b31bfc59fd)) * add confirmation for opening external apps ([#1618](https://github.com/RSSNext/follow/issues/1618)) ([08a1e76](https://github.com/RSSNext/follow/commit/08a1e76247b3c7ddeb7ef906a27f149e1a46e3d6)) +* add cookie to api fetch ([f5e1d33](https://github.com/RSSNext/follow/commit/f5e1d33e2a42f1ce44788ec65d51d54f66056f29)) +* add custom app delegate for tint color configuration ([#2579](https://github.com/RSSNext/follow/issues/2579)) ([6c28b4f](https://github.com/RSSNext/follow/commit/6c28b4f56ec2742b3e969a87750183e261f65d3d)) * add entry conditions for actions ([53810ba](https://github.com/RSSNext/follow/commit/53810badc1a7b37be6fd5c187c7a97428e85405f)) * add feed booster achievement with localization ([#1561](https://github.com/RSSNext/follow/issues/1561)) ([e769852](https://github.com/RSSNext/follow/commit/e7698526c2d51232cbdcb279aead6e473b7507a9)) * add follow user button in profile modal ([#1147](https://github.com/RSSNext/follow/issues/1147)) ([7c60d5e](https://github.com/RSSNext/follow/commit/7c60d5e91dd4a21872217427bd99fe8eae688ce1)) * add hydrate data type-safe helper ([cb6a65b](https://github.com/RSSNext/follow/commit/cb6a65b7cc1651d86b83b5b4089a7145cd59b0be)) +* add i18n for customize toolbar ([#2592](https://github.com/RSSNext/follow/issues/2592)) ([7e1618c](https://github.com/RSSNext/follow/commit/7e1618cfc204b5148bc6eee4338eafcdb0ecaa79)) * add image preview in picture gallery images ([59728a8](https://github.com/RSSNext/follow/commit/59728a89295c2a9c6130651d06699eef027fef5e)) * add list madeby ([03858b1](https://github.com/RSSNext/follow/commit/03858b18b8de5cffb2add05d72fdbf5e229e6cf4)) * add loading indicator and UI improvements in transaction ([#2480](https://github.com/RSSNext/follow/issues/2480)) ([6c3e8e8](https://github.com/RSSNext/follow/commit/6c3e8e82c64005466f21c3ee2680574da43fd604)) @@ -417,6 +447,7 @@ * add mobile top timeline setting ([fe48d7c](https://github.com/RSSNext/follow/commit/fe48d7cf02a202a1f14bbfe3abe4c813629ac968)) * add more actions to entry header ([#1993](https://github.com/RSSNext/follow/issues/1993)) ([5c4b5f0](https://github.com/RSSNext/follow/commit/5c4b5f0095cff53f1ce70d66f2275d0a9c73e985)) * add old browser check ([a4796ce](https://github.com/RSSNext/follow/commit/a4796ce4c9a0d1558b9467b80f1f0806dfaba321)) +* add proxy support for native fetch using undici ([#2537](https://github.com/RSSNext/follow/issues/2537)) ([6abb442](https://github.com/RSSNext/follow/commit/6abb442f031b9881fa4177390cf5c3406e27dd12)) * add random color for user fallback bg ([dcbae89](https://github.com/RSSNext/follow/commit/dcbae89020e88947be693c100c644ee603519e96)) * add reveal log file ([adbdf74](https://github.com/RSSNext/follow/commit/adbdf7472b6789a365489b2c5b09dd503ec648e9)) * add rsshub entrance ([19eea97](https://github.com/RSSNext/follow/commit/19eea97267086a1bfa6688fbd32b37660d6bea29)) @@ -437,10 +468,12 @@ * **auth:** enhance login modal with email login ([8bf9dd0](https://github.com/RSSNext/follow/commit/8bf9dd07e52e7188abfce7de990fb8b93d45711c)) * bigger font size in mobile ([3e9a92d](https://github.com/RSSNext/follow/commit/3e9a92d16a80139c851671748367f009a3af4de9)) * bring rehypeUrlToAnchor back ([6f0cc4d](https://github.com/RSSNext/follow/commit/6f0cc4d566c9f2f552f0fedf1a8e570a817e0f56)), closes [#1373](https://github.com/RSSNext/follow/issues/1373) +* checkLanguage utils ([225e470](https://github.com/RSSNext/follow/commit/225e470db84649bd23657d82b3c6a71b94cccc5f)) * copy button for ai summary ([b3ee572](https://github.com/RSSNext/follow/commit/b3ee5727e29d809092a8f4631d3e13c8d81095b9)) * copy profile email ([1e12588](https://github.com/RSSNext/follow/commit/1e1258811163d72ae36f29714eef5d50834c348b)) * customizable columns for masonry view, closed [#1749](https://github.com/RSSNext/follow/issues/1749) ([0e0ce84](https://github.com/RSSNext/follow/commit/0e0ce843235f01f33f4c5b9708aa67dac5901b46)) * customize toolbar ([#2468](https://github.com/RSSNext/follow/issues/2468)) ([38fe110](https://github.com/RSSNext/follow/commit/38fe11075424a31a60c9db1c2758980f957c19c2)) +* disable more line clamp for translation ([3c5dba4](https://github.com/RSSNext/follow/commit/3c5dba4d7aee870d1594e39a6e185622dff46abe)) * discover rsshub card background use single color ([7eeea5e](https://github.com/RSSNext/follow/commit/7eeea5e694c142803a37564ef8886d4fc4d2dab4)) * **discover:** enhance RSSHub recommendations with filters ([#1481](https://github.com/RSSNext/follow/issues/1481)) ([eb70126](https://github.com/RSSNext/follow/commit/eb70126b8283b6e0b246f86751e588a37cb34902)) * **discover:** implement searchable header and enhance UI with animated search bar ([2300996](https://github.com/RSSNext/follow/commit/2300996b87c2850f8f496bdd982e7ca8e5938d7f)) @@ -450,8 +483,11 @@ * edit rsshub instance ([7f080aa](https://github.com/RSSNext/follow/commit/7f080aa00b36d42f614bce6408a4400748ee3fb4)) * email verification ([3497623](https://github.com/RSSNext/follow/commit/3497623b853f8321ed81788e32d43846be6d6135)) * email verification ([d4905fd](https://github.com/RSSNext/follow/commit/d4905fd0082b41de3f76afa597d67030761a4ae8)) +* email verification toast ([3a6f287](https://github.com/RSSNext/follow/commit/3a6f28796699c3e0e04620b6f3f4cc180a1bb98a)) * enhance deep link handling in FollowWebView component to support new routes for adding and following items ([b46e178](https://github.com/RSSNext/follow/commit/b46e1780f783c24e5706b9c350b980c62a3cd9c6)) +* enhance input fields for numeric entry ([00a0764](https://github.com/RSSNext/follow/commit/00a07647f830cb3cbb4acd5234d25d45d08298cb)) * enhance SocialMediaItem component with dynamic title styling and action bar positioning ([5d2fe70](https://github.com/RSSNext/follow/commit/5d2fe70b52a857c16ed7220fc36ecd5111f8cd4d)) +* enhance tab layout and settings screen functionality ([f17446d](https://github.com/RSSNext/follow/commit/f17446d027f5528c1258a38de6cfc10d3fed43c8)) * enhance table with new translations and tooltip for descriptions ([#2471](https://github.com/RSSNext/follow/issues/2471)) ([99011eb](https://github.com/RSSNext/follow/commit/99011ebcf2cd3c4096520394537bb97b5ea08865)) * entry image gallery modal ([e0d3e17](https://github.com/RSSNext/follow/commit/e0d3e17da4ee17217d7b78871b546f43af87d893)) * export database ([85b4502](https://github.com/RSSNext/follow/commit/85b4502f9c113b8de73ccf4a167aa514a3c149ea)) @@ -462,12 +498,14 @@ * focusable ([eb01d03](https://github.com/RSSNext/follow/commit/eb01d030129f6196d6e9855be42a218be9d5668b)) * fold the long text content of social media ([e46b30a](https://github.com/RSSNext/follow/commit/e46b30a07fe74e1bacb234962b1542c849d2177d)) * folder mode option for export, close [#874](https://github.com/RSSNext/follow/issues/874) ([db3db14](https://github.com/RSSNext/follow/commit/db3db14e5dc5e4e7b132b2aa6b47d375eb789d3a)) +* full width rsshub page ([f9c7e55](https://github.com/RSSNext/follow/commit/f9c7e55ef9411cb5021ffd81bf6aeb9bfe132ab3)) * hide trend ([2b92599](https://github.com/RSSNext/follow/commit/2b92599e18d3d33fd0371d4d3a5f3e3a6d94aeb6)) * html render for native ([#2346](https://github.com/RSSNext/follow/issues/2346)) ([313ad9b](https://github.com/RSSNext/follow/commit/313ad9b7db7bbcf47be78b127c866849a61b1c8d)) * **i18n:** added multiple text translations (zh-TW) ([#1621](https://github.com/RSSNext/follow/issues/1621)) ([a15f23a](https://github.com/RSSNext/follow/commit/a15f23a5c7bb697c75c7fecf8603612b2584baa8)) * **i18n:** discover categories and mark all read undo button ([#1506](https://github.com/RSSNext/follow/issues/1506)) ([06bdf6c](https://github.com/RSSNext/follow/commit/06bdf6cfb90b03e1624189ebc05a389495e8bedc)) * **i18n:** translations (zh-TW) ([#1942](https://github.com/RSSNext/follow/issues/1942)) ([fea74b4](https://github.com/RSSNext/follow/commit/fea74b47829921822e0c701ea36ab1d4ac1e2083)) * **i18n:** translations (zh-TW) ([#2166](https://github.com/RSSNext/follow/issues/2166)) ([45e37a0](https://github.com/RSSNext/follow/commit/45e37a07e8eee65ae5b02b524b5e09185c804c08)) +* **i18n:** update Japanese locale with new toolbar customization and profile link social fields ([#2498](https://github.com/RSSNext/follow/issues/2498)) ([7e627fd](https://github.com/RSSNext/follow/commit/7e627fd9a0f7bcda9258b62ecebb2b28a717fc41)) * **icon:** use gradient fallback background ([e827002](https://github.com/RSSNext/follow/commit/e8270025e469d9a0463d267d3da591a2991951bd)) * image zoom ([1e47ba2](https://github.com/RSSNext/follow/commit/1e47ba25671000408e69fc3bae2c4626d0bd664e)), closes [#1183](https://github.com/RSSNext/follow/issues/1183) * improve email verification display ([#2424](https://github.com/RSSNext/follow/issues/2424)) ([ebc6c4f](https://github.com/RSSNext/follow/commit/ebc6c4fdb6773f2b7d94b81b839a3a338787c4b3)) @@ -479,10 +517,13 @@ * List delete add secondary confirmation ([#1254](https://github.com/RSSNext/follow/issues/1254)) ([14f2bac](https://github.com/RSSNext/follow/commit/14f2bac8f3ba7e8f2dbd3958a4394c5b11ea2537)) * list preview ([ae390f7](https://github.com/RSSNext/follow/commit/ae390f7afabf3e312217bf13a175e2036c2c2763)) * load archived entries automatically ([5fe9e0c](https://github.com/RSSNext/follow/commit/5fe9e0c0e24460ca0fe435db03b753e9dcd3df17)) +* **locales:** dynamically update copyright year ([#2583](https://github.com/RSSNext/follow/issues/2583)) ([a231e17](https://github.com/RSSNext/follow/commit/a231e178c19ea339470c3b0f609d454fa5902183)) +* **locales:** enhance zh-HK locale with new user prompts ([#2556](https://github.com/RSSNext/follow/issues/2556)) ([6828606](https://github.com/RSSNext/follow/commit/6828606cc42445f46df9fddb9b547fd1e05e68f6)) * **locales:** enhance zh-HK translations ([#2208](https://github.com/RSSNext/follow/issues/2208)) ([2ecd89f](https://github.com/RSSNext/follow/commit/2ecd89ffde7a24580fb879192abdfa7ff95c6988)) * **locales:** update Japanese translations for RSSHub and related settings ([#2474](https://github.com/RSSNext/follow/issues/2474)) ([6ed05ce](https://github.com/RSSNext/follow/commit/6ed05cea8bff558930a94c0742abb3d350580e54)) * **locales:** update zh-TW translations ([#2290](https://github.com/RSSNext/follow/issues/2290)) ([f734e6c](https://github.com/RSSNext/follow/commit/f734e6cfa3348f8906b48cbae8188531fd266e5e)) * **locales:** update zh-TW translations ([#2446](https://github.com/RSSNext/follow/issues/2446)) ([dbae887](https://github.com/RSSNext/follow/commit/dbae887df1fd23a92593682d74907d3851fdf183)) +* **locales:** update zh-TW translations ([#2522](https://github.com/RSSNext/follow/issues/2522)) ([5780aea](https://github.com/RSSNext/follow/commit/5780aea3a4f1d2da1389a1140008a402498db82c)) * manual action ([#1867](https://github.com/RSSNext/follow/issues/1867)) ([0eedbba](https://github.com/RSSNext/follow/commit/0eedbbadc263b474e2d4bfd5b6c498ac39c16c34)) * manually link social account ([fd373fb](https://github.com/RSSNext/follow/commit/fd373fb5271e4d9a31e37d49830d3fe3d4927922)) * **mark-all-button:** add countdown to auto-confirm message ([#1414](https://github.com/RSSNext/follow/issues/1414)) ([e1a5fc6](https://github.com/RSSNext/follow/commit/e1a5fc63f20c941140071789c9b67685da19ea5c)) @@ -498,8 +539,12 @@ * multi select with shift ([2158b1c](https://github.com/RSSNext/follow/commit/2158b1cc255baf3ff5e4e1a5806d85b7df64bd55)), closes [#1256](https://github.com/RSSNext/follow/issues/1256) * new user guide in mobile ([9eca05e](https://github.com/RSSNext/follow/commit/9eca05ef6f3612658397719a0af00dfc30196ca3)) * **obsidian:** use readability content when available ([b4a3197](https://github.com/RSSNext/follow/commit/b4a3197e6ebd39bb259a6bc67f408569354a205e)) +* one more line clamp for translated title ([fe44c38](https://github.com/RSSNext/follow/commit/fe44c38b4985a1316cda2df9d3c03182397a0733)) * optimize action pages ([a10b78a](https://github.com/RSSNext/follow/commit/a10b78a79ce836bfbd5b40e9021c30dfef64a23e)) +* optimize language check ([f850474](https://github.com/RSSNext/follow/commit/f850474d13b88e9f388216e1402f4fa0dfbc2d64)) * optimize profile setting page ([61508a4](https://github.com/RSSNext/follow/commit/61508a478ecc87b26330bc9376260458ec39cea5)) +* optimize translation display ([75b9135](https://github.com/RSSNext/follow/commit/75b9135a02fb6e94abb5c0670c9f12f354a88f0e)) +* **podcast:** add PodcastButton component to mobile float bar ([#2514](https://github.com/RSSNext/follow/issues/2514)) ([037791e](https://github.com/RSSNext/follow/commit/037791eae13bf0b9322eba332a852f73cd737bd0)) * prefer origin addresses for content images ([d4d4345](https://github.com/RSSNext/follow/commit/d4d43451dec839b64239e1835b2ac1a1aa2478be)) * re-design pwa updated notice ([4947082](https://github.com/RSSNext/follow/commit/49470821dca772a040b2b845c3c019f057a6047c)) * **reader:** support custom css, fixed [#256](https://github.com/RSSNext/follow/issues/256) ([b251fa9](https://github.com/RSSNext/follow/commit/b251fa9421417c75d71707c8f08850f2cc902e1a)) @@ -517,13 +562,26 @@ * replace twMacro with unplugin-ast ([#1462](https://github.com/RSSNext/follow/issues/1462)) ([05da9ca](https://github.com/RSSNext/follow/commit/05da9ca7d3d1f66bd22250599df8b66ea0fd3a43)) * reset feed ([#1419](https://github.com/RSSNext/follow/issues/1419)) ([9066758](https://github.com/RSSNext/follow/commit/9066758c322b8b31c1a9a137be017283fa92bea8)) * **rn-component:** implement toast manager ([950b5af](https://github.com/RSSNext/follow/commit/950b5af8ec0316875ed3ce5a99baaaecb56c6d37)) +* **rn-component:** refactor context menu for ios ([4bae36a](https://github.com/RSSNext/follow/commit/4bae36ae70e5ce7c60eec385873f943976e798f1)) +* **rn-search:** impl search feeds and list view ([f747882](https://github.com/RSSNext/follow/commit/f747882565fd474c0bf9cbab7b4a08a033e6386f)) +* **rn/search:** implement search tabview ([0ab3ba9](https://github.com/RSSNext/follow/commit/0ab3ba9a3eff93cee0983928a66467a327a7d3b9)) +* **rn:** add login teams ([aef9406](https://github.com/RSSNext/follow/commit/aef9406defdde43874945fd92ed422f53ed669fc)) +* **rn:** adjust login page styles ([0fc90c7](https://github.com/RSSNext/follow/commit/0fc90c7c280e8d1eb170a5aca2374d1bd29d3b97)) * **rn:** feed form and subscribe ([f9d5d76](https://github.com/RSSNext/follow/commit/f9d5d7698c0a3b498684468abf4ce50b3c223390)) * **rn:** fix dismiss modal when follow done ([468a3e4](https://github.com/RSSNext/follow/commit/468a3e490a7c0c3424b721011f2dc72503ba479f)) * **rn:** follow modal form ([9beff37](https://github.com/RSSNext/follow/commit/9beff379f621e7756d55c4b6b7c0d3a9a6b1fecd)) * **rn:** impl markdown component for rn ([93b38a7](https://github.com/RSSNext/follow/commit/93b38a7c70dda2e934a424995bb07e24bdefd2ac)) +* **rn:** implement feed drawer ([#2443](https://github.com/RSSNext/follow/issues/2443)) ([b581393](https://github.com/RSSNext/follow/commit/b581393c9aeccb15f1c3e7265273538eb055d140)) +* **rn:** implement search list ([01122c9](https://github.com/RSSNext/follow/commit/01122c9480a56b5bd3a90d347d787f080ff6d754)) * **rn:** implements discover page ([#2385](https://github.com/RSSNext/follow/issues/2385)) ([66f97c0](https://github.com/RSSNext/follow/commit/66f97c0057f571717b3b33929baf5a13fc7e677c)) * **rn:** init follow feed modal ([1e8196a](https://github.com/RSSNext/follow/commit/1e8196aad4037af6c9d5d29bb6361bbb19b5d00c)) +* **rn:** init index page ([8db5d53](https://github.com/RSSNext/follow/commit/8db5d53307c5c3b345499fb1c8b29108de97bdf7)) +* **rn:** init setting page ([5f44d24](https://github.com/RSSNext/follow/commit/5f44d245fd07ec36da16837ed6134538340b114f)) +* **rn:** setting page list design ([4ead508](https://github.com/RSSNext/follow/commit/4ead50869f7049ff399594c27b4c2cbcbed1e766)) +* **rn:** setting page scroll magic ([0b09616](https://github.com/RSSNext/follow/commit/0b096166d5524b6628802df6db666eb678dea73e)) * **rn:** subscription list item transition ([9745f4d](https://github.com/RSSNext/follow/commit/9745f4d9d0248073e15658ef710c5b2083b273ba)) +* **rn:** subscription list refresh contril ([8abde40](https://github.com/RSSNext/follow/commit/8abde402b2bab6bcb6fe1fcd349055b418d1ae5f)) +* **rn:** support follow list ([284a510](https://github.com/RSSNext/follow/commit/284a510217bde660d79b8bfa834f304b7640284d)) * **rn:** TabView component ([#2358](https://github.com/RSSNext/follow/issues/2358)) ([b0f7e82](https://github.com/RSSNext/follow/commit/b0f7e82ad3ee73d26f7d3a1cfb825848e8c1efc3)) * rsshub add modal content ([613b3b5](https://github.com/RSSNext/follow/commit/613b3b5c1978a656023a6596c7d421be66211aed)) * rsshub add modal loading status ([b044713](https://github.com/RSSNext/follow/commit/b044713528f3f17780ef019c6218535e35255cf4)) @@ -534,16 +592,19 @@ * rsshub status api ([38a6f99](https://github.com/RSSNext/follow/commit/38a6f990e922e0010ea48ea7be743f06cd7ed7ca)) * rsshub use modal ([5252f97](https://github.com/RSSNext/follow/commit/5252f97617d9ff31b99b4923f57b96b100436332)) * Scroll to top when re-navigating to Discover page while already on it ([#2371](https://github.com/RSSNext/follow/issues/2371)) ([a59cf4a](https://github.com/RSSNext/follow/commit/a59cf4a4cffa51edd5e8da5008e38d8476535404)) +* **search:** enhance search components with new styles and functionality ([0227427](https://github.com/RSSNext/follow/commit/0227427d295cf8f7e8a5b04ff396929b3d18e052)) * separate packaging for macOS x64 and arm64 architectures ([#1389](https://github.com/RSSNext/follow/issues/1389)) ([3e8de30](https://github.com/RSSNext/follow/commit/3e8de308a3b3a07c58ab3fd7d26aad5aa2328c99)) * set default unreadOnly to true ([8c8c765](https://github.com/RSSNext/follow/commit/8c8c765ff518b6ccc6457959fdf3fe8a4ddeee22)) * **settings:** clean web app service worker cache ([8ad40c0](https://github.com/RSSNext/follow/commit/8ad40c004ef2bf8924e2362729f42be83c09d102)) * show progress in searching ([e592e97](https://github.com/RSSNext/follow/commit/e592e97c9e0e6da8451c09b9b5f27612e6dbb971)), closes [#1457](https://github.com/RSSNext/follow/issues/1457) +* smaller debug button ([3ef5dea](https://github.com/RSSNext/follow/commit/3ef5dea5d190ef81e91ae936f3d6c84898c569ac)) * **social:** improve image gallery grid ([5dc6d9e](https://github.com/RSSNext/follow/commit/5dc6d9ee227fa714f39957ff6e744e3079a3e57d)) * support alway on top, fixed [#1740](https://github.com/RSSNext/follow/issues/1740) ([09df663](https://github.com/RSSNext/follow/commit/09df663a1c0b9a5ec5d30f2609a36b09c3e66e56)) * support drag-and-drop when importing opml ([#2001](https://github.com/RSSNext/follow/issues/2001)) ([bad7be9](https://github.com/RSSNext/follow/commit/bad7be9a8fa0d49c92d2cd17d441303643564f57)) * support markdown for announcement content ([539ec03](https://github.com/RSSNext/follow/commit/539ec03293fc6e3a11dbec69bbf78fe1c5dd2e8c)) * support read indicator and back to top, fixed [#2040](https://github.com/RSSNext/follow/issues/2040) ([dac1a47](https://github.com/RSSNext/follow/commit/dac1a47b0eedc083b8a94291373873f4a2315894)) * support zen mode sidebar entry timeline selector ([4ab132c](https://github.com/RSSNext/follow/commit/4ab132c593a7acce0167d02f6025030775a9fac5)) +* sync icons ([870905d](https://github.com/RSSNext/follow/commit/870905d7b7e97238bd2b4c435d78a7f9d019d6d8)) * **trending:** support language filter ([1cb5b73](https://github.com/RSSNext/follow/commit/1cb5b73eb5901f242d79f357d52bca7af5d8c8b3)) * unified feed title ([b86afe5](https://github.com/RSSNext/follow/commit/b86afe5a9789ad13c5a026adf553d2c6bc333bff)) * uniq macos entry column position ([4b63023](https://github.com/RSSNext/follow/commit/4b63023389e125353de343b679840e5fbca1a4d3)) @@ -558,7 +619,9 @@ * update toast style ([cf5eeb6](https://github.com/RSSNext/follow/commit/cf5eeb63c488f9978f12659af59286315b768eea)) * update zh-HK file ([#1955](https://github.com/RSSNext/follow/issues/1955)) ([9364761](https://github.com/RSSNext/follow/commit/936476169caba5cbacafe6f4d35f7857add32802)) * upload arm64 dmg separately ([1d77c09](https://github.com/RSSNext/follow/commit/1d77c092ce18fbabe6cfbbd21d9a96e3445795da)) +* use expo-secure-store and better-auth buildin getCookie for auth ([accbf7f](https://github.com/RSSNext/follow/commit/accbf7fa601cacc6a9c7bed837f16b6e284a128a)) * use FeedTitle ([73d2c30](https://github.com/RSSNext/follow/commit/73d2c30416ad3f2da626db003996c0fb036025ac)) +* use non https debug host ([61d3ed4](https://github.com/RSSNext/follow/commit/61d3ed49b817997b8e7876efda2c0e29b08f9c4b)) * **ux:** support native space key to page down, fixed [#1121](https://github.com/RSSNext/follow/issues/1121) ([baa66fe](https://github.com/RSSNext/follow/commit/baa66feaf19416d2b9e5efe6b79f6af1b3966404)) * web push ([cd32115](https://github.com/RSSNext/follow/commit/cd3211587109d0bf3e762df128356708fcea24b1)) * withdraw as rss3 ([5af9cf5](https://github.com/RSSNext/follow/commit/5af9cf5ec7767ea440b85afbc8ad46dd283c4dc6)) @@ -581,10 +644,12 @@ * reduce re-render zooming ([9b35074](https://github.com/RSSNext/follow/commit/9b350741d16cfd81770e82b866e92d732498ccf3)) * reduce the number of items rendered on the first render ([6704d61](https://github.com/RSSNext/follow/commit/6704d61baac7f54368cedc0438845aaacfb98960)) * resolve feed item select area performance problem ([f87c0ff](https://github.com/RSSNext/follow/commit/f87c0ff2abfc430719894765796ca457c3f8e7f0)) +* **rn:** memo and optimize list ([89126ed](https://github.com/RSSNext/follow/commit/89126ed83ecc410cc36f568ba3e161dde141bf77)) ### Reverts +* Revert "docs: fix video preview" ([8499079](https://github.com/RSSNext/follow/commit/8499079e503ad8cfc35e390755cec159f9eb3b7a)) * Revert "build: use pnpm catalogs" ([37164a3](https://github.com/RSSNext/follow/commit/37164a3674fa0bdf5b10919e788142a3d1b1cce3)) * Revert "perf: reduce the number of items rendered on the first render" ([9e45d04](https://github.com/RSSNext/follow/commit/9e45d0441ef16fb334af555c22276f7d77a72733)), closes [#1659](https://github.com/RSSNext/follow/issues/1659) diff --git a/changelog/0.3.2.md b/changelog/0.3.2.md new file mode 100644 index 0000000000..a04b37390b --- /dev/null +++ b/changelog/0.3.2.md @@ -0,0 +1,5 @@ +# What's new in v0.3.2 + +## New Features + +- Protect your account logins and wallet operations with two-factor authentication (2FA). diff --git a/changelog/next.md b/changelog/next.md index 2839b2f13a..17888d80b6 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -2,4 +2,6 @@ ## New Features -- Protect your account logins and wallet operations with two-factor authentication (2FA). +## Improvements + +## Bug Fixes diff --git a/package.json b/package.json index 286f1e2f45..c3eda35d46 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "Follow", "type": "module", - "version": "0.3.1-beta.0", + "version": "0.3.2-beta.0", "private": true, "packageManager": "pnpm@9.12.3", "description": "Follow your favorites in one inbox", @@ -184,5 +184,5 @@ ] }, "productName": "Follow", - "mainHash": "b53d32acaa385ba52e4cc2e78a9710907229d10c52e90689b0abc0ae5d003f1b" + "mainHash": "1e87ac6080cfcc38786acfb9daba3c55e5f1ffa77df833c7182910971bae5ae9" } From 3479a89b1faefc5a4bcac47d71716a80ede259ef Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Fri, 17 Jan 2025 21:12:38 +0800 Subject: [PATCH 77/93] feat: basic entry data sync (#2597) --- apps/mobile/drizzle/0007_curvy_tarantula.sql | 1 + apps/mobile/drizzle/meta/0007_snapshot.json | 501 +++++++++++++++++++ apps/mobile/drizzle/meta/_journal.json | 7 + apps/mobile/drizzle/migrations.js | 2 + apps/mobile/src/database/schemas/index.ts | 1 + apps/mobile/src/morph/db-store.ts | 7 +- apps/mobile/src/morph/hono.ts | 36 ++ apps/mobile/src/morph/store-db.ts | 6 +- apps/mobile/src/morph/types.ts | 3 +- apps/mobile/src/services/entry.ts | 37 ++ apps/mobile/src/services/list.ts | 16 + apps/mobile/src/store/entry/getter.ts | 8 + apps/mobile/src/store/entry/hooks.ts | 78 +++ apps/mobile/src/store/entry/store.ts | 174 +++++++ apps/mobile/src/store/entry/types.ts | 13 + apps/mobile/src/store/entry/utils.ts | 56 +++ apps/mobile/src/store/list/hooks.ts | 6 + apps/mobile/src/store/list/store.ts | 28 ++ apps/mobile/src/store/subscription/store.ts | 1 + 19 files changed, 978 insertions(+), 3 deletions(-) create mode 100644 apps/mobile/drizzle/0007_curvy_tarantula.sql create mode 100644 apps/mobile/drizzle/meta/0007_snapshot.json create mode 100644 apps/mobile/src/services/entry.ts create mode 100644 apps/mobile/src/store/entry/getter.ts create mode 100644 apps/mobile/src/store/entry/hooks.ts create mode 100644 apps/mobile/src/store/entry/store.ts create mode 100644 apps/mobile/src/store/entry/types.ts create mode 100644 apps/mobile/src/store/entry/utils.ts diff --git a/apps/mobile/drizzle/0007_curvy_tarantula.sql b/apps/mobile/drizzle/0007_curvy_tarantula.sql new file mode 100644 index 0000000000..d780cd4f0d --- /dev/null +++ b/apps/mobile/drizzle/0007_curvy_tarantula.sql @@ -0,0 +1 @@ +ALTER TABLE `lists` ADD `entry_ids` text; \ No newline at end of file diff --git a/apps/mobile/drizzle/meta/0007_snapshot.json b/apps/mobile/drizzle/meta/0007_snapshot.json new file mode 100644 index 0000000000..843555bf50 --- /dev/null +++ b/apps/mobile/drizzle/meta/0007_snapshot.json @@ -0,0 +1,501 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "0d5c6f89-f31c-405c-8c2d-7b59ab03a275", + "prevId": "9f943ca5-ddaf-4916-98cd-d3e3f7ead201", + "tables": { + "entries": { + "name": "entries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guid": { + "name": "guid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_url": { + "name": "author_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_avatar": { + "name": "author_avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inserted_at": { + "name": "inserted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "media": { + "name": "media", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "categories": { + "name": "categories", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attachments": { + "name": "attachments", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "extra": { + "name": "extra", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "feed_id": { + "name": "feed_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inbox_handle": { + "name": "inbox_handle", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "read": { + "name": "read", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "feeds": { + "name": "feeds", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_at": { + "name": "error_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "site_url": { + "name": "site_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inboxes": { + "name": "inboxes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "lists": { + "name": "lists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "feed_ids": { + "name": "feed_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "view": { + "name": "view", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fee": { + "name": "fee", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entry_ids": { + "name": "entry_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "subscriptions": { + "name": "subscriptions", + "columns": { + "feed_id": { + "name": "feed_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "list_id": { + "name": "list_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inbox_id": { + "name": "inbox_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "view": { + "name": "view", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_private": { + "name": "is_private", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "unread": { + "name": "unread", + "columns": { + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_me": { + "name": "is_me", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/apps/mobile/drizzle/meta/_journal.json b/apps/mobile/drizzle/meta/_journal.json index e6a3090c50..9ec202076a 100644 --- a/apps/mobile/drizzle/meta/_journal.json +++ b/apps/mobile/drizzle/meta/_journal.json @@ -50,6 +50,13 @@ "when": 1737080887634, "tag": "0006_exotic_kid_colt", "breakpoints": true + }, + { + "idx": 7, + "version": "6", + "when": 1737108493439, + "tag": "0007_curvy_tarantula", + "breakpoints": true } ] } diff --git a/apps/mobile/drizzle/migrations.js b/apps/mobile/drizzle/migrations.js index 49ecb4cf9d..48decb123a 100644 --- a/apps/mobile/drizzle/migrations.js +++ b/apps/mobile/drizzle/migrations.js @@ -7,6 +7,7 @@ import m0003 from "./0003_known_roland_deschain.sql" import m0004 from "./0004_majestic_thunderbolt_ross.sql" import m0005 from "./0005_tense_sleepwalker.sql" import m0006 from "./0006_exotic_kid_colt.sql" +import m0007 from "./0007_curvy_tarantula.sql" import journal from "./meta/_journal.json" export default { @@ -19,5 +20,6 @@ export default { m0004, m0005, m0006, + m0007, }, } diff --git a/apps/mobile/src/database/schemas/index.ts b/apps/mobile/src/database/schemas/index.ts index 7b65634fd3..bab071cfa3 100644 --- a/apps/mobile/src/database/schemas/index.ts +++ b/apps/mobile/src/database/schemas/index.ts @@ -44,6 +44,7 @@ export const listsTable = sqliteTable("lists", { image: text("image"), fee: integer("fee"), ownerUserId: text("owner_user_id"), + entryIds: text("entry_ids", { mode: "json" }).$type(), }) export const unreadTable = sqliteTable("unread", { diff --git a/apps/mobile/src/morph/db-store.ts b/apps/mobile/src/morph/db-store.ts index e54d126c53..19e678882a 100644 --- a/apps/mobile/src/morph/db-store.ts +++ b/apps/mobile/src/morph/db-store.ts @@ -1,4 +1,5 @@ -import type { SubscriptionSchema } from "../database/schemas/types" +import type { EntrySchema, SubscriptionSchema } from "../database/schemas/types" +import type { EntryModel } from "../store/entry/types" import type { SubscriptionModel } from "../store/subscription/store" class DbStoreMorph { @@ -8,6 +9,10 @@ class DbStoreMorph { isPrivate: subscription.isPrivate ? true : false, } } + + toEntryModel(entry: EntrySchema): EntryModel { + return entry + } } export const dbStoreMorph = new DbStoreMorph() diff --git a/apps/mobile/src/morph/hono.ts b/apps/mobile/src/morph/hono.ts index c881ca0e3e..cb7d6f89cc 100644 --- a/apps/mobile/src/morph/hono.ts +++ b/apps/mobile/src/morph/hono.ts @@ -1,4 +1,5 @@ import type { FeedSchema, InboxSchema } from "../database/schemas/types" +import type { EntryModel } from "../store/entry/types" import type { ListModel } from "../store/list/store" import type { SubscriptionModel } from "../store/subscription/store" import type { HonoApiClient } from "./types" @@ -72,6 +73,7 @@ class Morph { ownerUserId: list.owner.id, feedIds: list.feedIds!, fee: list.fee!, + entryIds: [], }) } @@ -91,8 +93,42 @@ class Morph { ownerUserId: data.ownerUserId!, feedIds: data.feedIds!, fee: data.fee!, + entryIds: [], } } + + toEntry(data?: HonoApiClient.Entry_Get): EntryModel[] { + const entries: EntryModel[] = [] + for (const item of data ?? []) { + entries.push({ + id: item.entries.id, + title: item.entries.title, + url: item.entries.url, + content: "", + description: item.entries.description, + guid: item.entries.guid, + author: item.entries.author, + authorUrl: item.entries.authorUrl, + authorAvatar: item.entries.authorAvatar, + insertedAt: new Date(item.entries.insertedAt), + publishedAt: new Date(item.entries.publishedAt), + media: item.entries.media ?? null, + categories: item.entries.categories ?? null, + attachments: item.entries.attachments ?? null, + extra: item.entries.extra + ? { + links: item.entries.extra.links ?? undefined, + } + : null, + language: item.entries.language, + feedId: item.feeds.id, + // TODO: handle inboxHandle + inboxHandle: "", + read: false, + }) + } + return entries + } } export const honoMorph = new Morph() diff --git a/apps/mobile/src/morph/store-db.ts b/apps/mobile/src/morph/store-db.ts index e1ab6b0f39..f6ff8f1901 100644 --- a/apps/mobile/src/morph/store-db.ts +++ b/apps/mobile/src/morph/store-db.ts @@ -1,4 +1,5 @@ -import type { ListSchema, SubscriptionSchema } from "../database/schemas/types" +import type { EntrySchema, ListSchema, SubscriptionSchema } from "../database/schemas/types" +import type { EntryModel } from "../store/entry/types" import type { ListModel } from "../store/list/store" import type { SubscriptionModel } from "../store/subscription/store" @@ -16,6 +17,9 @@ class StoreDbMorph { isPrivate: subscription.isPrivate ? 1 : 0, } } + toEntrySchema(entry: EntryModel): EntrySchema { + return entry + } } export const storeDbMorph = new StoreDbMorph() diff --git a/apps/mobile/src/morph/types.ts b/apps/mobile/src/morph/types.ts index 4f06217f3c..375f38d5ae 100644 --- a/apps/mobile/src/morph/types.ts +++ b/apps/mobile/src/morph/types.ts @@ -3,9 +3,10 @@ import type { apiClient } from "../lib/api-fetch" // Add ExtractData type utility type ExtractData any> = - Awaited> extends { data: infer D } ? D : never + Awaited> extends { data?: infer D } ? D : never export namespace HonoApiClient { export type Subscription_Get = ExtractData export type List_Get = ExtractData + export type Entry_Get = ExtractData } diff --git a/apps/mobile/src/services/entry.ts b/apps/mobile/src/services/entry.ts new file mode 100644 index 0000000000..b1da6eaf5c --- /dev/null +++ b/apps/mobile/src/services/entry.ts @@ -0,0 +1,37 @@ +import { eq } from "drizzle-orm" + +import { db } from "../database" +import { entriesTable } from "../database/schemas" +import type { EntrySchema } from "../database/schemas/types" +import { dbStoreMorph } from "../morph/db-store" +import { entryActions } from "../store/entry/store" +import type { Hydratable, Resetable } from "./internal/base" +import { conflictUpdateAllExcept } from "./internal/utils" + +class EntryServiceStatic implements Hydratable, Resetable { + async reset() { + await db.delete(entriesTable).execute() + } + + async upsertMany(entries: EntrySchema[]) { + if (entries.length === 0) return + await db + .insert(entriesTable) + .values(entries) + .onConflictDoUpdate({ + target: [entriesTable.id], + set: conflictUpdateAllExcept(entriesTable, ["id"]), + }) + } + + async patch(entry: Partial & { id: string }) { + await db.update(entriesTable).set(entry).where(eq(entriesTable.id, entry.id)) + } + + async hydrate() { + const entries = await db.query.entriesTable.findMany() + entryActions.upsertManyInSession(entries.map((e) => dbStoreMorph.toEntryModel(e))) + } +} + +export const EntryService = new EntryServiceStatic() diff --git a/apps/mobile/src/services/list.ts b/apps/mobile/src/services/list.ts index 035bf73830..f54242e711 100644 --- a/apps/mobile/src/services/list.ts +++ b/apps/mobile/src/services/list.ts @@ -1,3 +1,5 @@ +import { eq } from "drizzle-orm" + import { db } from "../database" import { listsTable } from "../database/schemas" import type { ListSchema } from "../database/schemas/types" @@ -28,6 +30,20 @@ class ListServiceStatic implements Hydratable, Resetable { set: conflictUpdateAllExcept(listsTable, ["id"]), }) } + async addEntryIds(params: { listId: string; entryIds: string[] }) { + const list = await db.query.listsTable.findFirst({ + where: eq(listsTable.id, params.listId), + }) + + if (!list) return + + await db + .update(listsTable) + .set({ + entryIds: [...new Set([...(list.entryIds ?? []), ...params.entryIds])], + }) + .where(eq(listsTable.id, params.listId)) + } } export const ListService = new ListServiceStatic() diff --git a/apps/mobile/src/store/entry/getter.ts b/apps/mobile/src/store/entry/getter.ts new file mode 100644 index 0000000000..142fca1bdd --- /dev/null +++ b/apps/mobile/src/store/entry/getter.ts @@ -0,0 +1,8 @@ +import { useEntryStore } from "./store" +import type { EntryModel } from "./types" + +const get = useEntryStore.getState + +export const getEntry = (id: string): EntryModel | undefined => { + return get().data[id] +} diff --git a/apps/mobile/src/store/entry/hooks.ts b/apps/mobile/src/store/entry/hooks.ts new file mode 100644 index 0000000000..c324f79a05 --- /dev/null +++ b/apps/mobile/src/store/entry/hooks.ts @@ -0,0 +1,78 @@ +import type { FeedViewType } from "@follow/constants" +import { useQuery } from "@tanstack/react-query" +import { useCallback } from "react" + +import { getEntry } from "./getter" +import { entrySyncServices, useEntryStore } from "./store" +import type { EntryModel, FetchEntriesProps } from "./types" + +export const usePrefetchEntries = (props: FetchEntriesProps) => { + const { feedId, inboxId, listId, view, read, limit, pageParam, isArchived } = props + return useQuery({ + queryKey: ["entries", feedId, inboxId, listId, view, read, limit, pageParam, isArchived], + queryFn: () => entrySyncServices.fetchEntries(props), + }) +} + +export const useEntry = (id: string): EntryModel | undefined => { + return useEntryStore((state) => state.data[id]) +} + +function sortEntryIdsByPublishDate(a: string, b: string) { + const entryA = getEntry(a) + const entryB = getEntry(b) + if (!entryA || !entryB) return 0 + return entryB.publishedAt.getTime() - entryA.publishedAt.getTime() +} + +export const useEntryIdsByView = (view: FeedViewType) => { + return useEntryStore( + useCallback( + (state) => { + const ids = state.entryIdByView[view] + if (!ids) return [] + return Array.from(ids).sort((a, b) => sortEntryIdsByPublishDate(a, b)) + }, + [view], + ), + ) +} + +export const useEntryIdsByFeedId = (feedId: string) => { + return useEntryStore( + useCallback( + (state) => { + const ids = state.entryIdByFeed[feedId] + if (!ids) return [] + return Array.from(ids).sort((a, b) => sortEntryIdsByPublishDate(a, b)) + }, + [feedId], + ), + ) +} + +export const useEntryIdsByInboxId = (inboxId: string) => { + return useEntryStore( + useCallback( + (state) => { + const ids = state.entryIdByInbox[inboxId] + if (!ids) return [] + return Array.from(ids).sort((a, b) => sortEntryIdsByPublishDate(a, b)) + }, + [inboxId], + ), + ) +} + +export const useEntryIdsByCategory = (category: string) => { + return useEntryStore( + useCallback( + (state) => { + const ids = state.entryIdByCategory[category] + if (!ids) return [] + return Array.from(ids).sort((a, b) => sortEntryIdsByPublishDate(a, b)) + }, + [category], + ), + ) +} diff --git a/apps/mobile/src/store/entry/store.ts b/apps/mobile/src/store/entry/store.ts new file mode 100644 index 0000000000..41a8d01b78 --- /dev/null +++ b/apps/mobile/src/store/entry/store.ts @@ -0,0 +1,174 @@ +import { FeedViewType } from "@follow/constants" + +import { apiClient } from "@/src/lib/api-fetch" +import { honoMorph } from "@/src/morph/hono" +import { storeDbMorph } from "@/src/morph/store-db" +import { EntryService } from "@/src/services/entry" + +import { createImmerSetter, createTransaction, createZustandStore } from "../internal/helper" +import { listActions } from "../list/store" +import { getSubscription } from "../subscription/getter" +import type { EntryModel, FetchEntriesProps } from "./types" +import { getEntriesParams } from "./utils" + +type EntryId = string +type FeedId = string +type InboxId = string +type Category = string + +interface EntryState { + data: Record + entryIdByView: Record> + entryIdByCategory: Record> + entryIdByFeed: Record> + entryIdByInbox: Record> +} + +const defaultState: EntryState = { + data: {}, + entryIdByView: { + [FeedViewType.Articles]: new Set(), + [FeedViewType.Audios]: new Set(), + [FeedViewType.Notifications]: new Set(), + [FeedViewType.Pictures]: new Set(), + [FeedViewType.SocialMedia]: new Set(), + [FeedViewType.Videos]: new Set(), + }, + entryIdByCategory: {}, + entryIdByFeed: {}, + entryIdByInbox: {}, +} + +export const useEntryStore = createZustandStore("entry")(() => defaultState) + +const set = useEntryStore.setState +const immerSet = createImmerSetter(useEntryStore) + +class EntryActions { + private addEntryIdToFeed({ + draft, + feedId, + entryId, + }: { + draft: EntryState + feedId?: FeedId | null + entryId: EntryId + }) { + if (!feedId) return + const entryIdSetByFeed = draft.entryIdByFeed[feedId] + if (!entryIdSetByFeed) { + draft.entryIdByFeed[feedId] = new Set([entryId]) + } else { + entryIdSetByFeed.add(entryId) + } + + const subscription = getSubscription(feedId) + if (subscription?.view) { + draft.entryIdByView[subscription.view].add(entryId) + } + if (subscription?.category) { + const entryIdSetByCategory = draft.entryIdByCategory[subscription.category] + if (!entryIdSetByCategory) { + draft.entryIdByCategory[subscription.category] = new Set([entryId]) + } else { + entryIdSetByCategory.add(entryId) + } + } + } + + private addEntryIdToInbox({ + draft, + inboxHandle, + entryId, + }: { + draft: EntryState + inboxHandle?: InboxId | null + entryId: EntryId + }) { + if (!inboxHandle) return + const entryIdSetByInbox = draft.entryIdByInbox[inboxHandle] + if (!entryIdSetByInbox) { + draft.entryIdByInbox[inboxHandle] = new Set([entryId]) + } else { + entryIdSetByInbox.add(entryId) + } + } + + upsertManyInSession(entries: EntryModel[]) { + if (entries.length === 0) return + + immerSet((draft) => { + for (const entry of entries) { + draft.data[entry.id] = entry + + const { feedId, inboxHandle } = entry + this.addEntryIdToFeed({ + draft, + feedId, + entryId: entry.id, + }) + + this.addEntryIdToInbox({ + draft, + inboxHandle, + entryId: entry.id, + }) + } + }) + } + + async upsertMany(entries: EntryModel[]) { + const tx = createTransaction() + tx.store(() => { + this.upsertManyInSession(entries) + }) + + tx.persist(() => { + return EntryService.upsertMany(entries.map((e) => storeDbMorph.toEntrySchema(e))) + }) + + await tx.run() + } + + reset() { + set(defaultState) + } +} + +class EntrySyncServices { + async fetchEntries(props: FetchEntriesProps) { + const { feedId, inboxId, listId, view, read, limit, pageParam, isArchived } = props + const params = getEntriesParams({ + feedId, + inboxId, + listId, + view, + }) + const res = await apiClient.entries.$post({ + json: { + publishedAfter: pageParam, + read, + limit, + isArchived, + ...params, + }, + }) + + if (!pageParam) { + entryActions.reset() + } + + const entries = honoMorph.toEntry(res.data) + await entryActions.upsertMany(entries) + if (params.listId) { + await listActions.addEntryIds({ + listId: params.listId, + entryIds: entries.map((e) => e.id), + }) + } + return entries + } +} + +export const entrySyncServices = new EntrySyncServices() +export const entryActions = new EntryActions() diff --git a/apps/mobile/src/store/entry/types.ts b/apps/mobile/src/store/entry/types.ts new file mode 100644 index 0000000000..30c7eb98e1 --- /dev/null +++ b/apps/mobile/src/store/entry/types.ts @@ -0,0 +1,13 @@ +import type { EntrySchema } from "@/src/database/schemas/types" + +export type EntryModel = EntrySchema +export type FetchEntriesProps = { + feedId?: number | string + inboxId?: number | string + listId?: number | string + view?: number + read?: boolean + limit?: number + pageParam?: string + isArchived?: boolean +} diff --git a/apps/mobile/src/store/entry/utils.ts b/apps/mobile/src/store/entry/utils.ts new file mode 100644 index 0000000000..a4fefeea5f --- /dev/null +++ b/apps/mobile/src/store/entry/utils.ts @@ -0,0 +1,56 @@ +import { FeedViewType } from "@follow/constants" + +/// Feed +export const FEED_COLLECTION_LIST = "collections" + +/// Route Keys +export const ROUTE_FEED_PENDING = "all" +export const ROUTE_ENTRY_PENDING = "pending" +export const ROUTE_FEED_IN_FOLDER = "folder-" +export const ROUTE_FEED_IN_LIST = "list-" +export const ROUTE_FEED_IN_INBOX = "inbox-" + +export const INBOX_PREFIX_ID = "inbox-" + +export function getEntriesParams({ + feedId, + inboxId, + listId, + view, +}: { + feedId?: number | string + inboxId?: number | string + listId?: number | string + view?: number +}) { + const params: { + feedId?: string + feedIdList?: string[] + isCollection?: boolean + withContent?: boolean + inboxId?: string + listId?: string + } = {} + if (inboxId) { + params.inboxId = `${inboxId}` + } else if (listId) { + params.listId = `${listId}` + } else if (feedId) { + if (feedId === FEED_COLLECTION_LIST) { + params.isCollection = true + } else if (feedId !== ROUTE_FEED_PENDING) { + if (feedId.toString().includes(",")) { + params.feedIdList = `${feedId}`.split(",") + } else { + params.feedId = `${feedId}` + } + } + } + if (view === FeedViewType.SocialMedia) { + params.withContent = true + } + return { + view, + ...params, + } +} diff --git a/apps/mobile/src/store/list/hooks.ts b/apps/mobile/src/store/list/hooks.ts index f86dd3badb..630e2df7b1 100644 --- a/apps/mobile/src/store/list/hooks.ts +++ b/apps/mobile/src/store/list/hooks.ts @@ -12,3 +12,9 @@ export const useIsOwnList = (id: string) => { return state.lists[id]?.userId === whoami()?.id }) } + +export const useListEntryIds = (id: string) => { + return useListStore((state) => { + return state.lists[id]?.entryIds || [] + }) +} diff --git a/apps/mobile/src/store/list/store.ts b/apps/mobile/src/store/list/store.ts index 8390d0f162..35563b1aba 100644 --- a/apps/mobile/src/store/list/store.ts +++ b/apps/mobile/src/store/list/store.ts @@ -45,6 +45,34 @@ class ListActions { }) tx.run() } + + addEntryIdsInSession(params: { listId: string; entryIds: string[] }) { + const state = get() + const list = state.lists[params.listId] + + if (!list) return + + set({ + ...state, + lists: { + ...state.lists, + [params.listId]: { ...list, feedIds: [...list.feedIds, ...params.entryIds] }, + }, + }) + } + + async addEntryIds(params: { listId: string; entryIds: string[] }) { + const tx = createTransaction() + tx.store(() => { + this.addEntryIdsInSession(params) + }) + + tx.persist(() => { + return ListService.addEntryIds(params) + }) + await tx.run() + } + reset() { set(defaultState) } diff --git a/apps/mobile/src/store/subscription/store.ts b/apps/mobile/src/store/subscription/store.ts index 470337c87a..7e2c3366d9 100644 --- a/apps/mobile/src/store/subscription/store.ts +++ b/apps/mobile/src/store/subscription/store.ts @@ -211,6 +211,7 @@ class SubscriptionSyncService { { ...data.list, userId: data.list.ownerUserId, + entryIds: [], }, ]) } From 818b024bbc61e4488f207a8e23838cd98a40bf3e Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Fri, 17 Jan 2025 21:19:16 +0800 Subject: [PATCH 78/93] chore: update hono type --- .../power/my-wallet-section/withdraw.tsx | 1 - packages/shared/src/hono.ts | 21 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/renderer/src/modules/power/my-wallet-section/withdraw.tsx b/apps/renderer/src/modules/power/my-wallet-section/withdraw.tsx index 54f1724339..ad427fcc92 100644 --- a/apps/renderer/src/modules/power/my-wallet-section/withdraw.tsx +++ b/apps/renderer/src/modules/power/my-wallet-section/withdraw.tsx @@ -79,7 +79,6 @@ const WithdrawModalContent = ({ dismiss }: { dismiss: () => void }) => { toRss3?: boolean }) => { const amountBigInt = from(amount, 18)[0] - // @ts-expect-error FIXME: remove this line after API is back await apiClient.wallets.transactions.withdraw.$post({ json: { address, diff --git a/packages/shared/src/hono.ts b/packages/shared/src/hono.ts index 3859404aa4..bcd635b41a 100644 --- a/packages/shared/src/hono.ts +++ b/packages/shared/src/hono.ts @@ -15677,6 +15677,27 @@ declare const _routes: hono_hono_base.HonoBase Date: Fri, 17 Jan 2025 21:22:50 +0800 Subject: [PATCH 79/93] feat: enhance tab screen with blur effect and update action buttons - Added BlurEffect to the header for improved aesthetics. - Replaced the Edit button with a drawer opener icon for better UX. - Removed unused SortActionButton from the right action area. Signed-off-by: Innei --- apps/mobile/src/screens/(stack)/(tabs)/index.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/apps/mobile/src/screens/(stack)/(tabs)/index.tsx b/apps/mobile/src/screens/(stack)/(tabs)/index.tsx index 6033a5e7c5..eb8fa3236b 100644 --- a/apps/mobile/src/screens/(stack)/(tabs)/index.tsx +++ b/apps/mobile/src/screens/(stack)/(tabs)/index.tsx @@ -3,12 +3,13 @@ import { useEffect } from "react" import { Text, TouchableOpacity, View } from "react-native" import { useSafeAreaInsets } from "react-native-safe-area-context" +import { BlurEffect } from "@/src/components/common/HeaderBlur" import { SafeNavigationScrollView } from "@/src/components/common/SafeNavigationScrollView" import { views } from "@/src/constants/views" import { AddCuteReIcon } from "@/src/icons/add_cute_re" +import { LayoutLeftbarOpenCuteReIcon } from "@/src/icons/layout_leftbar_open_cute_re" import { useFeedDrawer, useSetDrawerSwipeDisabled } from "@/src/modules/feed-drawer/atoms" import { useCurrentView } from "@/src/modules/subscription/atoms" -import { SortActionButton } from "@/src/modules/subscription/header-actions" import { usePrefetchUnread } from "@/src/store/unread/hooks" import { accentColor } from "@/src/theme/colors" @@ -32,8 +33,10 @@ export default function Index() { headerLeft: LeftAction, headerRight: RightAction, headerTransparent: true, + headerBackground: BlurEffect, }} /> + EntryList Placeholder @@ -50,16 +53,12 @@ const useActionPadding = () => { function LeftAction() { const { openDrawer } = useFeedDrawer() - const handleEdit = () => { - openDrawer() - //TODO - } const insets = useActionPadding() return ( - - Edit + + ) } @@ -69,7 +68,6 @@ function RightAction() { return ( - From 8a52f041bb75bb97e111ef065eb70a5bf07b9661 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Fri, 17 Jan 2025 21:31:00 +0800 Subject: [PATCH 80/93] chore: lint --- apps/mobile/src/modules/login/email.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/mobile/src/modules/login/email.tsx b/apps/mobile/src/modules/login/email.tsx index f90a553610..984eecf59d 100644 --- a/apps/mobile/src/modules/login/email.tsx +++ b/apps/mobile/src/modules/login/email.tsx @@ -1,6 +1,5 @@ import { zodResolver } from "@hookform/resolvers/zod" import { useMutation } from "@tanstack/react-query" -import { router } from "expo-router" import { useContext, useEffect } from "react" import type { Control } from "react-hook-form" import { useController, useForm } from "react-hook-form" From 346cd2531182af26b6e72e7ab39da6dd4fd9c476 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Fri, 17 Jan 2025 21:32:52 +0800 Subject: [PATCH 81/93] chore: disable typechecked lint rules --- eslint.config.mjs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 82058ccc45..b9f9f03f2e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -2,7 +2,7 @@ import path from "node:path" import { fixupPluginRules } from "@eslint/compat" -import { defineConfig, GLOB_TS_SRC } from "eslint-config-hyoban" +import { defineConfig } from "eslint-config-hyoban" import reactNative from "eslint-plugin-react-native" import checkI18nJson from "./plugins/eslint/eslint-check-i18n-json.js" @@ -24,24 +24,10 @@ export default defineConfig( "apps/mobile/.expo", ], preferESM: false, - projectService: { - allowDefaultProject: ["apps/main/preload/index.d.ts"], - defaultProject: "tsconfig.json", - }, - typeChecked: "essential", tailwindCSS: { order: false, }, }, - { - files: GLOB_TS_SRC, - rules: { - "@typescript-eslint/require-await": "off", - "@typescript-eslint/await-thenable": "off", - "@typescript-eslint/no-floating-promises": "off", - "@typescript-eslint/no-misused-promises": "off", - }, - }, { settings: { tailwindcss: { From b21d92629fa2710abdba8adb481c7b62a655d792 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Fri, 17 Jan 2025 21:49:51 +0800 Subject: [PATCH 82/93] chore: use tsslint for lint rule require type check --- .vscode/extensions.json | 7 ++- package.json | 6 ++- pnpm-lock.yaml | 100 ++++++++++++++++++++++++++++++++++++---- tsslint.config.ts | 12 +++++ 4 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 tsslint.config.ts diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 940260d856..8734d68f37 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,8 @@ { - "recommendations": ["dbaeumer.vscode-eslint"] + "recommendations": [ + "dbaeumer.vscode-eslint", + "johnsoncodehk.vscode-tsslint", + "esbenp.prettier-vscode", + "bradlc.vscode-tailwindcss" + ] } diff --git a/package.json b/package.json index c3eda35d46..dac1c6618e 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,9 @@ "format:check": "prettier --check .", "generator:i18n-template": "tsx scripts/generate-i18n-locale.ts", "hotfix": "vv -c bump.hotfix.config.js", - "lint": "eslint", + "lint": "pnpm run lint:tsl && eslint", "lint:fix": "eslint --fix", + "lint:tsl": "tsslint --project apps/*/tsconfig.json", "mitproxy": "bash scripts/run-proxy.sh", "polyfill-optimize": "pnpx nolyfill install", "prepare": "simple-git-hooks", @@ -68,6 +69,9 @@ "@t3-oss/env-core": "^0.11.1", "@tailwindcss/container-queries": "0.1.1", "@tailwindcss/typography": "0.5.15", + "@tsslint/cli": "^1.5.8", + "@tsslint/config": "^1.5.8", + "@tsslint/eslint": "^1.5.8", "@types/html-minifier-terser": "7.0.2", "@types/js-yaml": "4.0.9", "@types/node": "^22.10.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a5fb16189..e2362c057d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,6 +117,15 @@ importers: '@tailwindcss/typography': specifier: 0.5.15 version: 0.5.15(tailwindcss@3.4.16(ts-node@10.9.1(@types/node@22.10.1)(typescript@5.7.2))) + '@tsslint/cli': + specifier: ^1.5.8 + version: 1.5.8(typescript@5.7.2) + '@tsslint/config': + specifier: ^1.5.8 + version: 1.5.8(typescript@5.7.2) + '@tsslint/eslint': + specifier: ^1.5.8 + version: 1.5.8(jiti@2.4.2)(typescript@5.7.2) '@types/html-minifier-terser': specifier: 7.0.2 version: 7.0.2 @@ -5725,6 +5734,24 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@tsslint/cli@1.5.8': + resolution: {integrity: sha512-ty2ZxHrRPAtGlu/v3DZheU4ciQxodSIvkzuRYLy7Or16JoDcjUjIMcBBOCADuw1lsj45HERCzUDX7HBMTE5Wjw==} + hasBin: true + peerDependencies: + typescript: '*' + + '@tsslint/config@1.5.8': + resolution: {integrity: sha512-kJjCgp8+4IQDvUpUNIMA6YUg+D6Cl3FtxKw6ZMol+mFAhEbxcZ1PPtYc/oDTAmejvOG2TWFHmeJRQzSyPnCbWA==} + + '@tsslint/core@1.5.8': + resolution: {integrity: sha512-TC8KslLSD+nEVs1tSFe9odSNdU7TywuHR1UesmNWJ8YKQ5r/J1f2bc3f8vIsuS50AykdQlfVNdsgAdwUd9PHBg==} + + '@tsslint/eslint@1.5.8': + resolution: {integrity: sha512-XAmRXRuTIsRbZaUQJPDbt8tOJFO5b8Lwgd7Vvw4rrsV4yuHLz//nJ5zFLwKUVz/dYlUhsWBsPCc2ExRU/MvH+w==} + + '@tsslint/types@1.5.8': + resolution: {integrity: sha512-YTu/m/rEov3mKA/HFxuTX1lb97dEaLlPsCJ1M88T2wf9ILNB/t5lvwxGF7OXDTv5N5xZOOE/a3R5rUUK3Wv+yg==} + '@types/appdmg@0.5.5': resolution: {integrity: sha512-G+n6DgZTZFOteITE30LnWj+HRVIGr7wMlAiLWOO02uJFWVEitaPU9JVXm9wJokkgshBawb2O1OykdcsmkkZfgg==} @@ -6093,6 +6120,15 @@ packages: '@vitest/utils@2.1.8': resolution: {integrity: sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==} + '@volar/language-core@2.4.11': + resolution: {integrity: sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg==} + + '@volar/source-map@2.4.11': + resolution: {integrity: sha512-ZQpmafIGvaZMn/8iuvCFGrW3smeqkq/IIh9F1SdSx9aUl0J4Iurzd6/FhmjNO5g2ejF3rT45dKskgXWiofqlZQ==} + + '@volar/typescript@2.4.11': + resolution: {integrity: sha512-2DT+Tdh88Spp5PyPbqhyoYavYCPDsqbHLFwcUI9K1NlY1YgUJvujGdrqUp0zWxnW7KWNTr3xSpMuv2WnaTKDAw==} + '@vscode/vscode-languagedetection@1.0.22': resolution: {integrity: sha512-rQ/BgMyLuIXSmbA0MSkIPHtcOw14QkeDbAq19sjvaS9LTRr905yij0S8lsyqN5JgOsbtIx7pAcyOxFMzPmqhZQ==} hasBin: true @@ -14822,6 +14858,9 @@ packages: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + w3c-xmlserializer@4.0.0: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} engines: {node: '>=14'} @@ -17346,7 +17385,7 @@ snapshots: undici: 6.19.8 unique-string: 2.0.0 wrap-ansi: 7.0.0 - ws: 8.18.0(bufferutil@4.0.8) + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) transitivePeerDependencies: - bufferutil - encoding @@ -20785,6 +20824,43 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@tsslint/cli@1.5.8(typescript@5.7.2)': + dependencies: + '@clack/prompts': 0.8.2 + '@tsslint/config': 1.5.8(typescript@5.7.2) + '@tsslint/core': 1.5.8 + '@volar/language-core': 2.4.11 + '@volar/typescript': 2.4.11 + glob: 10.4.5 + json5: 2.2.3 + typescript: 5.7.2 + + '@tsslint/config@1.5.8(typescript@5.7.2)': + dependencies: + '@tsslint/types': 1.5.8 + ts-api-utils: 2.0.0(typescript@5.7.2) + transitivePeerDependencies: + - typescript + + '@tsslint/core@1.5.8': + dependencies: + '@tsslint/types': 1.5.8 + error-stack-parser: 2.1.4 + esbuild: 0.24.0 + minimatch: 10.0.1 + + '@tsslint/eslint@1.5.8(jiti@2.4.2)(typescript@5.7.2)': + dependencies: + '@tsslint/config': 1.5.8(typescript@5.7.2) + '@typescript-eslint/parser': 8.20.0(eslint@9.18.0(jiti@2.4.2))(typescript@5.7.2) + eslint: 9.18.0(jiti@2.4.2) + transitivePeerDependencies: + - jiti + - supports-color + - typescript + + '@tsslint/types@1.5.8': {} + '@types/appdmg@0.5.5': dependencies: '@types/node': 22.10.1 @@ -21311,6 +21387,18 @@ snapshots: loupe: 3.1.2 tinyrainbow: 1.2.0 + '@volar/language-core@2.4.11': + dependencies: + '@volar/source-map': 2.4.11 + + '@volar/source-map@2.4.11': {} + + '@volar/typescript@2.4.11': + dependencies: + '@volar/language-core': 2.4.11 + path-browserify: 1.0.1 + vscode-uri: 3.0.8 + '@vscode/vscode-languagedetection@1.0.22': {} '@web3-storage/multipart-parser@1.0.0': {} @@ -22396,7 +22484,7 @@ snapshots: centra@2.7.0: dependencies: - follow-redirects: 1.15.9 + follow-redirects: 1.15.9(debug@4.4.0) transitivePeerDependencies: - debug @@ -25269,8 +25357,6 @@ snapshots: imul: 1.0.1 optional: true - follow-redirects@1.15.9: {} - follow-redirects@1.15.9(debug@4.4.0): optionalDependencies: debug: 4.4.0(supports-color@8.1.1) @@ -31718,6 +31804,8 @@ snapshots: void-elements@3.1.0: {} + vscode-uri@3.0.8: {} + w3c-xmlserializer@4.0.0: dependencies: xml-name-validator: 4.0.0 @@ -32025,10 +32113,6 @@ snapshots: optionalDependencies: bufferutil: 4.0.8 - ws@8.18.0(bufferutil@4.0.8): - optionalDependencies: - bufferutil: 4.0.8 - ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4): optionalDependencies: bufferutil: 4.0.8 diff --git a/tsslint.config.ts b/tsslint.config.ts new file mode 100644 index 0000000000..a7be53a83d --- /dev/null +++ b/tsslint.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "@tsslint/config" +import { convertRule } from "@tsslint/eslint" + +export default defineConfig({ + rules: { + "no-leaked-conditional-rendering": convertRule( + await import("./node_modules/eslint-plugin-react-x/dist/index.mjs").then( + (module) => module.default.rules["no-leaked-conditional-rendering"], + ), + ), + }, +}) From eb47ec2ccaa9a4300ad8b5078bbf6c048d6f84a3 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Fri, 17 Jan 2025 22:31:58 +0800 Subject: [PATCH 83/93] fix: pass token when resetting password --- apps/server/client/pages/(login)/reset-password.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/server/client/pages/(login)/reset-password.tsx b/apps/server/client/pages/(login)/reset-password.tsx index 2dac23add9..4c25ed84e1 100644 --- a/apps/server/client/pages/(login)/reset-password.tsx +++ b/apps/server/client/pages/(login)/reset-password.tsx @@ -50,7 +50,12 @@ export function Component() { const navigate = useNavigate() const updateMutation = useMutation({ mutationFn: async (values: z.infer) => { - const res = await resetPassword({ newPassword: values.newPassword }) + const token = new URLSearchParams(window.location.search).get("token") + if (!token) { + throw new Error("Token not found") + } + + const res = await resetPassword({ newPassword: values.newPassword, token }) const error = res.error?.message if (error) { throw new Error(error) From dbd238a0f93abe13af5dd70c96d2922f89537fc4 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Fri, 17 Jan 2025 22:36:57 +0800 Subject: [PATCH 84/93] fix: no password hint when enable 2fa --- apps/renderer/src/modules/profile/two-factor.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/renderer/src/modules/profile/two-factor.tsx b/apps/renderer/src/modules/profile/two-factor.tsx index b195dd9021..7e11886eff 100644 --- a/apps/renderer/src/modules/profile/two-factor.tsx +++ b/apps/renderer/src/modules/profile/two-factor.tsx @@ -140,6 +140,7 @@ export function PasswordForm({ onSubmitMutationFn, onSuccess, }: PasswordFormProps) { + const { data: hasPassword, isLoading } = useHasPassword() const { t } = useTranslation("settings") const form = useForm({ @@ -183,6 +184,7 @@ export function PasswordForm({ )} /> + {!hasPassword && !isLoading && }