fix(mobile): avoid duplicate shared links

This commit is contained in:
DIYgod 2026-05-26 14:11:11 +08:00
parent c6f942436d
commit 2b0b412dbe
7 changed files with 127 additions and 30 deletions

View File

@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest"
import { createLinkShareContent } from "./share"
describe("createLinkShareContent", () => {
it("uses a single URL item on iOS", () => {
const url = "https://example.com/post/1"
const content = createLinkShareContent({
platform: "ios",
title: "Post title",
url,
message: url,
})
expect(content).toEqual({
title: "Post title",
url,
})
expect(content).not.toHaveProperty("message")
})
it("uses a single text item on Android", () => {
const url = "https://example.com/post/1"
const message = `Check out this post: ${url}`
const content = createLinkShareContent({
platform: "android",
title: "Post title",
url,
message,
})
expect(content).toEqual({
title: "Post title",
message,
})
expect(content).not.toHaveProperty("url")
})
it("falls back to the URL as Android share text", () => {
const url = "https://example.com/post/1"
expect(
createLinkShareContent({
platform: "android",
title: "Post title",
url,
}),
).toEqual({
title: "Post title",
message: url,
})
})
})

View File

@ -0,0 +1,29 @@
import type { ShareContent } from "react-native"
type LinkSharePlatform = "android" | "ios" | "macos" | "native" | "web" | "windows"
interface CreateLinkShareContentOptions {
platform: LinkSharePlatform
title?: string
url: string
message?: string
}
export const createLinkShareContent = ({
platform,
title,
url,
message,
}: CreateLinkShareContentOptions): ShareContent => {
if (platform === "ios") {
return {
title,
url,
}
}
return {
title,
message: message || url,
}
}

View File

