diff --git a/apps/mobile/src/components/ui/overlay/Overlay.tsx b/apps/mobile/src/components/ui/overlay/Overlay.tsx
new file mode 100644
index 000000000..e2d91b3b1
--- /dev/null
+++ b/apps/mobile/src/components/ui/overlay/Overlay.tsx
@@ -0,0 +1,15 @@
+import type { FC } from "react"
+import { FadeIn, FadeOut } from "react-native-reanimated"
+
+import { ReAnimatedPressable } from "../../common/AnimatedComponents"
+
+export const Overlay: FC<{ onPress: () => void }> = ({ onPress }) => {
+ return (
+
+ )
+}
diff --git a/apps/mobile/src/icons/search_2_cute_re.tsx b/apps/mobile/src/icons/search_2_cute_re.tsx
new file mode 100644
index 000000000..fa803559d
--- /dev/null
+++ b/apps/mobile/src/icons/search_2_cute_re.tsx
@@ -0,0 +1,26 @@
+import * as React from "react"
+import Svg, { Path } from "react-native-svg"
+
+interface Search2CuteReIconProps {
+ width?: number
+ height?: number
+ color?: string
+}
+
+export const Search2CuteReIcon = ({
+ width = 24,
+ height = 24,
+ color = "#10161F",
+}: Search2CuteReIconProps) => {
+ return (
+
+ )
+}
diff --git a/apps/mobile/src/modules/discover/search.tsx b/apps/mobile/src/modules/discover/search.tsx
index 0048baca2..93288076c 100644
--- a/apps/mobile/src/modules/discover/search.tsx
+++ b/apps/mobile/src/modules/discover/search.tsx
@@ -20,6 +20,7 @@ import { useSafeAreaFrame, useSafeAreaInsets } from "react-native-safe-area-cont
import { BlurEffect } from "@/src/components/common/BlurEffect"
import { TabBar } from "@/src/components/ui/tabview/TabBar"
+import { Search2CuteReIcon } from "@/src/icons/search_2_cute_re"
import { Search3CuteReIcon } from "@/src/icons/search_3_cute_re"
import { accentColor, useColor } from "@/src/theme/colors"
@@ -215,7 +216,7 @@ const SearchInput = () => {
}}
className="absolute inset-y-0 left-3 flex flex-row items-center justify-center"
>
-
+
{!searchValue && !tempSearchValue && (
Search
@@ -251,7 +252,7 @@ const SearchInput = () => {
className="absolute inset-0 flex flex-row items-center justify-center"
pointerEvents="none"
>
-
+
Search
diff --git a/apps/mobile/src/modules/feed/FollowFeed.tsx b/apps/mobile/src/modules/feed/FollowFeed.tsx
index ce036d195..caf9e7682 100644
--- a/apps/mobile/src/modules/feed/FollowFeed.tsx
+++ b/apps/mobile/src/modules/feed/FollowFeed.tsx
@@ -1,7 +1,7 @@
import { FeedViewType } from "@follow/constants"
import { zodResolver } from "@hookform/resolvers/zod"
import { StackActions } from "@react-navigation/native"
-import { router, Stack, useLocalSearchParams, useNavigation } from "expo-router"
+import { router, Stack, useNavigation } from "expo-router"
import { useState } from "react"
import { Controller, useForm } from "react-hook-form"
import { ActivityIndicator, ScrollView, Text, View } from "react-native"
@@ -20,7 +20,7 @@ import { GroupedInsetListCard } from "@/src/components/ui/grouped/GroupedList"
import { FeedIcon } from "@/src/components/ui/icon/feed-icon"
import { useIsRouteOnlyOne } from "@/src/hooks/useIsRouteOnlyOne"
import { FeedViewSelector } from "@/src/modules/feed/view-selector"
-import { useFeed, usePrefetchFeed } from "@/src/store/feed/hooks"
+import { useFeed, usePrefetchFeed, usePrefetchFeedByUrl } 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"
@@ -45,10 +45,31 @@ export function FollowFeed(props: { id: string }) {
)
}
- return
+ return
}
-function FollowImpl() {
- const { id } = useLocalSearchParams()
+
+export function FollowUrl(props: { url: string }) {
+ const { url } = props
+
+ const { isLoading, data, error } = usePrefetchFeedByUrl(url)
+
+ if (isLoading) {
+ return (
+
+
+
+ )
+ }
+
+ if (!data) {
+ return {error?.message}
+ }
+
+ return
+}
+
+function FollowImpl(props: { feedId: string }) {
+ const { feedId: id } = props
const feed = useFeed(id as string)!
const isSubscribed = useSubscriptionByFeedId(feed?.id || "")
@@ -129,7 +150,7 @@ function FollowImpl() {
{/* Group 2 */}
-
+
(
{
const insets = useSafeAreaInsets()
@@ -28,11 +33,86 @@ export function HomeRightAction() {
return (
-
-
-
-
-
+
)
}
+
+const AddFeedButton = () => {
+ const [isOpen, setIsOpen] = useState(false)
+ const label = useColor("label")
+ const handleClose = () => setIsOpen(false)
+
+ const valueRef = useRef("")
+ const handleAdd = () => {
+ handleClose()
+
+ const value = valueRef.current
+ if (!value) return
+ router.push({
+ pathname: "/follow",
+ params: {
+ url: value,
+ type: "url",
+ },
+ })
+ }
+ return (
+ <>
+ setIsOpen((s) => !s)}>
+
+
+
+
+ {isOpen && (
+ <>
+
+
+
+
+
+
+
+
+ Enter Feed URL or RSSHub URL
+
+
+ (valueRef.current = text)}
+ autoFocus
+ enterKeyHint="done"
+ cursorColor={accentColor}
+ selectionColor={accentColor}
+ onSubmitEditing={handleAdd}
+ className="bg-system-background dark:bg-secondary-system-fill/30 text-text my-3 rounded-xl"
+ placeholder="https:// or rsshub://"
+ />
+
+
+
+
+ Cancel
+
+
+
+ Add
+
+
+
+
+ >
+ )}
+
+ >
+ )
+}
diff --git a/apps/mobile/src/screens/(modal)/add.tsx b/apps/mobile/src/screens/(modal)/add.tsx
deleted file mode 100644
index 5780f2759..000000000
--- a/apps/mobile/src/screens/(modal)/add.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { Stack } from "expo-router"
-import { useState } from "react"
-import { Text, TextInput, TouchableOpacity, View } from "react-native"
-
-import { ModalHeaderCloseButton } from "@/src/components/common/ModalSharedComponents"
-import { Search3CuteReIcon } from "@/src/icons/search_3_cute_re"
-import { accentColor, useColor } from "@/src/theme/colors"
-
-export default function Add() {
- const [url, setUrl] = useState("")
-
- const label = useColor("label")
- // @ts-expect-error FIXME: use the right color
- const disabled = useColor("disabled")
- return (
-
- (
- {
- if (!url) return
-
- // TODO impl search
- }}
- >
-
-
- ),
- }}
- />
-
- Feed URL
-
-
-
- )
-}
diff --git a/apps/mobile/src/screens/(modal)/follow.tsx b/apps/mobile/src/screens/(modal)/follow.tsx
index 6a4d549c0..f2616e5e1 100644
--- a/apps/mobile/src/screens/(modal)/follow.tsx
+++ b/apps/mobile/src/screens/(modal)/follow.tsx
@@ -1,10 +1,10 @@
import { useLocalSearchParams } from "expo-router"
-import { FollowFeed } from "@/src/modules/feed/FollowFeed"
+import { FollowFeed, FollowUrl } from "@/src/modules/feed/FollowFeed"
import { FollowList } from "@/src/modules/list/FollowList"
export default function Follow() {
- const { id, type = "feed" } = useLocalSearchParams()
+ const { id, type = "feed", url } = useLocalSearchParams()
switch (type) {
case "feed": {
@@ -13,6 +13,9 @@ export default function Follow() {
case "list": {
return
}
+ case "url": {
+ return
+ }
default: {
return
}
diff --git a/apps/mobile/src/store/feed/hooks.ts b/apps/mobile/src/store/feed/hooks.ts
index 7a9a65a3f..11233b3c8 100644
--- a/apps/mobile/src/store/feed/hooks.ts
+++ b/apps/mobile/src/store/feed/hooks.ts
@@ -31,3 +31,11 @@ export const usePrefetchFeed = (id: string, options?: { enabled?: boolean }) =>
...options,
})
}
+
+export const usePrefetchFeedByUrl = (url: string, options?: { enabled?: boolean }) => {
+ return useQuery({
+ queryKey: ["feed", url],
+ queryFn: () => feedSyncServices.fetchFeedByUrl({ url }),
+ ...options,
+ })
+}
diff --git a/apps/mobile/src/store/feed/store.ts b/apps/mobile/src/store/feed/store.ts
index 79f723f7f..79606e580 100644
--- a/apps/mobile/src/store/feed/store.ts
+++ b/apps/mobile/src/store/feed/store.ts
@@ -39,7 +39,7 @@ class FeedActions {
})
tx.persist(async () => {
- await FeedService.upsertMany(feeds)
+ await FeedService.upsertMany(feeds.filter((feed) => !("nonce" in feed)))
})
tx.run()
@@ -78,6 +78,25 @@ class FeedSyncServices {
return !finalData.id ? { ...finalData, id: nonce } : finalData
}
+
+ async fetchFeedByUrl({ url }: FeedQueryParams) {
+ const res = await apiClient.feeds.$get({
+ query: {
+ url,
+ },
+ })
+
+ const nonce = Math.random().toString(36).slice(2, 15)
+
+ const finalData = res.data.feed as FeedModel
+ if (!finalData.id) {
+ finalData["nonce"] = nonce
+ finalData["id"] = nonce
+ }
+ feedActions.upsertMany([finalData])
+
+ return finalData
+ }
}
export const feedSyncServices = new FeedSyncServices()
export const feedActions = new FeedActions()
diff --git a/icons/mgc/search_2_cute_re.svg b/icons/mgc/search_2_cute_re.svg
new file mode 100644
index 000000000..7c5e90731
--- /dev/null
+++ b/icons/mgc/search_2_cute_re.svg
@@ -0,0 +1 @@
+
\ No newline at end of file