Skip to content

Commit

Permalink
feat: refactor selecable text
Browse files Browse the repository at this point in the history
  • Loading branch information
liaoliao666 committed Oct 3, 2023
1 parent 7637f12 commit 7d54c01
Show file tree
Hide file tree
Showing 13 changed files with 136 additions and 84 deletions.
4 changes: 2 additions & 2 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"expo": {
"name": "V2Fun",
"slug": "v2ex",
"version": "1.5.0",
"version": "1.5.1",
"scheme": "v2fun",
"jsEngine": "jsc",
"icon": "./assets/icon.png",
Expand All @@ -20,7 +20,7 @@
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.liaoliao666.v2ex",
"buildNumber": "1.5.0.3"
"buildNumber": "1.5.1.1"
},
"android": {
"adaptiveIcon": {
Expand Down
4 changes: 2 additions & 2 deletions components/Html/HtmlContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ export const HtmlContext = createContext<{
onPreview: (url: string) => void
paddingX: number
inModalScreen?: boolean
disabledPartialSelectable?: boolean
}>({ onPreview: () => {}, paddingX: 32 })
onSelectText: () => void
}>({ onPreview: () => {}, paddingX: 32, onSelectText: () => {} })
14 changes: 11 additions & 3 deletions components/Html/ImageRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ const ImageRenderer: CustomBlockRenderer = ({ tnode, style }) => {
}, [tnode.domNode])

const screenWidth = useScreenWidth()
const ancestorHasTdTag =
tnode.parent?.tagName === 'td' || tnode.parent?.parent?.tagName === 'td'
const containerWidth = !ancestorHasTdTag ? screenWidth - paddingX : undefined
const ancestorHasPadding = ancestorIs('td', tnode) || ancestorIs('li', tnode)
const containerWidth = !ancestorHasPadding
? screenWidth - paddingX
: undefined

if (url && isSvgURL(url))
return <StyledImage style={style as any} source={{ uri: url }} />
Expand All @@ -42,4 +43,11 @@ const ImageRenderer: CustomBlockRenderer = ({ tnode, style }) => {
)
}

function ancestorIs(tagName: string, tnode: any): boolean {
return (
tnode.parent?.tagName === tagName ||
tnode.parent?.parent?.tagName === tagName
)
}

export default ImageRenderer
51 changes: 15 additions & 36 deletions components/Html/TextRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,27 @@
import { some } from 'lodash-es'
import { useContext } from 'react'
import { Platform, Text, TextInput } from 'react-native'
import { Platform, Text } from 'react-native'
import {
CustomTextualRenderer,
TNode,
getNativePropsForTNode,
} from 'react-native-render-html'

import { HtmlContext } from './HtmlContext'

const resetTextInputStyle = {
paddingTop: 0,
marginTop: -3,
paddingBottom: 3,
}

const TextRenderer: CustomTextualRenderer = props => {
const renderProps = getNativePropsForTNode(props)

const { disabledPartialSelectable } = useContext(HtmlContext)

if (
disabledPartialSelectable ||
!renderProps.selectable ||
Platform.OS === 'android' ||
hasLink(props.tnode)
)
return <Text {...renderProps} />

return (
<TextInput
editable={false}
multiline
scrollEnabled={false}
style={resetTextInputStyle}
textAlignVertical="top"
>
<Text {...renderProps} />
</TextInput>
)
}

function hasLink(tnode: TNode): boolean {
return tnode.domNode?.name === 'a' || some(tnode.children, hasLink)
const { onSelectText } = useContext(HtmlContext)
let renderProps = getNativePropsForTNode(props)

if (Platform.OS === 'ios' && renderProps.selectable) {
renderProps = {
...renderProps,
selectable: false,
onLongPress: () => {
onSelectText()
},
}
}

return <Text {...renderProps} />
}

export default TextRenderer
22 changes: 10 additions & 12 deletions components/Html/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { getFontSize } from '@/jotai/fontSacleAtom'
import { imageViewerAtom } from '@/jotai/imageViewerAtom'
import { store } from '@/jotai/store'
import { colorSchemeAtom } from '@/jotai/themeAtom'
import { navigation } from '@/navigation/navigationRef'
import tw from '@/utils/tw'
import { useScreenWidth } from '@/utils/useScreenWidth'

