1
0
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:
Zhiyuan Zheng
2021-01-18 00:23:40 +01:00
parent f977fdfa8b
commit 5c4f7ce8c7
20 changed files with 302 additions and 108 deletions

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

View File

@ -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",

View File

@ -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}
</> </>
) )
} }

View File

@ -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()
} }

View File

@ -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

View File

@ -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,

View File

@ -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' }

View File

@ -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}
/> />

View File

@ -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'
}, },

View File

@ -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
} }
}) })

View File

@ -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'

View File

@ -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: {

View File

@ -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

View File

@ -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}}'
} }
} }

View File

@ -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>

View File

@ -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' }

View File

@ -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)
}) })

View 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

View File

@ -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"