@ -9,7 +9,7 @@ import { PortalProvider } from "@gorhom/portal"
import type { PropsWithChildren } from "react"
import { useCallback } from "react"
import { useTranslation } from "react-i18next"
import { Share, View } from "react-native"
import { Platform, Share, View } from "react-native"
import { getHideAllReadSubscriptions } from "@/src/atoms/settings/general"
import { EntryContentWebView } from "@/src/components/native/webview/EntryContentWebView"
@ -17,6 +17,7 @@ import { WebViewManager } from "@/src/components/native/webview/webview-manager"
import { ContextMenu } from "@/src/components/ui/context-menu"
import { Text } from "@/src/components/ui/typography/Text"
import { useNavigation } from "@/src/lib/navigation/hooks"
import { createLinkShareContent } from "@/src/lib/share"
import { toast } from "@/src/lib/toast"
import { playEntryTts } from "@/src/modules/player/entry-tts"
import { EntryDetailScreen } from "@/src/screens/(stack)/entries/[entryId]/EntryDetailScreen"
@ -204,11 +205,13 @@ export const EntryItemContextMenu = ({
key="Share"
onSelect={async () => {
if (!entry.url) return
await Share.share({
message: entry.url,
url: entry.url,
title: entry.title || "Shared Link",
})
await Share.share(
createLinkShareContent({
platform: Platform.OS,
title: entry.title || "Shared Link",
url: entry.url,
}),
)
}}
>
<ContextMenu.ItemIcon

View File

@ -6,9 +6,10 @@ import { unreadSyncService } from "@follow/store/unread/store"
import { useIsLoggedIn } from "@follow/store/user/hooks"
import type { PropsWithChildren } from "react"
import { useTranslation } from "react-i18next"
import { Share } from "react-native"
import { Platform, Share } from "react-native"
import { ContextMenu } from "@/src/components/ui/context-menu"
import { createLinkShareContent } from "@/src/lib/share"
import { toast } from "@/src/lib/toast"
type VideoContextMenuProps = PropsWithChildren<{
@ -87,11 +88,14 @@ export const VideoContextMenu = ({ entryId, children }: VideoContextMenuProps) =
key="Share"
onSelect={async () => {
if (!entry.url) return
await Share.share({
message: [entry.title, entry.url].filter(Boolean).join("\n"),
url: entry.url,
title: entry.title || "Shared Video",
})
await Share.share(
createLinkShareContent({
platform: Platform.OS,
title: entry.title || "Shared Video",
url: entry.url,
message: [entry.title, entry.url].filter(Boolean).join("\n"),
}),
)
return
}}
>

View File

@ -10,7 +10,7 @@ import { setStringAsync } from "expo-clipboard"
import { useAtom } from "jotai"
import { useCallback, useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { Pressable, Share, View } from "react-native"
import { Platform, Pressable, Share, View } from "react-native"
import type { SharedValue } from "react-native-reanimated"
import Animated, { interpolate, useAnimatedStyle } from "react-native-reanimated"
import { useColor } from "react-native-uikit-colors"
@ -27,6 +27,7 @@ import { StarCuteReIcon } from "@/src/icons/star_cute_re"
import { Translate2CuteReIcon } from "@/src/icons/translate_2_cute_re"
import { VoiceCuteReIcon } from "@/src/icons/voice_cute_re"
import { hideIntelligenceGlowEffect, openLink } from "@/src/lib/native"
import { createLinkShareContent } from "@/src/lib/share"
import { toast } from "@/src/lib/toast"
import { playEntryTts } from "@/src/modules/player/entry-tts"
@ -100,11 +101,9 @@ const HeaderRightActionsImpl = ({
const handleShare = () => {
if (!entry?.title || !entry?.url) return
Share.share({
message: entry.url,
title: entry.title,
url: entry.url,
})
Share.share(
createLinkShareContent({ platform: Platform.OS, title: entry.title, url: entry.url }),
)
}
const toggleAITranslation = () => {

View File

@ -5,7 +5,7 @@ import * as Haptics from "expo-haptics"
import type { PropsWithChildren } from "react"
import { useCallback } from "react"
import { useTranslation } from "react-i18next"
import { Pressable, Share, View } from "react-native"
import { Platform, Pressable, Share, View } from "react-native"
import { setGeneralSetting, useGeneralSettingKey } from "@/src/atoms/settings/general"
import { UserAvatar } from "@/src/components/ui/avatar/UserAvatar"
@ -17,6 +17,7 @@ import { ShareForwardCuteReIcon } from "@/src/icons/share_forward_cute_re"
import { Dialog } from "@/src/lib/dialog"
import { useNavigation } from "@/src/lib/navigation/hooks"
import { proxyEnv } from "@/src/lib/proxy-env"
import { createLinkShareContent } from "@/src/lib/share"
import { toast } from "@/src/lib/toast"
import { LoginScreen } from "@/src/screens/(modal)/LoginScreen"
import { ProfileScreen } from "@/src/screens/(modal)/ProfileScreen"
@ -124,11 +125,14 @@ export const FeedShareActionButton = ({
const feed = getFeedById(feedId)
if (!feed) return
const url = `${proxyEnv.WEB_URL}/share/feeds/${feedId}`
Share.share({
message: `Check out ${feed.title} on Folo: ${url}`,
title: feed.title!,
url,
})
Share.share(
createLinkShareContent({
platform: Platform.OS,
title: feed.title!,
url,
message: `Check out ${feed.title} on Folo: ${url}`,
}),
)
}}
/>
)

View File

@ -7,7 +7,7 @@ import { usePrefetchUser, useUserById, useWhoami } from "@follow/store/user/hook
import { Image as ExpoImage } from "expo-image"
import { createContext, Fragment, use, useCallback, useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { Alert, FlatList, Pressable, Share, View } from "react-native"
import { Alert, FlatList, Platform, Pressable, Share, View } from "react-native"
import Animated, {
interpolate,
useAnimatedScrollHandler,
@ -42,6 +42,7 @@ import { ShareForwardCuteReIcon } from "@/src/icons/share_forward_cute_re"
import type { followClient } from "@/src/lib/api-client"
import { Navigation } from "@/src/lib/navigation/Navigation"
import type { NavigationControllerView } from "@/src/lib/navigation/types"
import { createLinkShareContent } from "@/src/lib/share"
import { toast } from "@/src/lib/toast"
import { useShareSubscription } from "@/src/modules/settings/hooks/useShareSubscription"
import { UserHeaderBanner } from "@/src/modules/settings/UserHeaderBanner"
@ -95,11 +96,13 @@ function ProfileScreenImpl(props: { userId: string }) {
const openShareUrl = useCallback(() => {
if (!user?.id) return
const shareUrl = `https://app.folo.is/share/users/${user.id}`
Share.share({
message: shareUrl,
url: shareUrl,
title: `Folo | ${user.name}'s Profile`,
})
Share.share(
createLinkShareContent({
platform: Platform.OS,
title: `Folo | ${user.name}'s Profile`,
url: shareUrl,
}),
)
}, [user?.id, user?.name])
const whoami = useWhoami()