Expand All @@ -29,12 +30,10 @@ export default memo(
function Html({
inModalScreen,
paddingX = 32,
disabledPartialSelectable,
...renderHTMLProps
}: RenderHTMLProps & {
inModalScreen?: boolean
paddingX?: number
disabledPartialSelectable?: boolean
}) {
const mergedProps = {
...getDefaultProps({ inModalScreen }),
Expand Down Expand Up @@ -80,16 +79,14 @@ function Html({
imageUrls,
})
},
onSelectText: () => {
navigation.navigate('SelectableText', {
html,
})
},
paddingX,
disabledPartialSelectable,
}),
[
imageUrls,
setImageViewer,
paddingX,
inModalScreen,
disabledPartialSelectable,
]
[imageUrls, setImageViewer, paddingX, inModalScreen, html]
)}
>
<RenderHtml
Expand All @@ -116,14 +113,15 @@ function Html({
},
...mergedProps.tagsStyles,
}}
contentWidth={screenWidth - paddingX}
{...mergedProps}
renderers={{
pre: CodeRenderer,
img: ImageRenderer,
iframe: IFrameRenderer,
_TEXT_: TextRenderer,
...mergedProps.renderers,
}}
contentWidth={screenWidth - paddingX}
{...mergedProps}
/>
</HtmlContext.Provider>
)
Expand Down
60 changes: 38 additions & 22 deletions components/StyledImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { View, ViewStyle } from 'react-native'
import { SvgXml, UriProps } from 'react-native-svg'

