Skip to content

Commit 9113c0a

Browse files
committed
feat(mobile): Add media preview accessories and no-media rendering support
- Enhance PreviewImage with optional Accessory component - Update PreviewPageProvider to support rendering accessories in image preview - Add noMedia prop to EntryContentWebView for selective media rendering - Implement EntryGridItemAccessory to show entry details in image preview - Update HTML renderer to support noMedia configuration Signed-off-by: Innei <tukon479@gmail.com>
1 parent 27818f6 commit 9113c0a

File tree

7 files changed

+149
-64
lines changed

7 files changed

+149
-64
lines changed

apps/mobile/src/components/native/webview/EntryContentWebView.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const NativeView: React.ComponentType<
2222

2323
type EntryContentWebViewProps = {
2424
entry: EntryModel
25+
noMedia?: boolean
2526
}
2627

2728
const setCodeTheme = (light: string, dark: string) => {
@@ -36,6 +37,10 @@ export const setWebViewEntry = (entry: EntryModel) => {
3637
)
3738
}
3839

40+
const setNoMedia = (value: boolean) => {
41+
SharedWebViewModule.evaluateJavaScript(`setNoMedia(${value})`)
42+
}
43+
3944
const setReaderRenderInlineStyle = (value: boolean) => {
4045
SharedWebViewModule.evaluateJavaScript(`setReaderRenderInlineStyle(${value})`)
4146
}
@@ -46,17 +51,22 @@ export function EntryContentWebView(props: EntryContentWebViewProps) {
4651
const codeThemeLight = useUISettingKey("codeHighlightThemeLight")
4752
const codeThemeDark = useUISettingKey("codeHighlightThemeDark")
4853
const readerRenderInlineStyle = useUISettingKey("readerRenderInlineStyle")
49-
const { entry } = props
54+
const { entry, noMedia } = props
55+
56+
const [mode, setMode] = React.useState<"normal" | "debug">("normal")
5057

5158
useEffect(() => {
52-
setCodeTheme(codeThemeLight, codeThemeDark)
53-
}, [codeThemeLight, codeThemeDark])
59+
setNoMedia(!!noMedia)
60+
}, [noMedia, mode])
5461

5562
useEffect(() => {
5663
setReaderRenderInlineStyle(readerRenderInlineStyle)
57-
}, [readerRenderInlineStyle])
64+
}, [readerRenderInlineStyle, mode])
65+
66+
useEffect(() => {
67+
setCodeTheme(codeThemeLight, codeThemeDark)
68+
}, [codeThemeLight, codeThemeDark, mode])
5869

59-
const [mode, setMode] = React.useState<"normal" | "debug">("normal")
6070
React.useEffect(() => {
6171
setWebViewEntry(entry)
6272
}, [entry])

apps/mobile/src/components/ui/image/PreviewImage.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Image } from "expo-image"
2+
import type { FC } from "react"
23
import { useRef } from "react"
34
import { Pressable, View } from "react-native"
45

@@ -8,9 +9,17 @@ interface PreviewImageProps {
89
imageUrl: string
910
blurhash?: string | undefined
1011
aspectRatio: number
12+
Accessory?: FC<any>
13+
AccessoryProps?: any
1114
}
1215

