Merge pull request #314 from tooot-app/main

Further test v4.0.4
This commit is contained in:
xmflsct 2022-06-02 01:04:44 +02:00 committed by GitHub
commit f6f078dc1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 590 additions and 165 deletions

View File

@ -5,7 +5,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { adaptiveScale } from '@utils/styles/scaling'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useMemo } from 'react'
import { StyleSheet } from 'react-native'
import { Platform, StyleSheet } from 'react-native'
import FastImage from 'react-native-fast-image'
import { useSelector } from 'react-redux'
import validUrl from 'valid-url'
@ -51,7 +51,13 @@ const ParseEmojis = React.memo(
image: {
width: adaptedFontsize,
height: adaptedFontsize,
transform: [{ translateY: -2 }]
...(Platform.OS === 'ios'
? {
transform: [{ translateY: -2 }]
}
: {
transform: [{ translateY: 1 }]
})
}
})
}, [theme, adaptiveFontsize])

View File

@ -13,7 +13,7 @@ import { adaptiveScale } from '@utils/styles/scaling'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native'
import { Platform, Pressable, View } from 'react-native'
import HTMLView from 'react-native-htmlview'
import { useSelector } from 'react-redux'
@ -139,7 +139,13 @@ const renderNode = ({
name='ExternalLink'
size={adaptedFontsize}
style={{
transform: [{ translateY: -2 }]
...(Platform.OS === 'ios'
? {
transform: [{ translateY: -2 }]
}
: {
transform: [{ translateY: 1 }]
})
}}
/>
) : null}

View File

