mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Added store review
This commit is contained in:
BIN
assets/icon.png
BIN
assets/icon.png
Binary file not shown.
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 32 KiB |
Binary file not shown.
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
@ -39,6 +39,7 @@
|
|||||||
"expo-secure-store": "~9.3.0",
|
"expo-secure-store": "~9.3.0",
|
||||||
"expo-splash-screen": "~0.8.1",
|
"expo-splash-screen": "~0.8.1",
|
||||||
"expo-status-bar": "~1.0.3",
|
"expo-status-bar": "~1.0.3",
|
||||||
|
"expo-store-review": "~2.3.0",
|
||||||
"expo-video-thumbnails": "~4.4.0",
|
"expo-video-thumbnails": "~4.4.0",
|
||||||
"expo-web-browser": "~8.6.0",
|
"expo-web-browser": "~8.6.0",
|
||||||
"gl-react": "^4.0.1",
|
"gl-react": "^4.0.1",
|
||||||
|
@ -122,7 +122,7 @@ const Button: React.FC<Props> = ({
|
|||||||
style={{ opacity: loading ? 0 : 1 }}
|
style={{ opacity: loading ? 0 : 1 }}
|
||||||
size={StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1)}
|
size={StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1)}
|
||||||
/>
|
/>
|
||||||
{loading && loadingSpinkit}
|
{loading ? loadingSpinkit : null}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
case 'text':
|
case 'text':
|
||||||
@ -141,7 +141,7 @@ const Button: React.FC<Props> = ({
|
|||||||
children={content}
|
children={content}
|
||||||
testID='text'
|
testID='text'
|
||||||
/>
|
/>
|
||||||
{loading && loadingSpinkit}
|
{loading ? loadingSpinkit : null}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -14,26 +14,57 @@ import { Image as ImageCache } from 'react-native-expo-image-cache'
|
|||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
|
|
||||||
type CancelPromise = ((reason?: Error) => void) | undefined
|
type CancelPromise = ((reason?: Error) => void) | undefined
|
||||||
type ImageSize = { width: number; height: number }
|
|
||||||
interface ImageSizeOperation {
|
interface ImageSizeOperation {
|
||||||
start: () => Promise<ImageSize>
|
start: () => Promise<string>
|
||||||
cancel: CancelPromise
|
cancel: CancelPromise
|
||||||
}
|
}
|
||||||
const getImageSize = (uri: string): ImageSizeOperation => {
|
const getImageSize = ({
|
||||||
|
preview,
|
||||||
|
original,
|
||||||
|
remote
|
||||||
|
}: {
|
||||||
|
preview?: string
|
||||||
|
original: string
|
||||||
|
remote?: string
|
||||||
|
}): ImageSizeOperation => {
|
||||||
let cancel: CancelPromise
|
let cancel: CancelPromise
|
||||||
const start = (): Promise<ImageSize> =>
|
const start = (): Promise<string> =>
|
||||||
new Promise<{ width: number; height: number }>((resolve, reject) => {
|
new Promise<string>((resolve, reject) => {
|
||||||
cancel = reject
|
cancel = reject
|
||||||
Image.getSize(
|
Image.getSize(
|
||||||
uri,
|
preview || '',
|
||||||
(width, height) => {
|
() => {
|
||||||
cancel = undefined
|
cancel = undefined
|
||||||
resolve({ width, height })
|
resolve(preview!)
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
cancel = reject
|
||||||
|
Image.getSize(
|
||||||
|
original,
|
||||||
|
() => {
|
||||||
|
cancel = undefined
|
||||||
|
resolve(original)
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
cancel = reject
|
||||||
|
if (!remote) {
|
||||||
|
reject()
|
||||||
|
} else {
|
||||||
|
Image.getSize(
|
||||||
|
remote,
|
||||||
|
() => {
|
||||||
|
cancel = undefined
|
||||||
|
resolve(remote)
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
reject(error)
|
reject(error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
return { start, cancel }
|
return { start, cancel }
|
||||||
@ -61,51 +92,22 @@ const GracefullyImage: React.FC<Props> = ({
|
|||||||
const { mode, theme } = useTheme()
|
const { mode, theme } = useTheme()
|
||||||
|
|
||||||
const [imageVisible, setImageVisible] = useState<string>()
|
const [imageVisible, setImageVisible] = useState<string>()
|
||||||
const [imageLoadingFailed, setImageLoadingFailed] = useState(false)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let mounted = true
|
||||||
let cancel: CancelPromise
|
let cancel: CancelPromise
|
||||||
const sideEffect = async (): Promise<void> => {
|
const sideEffect = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
if (uri.preview) {
|
const prefetchImage = getImageSize(uri as { original: string })
|
||||||
const tryPreview = getImageSize(uri.preview)
|
cancel = prefetchImage.cancel
|
||||||
cancel = tryPreview.cancel
|
const res = await prefetchImage.start()
|
||||||
const res = await tryPreview.start()
|
if (mounted) {
|
||||||
if (res) {
|
setImageVisible(res)
|
||||||
setImageVisible(uri.preview)
|
}
|
||||||
return
|
return
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (__DEV__) console.warn('Image preview', error)
|
if (__DEV__) console.warn('Image preview', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const tryOriginal = getImageSize(uri.original!)
|
|
||||||
cancel = tryOriginal.cancel
|
|
||||||
const res = await tryOriginal.start()
|
|
||||||
if (res) {
|
|
||||||
setImageVisible(uri.original!)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (__DEV__) console.warn('Image original', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (uri.remote) {
|
|
||||||
const tryRemote = getImageSize(uri.remote)
|
|
||||||
cancel = tryRemote.cancel
|
|
||||||
const res = await tryRemote.start()
|
|
||||||
if (res) {
|
|
||||||
setImageVisible(uri.remote)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (__DEV__) console.warn('Image remote', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
setImageLoadingFailed(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uri.original) {
|
if (uri.original) {
|
||||||
@ -113,6 +115,7 @@ const GracefullyImage: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
mounted = false
|
||||||
if (cancel) {
|
if (cancel) {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ export interface Props {
|
|||||||
|
|
||||||
title: string
|
title: string
|
||||||
description?: string
|
description?: string
|
||||||
content?: string
|
content?: string | React.ReactNode
|
||||||
|
|
||||||
switchValue?: boolean
|
switchValue?: boolean
|
||||||
switchDisabled?: boolean
|
switchDisabled?: boolean
|
||||||
@ -90,11 +90,10 @@ const MenuRow: React.FC<Props> = ({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{(content && content.length) ||
|
{content || switchValue !== undefined || iconBack ? (
|
||||||
switchValue !== undefined ||
|
|
||||||
iconBack ? (
|
|
||||||
<View style={styles.back}>
|
<View style={styles.back}>
|
||||||
{content && content.length ? (
|
{content ? (
|
||||||
|
typeof content === 'string' ? (
|
||||||
<>
|
<>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
@ -110,6 +109,9 @@ const MenuRow: React.FC<Props> = ({
|
|||||||
</Text>
|
</Text>
|
||||||
{loading && !iconBack && loadingSpinkit}
|
{loading && !iconBack && loadingSpinkit}
|
||||||
</>
|
</>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
)
|
||||||
) : null}
|
) : null}
|
||||||
{switchValue !== undefined ? (
|
{switchValue !== undefined ? (
|
||||||
<Switch
|
<Switch
|
||||||
|
@ -16,10 +16,11 @@ import {
|
|||||||
StyleSheet
|
StyleSheet
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import { FlatList } from 'react-native-gesture-handler'
|
import { FlatList } from 'react-native-gesture-handler'
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
|
||||||
import { findIndex } from 'lodash'
|
import { findIndex } from 'lodash'
|
||||||
import { InfiniteData, useQueryClient } from 'react-query'
|
import { InfiniteData, useQueryClient } from 'react-query'
|
||||||
|
import { getPublicRemoteNotice } from '@utils/slices/contextsSlice'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
page: App.Pages
|
page: App.Pages
|
||||||
@ -212,6 +213,8 @@ const Timeline: React.FC<Props> = ({
|
|||||||
)
|
)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const publicRemoteNotice = useSelector(getPublicRemoteNotice).hidden
|
||||||
|
|
||||||
useScrollToTop(flRef)
|
useScrollToTop(flRef)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -231,7 +234,8 @@ const Timeline: React.FC<Props> = ({
|
|||||||
{...(!disableRefresh && { refreshControl })}
|
{...(!disableRefresh && { refreshControl })}
|
||||||
ItemSeparatorComponent={ItemSeparatorComponent}
|
ItemSeparatorComponent={ItemSeparatorComponent}
|
||||||
{...(queryKey &&
|
{...(queryKey &&
|
||||||
queryKey[1].page === 'RemotePublic' && { ListHeaderComponent })}
|
queryKey[1].page === 'RemotePublic' &&
|
||||||
|
!publicRemoteNotice && { ListHeaderComponent })}
|
||||||
{...(toot && isSuccess && { onScrollToIndexFailed })}
|
{...(toot && isSuccess && { onScrollToIndexFailed })}
|
||||||
maintainVisibleContentPosition={{
|
maintainVisibleContentPosition={{
|
||||||
minIndexForVisible: 0,
|
minIndexForVisible: 0,
|
||||||
|
@ -2,11 +2,14 @@ import { useNavigation } from '@react-navigation/native'
|
|||||||
import Icon from '@root/components/Icon'
|
import Icon from '@root/components/Icon'
|
||||||
import { StyleConstants } from '@root/utils/styles/constants'
|
import { StyleConstants } from '@root/utils/styles/constants'
|
||||||
import { useTheme } from '@root/utils/styles/ThemeManager'
|
import { useTheme } from '@root/utils/styles/ThemeManager'
|
||||||
|
import { updatePublicRemoteNotice } from '@utils/slices/contextsSlice'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { StyleSheet, Text, View } from 'react-native'
|
import { StyleSheet, Text, View } from 'react-native'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
|
|
||||||
const TimelineHeader = React.memo(
|
const TimelineHeader = React.memo(
|
||||||
() => {
|
() => {
|
||||||
|
const dispatch = useDispatch()
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
@ -17,6 +20,7 @@ const TimelineHeader = React.memo(
|
|||||||
<Text
|
<Text
|
||||||
style={{ color: theme.blue }}
|
style={{ color: theme.blue }}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
|
dispatch(updatePublicRemoteNotice(1))
|
||||||
navigation.navigate('Screen-Me', {
|
navigation.navigate('Screen-Me', {
|
||||||
screen: 'Screen-Me-Root',
|
screen: 'Screen-Me-Root',
|
||||||
params: { navigateAway: 'Screen-Me-Settings-UpdateRemote' }
|
params: { navigateAway: 'Screen-Me-Settings-UpdateRemote' }
|
||||||
|
@ -57,9 +57,10 @@ const TimelineAttachment: React.FC<Props> = ({ status }) => {
|
|||||||
return (
|
return (
|
||||||
<AttachmentImage
|
<AttachmentImage
|
||||||
key={index}
|
key={index}
|
||||||
|
total={status.media_attachments.length}
|
||||||
|
index={index}
|
||||||
sensitiveShown={sensitiveShown}
|
sensitiveShown={sensitiveShown}
|
||||||
image={attachment}
|
image={attachment}
|
||||||
imageIndex={index}
|
|
||||||
navigateToImagesViewer={navigateToImagesViewer}
|
navigateToImagesViewer={navigateToImagesViewer}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@ -67,6 +68,8 @@ const TimelineAttachment: React.FC<Props> = ({ status }) => {
|
|||||||
return (
|
return (
|
||||||
<AttachmentVideo
|
<AttachmentVideo
|
||||||
key={index}
|
key={index}
|
||||||
|
total={status.media_attachments.length}
|
||||||
|
index={index}
|
||||||
sensitiveShown={sensitiveShown}
|
sensitiveShown={sensitiveShown}
|
||||||
video={attachment}
|
video={attachment}
|
||||||
/>
|
/>
|
||||||
@ -75,6 +78,8 @@ const TimelineAttachment: React.FC<Props> = ({ status }) => {
|
|||||||
return (
|
return (
|
||||||
<AttachmentVideo
|
<AttachmentVideo
|
||||||
key={index}
|
key={index}
|
||||||
|
total={status.media_attachments.length}
|
||||||
|
index={index}
|
||||||
sensitiveShown={sensitiveShown}
|
sensitiveShown={sensitiveShown}
|
||||||
video={attachment}
|
video={attachment}
|
||||||
/>
|
/>
|
||||||
@ -83,6 +88,8 @@ const TimelineAttachment: React.FC<Props> = ({ status }) => {
|
|||||||
return (
|
return (
|
||||||
<AttachmentAudio
|
<AttachmentAudio
|
||||||
key={index}
|
key={index}
|
||||||
|
total={status.media_attachments.length}
|
||||||
|
index={index}
|
||||||
sensitiveShown={sensitiveShown}
|
sensitiveShown={sensitiveShown}
|
||||||
audio={attachment}
|
audio={attachment}
|
||||||
/>
|
/>
|
||||||
@ -91,6 +98,8 @@ const TimelineAttachment: React.FC<Props> = ({ status }) => {
|
|||||||
return (
|
return (
|
||||||
<AttachmentUnsupported
|
<AttachmentUnsupported
|
||||||
key={index}
|
key={index}
|
||||||
|
total={status.media_attachments.length}
|
||||||
|
index={index}
|
||||||
sensitiveShown={sensitiveShown}
|
sensitiveShown={sensitiveShown}
|
||||||
attachment={attachment}
|
attachment={attachment}
|
||||||
/>
|
/>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
|
import GracefullyImage from '@components/GracefullyImage'
|
||||||
import { Slider } from '@sharcoux/slider'
|
import { Slider } from '@sharcoux/slider'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
@ -7,14 +8,21 @@ import { Surface } from 'gl-react-expo'
|
|||||||
import { Blurhash } from 'gl-react-blurhash'
|
import { Blurhash } from 'gl-react-blurhash'
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { StyleSheet, View } from 'react-native'
|
import { StyleSheet, View } from 'react-native'
|
||||||
import GracefullyImage from '@components/GracefullyImage'
|
import attachmentAspectRatio from './aspectRatio'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
total: number
|
||||||
|
index: number
|
||||||
sensitiveShown: boolean
|
sensitiveShown: boolean
|
||||||
audio: Mastodon.AttachmentAudio
|
audio: Mastodon.AttachmentAudio
|
||||||
}
|
}
|
||||||
|
|
||||||
const AttachmentAudio: React.FC<Props> = ({ sensitiveShown, audio }) => {
|
const AttachmentAudio: React.FC<Props> = ({
|
||||||
|
total,
|
||||||
|
index,
|
||||||
|
sensitiveShown,
|
||||||
|
audio
|
||||||
|
}) => {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
const [audioPlayer, setAudioPlayer] = useState<Audio.Sound>()
|
const [audioPlayer, setAudioPlayer] = useState<Audio.Sound>()
|
||||||
@ -39,9 +47,17 @@ const AttachmentAudio: React.FC<Props> = ({ sensitiveShown, audio }) => {
|
|||||||
audioPlayer!.pauseAsync()
|
audioPlayer!.pauseAsync()
|
||||||
setAudioPlaying(false)
|
setAudioPlaying(false)
|
||||||
}, [audioPlayer])
|
}, [audioPlayer])
|
||||||
console.log(audio)
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.base, { backgroundColor: theme.disabled }]}>
|
<View
|
||||||
|
style={[
|
||||||
|
styles.base,
|
||||||
|
{
|
||||||
|
backgroundColor: theme.disabled,
|
||||||
|
aspectRatio: attachmentAspectRatio({ total, index })
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
<View style={styles.overlay}>
|
<View style={styles.overlay}>
|
||||||
{sensitiveShown ? (
|
{sensitiveShown ? (
|
||||||
audio.blurhash && (
|
audio.blurhash && (
|
||||||
@ -116,7 +132,6 @@ const styles = StyleSheet.create({
|
|||||||
base: {
|
base: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexBasis: '50%',
|
flexBasis: '50%',
|
||||||
aspectRatio: 16 / 9,
|
|
||||||
padding: StyleConstants.Spacing.XS / 2,
|
padding: StyleConstants.Spacing.XS / 2,
|
||||||
flexDirection: 'row'
|
flexDirection: 'row'
|
||||||
},
|
},
|
||||||
|
@ -1,22 +1,25 @@
|
|||||||
|
import GracefullyImage from '@components/GracefullyImage'
|
||||||
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { StyleSheet } from 'react-native'
|
import { StyleSheet } from 'react-native'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import attachmentAspectRatio from './aspectRatio'
|
||||||
import GracefullyImage from '@components/GracefullyImage'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
total: number
|
||||||
|
index: number
|
||||||
sensitiveShown: boolean
|
sensitiveShown: boolean
|
||||||
image: Mastodon.AttachmentImage
|
image: Mastodon.AttachmentImage
|
||||||
imageIndex: number
|
|
||||||
navigateToImagesViewer: (imageIndex: number) => void
|
navigateToImagesViewer: (imageIndex: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AttachmentImage: React.FC<Props> = ({
|
const AttachmentImage: React.FC<Props> = ({
|
||||||
|
total,
|
||||||
|
index,
|
||||||
sensitiveShown,
|
sensitiveShown,
|
||||||
image,
|
image,
|
||||||
imageIndex,
|
|
||||||
navigateToImagesViewer
|
navigateToImagesViewer
|
||||||
}) => {
|
}) => {
|
||||||
const onPress = useCallback(() => navigateToImagesViewer(imageIndex), [])
|
const onPress = useCallback(() => navigateToImagesViewer(index), [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GracefullyImage
|
<GracefullyImage
|
||||||
@ -28,7 +31,10 @@ const AttachmentImage: React.FC<Props> = ({
|
|||||||
}}
|
}}
|
||||||
blurhash={image.blurhash}
|
blurhash={image.blurhash}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
style={styles.base}
|
style={[
|
||||||
|
styles.base,
|
||||||
|
{ aspectRatio: attachmentAspectRatio({ total, index }) }
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -37,7 +43,6 @@ const styles = StyleSheet.create({
|
|||||||
base: {
|
base: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexBasis: '50%',
|
flexBasis: '50%',
|
||||||
aspectRatio: 16 / 9,
|
|
||||||
padding: StyleConstants.Spacing.XS / 2
|
padding: StyleConstants.Spacing.XS / 2
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -7,13 +7,18 @@ import { Surface } from 'gl-react-expo'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet, Text, View } from 'react-native'
|
import { StyleSheet, Text, View } from 'react-native'
|
||||||
|
import attachmentAspectRatio from './aspectRatio'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
total: number
|
||||||
|
index: number
|
||||||
sensitiveShown: boolean
|
sensitiveShown: boolean
|
||||||
attachment: Mastodon.AttachmentUnknown
|
attachment: Mastodon.AttachmentUnknown
|
||||||
}
|
}
|
||||||
|
|
||||||
const AttachmentUnsupported: React.FC<Props> = ({
|
const AttachmentUnsupported: React.FC<Props> = ({
|
||||||
|
total,
|
||||||
|
index,
|
||||||
sensitiveShown,
|
sensitiveShown,
|
||||||
attachment
|
attachment
|
||||||
}) => {
|
}) => {
|
||||||
@ -21,7 +26,12 @@ const AttachmentUnsupported: React.FC<Props> = ({
|
|||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.base}>
|
<View
|
||||||
|
style={[
|
||||||
|
styles.base,
|
||||||
|
{ aspectRatio: attachmentAspectRatio({ total, index }) }
|
||||||
|
]}
|
||||||
|
>
|
||||||
{attachment.blurhash ? (
|
{attachment.blurhash ? (
|
||||||
<Surface
|
<Surface
|
||||||
style={{
|
style={{
|
||||||
@ -62,7 +72,6 @@ const styles = StyleSheet.create({
|
|||||||
base: {
|
base: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexBasis: '50%',
|
flexBasis: '50%',
|
||||||
aspectRatio: 16 / 9,
|
|
||||||
padding: StyleConstants.Spacing.XS / 2,
|
padding: StyleConstants.Spacing.XS / 2,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center'
|
alignItems: 'center'
|
||||||
|
@ -1,17 +1,25 @@
|
|||||||
import React, { useCallback, useRef, useState } from 'react'
|
|
||||||
import { Pressable, StyleSheet, View } from 'react-native'
|
|
||||||
import { Video } from 'expo-av'
|
|
||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
|
import { Video } from 'expo-av'
|
||||||
import { Surface } from 'gl-react-expo'
|
import { Surface } from 'gl-react-expo'
|
||||||
import { Blurhash } from 'gl-react-blurhash'
|
import { Blurhash } from 'gl-react-blurhash'
|
||||||
import { StyleConstants } from '@root/utils/styles/constants'
|
import React, { useCallback, useRef, useState } from 'react'
|
||||||
|
import { Pressable, StyleSheet, View } from 'react-native'
|
||||||
|
import attachmentAspectRatio from './aspectRatio'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
total: number
|
||||||
|
index: number
|
||||||
sensitiveShown: boolean
|
sensitiveShown: boolean
|
||||||
video: Mastodon.AttachmentVideo | Mastodon.AttachmentGifv
|
video: Mastodon.AttachmentVideo | Mastodon.AttachmentGifv
|
||||||
}
|
}
|
||||||
|
|
||||||
const AttachmentVideo: React.FC<Props> = ({ sensitiveShown, video }) => {
|
const AttachmentVideo: React.FC<Props> = ({
|
||||||
|
total,
|
||||||
|
index,
|
||||||
|
sensitiveShown,
|
||||||
|
video
|
||||||
|
}) => {
|
||||||
const videoPlayer = useRef<Video>(null)
|
const videoPlayer = useRef<Video>(null)
|
||||||
const [videoLoading, setVideoLoading] = useState(false)
|
const [videoLoading, setVideoLoading] = useState(false)
|
||||||
const [videoLoaded, setVideoLoaded] = useState(false)
|
const [videoLoaded, setVideoLoaded] = useState(false)
|
||||||
@ -23,7 +31,6 @@ const AttachmentVideo: React.FC<Props> = ({ sensitiveShown, video }) => {
|
|||||||
}
|
}
|
||||||
await videoPlayer.current?.setPositionAsync(videoPosition)
|
await videoPlayer.current?.setPositionAsync(videoPosition)
|
||||||
await videoPlayer.current?.presentFullscreenPlayer()
|
await videoPlayer.current?.presentFullscreenPlayer()
|
||||||
console.log('playing!!!')
|
|
||||||
videoPlayer.current?.playAsync()
|
videoPlayer.current?.playAsync()
|
||||||
setVideoLoading(false)
|
setVideoLoading(false)
|
||||||
videoPlayer.current?.setOnPlaybackStatusUpdate(props => {
|
videoPlayer.current?.setOnPlaybackStatusUpdate(props => {
|
||||||
@ -39,7 +46,12 @@ const AttachmentVideo: React.FC<Props> = ({ sensitiveShown, video }) => {
|
|||||||
}, [videoLoaded, videoPosition])
|
}, [videoLoaded, videoPosition])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.base}>
|
<View
|
||||||
|
style={[
|
||||||
|
styles.base,
|
||||||
|
{ aspectRatio: attachmentAspectRatio({ total, index }) }
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Video
|
<Video
|
||||||
ref={videoPlayer}
|
ref={videoPlayer}
|
||||||
style={{
|
style={{
|
||||||
@ -90,7 +102,6 @@ const styles = StyleSheet.create({
|
|||||||
base: {
|
base: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexBasis: '50%',
|
flexBasis: '50%',
|
||||||
aspectRatio: 16 / 9,
|
|
||||||
padding: StyleConstants.Spacing.XS / 2
|
padding: StyleConstants.Spacing.XS / 2
|
||||||
},
|
},
|
||||||
overlay: {
|
overlay: {
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
const attachmentAspectRatio = ({
|
||||||
|
total,
|
||||||
|
index
|
||||||
|
}: {
|
||||||
|
total: number
|
||||||
|
index?: number
|
||||||
|
}) => {
|
||||||
|
switch (total) {
|
||||||
|
case 1:
|
||||||
|
case 4:
|
||||||
|
return 16 / 9
|
||||||
|
case 2:
|
||||||
|
return 8 / 9
|
||||||
|
case 3:
|
||||||
|
if (index === 2) {
|
||||||
|
return 32 / 9
|
||||||
|
} else {
|
||||||
|
return 16 / 9
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return 16 / 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default attachmentAspectRatio
|
@ -4,7 +4,7 @@ export default {
|
|||||||
language: {
|
language: {
|
||||||
heading: '切换语言',
|
heading: '切换语言',
|
||||||
options: {
|
options: {
|
||||||
'en': 'English',
|
en: 'English',
|
||||||
'zh-Hans': '简体中文',
|
'zh-Hans': '简体中文',
|
||||||
cancel: '$t(common:buttons.cancel)'
|
cancel: '$t(common:buttons.cancel)'
|
||||||
}
|
}
|
||||||
@ -38,9 +38,6 @@ export default {
|
|||||||
heading: '帮助我们改进',
|
heading: '帮助我们改进',
|
||||||
description: '允许我们收集不与用户相关联的使用信息'
|
description: '允许我们收集不与用户相关联的使用信息'
|
||||||
},
|
},
|
||||||
copyrights: {
|
|
||||||
heading: '版权信息'
|
|
||||||
},
|
|
||||||
version: '版本 v{{version}}'
|
version: '版本 v{{version}}'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import haptics from '@components/haptics'
|
import haptics from '@components/haptics'
|
||||||
|
import Icon from '@components/Icon'
|
||||||
import { MenuContainer, MenuRow } from '@components/Menu'
|
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||||
import { useActionSheet } from '@expo/react-native-action-sheet'
|
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
@ -22,6 +23,8 @@ import {
|
|||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import Constants from 'expo-constants'
|
import Constants from 'expo-constants'
|
||||||
|
import * as Linking from 'expo-linking'
|
||||||
|
import * as StoreReview from 'expo-store-review'
|
||||||
import prettyBytes from 'pretty-bytes'
|
import prettyBytes from 'pretty-bytes'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -102,15 +105,14 @@ const ScreenMeSettings: React.FC = () => {
|
|||||||
const availableLanguages = Object.keys(
|
const availableLanguages = Object.keys(
|
||||||
i18n.services.resourceStore.data
|
i18n.services.resourceStore.data
|
||||||
)
|
)
|
||||||
|
const options = availableLanguages
|
||||||
|
.map(language => t(`content.language.options.${language}`))
|
||||||
|
.concat(t('content.language.options.cancel'))
|
||||||
|
|
||||||
showActionSheetWithOptions(
|
showActionSheetWithOptions(
|
||||||
{
|
{
|
||||||
title: t('content.language.heading'),
|
title: t('content.language.heading'),
|
||||||
options: [
|
options,
|
||||||
...availableLanguages.map(language =>
|
|
||||||
t(`content.language.options.${language}`)
|
|
||||||
),
|
|
||||||
t('content.language.options.cancel')
|
|
||||||
],
|
|
||||||
cancelButtonIndex: i18n.languages.length
|
cancelButtonIndex: i18n.languages.length
|
||||||
},
|
},
|
||||||
buttonIndex => {
|
buttonIndex => {
|
||||||
@ -210,6 +212,36 @@ const ScreenMeSettings: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</MenuContainer>
|
</MenuContainer>
|
||||||
|
<MenuContainer>
|
||||||
|
<MenuRow
|
||||||
|
title={t('content.support.heading')}
|
||||||
|
content={
|
||||||
|
<Icon
|
||||||
|
name='Heart'
|
||||||
|
size={StyleConstants.Font.Size.M}
|
||||||
|
color={theme.red}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
iconBack='ChevronRight'
|
||||||
|
onPress={() => Linking.openURL('https://www.patreon.com/xmflsct')}
|
||||||
|
/>
|
||||||
|
<MenuRow
|
||||||
|
title={t('content.copyrights.heading')}
|
||||||
|
content={
|
||||||
|
<Icon
|
||||||
|
name='Star'
|
||||||
|
size={StyleConstants.Font.Size.M}
|
||||||
|
color='#FF9500'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
iconBack='ChevronRight'
|
||||||
|
onPress={() =>
|
||||||
|
StoreReview.isAvailableAsync().then(() =>
|
||||||
|
StoreReview.requestReview()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</MenuContainer>
|
||||||
<MenuContainer>
|
<MenuContainer>
|
||||||
<MenuRow
|
<MenuRow
|
||||||
title={t('content.analytics.heading')}
|
title={t('content.analytics.heading')}
|
||||||
@ -219,10 +251,6 @@ const ScreenMeSettings: React.FC = () => {
|
|||||||
dispatch(changeAnalytics(!settingsAnalytics))
|
dispatch(changeAnalytics(!settingsAnalytics))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<MenuRow
|
|
||||||
title={t('content.copyrights.heading')}
|
|
||||||
iconBack='ChevronRight'
|
|
||||||
/>
|
|
||||||
<Text style={[styles.version, { color: theme.secondary }]}>
|
<Text style={[styles.version, { color: theme.secondary }]}>
|
||||||
{t('content.version', { version: Constants.manifest.version })}
|
{t('content.version', { version: Constants.manifest.version })}
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -4,6 +4,7 @@ import { store } from '@root/store'
|
|||||||
import formatText from '@screens/Shared/Compose/formatText'
|
import formatText from '@screens/Shared/Compose/formatText'
|
||||||
import ComposeRoot from '@screens/Shared/Compose/Root'
|
import ComposeRoot from '@screens/Shared/Compose/Root'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
|
import { updateStoreReview } from '@utils/slices/contextsSlice'
|
||||||
import { getLocalAccount } from '@utils/slices/instancesSlice'
|
import { getLocalAccount } from '@utils/slices/instancesSlice'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
@ -19,6 +20,7 @@ import {
|
|||||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||||
import { useQueryClient } from 'react-query'
|
import { useQueryClient } from 'react-query'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
import ComposeEditAttachment from './Compose/EditAttachment'
|
import ComposeEditAttachment from './Compose/EditAttachment'
|
||||||
import ComposeContext from './Compose/utils/createContext'
|
import ComposeContext from './Compose/utils/createContext'
|
||||||
import composeInitialState from './Compose/utils/initialState'
|
import composeInitialState from './Compose/utils/initialState'
|
||||||
@ -161,6 +163,7 @@ const Compose: React.FC<SharedComposeProp> = ({
|
|||||||
),
|
),
|
||||||
[totalTextCount]
|
[totalTextCount]
|
||||||
)
|
)
|
||||||
|
const dispatch = useDispatch()
|
||||||
const headerRight = useCallback(
|
const headerRight = useCallback(
|
||||||
() => (
|
() => (
|
||||||
<HeaderRight
|
<HeaderRight
|
||||||
@ -172,6 +175,7 @@ const Compose: React.FC<SharedComposeProp> = ({
|
|||||||
composePost(params, composeState)
|
composePost(params, composeState)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
haptics('Success')
|
haptics('Success')
|
||||||
|
dispatch(updateStoreReview(1))
|
||||||
const queryKey: QueryKeyTimeline = [
|
const queryKey: QueryKeyTimeline = [
|
||||||
'Timeline',
|
'Timeline',
|
||||||
{ page: 'Following' }
|
{ page: 'Following' }
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
configureStore,
|
configureStore,
|
||||||
getDefaultMiddleware
|
getDefaultMiddleware
|
||||||
} from '@reduxjs/toolkit'
|
} from '@reduxjs/toolkit'
|
||||||
|
import contextsSlice from '@utils/slices/contextsSlice'
|
||||||
import instancesSlice from '@utils/slices/instancesSlice'
|
import instancesSlice from '@utils/slices/instancesSlice'
|
||||||
import settingsSlice from '@utils/slices/settingsSlice'
|
import settingsSlice from '@utils/slices/settingsSlice'
|
||||||
import { persistReducer, persistStore } from 'redux-persist'
|
import { persistReducer, persistStore } from 'redux-persist'
|
||||||
@ -13,6 +14,12 @@ const secureStorage = createSecureStore()
|
|||||||
|
|
||||||
const prefix = 'mastodon_app'
|
const prefix = 'mastodon_app'
|
||||||
|
|
||||||
|
const contextsPersistConfig = {
|
||||||
|
key: 'contexts',
|
||||||
|
prefix,
|
||||||
|
storage: AsyncStorage
|
||||||
|
}
|
||||||
|
|
||||||
const instancesPersistConfig = {
|
const instancesPersistConfig = {
|
||||||
key: 'instances',
|
key: 'instances',
|
||||||
prefix,
|
prefix,
|
||||||
@ -35,6 +42,7 @@ const rootPersistConfig = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
|
contexts: persistReducer(contextsPersistConfig, contextsSlice),
|
||||||
instances: persistReducer(instancesPersistConfig, instancesSlice),
|
instances: persistReducer(instancesPersistConfig, instancesSlice),
|
||||||
settings: persistReducer(settingsPersistConfig, settingsSlice)
|
settings: persistReducer(settingsPersistConfig, settingsSlice)
|
||||||
})
|
})
|
||||||
|
64
src/utils/slices/contextsSlice.ts
Normal file
64
src/utils/slices/contextsSlice.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
|
import { RootState } from '@root/store'
|
||||||
|
import * as StoreReview from 'expo-store-review'
|
||||||
|
|
||||||
|
export const supportedLngs = ['zh-Hans', 'en']
|
||||||
|
|
||||||
|
export type ContextsState = {
|
||||||
|
storeReview: {
|
||||||
|
context: Readonly<number>
|
||||||
|
current: number
|
||||||
|
shown: boolean
|
||||||
|
}
|
||||||
|
publicRemoteNotice: {
|
||||||
|
context: Readonly<number>
|
||||||
|
current: number
|
||||||
|
hidden: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const contextsInitialState = {
|
||||||
|
// After 3 successful postings
|
||||||
|
storeReview: {
|
||||||
|
context: 3,
|
||||||
|
current: 0,
|
||||||
|
shown: false
|
||||||
|
},
|
||||||
|
// After public remote settings has been used once
|
||||||
|
publicRemoteNotice: {
|
||||||
|
context: 1,
|
||||||
|
current: 0,
|
||||||
|
hidden: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextsSlice = createSlice({
|
||||||
|
name: 'settings',
|
||||||
|
initialState: contextsInitialState as ContextsState,
|
||||||
|
reducers: {
|
||||||
|
updateStoreReview: (state, action: PayloadAction<1>) => {
|
||||||
|
state.storeReview.current = state.storeReview.current + action.payload
|
||||||
|
if (state.storeReview.current === state.storeReview.context) {
|
||||||
|
StoreReview.isAvailableAsync().then(() => StoreReview.requestReview())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updatePublicRemoteNotice: (state, action: PayloadAction<1>) => {
|
||||||
|
state.publicRemoteNotice.current =
|
||||||
|
state.publicRemoteNotice.current + action.payload
|
||||||
|
if (
|
||||||
|
state.publicRemoteNotice.current === state.publicRemoteNotice.context
|
||||||
|
) {
|
||||||
|
state.publicRemoteNotice.hidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getPublicRemoteNotice = (state: RootState) =>
|
||||||
|
state.contexts.publicRemoteNotice
|
||||||
|
|
||||||
|
export const {
|
||||||
|
updateStoreReview,
|
||||||
|
updatePublicRemoteNotice
|
||||||
|
} = contextsSlice.actions
|
||||||
|
export default contextsSlice.reducer
|
@ -4260,6 +4260,11 @@ expo-status-bar@~1.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/expo-status-bar/-/expo-status-bar-1.0.3.tgz#62b4d6145680abd43ba6ecfa465f835e88bf6263"
|
resolved "https://registry.yarnpkg.com/expo-status-bar/-/expo-status-bar-1.0.3.tgz#62b4d6145680abd43ba6ecfa465f835e88bf6263"
|
||||||
integrity sha512-/Orgla1nkIrfswNHbuAOTbPVq0g3+GrhoQVk7MRafY2dwrFLgXhaPExS+eN2hpmzqPv2LG5cqAZDCQUAjmZYBQ==
|
integrity sha512-/Orgla1nkIrfswNHbuAOTbPVq0g3+GrhoQVk7MRafY2dwrFLgXhaPExS+eN2hpmzqPv2LG5cqAZDCQUAjmZYBQ==
|
||||||
|
|
||||||
|
expo-store-review@~2.3.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/expo-store-review/-/expo-store-review-2.3.0.tgz#16e0d9445fca61c50402e609417dccc058288247"
|
||||||
|
integrity sha512-bhwRW+eh2CdmscsVDST+PROgYAqn9NpvGeJBNQB5xOIntZq/O62pYFpMrOsdjkVSK21WPWuVLYY2dGnF/dzKvw==
|
||||||
|
|
||||||
expo-updates@~0.3.5:
|
expo-updates@~0.3.5:
|
||||||
version "0.3.5"
|
version "0.3.5"
|
||||||
resolved "https://registry.yarnpkg.com/expo-updates/-/expo-updates-0.3.5.tgz#cd9aafeb5cbe16399df7d39243d00d330d99e674"
|
resolved "https://registry.yarnpkg.com/expo-updates/-/expo-updates-0.3.5.tgz#cd9aafeb5cbe16399df7d39243d00d330d99e674"
|
||||||
|
Reference in New Issue
Block a user