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