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,24 +14,55 @@ 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!) | ||||||
|         }, |         }, | ||||||
|         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 { 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,26 +90,28 @@ 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 |                   <> | ||||||
|                     style={[ |                     <Text | ||||||
|                       styles.content, |                       style={[ | ||||||
|                       { |                         styles.content, | ||||||
|                         color: theme.secondary, |                         { | ||||||
|                         opacity: !iconBack && loading ? 0 : 1 |                           color: theme.secondary, | ||||||
|                       } |                           opacity: !iconBack && loading ? 0 : 1 | ||||||
|                     ]} |                         } | ||||||
|                     numberOfLines={1} |                       ]} | ||||||
|                   > |                       numberOfLines={1} | ||||||
|                     {content} |                     > | ||||||
|                   </Text> |                       {content} | ||||||
|                   {loading && !iconBack && loadingSpinkit} |                     </Text> | ||||||
|                 </> |                     {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