13-
export const PreviewImage = ({ imageUrl, blurhash, aspectRatio }: PreviewImageProps) => {
16+
export const PreviewImage = ({
17+
imageUrl,
18+
blurhash,
19+
aspectRatio,
20+
Accessory,
21+
AccessoryProps,
22+
}: PreviewImageProps) => {
1423
const imageRef = useRef<View>(null)
1524

1625
const { openPreview } = usePreviewImage()
@@ -20,6 +29,7 @@ export const PreviewImage = ({ imageUrl, blurhash, aspectRatio }: PreviewImagePr
2029
openPreview({
2130
imageRef,
2231
images: [{ imageUrl, aspectRatio, blurhash, recyclingKey: imageUrl }],
32+
accessoriesElement: Accessory ? <Accessory {...AccessoryProps} /> : undefined,
2333
})
2434
}
2535
>

apps/mobile/src/components/ui/image/PreviewPageProvider.tsx

Lines changed: 60 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { PortalProvider } from "@gorhom/portal"
12
import { Image } from "expo-image"
23
import type { RefObject } from "react"
34
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"
@@ -24,12 +25,15 @@ interface PreviewImageProps {
2425
recyclingKey?: string
2526
}
2627

28+
interface OpenPreviewParams {
29+
imageRef: RefObject<View>
30+
images: PreviewImageProps[]
31+
initialIndex?: number
32+
accessoriesElement?: React.ReactNode
33+
}
34+
2735
interface PreviewImageContextType {
28-
openPreview: (params: {
29-
imageRef: RefObject<View>
30-
images: PreviewImageProps[]
31-
initialIndex?: number
32-
}) => void
36+
openPreview: (params: OpenPreviewParams) => void
3337
}
3438
const PreviewImageContext = createContext<PreviewImageContextType | null>(null)
3539

@@ -48,7 +52,7 @@ export const PreviewImageProvider = ({ children }: { children: React.ReactNode }
4852
const [currentState, setCurrentState] = useState<PreviewImageProps[] | null>(null)
4953
const [imageRef, setImageRef] = useState<View | null>(null)
5054
const [currentIndex, setCurrentIndex] = useState(0)
51-
55+
const [accessoriesElement, setAccessoriesElement] = useState<React.ReactNode | null>(null)
5256
const layoutScale = useSharedValue(1)
5357
const layoutTransformX = useSharedValue(0)
5458
const layoutTransformY = useSharedValue(0)
@@ -343,10 +347,11 @@ export const PreviewImageProvider = ({ children }: { children: React.ReactNode }
343347
}
344348

345349
const openPreview = useCallback(
346-
(params: { imageRef: RefObject<View>; images: PreviewImageProps[]; initialIndex?: number }) => {
350+
(params: OpenPreviewParams) => {
347351
setCurrentState(params.images)
348352
setCurrentIndex(params.initialIndex || 0)
349353
setImageRef(params.imageRef.current)
354+
setAccessoriesElement(params.accessoriesElement)
350355
params.imageRef.current?.measureInWindow((pageX, pageY, w1, h1) => {
351356
setPreviewModalOpen(true)
352357

@@ -388,52 +393,55 @@ export const PreviewImageProvider = ({ children }: { children: React.ReactNode }
388393
{children}
389394
{currentState && (
390395
<Modal transparent visible={previewModalOpen}>
391-
<Animated.View style={overlayStyle} className="absolute inset-0" />
392-
<Animated.View style={modalStyle} className="w-full flex-1">
393-
<GestureHandlerRootView className="w-full flex-1">
394-
<View className="flex-1 items-center justify-center">
395-
<Pressable
396-
className="absolute right-2 top-safe-offset-2"
397-
onPress={fadeOutCloseAnimation}
398-
>
399-
<CloseCuteReIcon color="#fff" />
400-
</Pressable>
401-
<GestureDetector gesture={composed}>
402-
<Animated.View style={animatedStyle}>
403-
<Animated.View
404-
style={{
405-
transform: [
406-
{
407-
scale: layoutScale,
408-
},
409-
{
410-
translateX: layoutTransformX,
411-
},
412-
{
413-
translateY: layoutTransformY,
414-
},
415-
],
416-
}}
417-
>
418-
<ImageContextMenu imageUrl={currentState[currentIndex]?.imageUrl}>
419-
<Image
420-
recyclingKey={currentState[currentIndex]?.recyclingKey}
421-
source={{ uri: currentState[currentIndex]?.imageUrl }}
422-
className="w-full"
423-
style={{
424-
aspectRatio: currentState[currentIndex]?.aspectRatio,
425-
}}
426-
placeholder={{
427-
blurhash: currentState[currentIndex]?.blurhash,
428-
}}
429-
/>
430-
</ImageContextMenu>
396+
<PortalProvider>
397+
<Animated.View style={overlayStyle} className="absolute inset-0" />
398+
<Animated.View style={modalStyle} className="w-full flex-1">
399+
<GestureHandlerRootView className="w-full flex-1">
400+
<View className="flex-1 items-center justify-center">
401+
<Pressable
402+
className="absolute right-2 top-safe-offset-2"
403+
onPress={fadeOutCloseAnimation}
404+
>
405+
<CloseCuteReIcon color="#fff" />
406+
</Pressable>
407+
<GestureDetector gesture={composed}>
408+
<Animated.View style={animatedStyle}>
409+
<Animated.View
410+
style={{
411+
transform: [
412+
{
413+
scale: layoutScale,
414+
},
415+
{
416+
translateX: layoutTransformX,
417+
},
418+
{
419+
translateY: layoutTransformY,
420+
},
421+
],
422+
}}
423+
>
424+
<ImageContextMenu imageUrl={currentState[currentIndex]?.imageUrl}>
425+
<Image
426+
recyclingKey={currentState[currentIndex]?.recyclingKey}
427+
source={{ uri: currentState[currentIndex]?.imageUrl }}
428+
className="w-full"
429+
style={{
430+
aspectRatio: currentState[currentIndex]?.aspectRatio,
431+
}}
432+
placeholder={{
433+
blurhash: currentState[currentIndex]?.blurhash,
434+
}}
435+
/>
436+
</ImageContextMenu>
437+
</Animated.View>
431438
</Animated.View>
432-
</Animated.View>
433-
</GestureDetector>
434-
</View>
435-
</GestureHandlerRootView>
436-
</Animated.View>
439+
</GestureDetector>
440+
</View>
441+
</GestureHandlerRootView>
442+
{accessoriesElement}
443+
</Animated.View>
444+
</PortalProvider>
437445
</Modal>
438446
)}
439447
</PreviewImageContext.Provider>

apps/mobile/src/modules/entry-list/templates/EntryGridItem.tsx

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import { FeedViewType } from "@follow/constants"
2+
import { LinearGradient } from "expo-linear-gradient"
23
import { useMemo } from "react"
3-
import { Text, View } from "react-native"
4+
import { ScrollView, Text, View } from "react-native"
5+
import Animated from "react-native-reanimated"
6+
import { useSafeAreaInsets } from "react-native-safe-area-context"
47

58
import { useUISettingKey } from "@/src/atoms/settings/ui"
9+
import { EntryContentWebView } from "@/src/components/native/webview/EntryContentWebView"
10+
import { RelativeDateTime } from "@/src/components/ui/datetime/RelativeDateTime"
11+
import { FeedIcon } from "@/src/components/ui/icon/feed-icon"
612
import { ImageContextMenu } from "@/src/components/ui/image/ImageContextMenu"
713
import { PreviewImage } from "@/src/components/ui/image/PreviewImage"
814
import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable"
915
import { useEntry } from "@/src/store/entry/hooks"
16+
import { useFeed } from "@/src/store/feed/hooks"
1017

1118
import { useSelectedView } from "../../screen/atoms"
1219

@@ -41,7 +48,15 @@ export function EntryGridItem(props: MasonryItem) {
4148

4249
return imageUrl ? (
4350
<ImageContextMenu imageUrl={imageUrl}>
44-
<PreviewImage imageUrl={imageUrl} blurhash={blurhash} aspectRatio={aspectRatio} />
51+
<PreviewImage
52+
imageUrl={imageUrl}
53+
blurhash={blurhash}
54+
aspectRatio={aspectRatio}
55+
Accessory={EntryGridItemAccessory}
56+
AccessoryProps={{
57+
id,
58+
}}
59+
/>
4560
</ImageContextMenu>
4661
) : (
4762
<View className="aspect-video w-full items-center justify-center">
@@ -85,3 +100,34 @@ export function EntryGridItem(props: MasonryItem) {
85100

86101
return <ItemPressable className="m-1 overflow-hidden rounded-md">{Content}</ItemPressable>
87102
}
103+
104+
const EntryGridItemAccessory = ({ id }: { id: string }) => {
105+
const entry = useEntry(id)
106+
const feed = useFeed(entry?.feedId || "")
107+
const insets = useSafeAreaInsets()
108+
if (!entry) {
109+
return null
110+
}
111+
return (
112+
<Animated.View className="absolute inset-x-0 bottom-0">
113+
<LinearGradient colors={["transparent", "#000"]} locations={[0.1, 1]} className="flex-1">
114+
<View className="flex-row items-center gap-2">
115+
<View className="border-non-opaque-separator overflow-hidden rounded-full border">
116+
<FeedIcon fallback feed={feed} size={40} />
117+
</View>
118+
<View>
119+
<Text className="text-label text-lg font-medium">{entry.author}</Text>
120+
<RelativeDateTime className="text-secondary-label" date={entry.publishedAt} />
121+
</View>
122+
</View>
123+
124+
<ScrollView
125+
className="mt-2 max-h-48"
126+
contentContainerStyle={{ paddingBottom: insets.bottom }}
127+
>
128+
<EntryContentWebView entry={entry} noMedia />
129+
</ScrollView>
130+
</LinearGradient>
131+
</Animated.View>
132+
)
133+
}

apps/mobile/web-app/html-renderer/src/App.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
codeThemeDarkAtom,
66
codeThemeLightAtom,
77
entryAtom,
8+
noMediaAtom,
89
readerRenderInlineStyleAtom,
910
} from "./atoms"
1011
import { HTML } from "./HTML"
@@ -23,6 +24,9 @@ Object.assign(window, {
2324
setReaderRenderInlineStyle(value: boolean) {
2425
store.set(readerRenderInlineStyleAtom, value)
2526
},
27+
setNoMedia(value: boolean) {
28+
store.set(noMediaAtom, value)
29+
},
2630
reset() {
2731
store.set(entryAtom, null)
2832
bridge.measure()
@@ -32,9 +36,14 @@ Object.assign(window, {
3236
export const App = () => {
3337
const entry = useAtomValue(entryAtom, { store })
3438
const readerRenderInlineStyle = useAtomValue(readerRenderInlineStyleAtom, { store })
39+
const noMedia = useAtomValue(noMediaAtom, { store })
3540
return (
3641
<Provider store={store}>
37-
<HTML children={entry?.content} renderInlineStyle={readerRenderInlineStyle} />
42+
<HTML
43+
children={entry?.content}
44+
renderInlineStyle={readerRenderInlineStyle}
45+
noMedia={noMedia}
46+
/>
3847
</Provider>
3948
)
4049
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { atom } from "jotai"
22

3-
import type { EntryModel } from "../types"
3+
import type { EntryModel } from "../../types"
44

55
export const entryAtom = atom<EntryModel | null>(null)
66

77
export const codeThemeLightAtom = atom<string | null>(null)
88
export const codeThemeDarkAtom = atom<string | null>(null)
99
export const readerRenderInlineStyleAtom = atom<boolean>(false)
10+
export const noMediaAtom = atom<boolean>(false)

apps/mobile/web-app/html-renderer/vite.config.mts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import { defineConfig } from "vite"
66
import { viteRenderBaseConfig } from "../../../../configs/vite.render.config"
77
import { astPlugin } from "../../../../plugins/vite/ast"
88

9-
const isDev = process.env.NODE_ENV === "development"
9+
// const isDev = process.env.NODE_ENV === "development"
10+
const isCI = !!process.env.CI
1011
export default defineConfig({
1112
...viteRenderBaseConfig,
1213
base: "",
1314
build: {
14-
outDir: isDev
15+
outDir: !isCI
1516
? path.resolve(import.meta.dirname, "../../../../out/rn-web/html-renderer")
1617
: path.resolve("/tmp/rn-web/html-renderer"),
1718
},

0 commit comments

Comments
 (0)