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-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,26 +14,57 @@ 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!) | ||||
|         }, | ||||
|         () => { | ||||
|           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) | ||||
|                   } | ||||
|                 ) | ||||
|               } | ||||
|             } | ||||
|           ) | ||||
|         } | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|   return { start, cancel } | ||||
| @@ -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) | ||||
|         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,11 +90,10 @@ const MenuRow: React.FC<Props> = ({ | ||||
|             </View> | ||||
|           </View> | ||||
|  | ||||
|           {(content && content.length) || | ||||
|           switchValue !== undefined || | ||||
|           iconBack ? ( | ||||
|           {content || switchValue !== undefined || iconBack ? ( | ||||
|             <View style={styles.back}> | ||||
|               {content && content.length ? ( | ||||
|               {content ? ( | ||||
|                 typeof content === 'string' ? ( | ||||
|                   <> | ||||
|                     <Text | ||||
|                       style={[ | ||||
| @@ -110,6 +109,9 @@ const MenuRow: React.FC<Props> = ({ | ||||
|                     </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" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user