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,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"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user