@ -1,10 +1,6 @@
import ComponentSeparator from '@components/Separator'
import { useScrollToTop } from '@react-navigation/native'
import {
QueryKeyTimeline,
TimelineData,
useTimelineQuery
} from '@utils/queryHooks/timeline'
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
import { getInstanceActive } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
@ -16,10 +12,20 @@ import {
RefreshControl,
StyleSheet
} from 'react-native'
import { InfiniteData, useQueryClient } from 'react-query'
import Animated, {
useAnimatedScrollHandler,
useSharedValue
} from 'react-native-reanimated'
import { useQueryClient } from 'react-query'
import { useSelector } from 'react-redux'
import TimelineEmpty from './Timeline/Empty'
import TimelineFooter from './Timeline/Footer'
import TimelineRefresh, {
SEPARATION_Y_1,
SEPARATION_Y_2
} from './Timeline/Refresh'
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList)
export interface Props {
flRef?: RefObject<FlatList<any>>
@ -40,15 +46,12 @@ const Timeline: React.FC<Props> = ({
}) => {
const { colors } = useTheme()
const queryClient = useQueryClient()
const {
data,
refetch,
isFetching,
isLoading,
fetchPreviousPage,
fetchNextPage,
isFetchingPreviousPage,
isFetchingNextPage
} = useTimelineQuery({
...queryKey[1],
@ -57,12 +60,6 @@ const Timeline: React.FC<Props> = ({
ios: ['dataUpdatedAt', 'isFetching'],
android: ['dataUpdatedAt', 'isFetching', 'isLoading']
}),
getPreviousPageParam: firstPage =>
firstPage?.links?.prev && {
min_id: firstPage.links.prev,
// https://github.com/facebook/react-native/issues/25239
limit: '10'
},
getNextPageParam: lastPage =>
lastPage?.links?.next && {
max_id: lastPage.links.next
@ -92,6 +89,27 @@ const Timeline: React.FC<Props> = ({
const flRef = useRef<FlatList>(null)
const scrollY = useSharedValue(0)
const fetchingType = useSharedValue<0 | 1 | 2>(0)
const onScroll = useAnimatedScrollHandler(
{
onScroll: ({ contentOffset: { y } }) => {
scrollY.value = y
},
onEndDrag: ({ contentOffset: { y } }) => {
if (!disableRefresh && !isFetching) {
if (y <= SEPARATION_Y_2) {
fetchingType.value = 2
} else if (y <= SEPARATION_Y_1) {
fetchingType.value = 1
}
}
}
},
[isFetching]
)
const androidRefreshControl = Platform.select({
android: {
refreshControl: (
@ -115,46 +133,40 @@ const Timeline: React.FC<Props> = ({
})
return (
<FlatList
ref={customFLRef || flRef}
scrollEventThrottle={16}
windowSize={7}
data={flattenData}
initialNumToRender={6}
maxToRenderPerBatch={3}
style={styles.flatList}
onEndReached={onEndReached}
onEndReachedThreshold={0.75}
ListFooterComponent={
<TimelineFooter queryKey={queryKey} disableInfinity={disableInfinity} />
}
ListEmptyComponent={<TimelineEmpty queryKey={queryKey} />}
ItemSeparatorComponent={ItemSeparatorComponent}
{...(isFetchingPreviousPage && {
maintainVisibleContentPosition: { minIndexForVisible: 0 }
})}
refreshing={isFetchingPreviousPage}
onRefresh={() => {
if (!disableRefresh && !isFetchingPreviousPage) {
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(
queryKey,
data => {
if (data?.pages[0] && data.pages[0].body.length === 0) {
return {
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1)
}
} else {
return data
}
}
)
fetchPreviousPage()
<>
<TimelineRefresh
flRef={flRef}
queryKey={queryKey}
scrollY={scrollY}
fetchingType={fetchingType}
disableRefresh={disableRefresh}
/>
<AnimatedFlatList
ref={customFLRef || flRef}
scrollEventThrottle={16}
onScroll={onScroll}
windowSize={7}
data={flattenData}
initialNumToRender={6}
maxToRenderPerBatch={3}
style={styles.flatList}
onEndReached={onEndReached}
onEndReachedThreshold={0.75}
ListFooterComponent={
<TimelineFooter
queryKey={queryKey}
disableInfinity={disableInfinity}
/>
}
}}
{...androidRefreshControl}
{...customProps}
/>
ListEmptyComponent={<TimelineEmpty queryKey={queryKey} />}
ItemSeparatorComponent={ItemSeparatorComponent}
maintainVisibleContentPosition={{
minIndexForVisible: 0
}}
{...androidRefreshControl}
{...customProps}
/>
</>
)
}

View File

@ -0,0 +1,323 @@
import haptics from '@components/haptics'
import Icon from '@components/Icon'
import {
QueryKeyTimeline,
TimelineData,
useTimelineQuery
} from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FlatList, Platform, StyleSheet, Text, View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import Animated, {
Extrapolate,
interpolate,
runOnJS,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
withTiming
} from 'react-native-reanimated'
import { InfiniteData, useQueryClient } from 'react-query'
export interface Props {
flRef: RefObject<FlatList<any>>
queryKey: QueryKeyTimeline
scrollY: Animated.SharedValue<number>
fetchingType: Animated.SharedValue<0 | 1 | 2>
disableRefresh?: boolean
}
const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2.5
export const SEPARATION_Y_1 = -(
CONTAINER_HEIGHT / 2 +
StyleConstants.Font.Size.S / 2
)
export const SEPARATION_Y_2 = -(
CONTAINER_HEIGHT * 1.5 +
StyleConstants.Font.Size.S / 2
)
const TimelineRefresh: React.FC<Props> = ({
flRef,
queryKey,
scrollY,
fetchingType,
disableRefresh = false
}) => {
if (Platform.OS !== 'ios') {
return null
}
if (disableRefresh) {
return null
}
const fetchingLatestIndex = useRef(0)
const refetchActive = useRef(false)
const {
refetch,
isFetching,
isLoading,
fetchPreviousPage,
hasPreviousPage,
isFetchingNextPage
} = useTimelineQuery({
...queryKey[1],
options: {
getPreviousPageParam: firstPage =>
firstPage?.links?.prev && {
min_id: firstPage.links.prev,
// https://github.com/facebook/react-native/issues/25239#issuecomment-731100372
limit: '3'
},
select: data => {
if (refetchActive.current) {
data.pageParams = [data.pageParams[0]]
data.pages = [data.pages[0]]
refetchActive.current = false
}
return data
},
onSuccess: () => {
if (fetchingLatestIndex.current > 0) {
if (fetchingLatestIndex.current > 5) {
clearFirstPage()
fetchingLatestIndex.current = 0
} else {
if (hasPreviousPage) {
fetchPreviousPage()
fetchingLatestIndex.current++
} else {
clearFirstPage()
fetchingLatestIndex.current = 0
}
}
}
}
}
})
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
const queryClient = useQueryClient()
const clearFirstPage = () => {
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(
queryKey,
data => {
if (data?.pages[0] && data.pages[0].body.length === 0) {
return {
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1)
}
} else {
return data
}
}
)
}
const prepareRefetch = () => {
refetchActive.current = true
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(
queryKey,
data => {
if (data) {
data.pageParams = [undefined]
const newFirstPage: TimelineData = { body: [] }
for (let page of data.pages) {
// @ts-ignore
newFirstPage.body.push(...page.body)
if (newFirstPage.body.length > 10) break
}
data.pages = [newFirstPage]
}
return data
}
)
}
const callRefetch = async () => {
await refetch()
setTimeout(() => flRef.current?.scrollToOffset({ offset: 1 }), 50)
}
const [textRight, setTextRight] = useState(0)
const arrowY = useAnimatedStyle(() => ({
transform: [
{
translateY: interpolate(
scrollY.value,
[0, SEPARATION_Y_1],
[
-CONTAINER_HEIGHT / 2 - StyleConstants.Font.Size.M / 2,
CONTAINER_HEIGHT / 2 - StyleConstants.Font.Size.S / 2
],
Extrapolate.CLAMP
)
}
]
}))
const arrowTop = useAnimatedStyle(() => ({
marginTop:
scrollY.value < SEPARATION_Y_2
? withTiming(CONTAINER_HEIGHT)
: withTiming(0)
}))
const arrowStage = useSharedValue(0)
const onLayout = useCallback(
({ nativeEvent }) => {
if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) {
setTextRight(nativeEvent.layout.x + nativeEvent.layout.width)
}
},
[textRight]
)
useAnimatedReaction(
() => {
if (isFetching) {
return false
}
switch (arrowStage.value) {
case 0:
if (scrollY.value < SEPARATION_Y_1) {
arrowStage.value = 1
return true
}
return false
case 1:
if (scrollY.value < SEPARATION_Y_2) {
arrowStage.value = 2
return true
}
if (scrollY.value > SEPARATION_Y_1) {
arrowStage.value = 0
return false
}
return false
case 2:
if (scrollY.value > SEPARATION_Y_2) {
arrowStage.value = 1
return false
}
return false
}
},
data => {
if (data) {
runOnJS(haptics)('Light')
}
},
[isFetching]
)
const wrapperStartLatest = () => {
fetchingLatestIndex.current = 1
}
useAnimatedReaction(
() => {
return fetchingType.value
},
data => {
fetchingType.value = 0
switch (data) {
case 1:
runOnJS(wrapperStartLatest)()
runOnJS(clearFirstPage)()
runOnJS(fetchPreviousPage)()
break
case 2:
runOnJS(prepareRefetch)()
runOnJS(callRefetch)()
break
}
},
[]
)
const headerPadding = useAnimatedStyle(
() => ({
paddingTop:
fetchingLatestIndex.current !== 0 ||
(isFetching && !isLoading && !isFetchingNextPage)
? withTiming(StyleConstants.Spacing.M * 2.5)
: withTiming(0)
}),
[fetchingLatestIndex.current, isFetching, isFetchingNextPage, isLoading]
)
return (
<Animated.View style={headerPadding}>
<View style={styles.base}>
{isFetching ? (
<View style={styles.container2}>
<Circle
size={StyleConstants.Font.Size.L}
color={colors.secondary}
/>
</View>
) : (
<>
<View style={styles.container1}>
<Text
style={[styles.explanation, { color: colors.primaryDefault }]}
onLayout={onLayout}
children={t('refresh.fetchPreviousPage')}
/>
<Animated.View
style={[
{
position: 'absolute',
left: textRight + StyleConstants.Spacing.S
},
arrowY,
arrowTop
]}
children={
<Icon
name='ArrowLeft'
size={StyleConstants.Font.Size.M}
color={colors.primaryDefault}
/>
}
/>
</View>
<View style={styles.container2}>
<Text
style={[styles.explanation, { color: colors.primaryDefault }]}
onLayout={onLayout}
children={t('refresh.refetch')}
/>
</View>
</>
)}
</View>
</Animated.View>
)
}
const styles = StyleSheet.create({
base: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: CONTAINER_HEIGHT * 2,
alignItems: 'center'
},
container1: {
flex: 1,
flexDirection: 'row',
height: CONTAINER_HEIGHT
},
container2: { height: CONTAINER_HEIGHT, justifyContent: 'center' },
explanation: {
fontSize: StyleConstants.Font.Size.S,
lineHeight: CONTAINER_HEIGHT
}
})
export default TimelineRefresh

View File

@ -8,11 +8,13 @@ import AttachmentVideo from '@components/Timeline/Shared/Attachment/Video'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { RootStackParamList } from '@utils/navigation/navigators'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
import React, { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native'
import { useSelector } from 'react-redux'
export interface Props {
status: Pick<Mastodon.Status, 'media_attachments' | 'sensitive'>
@ -22,7 +24,23 @@ const TimelineAttachment = React.memo(
({ status }: Props) => {
const { t } = useTranslation('componentTimeline')
const [sensitiveShown, setSensitiveShown] = useState(status.sensitive)
const account = useSelector(
getInstanceAccount,
(prev, next) =>
prev.preferences['reading:expand:media'] ===
next.preferences['reading:expand:media']
)
const defaultSensitive = () => {
switch (account.preferences['reading:expand:media']) {
case 'show_all':
return false
case 'hide_all':
return true
default:
return status.sensitive
}
}
const [sensitiveShown, setSensitiveShown] = useState(defaultSensitive())
const imageUrls = useRef<
RootStackParamList['Screen-ImagesViewer']['imageUrls']
@ -151,7 +169,7 @@ const TimelineAttachment = React.memo(
})}
</View>
{status.sensitive &&
{defaultSensitive() &&
(sensitiveShown ? (
<Pressable
style={{

View File

@ -55,7 +55,7 @@ export const shouldFilter = ({
) // $& means the whole matched string
switch (filter.whole_word) {
case true:
if (new RegExp('\\B' + escapedPhrase + '\\B').test(text)) {
if (new RegExp('\\b' + escapedPhrase + '\\b').test(text)) {
shouldFilter = true
}
break
@ -100,6 +100,7 @@ export const shouldFilter = ({
})
}
})
status.spoiler_text && parser.write(status.spoiler_text)
parser.write(status.content)
parser.end()
}

View File

@ -168,6 +168,7 @@
"header": {
"title": "Entwurf"
},
"warning": "",
"content": {
"accessibilityHint": "Gespeicherter Entwurf, tippe, um diesen zu bearbeiten",
"textEmpty": "Kein Inhalt"

View File

@ -169,6 +169,9 @@
"follow": {
"heading": "Neue Follower"
},
"follow_request": {
"heading": ""
},
"favourite": {
"heading": "Favoriten"
},
@ -181,6 +184,9 @@
"poll": {
"heading": "Umfrageupdate"
},
"status": {
"heading": ""
},
"howitworks": "Erfahre, wie das Routing funktioniert"
},
"root": {

View File

@ -168,6 +168,7 @@
"header": {
"title": "Bozza"
},
"warning": "",
"content": {
"accessibilityHint": "Bozza salvata, premi per modificarla",
"textEmpty": "Testo vuoto"

View File

@ -169,6 +169,9 @@
"follow": {
"heading": "Nuovi seguaci"
},
"follow_request": {
"heading": ""
},
"favourite": {
"heading": "Apprezzamenti"
},
@ -181,6 +184,9 @@
"poll": {
"heading": "Novità sui sondaggi"
},
"status": {
"heading": ""
},
"howitworks": "Scopri come funziona il traversamento dei messaggi"
},
"root": {

View File

@ -168,6 +168,7 @@
"header": {
"title": "초안"
},
"warning": "",
"content": {
"accessibilityHint": "저장된 초안, 수정하려면 탭하세요",
"textEmpty": "콘텐츠 빔"

View File

@ -169,6 +169,9 @@
"follow": {
"heading": "새 팔로워"
},
"follow_request": {
"heading": ""
},
"favourite": {
"heading": "즐겨찾기됨"
},
@ -181,6 +184,9 @@
"poll": {
"heading": "투표 업데이트"
},
"status": {
"heading": ""
},
"howitworks": "라우팅 방법 알아보기"
},
"root": {

View File

@ -168,6 +168,7 @@
"header": {
"title": "Rascunho"
},
"warning": "",
"content": {
"accessibilityHint": "Toque para editar este rascunho",
"textEmpty": "O conteúdo está vazio"

View File

@ -169,6 +169,9 @@
"follow": {
"heading": "Novo seguidor"
},
"follow_request": {
"heading": ""
},
"favourite": {
"heading": "Favoritos"
},
@ -181,6 +184,9 @@
"poll": {
"heading": "Pesquisa atualizada"
},
"status": {
"heading": ""
},
"howitworks": "Saiba como funciona o roteamento"
},
"root": {

View File

@ -30,7 +30,7 @@
"default": "{{name}} đăng lại",
"notification": "{{name}} đăng lại tút của bạn"
},
"update": ""
"update": "Đăng lại đã được sửa"
},
"actions": {
"reply": {

View File

@ -13,8 +13,8 @@
"reblog": "$t(screenTabs:me.push.reblog.heading)",
"mention": "$t(screenTabs:me.push.mention.heading)",
"poll": "$t(screenTabs:me.push.poll.heading)",
"status": "",
"update": ""
"status": "Tút từ người đã theo dõi",
"update": "Đăng lại đã được sửa"
}
}
}

View File

@ -168,6 +168,7 @@
"header": {
"title": "Nháp"
},
"warning": "Tút nháp chỉ được lưu trữ trên điện thoại và có thể bị mất nếu có sự cố. Hãy cẩn thận.",
"content": {
"accessibilityHint": "Đã lưu nháp, nhấn để tiếp tục viết",
"textEmpty": "Chưa có nội dung"

View File

@ -169,6 +169,9 @@
"follow": {
"heading": "Người theo dõi mới"
},
"follow_request": {
"heading": "Yêu cầu theo dõi"
},
"favourite": {
"heading": "Lượt thích"
},
@ -181,6 +184,9 @@
"poll": {
"heading": "Kết quả bình chọn"
},
"status": {
"heading": "Tút từ người đã theo dõi"
},
"howitworks": "Tìm hiểu cách truyền"
},
"root": {

View File

@ -168,6 +168,7 @@
"header": {
"title": "草稿"
},
"warning": "草稿只存储在本地,在部分情况下可能丢失。建议不要长期存储草稿。",
"content": {
"accessibilityHint": "已保存的草稿,点击编辑此草稿",
"textEmpty": "无正文内容"

View File

@ -169,6 +169,9 @@
"follow": {
"heading": "新关注者"
},
"follow_request": {
"heading": "关注请求"
},
"favourite": {
"heading": "嘟文被喜欢"
},
@ -181,6 +184,9 @@
"poll": {
"heading": "投票更新"
},
"status": {
"heading": "订阅用户的嘟文"
},
"howitworks": "了解通知消息转发如何工作"
},
"root": {

View File

@ -168,6 +168,7 @@
"header": {
"title": ""
},
"warning": "",
"content": {
"accessibilityHint": "",
"textEmpty": ""

View File

@ -169,6 +169,9 @@
"follow": {
"heading": ""
},
"follow_request": {
"heading": ""
},
"favourite": {
"heading": ""
},
@ -181,6 +184,9 @@
"poll": {
"heading": ""
},
"status": {
"heading": ""
},
"howitworks": ""
},
"root": {

View File

@ -100,8 +100,8 @@ const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
)}
source={{ uri }}
style={{
width: 32,
height: 32,
width: 36,
height: 36,
padding: StyleConstants.Spacing.S,
margin: StyleConstants.Spacing.S
}}
@ -119,8 +119,8 @@ const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
)}
source={{ uri }}
style={{
width: 32,
height: 32,
width: 36,
height: 36,
padding: StyleConstants.Spacing.S,
margin: StyleConstants.Spacing.S
}}
@ -145,7 +145,7 @@ const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-around',
height: 260
height: 280
}}
>
<SectionList

View File

@ -19,94 +19,6 @@ import {
import ImageViewer from './ImageViewer/Root'
import saveImage from './ImageViewer/save'
const HeaderComponent = React.memo(
({
messageRef,
navigation,
currentIndex,
imageUrls
}: {
messageRef: RefObject<FlashMessage>
navigation: NativeStackNavigationProp<
RootStackParamList,
'Screen-ImagesViewer'
>
currentIndex: number
imageUrls: RootStackParamList['Screen-ImagesViewer']['imageUrls']
}) => {
const insets = useSafeAreaInsets()
const { mode, theme } = useTheme()
const { t } = useTranslation('screenImageViewer')
const { showActionSheetWithOptions } = useActionSheet()
const onPress = useCallback(() => {
analytics('imageviewer_more_press')
showActionSheetWithOptions(
{
options: [
t('content.options.save'),
t('content.options.share'),
t('content.options.cancel')
],
cancelButtonIndex: 2,
userInterfaceStyle: mode
},
async buttonIndex => {
switch (buttonIndex) {
case 0:
analytics('imageviewer_more_save_press')
saveImage({ messageRef, theme, image: imageUrls[currentIndex] })
break
case 1:
analytics('imageviewer_more_share_press')
switch (Platform.OS) {
case 'ios':
await Share.share({ url: imageUrls[currentIndex].url })
break
case 'android':
await Share.share({ message: imageUrls[currentIndex].url })
break
}
break
}
}
)
}, [currentIndex])
return (
<View
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: insets.top
}}
>
<HeaderLeft
content='X'
native={false}
background
onPress={() => navigation.goBack()}
/>
<HeaderCenter
inverted
content={`${currentIndex + 1} / ${imageUrls.length}`}
/>
<HeaderRight
accessibilityLabel={t('content.actions.accessibilityLabel')}
accessibilityHint={t('content.actions.accessibilityHint')}
content='MoreHorizontal'
native={false}
background
onPress={onPress}
/>
</View>
)
},
(prev, next) => prev.currentIndex === next.currentIndex
)
const ScreenImagesViewer = ({
route: {
params: { imageUrls, id }
@ -118,13 +30,51 @@ const ScreenImagesViewer = ({
return null
}
const { theme } = useTheme()
const insets = useSafeAreaInsets()
const { mode, theme } = useTheme()
const { t } = useTranslation('screenImageViewer')
const initialIndex = imageUrls.findIndex(image => image.id === id)
const [currentIndex, setCurrentIndex] = useState(initialIndex)
const messageRef = useRef<FlashMessage>(null)
const { showActionSheetWithOptions } = useActionSheet()
const onPress = useCallback(() => {
analytics('imageviewer_more_press')
showActionSheetWithOptions(
{
options: [
t('content.options.save'),
t('content.options.share'),
t('content.options.cancel')
],
cancelButtonIndex: 2,
userInterfaceStyle: mode
},
async buttonIndex => {
switch (buttonIndex) {
case 0:
analytics('imageviewer_more_save_press')
saveImage({ messageRef, theme, image: imageUrls[currentIndex] })
break
case 1:
analytics('imageviewer_more_share_press')
switch (Platform.OS) {
case 'ios':
await Share.share({ url: imageUrls[currentIndex].url })
break
case 'android':
await Share.share({ message: imageUrls[currentIndex].url })
break
}
break
}
}
)
}, [currentIndex])
return (
<SafeAreaProvider>
<StatusBar hidden />
@ -133,14 +83,74 @@ const ScreenImagesViewer = ({
imageIndex={initialIndex}
onImageIndexChange={index => setCurrentIndex(index)}
onRequestClose={() => navigation.goBack()}
onLongPress={image => saveImage({ messageRef, theme, image })}
onLongPress={() => {
analytics('imageviewer_more_press')
showActionSheetWithOptions(
{
options: [
t('content.options.save'),
t('content.options.share'),
t('content.options.cancel')
],
cancelButtonIndex: 2,
userInterfaceStyle: mode
},
async buttonIndex => {
switch (buttonIndex) {
case 0:
analytics('imageviewer_more_save_press')
saveImage({
messageRef,
theme,
image: imageUrls[currentIndex]
})
break
case 1:
analytics('imageviewer_more_share_press')
switch (Platform.OS) {
case 'ios':
await Share.share({ url: imageUrls[currentIndex].url })
break
case 'android':
await Share.share({
message: imageUrls[currentIndex].url
})
break
}
break
}
}
)
}}
HeaderComponent={() => (
<HeaderComponent
messageRef={messageRef}
navigation={navigation}
currentIndex={currentIndex}
imageUrls={imageUrls}
/>
<View
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: insets.top
}}
>
<HeaderLeft
content='X'
native={false}
background
onPress={() => navigation.goBack()}
/>
<HeaderCenter
inverted
content={`${currentIndex + 1} / ${imageUrls.length}`}
/>
<HeaderRight
accessibilityLabel={t('content.actions.accessibilityLabel')}
accessibilityHint={t('content.actions.accessibilityHint')}
content='MoreHorizontal'
native={false}
background
onPress={onPress}
/>
</View>
)}
/>
<Message ref={messageRef} />