import { svgQuery } from '@/servicies/other'
import { getCompressedImage } from '@/utils/compressImage'
import { getCompressedImagePromise } from '@/utils/compressImage'
import { hasSize } from '@/utils/hasSize'
import tw from '@/utils/tw'
import { isSvgURL, resolveURL } from '@/utils/url'
Expand All @@ -37,25 +37,14 @@ function CustomImage({
containerWidth,
...props
}: StyledImageProps) {
const uri =
isObject(source) && !isArray(source) && isString(source.uri)
? resolveURL(source.uri)
: undefined

const uri = isObject(source) && !isArray(source) ? source.uri : undefined
const size = uriToSize.get(uri)
const update = useUpdate()

return (
<Image
{...props}
source={
isObject(source)
? {
...source,
uri,
}
: source
}
source={source}
onLoad={ev => {
const newSize: any = pick(ev.source, ['width', 'height'])
if (!isEqual(size, newSize)) {
Expand Down Expand Up @@ -126,6 +115,13 @@ function computeImageSize(

const actualWidth = Math.min(aspectRatio * MAX_IMAGE_HEIGHT, containerWidth)

if (actualWidth === containerWidth) {
return {
aspectRatio,
width: `100%`,
}
}

return {
width: actualWidth,
height: actualWidth / aspectRatio,
Expand Down Expand Up @@ -163,20 +159,40 @@ function CustomSvgUri({
}

function StyledImage({ source, ...props }: StyledImageProps) {
if (isObject(source) && !isArray(source) && isString(source.uri)) {
if (isSvgURL(source.uri))
return <CustomSvgUri uri={source.uri} {...(props as any)} />
const resolvedURI =
isObject(source) && !isArray(source) && isString(source.uri)
? resolveURL(source.uri)
: undefined

if (isString(resolvedURI)) {
if (isSvgURL(resolvedURI)) {
return <CustomSvgUri uri={resolvedURI} {...(props as any)} />
}

if (!hasSize(props.style)) {
const { uri, size } = use(getCompressedImage(source.uri))
if (!uriToSize.has(uri) && hasSize(size)) {
uriToSize.set(uri, size)
const { uri, size } = use(getCompressedImagePromise(resolvedURI))

if (!uriToSize.has(resolvedURI) && hasSize(size)) {
uriToSize.set(resolvedURI, size)
}
return <CustomImage source={{ ...source, uri }} {...props} />

return <CustomImage source={{ ...(source as any), uri }} {...props} />
}
}

return <CustomImage source={source} {...props} />
return (
<CustomImage
source={
isObject(source)
? {
...source,
uri: resolvedURI,
}
: source
}
{...props}
/>
)
}

export default withQuerySuspense(StyledImage, {
Expand Down
1 change: 0 additions & 1 deletion components/topic/ReplyItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,6 @@ function ReplyItem({
}}
inModalScreen={inModalScreen}
paddingX={32 + 36}
disabledPartialSelectable
/>
</View>

Expand Down
9 changes: 9 additions & 0 deletions navigation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import SearchNodeScreen from '@/screens/SearchNodeScreen'
import SearchOptionsScreen from '@/screens/SearchOptionsScreen'
import SearchReplyMemberScreen from '@/screens/SearchReplyMemberScreen'
import SearchScreen from '@/screens/SearchScreen'
import SelectableTextScreen from '@/screens/SelectableTextScreen'
import SettingScreen from '@/screens/SettingScreen'
import SortTabsScreen from '@/screens/SortTabsScreen'
import TopicDetailScreen from '@/screens/TopicDetailScreen'
Expand Down Expand Up @@ -220,6 +221,14 @@ function StackNavigator() {
component={SearchNodeScreen}
/>

<Stack.Screen
name="SelectableText"
options={{
presentation: 'modal',
}}
component={SelectableTextScreen}
/>

<Stack.Screen name="Login" component={LoginScreen} />

<Stack.Screen name="WriteTopic" component={WriteTopicScreen} />
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"jotai": "^1.12.0",
"js-base64": "^3.7.5",
"lodash-es": "^4.17.21",
"quaere": "^0.0.8",
"quaere": "^0.0.9",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-error-boundary": "^3.1.4",
Expand Down
38 changes: 38 additions & 0 deletions screens/SelectableTextScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { RouteProp, useRoute } from '@react-navigation/native'
import { ScrollView, TextInput, View } from 'react-native'
import {
CustomTextualRenderer,
getNativePropsForTNode,
} from 'react-native-render-html'

import Html from '@/components/Html'
import NavBar from '@/components/NavBar'
import { RootStackParamList } from '@/types'
import tw from '@/utils/tw'

const TextRenderer: CustomTextualRenderer = props => {
return (
<TextInput {...getNativePropsForTNode(props)} multiline editable={false} />
)
}

export default function SelectableTextScreen() {
const {
params: { html },
} = useRoute<RouteProp<RootStackParamList, 'SelectableText'>>()

return (
<View style={tw`bg-body-1 flex-1`}>
<NavBar title="选择文本" hideSafeTop />

<ScrollView style={tw`px-4`}>
<Html
source={{ html }}
renderers={{
_TEXT_: TextRenderer,
}}
/>
</ScrollView>
</View>
)
}
3 changes: 3 additions & 0 deletions types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ export type RootStackParamList = {
url: string
}
ImgurConfig: undefined
SelectableText: {
html: string
}
}

export type RootStackScreenProps<Screen extends keyof RootStackParamList> =
Expand Down
4 changes: 3 additions & 1 deletion utils/compressImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ const compressImage = async (uri: string): Promise<CompressedImage> => {
}
}

export function getCompressedImage(uri: string): Promise<CompressedImage> {
export function getCompressedImagePromise(
uri: string
): Promise<CompressedImage> {
if (!compressPromises.has(uri)) {
compressPromises.set(uri, compressImage(uri))
}
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7133,10 +7133,10 @@ qs@^6.11.0:
dependencies:
side-channel "^1.0.4"

quaere@^0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/quaere/-/quaere-0.0.8.tgz#28dba9a5de412a25cf27bd9f9e16c44d1099b71d"
integrity sha512-lOLOCZ9zJGRMVpXvTvomCmEej2BCn8G/GGB4gWq7HKU/0fpc3TYq5PjZ2EB/rXqarVuDxr5JJEtBiwaULC6bUw==
quaere@^0.0.9:
version "0.0.9"
resolved "https://registry.yarnpkg.com/quaere/-/quaere-0.0.9.tgz#82aa1dfdc94cff2dc271f9048978664c4a6f39cc"
integrity sha512-CT5I3iXzdXaZ2R19P8GFjODfgnLQaQNgXF2tM3TmfnFFyUpzXb0bK1yAQK3ezQ3lvaxxessRuURl+Sz96HpU8g==
dependencies:
client-only "^0.0.1"

Expand Down

0 comments on commit 7d54c01

Please sign in to comment.