mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	Partially fixed #113
This commit is contained in:
		
							
								
								
									
										4
									
								
								src/@types/mastodon.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								src/@types/mastodon.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -28,7 +28,7 @@ declare namespace Mastodon {
 | 
			
		||||
    moved?: Account
 | 
			
		||||
    fields: Field[]
 | 
			
		||||
    bot: boolean
 | 
			
		||||
    source: Source
 | 
			
		||||
    source?: Source
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  type Announcement = {
 | 
			
		||||
@@ -258,7 +258,7 @@ declare namespace Mastodon {
 | 
			
		||||
  type Field = {
 | 
			
		||||
    name: string
 | 
			
		||||
    value: string
 | 
			
		||||
    verified_at?: string
 | 
			
		||||
    verified_at: string | null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  type List = {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								src/@types/react-navigation.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								src/@types/react-navigation.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -132,9 +132,23 @@ declare namespace Nav {
 | 
			
		||||
      list: Mastodon.List['id']
 | 
			
		||||
      title: Mastodon.List['title']
 | 
			
		||||
    }
 | 
			
		||||
    'Tab-Me-Profile': undefined
 | 
			
		||||
    'Tab-Me-Push': undefined
 | 
			
		||||
    'Tab-Me-Settings': undefined
 | 
			
		||||
    'Tab-Me-Settings-Fontsize': undefined
 | 
			
		||||
    'Tab-Me-Settings-Push': undefined
 | 
			
		||||
    'Tab-Me-Switch': undefined
 | 
			
		||||
  } & TabSharedStackParamList
 | 
			
		||||
 | 
			
		||||
  type TabMeProfileStackParamList = {
 | 
			
		||||
    'Tab-Me-Profile-Root': undefined
 | 
			
		||||
    'Tab-Me-Profile-Name': {
 | 
			
		||||
      display_name: Mastodon.Account['display_name']
 | 
			
		||||
    }
 | 
			
		||||
    'Tab-Me-Profile-Note': {
 | 
			
		||||
      note: Mastodon.Source['note']
 | 
			
		||||
    }
 | 
			
		||||
    'Tab-Me-Profile-Fields': {
 | 
			
		||||
      fields?: Mastodon.Source['fields']
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -168,7 +168,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
 | 
			
		||||
            options={{
 | 
			
		||||
              stackPresentation: 'transparentModal',
 | 
			
		||||
              stackAnimation: 'fade',
 | 
			
		||||
              headerShown: false // Android
 | 
			
		||||
              headerShown: false
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
          <Stack.Screen
 | 
			
		||||
@@ -177,7 +177,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
 | 
			
		||||
            options={{
 | 
			
		||||
              stackPresentation: 'transparentModal',
 | 
			
		||||
              stackAnimation: 'fade',
 | 
			
		||||
              headerShown: false // Android
 | 
			
		||||
              headerShown: false
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
          <Stack.Screen
 | 
			
		||||
@@ -185,7 +185,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
 | 
			
		||||
            component={ScreenCompose}
 | 
			
		||||
            options={{
 | 
			
		||||
              stackPresentation: 'fullScreenModal',
 | 
			
		||||
              headerShown: false // Android
 | 
			
		||||
              headerShown: false
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
          <Stack.Screen
 | 
			
		||||
@@ -194,7 +194,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
 | 
			
		||||
            options={{
 | 
			
		||||
              stackPresentation: 'fullScreenModal',
 | 
			
		||||
              stackAnimation: 'fade',
 | 
			
		||||
              headerShown: false // Android
 | 
			
		||||
              headerShown: false
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </Stack.Navigator>
 | 
			
		||||
@@ -206,6 +206,3 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default React.memo(Screens, () => true)
 | 
			
		||||
function toast (arg0: { type: string; content: string; autoHide: boolean }) {
 | 
			
		||||
  throw new Error('Function not implemented.')
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ const ctx = new chalk.Instance({ level: 3 })
 | 
			
		||||
 | 
			
		||||
export type Params = {
 | 
			
		||||
  method: 'get' | 'post' | 'put' | 'delete'
 | 
			
		||||
  domain?: string
 | 
			
		||||
  domain: string
 | 
			
		||||
  url: string
 | 
			
		||||
  params?: {
 | 
			
		||||
    [key: string]: string | number | boolean | string[] | number[] | boolean[]
 | 
			
		||||
@@ -25,10 +25,6 @@ const apiGeneral = async <T = unknown>({
 | 
			
		||||
  body,
 | 
			
		||||
  sentry = false
 | 
			
		||||
}: Params): Promise<{ body: T }> => {
 | 
			
		||||
  if (!domain) {
 | 
			
		||||
    return Promise.reject()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  console.log(
 | 
			
		||||
    ctx.bgGreen.bold(' API general ') +
 | 
			
		||||
      ' ' +
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import li from 'li'
 | 
			
		||||
const ctx = new chalk.Instance({ level: 3 })
 | 
			
		||||
 | 
			
		||||
export type Params = {
 | 
			
		||||
  method: 'get' | 'post' | 'put' | 'delete'
 | 
			
		||||
  method: 'get' | 'post' | 'put' | 'delete' | 'patch'
 | 
			
		||||
  version?: 'v1' | 'v2'
 | 
			
		||||
  url: string
 | 
			
		||||
  params?: {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import Icon from '@components/Icon'
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import layoutAnimation from '@utils/styles/layoutAnimation'
 | 
			
		||||
import { useTheme } from '@utils/styles/ThemeManager'
 | 
			
		||||
import React, { useEffect, useMemo, useRef } from 'react'
 | 
			
		||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
 | 
			
		||||
import {
 | 
			
		||||
  AccessibilityProps,
 | 
			
		||||
  Pressable,
 | 
			
		||||
@@ -121,9 +121,6 @@ const Button: React.FC<Props> = ({
 | 
			
		||||
                color: mainColor,
 | 
			
		||||
                fontSize:
 | 
			
		||||
                  StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1),
 | 
			
		||||
                fontWeight: destructive
 | 
			
		||||
                  ? StyleConstants.Font.Weight.Bold
 | 
			
		||||
                  : undefined,
 | 
			
		||||
                opacity: loading ? 0 : 1
 | 
			
		||||
              }}
 | 
			
		||||
              children={content}
 | 
			
		||||
@@ -135,12 +132,7 @@ const Button: React.FC<Props> = ({
 | 
			
		||||
    }
 | 
			
		||||
  }, [mode, content, loading, disabled])
 | 
			
		||||
 | 
			
		||||
  enum spacingMapping {
 | 
			
		||||
    XS = 'S',
 | 
			
		||||
    S = 'M',
 | 
			
		||||
    M = 'L',
 | 
			
		||||
    L = 'XL'
 | 
			
		||||
  }
 | 
			
		||||
  const [layoutHeight, setLayoutHeight] = useState<number | undefined>()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Pressable
 | 
			
		||||
@@ -161,10 +153,15 @@ const Button: React.FC<Props> = ({
 | 
			
		||||
          backgroundColor: colorBackground,
 | 
			
		||||
          paddingVertical: StyleConstants.Spacing[spacing],
 | 
			
		||||
          paddingHorizontal:
 | 
			
		||||
            StyleConstants.Spacing[round ? spacing : spacingMapping[spacing]]
 | 
			
		||||
            StyleConstants.Spacing[spacing] + StyleConstants.Spacing.XS,
 | 
			
		||||
          width: round && layoutHeight ? layoutHeight : undefined
 | 
			
		||||
        },
 | 
			
		||||
        customStyle
 | 
			
		||||
      ]}
 | 
			
		||||
      {...(round && {
 | 
			
		||||
        onLayout: ({ nativeEvent }) =>
 | 
			
		||||
          setLayoutHeight(nativeEvent.layout.height)
 | 
			
		||||
      })}
 | 
			
		||||
      testID='base'
 | 
			
		||||
      onPress={onPress}
 | 
			
		||||
      children={children}
 | 
			
		||||
@@ -176,7 +173,6 @@ const Button: React.FC<Props> = ({
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  button: {
 | 
			
		||||
    borderRadius: 100,
 | 
			
		||||
    flexDirection: 'row',
 | 
			
		||||
    justifyContent: 'center',
 | 
			
		||||
    alignItems: 'center'
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										161
									
								
								src/components/Emojis.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								src/components/Emojis.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,161 @@
 | 
			
		||||
import EmojisButton from '@components/Emojis/Button'
 | 
			
		||||
import EmojisList from '@components/Emojis/List'
 | 
			
		||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
 | 
			
		||||
import { useEmojisQuery } from '@utils/queryHooks/emojis'
 | 
			
		||||
import { chunk, forEach, groupBy, sortBy } from 'lodash'
 | 
			
		||||
import React, {
 | 
			
		||||
  createContext,
 | 
			
		||||
  Dispatch,
 | 
			
		||||
  MutableRefObject,
 | 
			
		||||
  SetStateAction,
 | 
			
		||||
  useCallback,
 | 
			
		||||
  useEffect,
 | 
			
		||||
  useReducer
 | 
			
		||||
} from 'react'
 | 
			
		||||
import FastImage from 'react-native-fast-image'
 | 
			
		||||
 | 
			
		||||
type EmojisState = {
 | 
			
		||||
  enabled: boolean
 | 
			
		||||
  active: boolean
 | 
			
		||||
  emojis: { title: string; data: Mastodon.Emoji[][] }[]
 | 
			
		||||
  shortcode: Mastodon.Emoji['shortcode'] | null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type EmojisAction =
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'load'
 | 
			
		||||
      payload: NonNullable<EmojisState['emojis']>
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'activate'
 | 
			
		||||
      payload: EmojisState['active']
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'shortcode'
 | 
			
		||||
      payload: EmojisState['shortcode']
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
const emojisReducer = (state: EmojisState, action: EmojisAction) => {
 | 
			
		||||
  switch (action.type) {
 | 
			
		||||
    case 'activate':
 | 
			
		||||
      return { ...state, active: action.payload }
 | 
			
		||||
    case 'load':
 | 
			
		||||
      return { ...state, emojis: action.payload }
 | 
			
		||||
    case 'shortcode':
 | 
			
		||||
      return { ...state, shortcode: action.payload }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ContextType = {
 | 
			
		||||
  emojisState: EmojisState
 | 
			
		||||
  emojisDispatch: Dispatch<EmojisAction>
 | 
			
		||||
}
 | 
			
		||||
const EmojisContext = createContext<ContextType>({} as ContextType)
 | 
			
		||||
 | 
			
		||||
const prefetchEmojis = (
 | 
			
		||||
  sortedEmojis: { title: string; data: Mastodon.Emoji[][] }[],
 | 
			
		||||
  reduceMotionEnabled: boolean
 | 
			
		||||
) => {
 | 
			
		||||
  const prefetches: { uri: string }[] = []
 | 
			
		||||
  let requestedIndex = 0
 | 
			
		||||
  sortedEmojis.forEach(sorted => {
 | 
			
		||||
    sorted.data.forEach(emojis =>
 | 
			
		||||
      emojis.forEach(emoji => {
 | 
			
		||||
        if (requestedIndex > 40) {
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        prefetches.push({
 | 
			
		||||
          uri: reduceMotionEnabled ? emoji.static_url : emoji.url
 | 
			
		||||
        })
 | 
			
		||||
        requestedIndex++
 | 
			
		||||
      })
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
  try {
 | 
			
		||||
    FastImage.preload(prefetches)
 | 
			
		||||
  } catch {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  enabled?: boolean
 | 
			
		||||
  value?: string
 | 
			
		||||
  setValue:
 | 
			
		||||
    | Dispatch<SetStateAction<string | undefined>>
 | 
			
		||||
    | Dispatch<SetStateAction<string>>
 | 
			
		||||
  selectionRange: MutableRefObject<{
 | 
			
		||||
    start: number
 | 
			
		||||
    end: number
 | 
			
		||||
  }>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ComponentEmojis: React.FC<Props> = ({
 | 
			
		||||
  enabled = false,
 | 
			
		||||
  value,
 | 
			
		||||
  setValue,
 | 
			
		||||
  selectionRange,
 | 
			
		||||
  children
 | 
			
		||||
}) => {
 | 
			
		||||
  const { reduceMotionEnabled } = useAccessibility()
 | 
			
		||||
 | 
			
		||||
  const [emojisState, emojisDispatch] = useReducer(emojisReducer, {
 | 
			
		||||
    enabled,
 | 
			
		||||
    active: false,
 | 
			
		||||
    emojis: [],
 | 
			
		||||
    shortcode: null
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (emojisState.shortcode) {
 | 
			
		||||
      addEmoji(emojisState.shortcode)
 | 
			
		||||
      emojisDispatch({
 | 
			
		||||
        type: 'shortcode',
 | 
			
		||||
        payload: null
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }, [emojisState.shortcode])
 | 
			
		||||
 | 
			
		||||
  const addEmoji = useCallback(
 | 
			
		||||
    (emojiShortcode: string) => {
 | 
			
		||||
      console.log(selectionRange.current)
 | 
			
		||||
      if (value?.length) {
 | 
			
		||||
        const contentFront = value.slice(0, selectionRange.current?.start)
 | 
			
		||||
        const contentRear = value.slice(selectionRange.current?.end)
 | 
			
		||||
 | 
			
		||||
        const whiteSpaceRear = /\s/g.test(contentRear.slice(-1))
 | 
			
		||||
 | 
			
		||||
        const newTextWithSpace = ` ${emojiShortcode}${
 | 
			
		||||
          whiteSpaceRear ? '' : ' '
 | 
			
		||||
        }`
 | 
			
		||||
        setValue([contentFront, newTextWithSpace, contentRear].join(''))
 | 
			
		||||
      } else {
 | 
			
		||||
        setValue(`${emojiShortcode} `)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [value, selectionRange.current?.start, selectionRange.current?.end]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const { data } = useEmojisQuery({ options: { enabled } })
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (data && data.length) {
 | 
			
		||||
      let sortedEmojis: { title: string; data: Mastodon.Emoji[][] }[] = []
 | 
			
		||||
      forEach(
 | 
			
		||||
        groupBy(sortBy(data, ['category', 'shortcode']), 'category'),
 | 
			
		||||
        (value, key) => sortedEmojis.push({ title: key, data: chunk(value, 5) })
 | 
			
		||||
      )
 | 
			
		||||
      emojisDispatch({
 | 
			
		||||
        type: 'load',
 | 
			
		||||
        payload: sortedEmojis
 | 
			
		||||
      })
 | 
			
		||||
      prefetchEmojis(sortedEmojis, reduceMotionEnabled)
 | 
			
		||||
    }
 | 
			
		||||
  }, [data, reduceMotionEnabled])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <EmojisContext.Provider
 | 
			
		||||
      value={{ emojisState, emojisDispatch }}
 | 
			
		||||
      children={children}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { ComponentEmojis, EmojisContext, EmojisButton, EmojisList }
 | 
			
		||||
							
								
								
									
										50
									
								
								src/components/Emojis/Button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/components/Emojis/Button.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
			
		||||
import { EmojisContext } from '@components/Emojis'
 | 
			
		||||
import Icon from '@components/Icon'
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import { useTheme } from '@utils/styles/ThemeManager'
 | 
			
		||||
import React, { useContext } from 'react'
 | 
			
		||||
import { Pressable, StyleSheet } from 'react-native'
 | 
			
		||||
 | 
			
		||||
const EmojisButton = React.memo(
 | 
			
		||||
  () => {
 | 
			
		||||
    const { theme } = useTheme()
 | 
			
		||||
    const { emojisState, emojisDispatch } = useContext(EmojisContext)
 | 
			
		||||
 | 
			
		||||
    return emojisState.enabled ? (
 | 
			
		||||
      <Pressable
 | 
			
		||||
        disabled={!emojisState.emojis || !emojisState.emojis.length}
 | 
			
		||||
        onPress={() =>
 | 
			
		||||
          emojisDispatch({ type: 'activate', payload: !emojisState.active })
 | 
			
		||||
        }
 | 
			
		||||
        hitSlop={StyleConstants.Spacing.S}
 | 
			
		||||
        style={styles.base}
 | 
			
		||||
        children={
 | 
			
		||||
          <Icon
 | 
			
		||||
            name={
 | 
			
		||||
              emojisState.emojis && emojisState.emojis.length
 | 
			
		||||
                ? emojisState.active
 | 
			
		||||
                  ? 'Type'
 | 
			
		||||
                  : 'Smile'
 | 
			
		||||
                : 'Meh'
 | 
			
		||||
            }
 | 
			
		||||
            size={StyleConstants.Font.Size.L}
 | 
			
		||||
            color={
 | 
			
		||||
              emojisState.emojis && emojisState.emojis.length
 | 
			
		||||
                ? theme.primaryDefault
 | 
			
		||||
                : theme.disabled
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
        }
 | 
			
		||||
      />
 | 
			
		||||
    ) : null
 | 
			
		||||
  },
 | 
			
		||||
  () => true
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  base: {
 | 
			
		||||
    paddingLeft: StyleConstants.Spacing.S
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default EmojisButton
 | 
			
		||||
							
								
								
									
										122
									
								
								src/components/Emojis/List.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								src/components/Emojis/List.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,122 @@
 | 
			
		||||
import { EmojisContext } from '@components/Emojis'
 | 
			
		||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import layoutAnimation from '@utils/styles/layoutAnimation'
 | 
			
		||||
import { useTheme } from '@utils/styles/ThemeManager'
 | 
			
		||||
import React, { useCallback, useContext, useEffect, useRef } from 'react'
 | 
			
		||||
import { useTranslation } from 'react-i18next'
 | 
			
		||||
import {
 | 
			
		||||
  AccessibilityInfo,
 | 
			
		||||
  findNodeHandle,
 | 
			
		||||
  Pressable,
 | 
			
		||||
  SectionList,
 | 
			
		||||
  StyleSheet,
 | 
			
		||||
  Text,
 | 
			
		||||
  View
 | 
			
		||||
} from 'react-native'
 | 
			
		||||
import FastImage from 'react-native-fast-image'
 | 
			
		||||
import validUrl from 'valid-url'
 | 
			
		||||
 | 
			
		||||
const EmojisList = React.memo(
 | 
			
		||||
  () => {
 | 
			
		||||
    const { reduceMotionEnabled } = useAccessibility()
 | 
			
		||||
    const { t } = useTranslation()
 | 
			
		||||
 | 
			
		||||
    const { emojisState, emojisDispatch } = useContext(EmojisContext)
 | 
			
		||||
    const { theme } = useTheme()
 | 
			
		||||
 | 
			
		||||
    const listHeader = useCallback(
 | 
			
		||||
      ({ section: { title } }) => (
 | 
			
		||||
        <Text style={[styles.group, { color: theme.secondary }]}>{title}</Text>
 | 
			
		||||
      ),
 | 
			
		||||
      []
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    const listItem = useCallback(
 | 
			
		||||
      ({ index, item }: { item: Mastodon.Emoji[]; index: number }) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <View key={index} style={styles.emojis}>
 | 
			
		||||
            {item.map(emoji => {
 | 
			
		||||
              const uri = reduceMotionEnabled ? emoji.static_url : emoji.url
 | 
			
		||||
              if (validUrl.isHttpsUri(uri)) {
 | 
			
		||||
                return (
 | 
			
		||||
                  <Pressable
 | 
			
		||||
                    key={emoji.shortcode}
 | 
			
		||||
                    onPress={() =>
 | 
			
		||||
                      emojisDispatch({
 | 
			
		||||
                        type: 'shortcode',
 | 
			
		||||
                        payload: `:${emoji.shortcode}:`
 | 
			
		||||
                      })
 | 
			
		||||
                    }
 | 
			
		||||
                  >
 | 
			
		||||
                    <FastImage
 | 
			
		||||
                      accessibilityLabel={t(
 | 
			
		||||
                        'common:customEmoji.accessibilityLabel',
 | 
			
		||||
                        {
 | 
			
		||||
                          emoji: emoji.shortcode
 | 
			
		||||
                        }
 | 
			
		||||
                      )}
 | 
			
		||||
                      accessibilityHint={t(
 | 
			
		||||
                        'screenCompose:content.root.footer.emojis.accessibilityHint'
 | 
			
		||||
                      )}
 | 
			
		||||
                      source={{ uri }}
 | 
			
		||||
                      style={styles.emoji}
 | 
			
		||||
                    />
 | 
			
		||||
                  </Pressable>
 | 
			
		||||
                )
 | 
			
		||||
              } else {
 | 
			
		||||
                return null
 | 
			
		||||
              }
 | 
			
		||||
            })}
 | 
			
		||||
          </View>
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      []
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    const listRef = useRef<SectionList>(null)
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
      layoutAnimation()
 | 
			
		||||
      const tagEmojis = findNodeHandle(listRef.current)
 | 
			
		||||
      if (emojisState.active) {
 | 
			
		||||
        tagEmojis && AccessibilityInfo.setAccessibilityFocus(tagEmojis)
 | 
			
		||||
      }
 | 
			
		||||
    }, [emojisState.active])
 | 
			
		||||
 | 
			
		||||
    return emojisState.active ? (
 | 
			
		||||
      <SectionList
 | 
			
		||||
        accessible
 | 
			
		||||
        ref={listRef}
 | 
			
		||||
        horizontal
 | 
			
		||||
        keyboardShouldPersistTaps='always'
 | 
			
		||||
        sections={emojisState.emojis}
 | 
			
		||||
        keyExtractor={item => item[0].shortcode}
 | 
			
		||||
        renderSectionHeader={listHeader}
 | 
			
		||||
        renderItem={listItem}
 | 
			
		||||
        windowSize={4}
 | 
			
		||||
      />
 | 
			
		||||
    ) : null
 | 
			
		||||
  },
 | 
			
		||||
  () => true
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  group: {
 | 
			
		||||
    position: 'absolute',
 | 
			
		||||
    ...StyleConstants.FontStyle.S
 | 
			
		||||
  },
 | 
			
		||||
  emojis: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    flexWrap: 'wrap',
 | 
			
		||||
    marginTop: StyleConstants.Spacing.M,
 | 
			
		||||
    marginRight: StyleConstants.Spacing.S
 | 
			
		||||
  },
 | 
			
		||||
  emoji: {
 | 
			
		||||
    width: 32,
 | 
			
		||||
    height: 32,
 | 
			
		||||
    padding: StyleConstants.Spacing.S,
 | 
			
		||||
    margin: StyleConstants.Spacing.S
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default EmojisList
 | 
			
		||||
							
								
								
									
										163
									
								
								src/components/Input.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								src/components/Input.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,163 @@
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import layoutAnimation from '@utils/styles/layoutAnimation'
 | 
			
		||||
import { useTheme } from '@utils/styles/ThemeManager'
 | 
			
		||||
import React, {
 | 
			
		||||
  Dispatch,
 | 
			
		||||
  SetStateAction,
 | 
			
		||||
  useCallback,
 | 
			
		||||
  useEffect,
 | 
			
		||||
  useRef,
 | 
			
		||||
  useState
 | 
			
		||||
} from 'react'
 | 
			
		||||
import { Platform, StyleSheet, Text, TextInput, View } from 'react-native'
 | 
			
		||||
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
 | 
			
		||||
import {
 | 
			
		||||
  ComponentEmojis,
 | 
			
		||||
  EmojisButton,
 | 
			
		||||
  EmojisContext,
 | 
			
		||||
  EmojisList
 | 
			
		||||
} from './Emojis'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  autoFocus?: boolean
 | 
			
		||||
 | 
			
		||||
  title?: string
 | 
			
		||||
 | 
			
		||||
  maxLength?: number
 | 
			
		||||
  multiline?: boolean
 | 
			
		||||
 | 
			
		||||
  emoji?: boolean
 | 
			
		||||
 | 
			
		||||
  value?: string
 | 
			
		||||
  setValue:
 | 
			
		||||
    | Dispatch<SetStateAction<string | undefined>>
 | 
			
		||||
    | Dispatch<SetStateAction<string>>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Input: React.FC<Props> = ({
 | 
			
		||||
  autoFocus = true,
 | 
			
		||||
  title,
 | 
			
		||||
  maxLength,
 | 
			
		||||
  multiline = false,
 | 
			
		||||
  emoji = false,
 | 
			
		||||
  value,
 | 
			
		||||
  setValue
 | 
			
		||||
}) => {
 | 
			
		||||
  const { mode, theme } = useTheme()
 | 
			
		||||
 | 
			
		||||
  const animateTitle = useAnimatedStyle(() => {
 | 
			
		||||
    if (value) {
 | 
			
		||||
      return {
 | 
			
		||||
        fontSize: withTiming(StyleConstants.Font.Size.S),
 | 
			
		||||
        paddingHorizontal: withTiming(StyleConstants.Spacing.XS),
 | 
			
		||||
        left: withTiming(StyleConstants.Spacing.S),
 | 
			
		||||
        top: withTiming(-(StyleConstants.Font.Size.S / 2) - 2),
 | 
			
		||||
        backgroundColor: withTiming(theme.backgroundDefault)
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      return {
 | 
			
		||||
        fontSize: withTiming(StyleConstants.Font.Size.M),
 | 
			
		||||
        paddingHorizontal: withTiming(0),
 | 
			
		||||
        left: withTiming(StyleConstants.Spacing.S),
 | 
			
		||||
        top: withTiming(StyleConstants.Spacing.S + 1),
 | 
			
		||||
        backgroundColor: withTiming(theme.backgroundDefaultTransparent)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [mode, value])
 | 
			
		||||
 | 
			
		||||
  const selectionRange = useRef<{ start: number; end: number }>(
 | 
			
		||||
    value
 | 
			
		||||
      ? {
 | 
			
		||||
          start: value.length,
 | 
			
		||||
          end: value.length
 | 
			
		||||
        }
 | 
			
		||||
      : { start: 0, end: 0 }
 | 
			
		||||
  )
 | 
			
		||||
  const onSelectionChange = useCallback(
 | 
			
		||||
    ({ nativeEvent: { selection } }) => (selectionRange.current = selection),
 | 
			
		||||
    []
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const [inputFocused, setInputFocused] = useState(false)
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    layoutAnimation()
 | 
			
		||||
  }, [inputFocused])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ComponentEmojis
 | 
			
		||||
      enabled={emoji}
 | 
			
		||||
      value={value}
 | 
			
		||||
      setValue={setValue}
 | 
			
		||||
      selectionRange={selectionRange}
 | 
			
		||||
    >
 | 
			
		||||
      <View style={[styles.base, { borderColor: theme.border }]}>
 | 
			
		||||
        <EmojisContext.Consumer>
 | 
			
		||||
          {({ emojisDispatch }) => (
 | 
			
		||||
            <TextInput
 | 
			
		||||
              autoFocus={autoFocus}
 | 
			
		||||
              onFocus={() => setInputFocused(true)}
 | 
			
		||||
              onBlur={() => {
 | 
			
		||||
                setInputFocused(false)
 | 
			
		||||
                emojisDispatch({ type: 'activate', payload: false })
 | 
			
		||||
              }}
 | 
			
		||||
              style={[
 | 
			
		||||
                styles.textInput,
 | 
			
		||||
                {
 | 
			
		||||
                  color: theme.primaryDefault,
 | 
			
		||||
                  minHeight:
 | 
			
		||||
                    Platform.OS === 'ios' && multiline
 | 
			
		||||
                      ? StyleConstants.Font.LineHeight.M * 5
 | 
			
		||||
                      : undefined
 | 
			
		||||
                }
 | 
			
		||||
              ]}
 | 
			
		||||
              onChangeText={setValue}
 | 
			
		||||
              onSelectionChange={onSelectionChange}
 | 
			
		||||
              value={value}
 | 
			
		||||
              maxLength={maxLength}
 | 
			
		||||
              {...(multiline && {
 | 
			
		||||
                multiline,
 | 
			
		||||
                numberOfLines: Platform.OS === 'android' ? 5 : undefined
 | 
			
		||||
              })}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
        </EmojisContext.Consumer>
 | 
			
		||||
        {title ? (
 | 
			
		||||
          <Animated.Text
 | 
			
		||||
            style={[styles.title, animateTitle, { color: theme.secondary }]}
 | 
			
		||||
          >
 | 
			
		||||
            {title}
 | 
			
		||||
          </Animated.Text>
 | 
			
		||||
        ) : null}
 | 
			
		||||
        {maxLength && value?.length ? (
 | 
			
		||||
          <Text style={[styles.maxLength, { color: theme.secondary }]}>
 | 
			
		||||
            {value?.length} / {maxLength}
 | 
			
		||||
          </Text>
 | 
			
		||||
        ) : null}
 | 
			
		||||
        {inputFocused ? <EmojisButton /> : null}
 | 
			
		||||
      </View>
 | 
			
		||||
      <EmojisList />
 | 
			
		||||
    </ComponentEmojis>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  base: {
 | 
			
		||||
    flexDirection: 'row',
 | 
			
		||||
    alignItems: 'flex-end',
 | 
			
		||||
    borderWidth: 1,
 | 
			
		||||
    marginVertical: StyleConstants.Spacing.S,
 | 
			
		||||
    padding: StyleConstants.Spacing.S
 | 
			
		||||
  },
 | 
			
		||||
  title: {
 | 
			
		||||
    position: 'absolute'
 | 
			
		||||
  },
 | 
			
		||||
  textInput: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    fontSize: StyleConstants.Font.Size.M
 | 
			
		||||
  },
 | 
			
		||||
  maxLength: {
 | 
			
		||||
    ...StyleConstants.FontStyle.S
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default Input
 | 
			
		||||
@@ -7,16 +7,13 @@ export interface Props {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const MenuContainer: React.FC<Props> = ({ children }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <View style={styles.base}>
 | 
			
		||||
      {children}
 | 
			
		||||
    </View>
 | 
			
		||||
  )
 | 
			
		||||
  return <View style={styles.base}>{children}</View>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  base: {
 | 
			
		||||
    marginBottom: StyleConstants.Spacing.L
 | 
			
		||||
    paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
 | 
			
		||||
    marginBottom: StyleConstants.Spacing.Global.PagePadding
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -19,8 +19,6 @@ const MenuHeader: React.FC<Props> = ({ heading }) => {
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  base: {
 | 
			
		||||
    paddingLeft: StyleConstants.Spacing.Global.PagePadding,
 | 
			
		||||
    paddingRight: StyleConstants.Spacing.Global.PagePadding,
 | 
			
		||||
    paddingBottom: StyleConstants.Spacing.S
 | 
			
		||||
  },
 | 
			
		||||
  text: {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ export interface Props {
 | 
			
		||||
  title: string
 | 
			
		||||
  description?: string
 | 
			
		||||
  content?: string | React.ReactNode
 | 
			
		||||
  badge?: boolean
 | 
			
		||||
 | 
			
		||||
  switchValue?: boolean
 | 
			
		||||
  switchDisabled?: boolean
 | 
			
		||||
@@ -33,6 +34,7 @@ const MenuRow: React.FC<Props> = ({
 | 
			
		||||
  title,
 | 
			
		||||
  description,
 | 
			
		||||
  content,
 | 
			
		||||
  badge = false,
 | 
			
		||||
  switchValue,
 | 
			
		||||
  switchDisabled,
 | 
			
		||||
  switchOnValueChange,
 | 
			
		||||
@@ -84,6 +86,17 @@ const MenuRow: React.FC<Props> = ({
 | 
			
		||||
                style={styles.iconFront}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            {badge ? (
 | 
			
		||||
              <View
 | 
			
		||||
                style={{
 | 
			
		||||
                  width: 8,
 | 
			
		||||
                  height: 8,
 | 
			
		||||
                  backgroundColor: theme.red,
 | 
			
		||||
                  borderRadius: 8,
 | 
			
		||||
                  marginRight: StyleConstants.Spacing.S
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            ) : null}
 | 
			
		||||
            <View style={styles.main}>
 | 
			
		||||
              <Text
 | 
			
		||||
                style={[styles.title, { color: theme.primaryDefault }]}
 | 
			
		||||
@@ -147,12 +160,12 @@ const MenuRow: React.FC<Props> = ({
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  base: {
 | 
			
		||||
    minHeight: 50
 | 
			
		||||
    minHeight: 46,
 | 
			
		||||
    paddingVertical: StyleConstants.Spacing.S
 | 
			
		||||
  },
 | 
			
		||||
  core: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    flexDirection: 'row',
 | 
			
		||||
    paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
 | 
			
		||||
    flexDirection: 'row'
 | 
			
		||||
  },
 | 
			
		||||
  front: {
 | 
			
		||||
    flex: 2,
 | 
			
		||||
@@ -167,7 +180,7 @@ const styles = StyleSheet.create({
 | 
			
		||||
    marginLeft: StyleConstants.Spacing.M
 | 
			
		||||
  },
 | 
			
		||||
  iconFront: {
 | 
			
		||||
    marginRight: 8
 | 
			
		||||
    marginRight: StyleConstants.Spacing.S
 | 
			
		||||
  },
 | 
			
		||||
  main: {
 | 
			
		||||
    flex: 1
 | 
			
		||||
@@ -176,9 +189,7 @@ const styles = StyleSheet.create({
 | 
			
		||||
    ...StyleConstants.FontStyle.M
 | 
			
		||||
  },
 | 
			
		||||
  description: {
 | 
			
		||||
    ...StyleConstants.FontStyle.S,
 | 
			
		||||
    marginTop: StyleConstants.Spacing.XS,
 | 
			
		||||
    paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
 | 
			
		||||
    ...StyleConstants.FontStyle.S
 | 
			
		||||
  },
 | 
			
		||||
  content: {
 | 
			
		||||
    ...StyleConstants.FontStyle.M
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import Icon from '@components/Icon'
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import { useTheme } from '@utils/styles/ThemeManager'
 | 
			
		||||
import { getTheme } from '@utils/styles/themes'
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import React, { RefObject } from 'react'
 | 
			
		||||
import { AccessibilityInfo } from 'react-native'
 | 
			
		||||
import FlashMessage, {
 | 
			
		||||
  hideMessage,
 | 
			
		||||
@@ -11,6 +11,7 @@ import FlashMessage, {
 | 
			
		||||
import haptics from './haptics'
 | 
			
		||||
 | 
			
		||||
const displayMessage = ({
 | 
			
		||||
  ref,
 | 
			
		||||
  duration = 'short',
 | 
			
		||||
  autoHide = true,
 | 
			
		||||
  message,
 | 
			
		||||
@@ -20,6 +21,7 @@ const displayMessage = ({
 | 
			
		||||
  type
 | 
			
		||||
}:
 | 
			
		||||
  | {
 | 
			
		||||
      ref?: RefObject<FlashMessage>
 | 
			
		||||
      duration?: 'short' | 'long'
 | 
			
		||||
      autoHide?: boolean
 | 
			
		||||
      message: string
 | 
			
		||||
@@ -29,6 +31,7 @@ const displayMessage = ({
 | 
			
		||||
      type?: undefined
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      ref?: RefObject<FlashMessage>
 | 
			
		||||
      duration?: 'short' | 'long'
 | 
			
		||||
      autoHide?: boolean
 | 
			
		||||
      message: string
 | 
			
		||||
@@ -54,63 +57,88 @@ const displayMessage = ({
 | 
			
		||||
    haptics('Error')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  showMessage({
 | 
			
		||||
    duration: type === 'error' ? 5000 : duration === 'short' ? 1500 : 3000,
 | 
			
		||||
    autoHide,
 | 
			
		||||
    message,
 | 
			
		||||
    description,
 | 
			
		||||
    onPress,
 | 
			
		||||
    ...(mode &&
 | 
			
		||||
      type && {
 | 
			
		||||
        renderFlashMessageIcon: () => {
 | 
			
		||||
          return (
 | 
			
		||||
            <Icon
 | 
			
		||||
              name={iconMapping[type]}
 | 
			
		||||
              size={StyleConstants.Font.LineHeight.M}
 | 
			
		||||
              color={getTheme(mode)[colorMapping[type]]}
 | 
			
		||||
              style={{ marginRight: StyleConstants.Spacing.S }}
 | 
			
		||||
            />
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
  })
 | 
			
		||||
  if (ref) {
 | 
			
		||||
    ref.current?.showMessage({
 | 
			
		||||
      duration: type === 'error' ? 5000 : duration === 'short' ? 1500 : 3000,
 | 
			
		||||
      autoHide,
 | 
			
		||||
      message,
 | 
			
		||||
      description,
 | 
			
		||||
      onPress,
 | 
			
		||||
      ...(mode &&
 | 
			
		||||
        type && {
 | 
			
		||||
          renderFlashMessageIcon: () => {
 | 
			
		||||
            return (
 | 
			
		||||
              <Icon
 | 
			
		||||
                name={iconMapping[type]}
 | 
			
		||||
                size={StyleConstants.Font.LineHeight.M}
 | 
			
		||||
                color={getTheme(mode)[colorMapping[type]]}
 | 
			
		||||
                style={{ marginRight: StyleConstants.Spacing.S }}
 | 
			
		||||
              />
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
    })
 | 
			
		||||
  } else {
 | 
			
		||||
    showMessage({
 | 
			
		||||
      duration: type === 'error' ? 5000 : duration === 'short' ? 1500 : 3000,
 | 
			
		||||
      autoHide,
 | 
			
		||||
      message,
 | 
			
		||||
      description,
 | 
			
		||||
      onPress,
 | 
			
		||||
      ...(mode &&
 | 
			
		||||
        type && {
 | 
			
		||||
          renderFlashMessageIcon: () => {
 | 
			
		||||
            return (
 | 
			
		||||
              <Icon
 | 
			
		||||
                name={iconMapping[type]}
 | 
			
		||||
                size={StyleConstants.Font.LineHeight.M}
 | 
			
		||||
                color={getTheme(mode)[colorMapping[type]]}
 | 
			
		||||
                style={{ marginRight: StyleConstants.Spacing.S }}
 | 
			
		||||
              />
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const removeMessage = () => {
 | 
			
		||||
  // if (ref) {
 | 
			
		||||
  //   ref.current?.hideMessage()
 | 
			
		||||
  // } else {
 | 
			
		||||
  hideMessage()
 | 
			
		||||
  // }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Message = React.memo(
 | 
			
		||||
  () => {
 | 
			
		||||
    const { mode, theme } = useTheme()
 | 
			
		||||
const Message = React.forwardRef<FlashMessage>((_, ref) => {
 | 
			
		||||
  const { mode, theme } = useTheme()
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <FlashMessage
 | 
			
		||||
        icon='auto'
 | 
			
		||||
        position='top'
 | 
			
		||||
        floating
 | 
			
		||||
        style={{
 | 
			
		||||
          backgroundColor: theme.backgroundDefault,
 | 
			
		||||
          shadowColor: theme.primaryDefault,
 | 
			
		||||
          shadowOffset: { width: 0, height: 0 },
 | 
			
		||||
          shadowOpacity: mode === 'light' ? 0.16 : 0.24,
 | 
			
		||||
          shadowRadius: 4
 | 
			
		||||
        }}
 | 
			
		||||
        titleStyle={{
 | 
			
		||||
          color: theme.primaryDefault,
 | 
			
		||||
          ...StyleConstants.FontStyle.M,
 | 
			
		||||
          fontWeight: StyleConstants.Font.Weight.Bold
 | 
			
		||||
        }}
 | 
			
		||||
        textStyle={{
 | 
			
		||||
          color: theme.primaryDefault,
 | 
			
		||||
          ...StyleConstants.FontStyle.S
 | 
			
		||||
        }}
 | 
			
		||||
        // @ts-ignore
 | 
			
		||||
        textProps={{ numberOfLines: 2 }}
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
  },
 | 
			
		||||
  () => true
 | 
			
		||||
)
 | 
			
		||||
  return (
 | 
			
		||||
    <FlashMessage
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      icon='auto'
 | 
			
		||||
      position='top'
 | 
			
		||||
      floating
 | 
			
		||||
      style={{
 | 
			
		||||
        backgroundColor: theme.backgroundDefault,
 | 
			
		||||
        shadowColor: theme.primaryDefault,
 | 
			
		||||
        shadowOffset: { width: 0, height: 0 },
 | 
			
		||||
        shadowOpacity: mode === 'light' ? 0.16 : 0.24,
 | 
			
		||||
        shadowRadius: 4
 | 
			
		||||
      }}
 | 
			
		||||
      titleStyle={{
 | 
			
		||||
        color: theme.primaryDefault,
 | 
			
		||||
        ...StyleConstants.FontStyle.M,
 | 
			
		||||
        fontWeight: StyleConstants.Font.Weight.Bold
 | 
			
		||||
      }}
 | 
			
		||||
      textStyle={{
 | 
			
		||||
        color: theme.primaryDefault,
 | 
			
		||||
        ...StyleConstants.FontStyle.S
 | 
			
		||||
      }}
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      textProps={{ numberOfLines: 2 }}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export { Message, displayMessage, removeMessage }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										133
									
								
								src/components/mediaSelector.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/components/mediaSelector.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,133 @@
 | 
			
		||||
import * as ImagePicker from 'expo-image-picker'
 | 
			
		||||
import { Alert, Linking } from 'react-native'
 | 
			
		||||
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
 | 
			
		||||
import i18next from 'i18next'
 | 
			
		||||
import analytics from '@components/analytics'
 | 
			
		||||
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  mediaTypes?: ImagePicker.MediaTypeOptions
 | 
			
		||||
  uploader: (imageInfo: ImageInfo) => void
 | 
			
		||||
  showActionSheetWithOptions: (
 | 
			
		||||
    options: ActionSheetOptions,
 | 
			
		||||
    callback: (i: number) => void
 | 
			
		||||
  ) => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const mediaSelector = async ({
 | 
			
		||||
  mediaTypes = ImagePicker.MediaTypeOptions.All,
 | 
			
		||||
  uploader,
 | 
			
		||||
  showActionSheetWithOptions
 | 
			
		||||
}: Props): Promise<any> => {
 | 
			
		||||
  showActionSheetWithOptions(
 | 
			
		||||
    {
 | 
			
		||||
      title: i18next.t('componentMediaSelector:title'),
 | 
			
		||||
      options: [
 | 
			
		||||
        i18next.t('componentMediaSelector:options.library'),
 | 
			
		||||
        i18next.t('componentMediaSelector:options.photo'),
 | 
			
		||||
        i18next.t('componentMediaSelector:options.cancel')
 | 
			
		||||
      ],
 | 
			
		||||
      cancelButtonIndex: 2
 | 
			
		||||
    },
 | 
			
		||||
    async buttonIndex => {
 | 
			
		||||
      if (buttonIndex === 0) {
 | 
			
		||||
        const {
 | 
			
		||||
          status
 | 
			
		||||
        } = await ImagePicker.requestMediaLibraryPermissionsAsync()
 | 
			
		||||
        if (status !== 'granted') {
 | 
			
		||||
          Alert.alert(
 | 
			
		||||
            i18next.t('componentMediaSelector:library.alert.title'),
 | 
			
		||||
            i18next.t('componentMediaSelector:library.alert.message'),
 | 
			
		||||
            [
 | 
			
		||||
              {
 | 
			
		||||
                text: i18next.t(
 | 
			
		||||
                  'componentMediaSelector:library.alert.buttons.cancel'
 | 
			
		||||
                ),
 | 
			
		||||
                style: 'cancel',
 | 
			
		||||
                onPress: () =>
 | 
			
		||||
                  analytics('mediaSelector_nopermission', { action: 'cancel' })
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                text: i18next.t(
 | 
			
		||||
                  'componentMediaSelector:library.alert.buttons.settings'
 | 
			
		||||
                ),
 | 
			
		||||
                style: 'default',
 | 
			
		||||
                onPress: () => {
 | 
			
		||||
                  analytics('mediaSelector_nopermission', {
 | 
			
		||||
                    action: 'settings'
 | 
			
		||||
                  })
 | 
			
		||||
                  Linking.openURL('app-settings:')
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            ]
 | 
			
		||||
          )
 | 
			
		||||
        } else {
 | 
			
		||||
          const result = await ImagePicker.launchImageLibraryAsync({
 | 
			
		||||
            mediaTypes,
 | 
			
		||||
            exif: false
 | 
			
		||||
          })
 | 
			
		||||
 | 
			
		||||
          if (!result.cancelled) {
 | 
			
		||||
            // https://github.com/expo/expo/issues/11214
 | 
			
		||||
            const fixResult = {
 | 
			
		||||
              ...result,
 | 
			
		||||
              uri: result.uri.replace('file:/data', 'file:///data')
 | 
			
		||||
            }
 | 
			
		||||
            uploader(fixResult)
 | 
			
		||||
            return
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } else if (buttonIndex === 1) {
 | 
			
		||||
        const { status } = await ImagePicker.requestCameraPermissionsAsync()
 | 
			
		||||
        if (status !== 'granted') {
 | 
			
		||||
          Alert.alert(
 | 
			
		||||
            i18next.t('componentMediaSelector:photo.alert.title'),
 | 
			
		||||
            i18next.t('componentMediaSelector:photo.alert.message'),
 | 
			
		||||
            [
 | 
			
		||||
              {
 | 
			
		||||
                text: i18next.t(
 | 
			
		||||
                  'componentMediaSelector:photo.alert.buttons.cancel'
 | 
			
		||||
                ),
 | 
			
		||||
                style: 'cancel',
 | 
			
		||||
                onPress: () => {
 | 
			
		||||
                  analytics('compose_addattachment_camera_nopermission', {
 | 
			
		||||
                    action: 'cancel'
 | 
			
		||||
                  })
 | 
			
		||||
                }
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                text: i18next.t(
 | 
			
		||||
                  'componentMediaSelector:photo.alert.buttons.settings'
 | 
			
		||||
                ),
 | 
			
		||||
                style: 'default',
 | 
			
		||||
                onPress: () => {
 | 
			
		||||
                  analytics('compose_addattachment_camera_nopermission', {
 | 
			
		||||
                    action: 'settings'
 | 
			
		||||
                  })
 | 
			
		||||
                  Linking.openURL('app-settings:')
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            ]
 | 
			
		||||
          )
 | 
			
		||||
        } else {
 | 
			
		||||
          const result = await ImagePicker.launchCameraAsync({
 | 
			
		||||
            mediaTypes,
 | 
			
		||||
            exif: false
 | 
			
		||||
          })
 | 
			
		||||
 | 
			
		||||
          if (!result.cancelled) {
 | 
			
		||||
            // https://github.com/expo/expo/issues/11214
 | 
			
		||||
            const fixResult = {
 | 
			
		||||
              ...result,
 | 
			
		||||
              uri: result.uri.replace('file:/data', 'file:///data')
 | 
			
		||||
            }
 | 
			
		||||
            uploader(fixResult)
 | 
			
		||||
            return
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default mediaSelector
 | 
			
		||||
@@ -9,6 +9,7 @@ export default {
 | 
			
		||||
  screenTabs: require('./screens/tabs'),
 | 
			
		||||
 | 
			
		||||
  componentInstance: require('./components/instance'),
 | 
			
		||||
  componentMediaSelector: require('./components/mediaSelector'),
 | 
			
		||||
  componentParse: require('./components/parse'),
 | 
			
		||||
  componentRelationship: require('./components/relationship'),
 | 
			
		||||
  componentRelativeTime: require('./components/relativeTime'),
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								src/i18n/en/components/mediaSelector.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/i18n/en/components/mediaSelector.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
{
 | 
			
		||||
  "title": "Select media source",
 | 
			
		||||
  "options": {
 | 
			
		||||
    "library": "Upload from library",
 | 
			
		||||
    "photo": "Take a photo",
 | 
			
		||||
    "cancel": "$t(common:buttons.cancel)"
 | 
			
		||||
  },
 | 
			
		||||
  "library": {
 | 
			
		||||
    "alert": {
 | 
			
		||||
      "title": "No permission",
 | 
			
		||||
      "message": "Require photo library read permission to upload",
 | 
			
		||||
      "buttons": {
 | 
			
		||||
        "settings": "Update setting",
 | 
			
		||||
        "cancel": "$t(common:buttons.cancel)"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "photo": {
 | 
			
		||||
    "alert": {
 | 
			
		||||
      "title": "No permission",
 | 
			
		||||
      "message": "Require camera usage permission to upload",
 | 
			
		||||
      "buttons": {
 | 
			
		||||
        "settings": "Update setting",
 | 
			
		||||
        "cancel": "$t(common:buttons.cancel)"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -104,33 +104,6 @@
 | 
			
		||||
        "attachment": {
 | 
			
		||||
          "accessibilityLabel": "Upload attachment",
 | 
			
		||||
          "accessibilityHint": "Poll function will be disabled when there is any attachment",
 | 
			
		||||
          "actions": {
 | 
			
		||||
            "options": {
 | 
			
		||||
              "library": "Upload from photo library",
 | 
			
		||||
              "photo": "Upload with camera",
 | 
			
		||||
              "cancel": "$t(common:buttons.cancel)"
 | 
			
		||||
            },
 | 
			
		||||
            "library": {
 | 
			
		||||
              "alert": {
 | 
			
		||||
                "title": "No permission",
 | 
			
		||||
                "message": "Require photo library read permission to upload",
 | 
			
		||||
                "buttons": {
 | 
			
		||||
                  "settings": "Update setting",
 | 
			
		||||
                  "cancel": "Cancel"
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            },
 | 
			
		||||
            "photo": {
 | 
			
		||||
              "alert": {
 | 
			
		||||
                "title": "No permission",
 | 
			
		||||
                "message": "Require camera usage permission to upload",
 | 
			
		||||
                "buttons": {
 | 
			
		||||
                  "settings": "Update setting",
 | 
			
		||||
                  "cancel": "Cancel"
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          "failed": {
 | 
			
		||||
            "alert": {
 | 
			
		||||
              "title": "Upload failed",
 | 
			
		||||
 
 | 
			
		||||
@@ -52,8 +52,20 @@
 | 
			
		||||
      "push": {
 | 
			
		||||
        "name": "Push Notification"
 | 
			
		||||
      },
 | 
			
		||||
      "profile": {
 | 
			
		||||
        "name": "Edit Profile"
 | 
			
		||||
      },
 | 
			
		||||
      "profileName": {
 | 
			
		||||
        "name": "Edit Display Name"
 | 
			
		||||
      },
 | 
			
		||||
      "profileNote": {
 | 
			
		||||
        "name": "Edit Description"
 | 
			
		||||
      },
 | 
			
		||||
      "profileFields": {
 | 
			
		||||
        "name": "Edit Metadata"
 | 
			
		||||
      },
 | 
			
		||||
      "settings": {
 | 
			
		||||
        "name": "Settings"
 | 
			
		||||
        "name": "App Settings"
 | 
			
		||||
      },
 | 
			
		||||
      "switch": {
 | 
			
		||||
        "name": "Switch Account"
 | 
			
		||||
@@ -71,13 +83,74 @@
 | 
			
		||||
        "XXL": "XXL"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "profile": {
 | 
			
		||||
      "cancellation": {
 | 
			
		||||
        "title": "Change Not Saved",
 | 
			
		||||
        "message": "Your change has not been saved. Would you discard saving the changes?",
 | 
			
		||||
        "buttons": {
 | 
			
		||||
          "cancel": "$t(common:buttons.cancel)",
 | 
			
		||||
          "discard": "Discard"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      "feedback": {
 | 
			
		||||
        "succeed": "{{type}} updated",
 | 
			
		||||
        "failed": "{{type}} update failed, please try again"
 | 
			
		||||
      },
 | 
			
		||||
      "root": {
 | 
			
		||||
        "name": {
 | 
			
		||||
          "title": "Display Name"
 | 
			
		||||
        },
 | 
			
		||||
        "avatar": {
 | 
			
		||||
          "title": "Avatar",
 | 
			
		||||
          "description": "Available in next version"
 | 
			
		||||
        },
 | 
			
		||||
        "banner": {
 | 
			
		||||
          "title": "Banner",
 | 
			
		||||
          "description": "Available in next version"
 | 
			
		||||
        },
 | 
			
		||||
        "note": {
 | 
			
		||||
          "title": "Description"
 | 
			
		||||
        },
 | 
			
		||||
        "fields": {
 | 
			
		||||
          "title": "Metadata",
 | 
			
		||||
          "total": "{{count}} field",
 | 
			
		||||
          "total_plural": "{{count}} fields"
 | 
			
		||||
        },
 | 
			
		||||
        "visibility": {
 | 
			
		||||
          "title": "Posting Visibility",
 | 
			
		||||
          "options": {
 | 
			
		||||
            "public": "Public",
 | 
			
		||||
            "unlisted": "Unlisted",
 | 
			
		||||
            "private": "Followers only",
 | 
			
		||||
            "direct": "Direct message",
 | 
			
		||||
            "cancel": "$t(common:buttons.cancel)"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "sensitive": {
 | 
			
		||||
          "title": "Posting Media Sensitive"
 | 
			
		||||
        },
 | 
			
		||||
        "lock": {
 | 
			
		||||
          "title": "Lock Account",
 | 
			
		||||
          "description": "Requires you to manually approve followers"
 | 
			
		||||
        },
 | 
			
		||||
        "bot": {
 | 
			
		||||
          "title": "Bot account",
 | 
			
		||||
          "description": "This account mainly performs automated actions and might not be monitored"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      "fields": {
 | 
			
		||||
        "group": "Group {{index}}",
 | 
			
		||||
        "label": "Label",
 | 
			
		||||
        "content": "Content"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "push": {
 | 
			
		||||
      "enable": {
 | 
			
		||||
        "direct": "Enable push notification",
 | 
			
		||||
        "settings": "Enable in settings"
 | 
			
		||||
      },
 | 
			
		||||
      "global": {
 | 
			
		||||
        "heading": "Enable push notification",
 | 
			
		||||
        "heading": "Enable for {{acct}}",
 | 
			
		||||
        "description": "Messages are routed through tooot's server"
 | 
			
		||||
      },
 | 
			
		||||
      "decode": {
 | 
			
		||||
@@ -112,6 +185,9 @@
 | 
			
		||||
          "empty": "None"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      "update": {
 | 
			
		||||
        "title": "Update to latest version"
 | 
			
		||||
      },
 | 
			
		||||
      "logout": {
 | 
			
		||||
        "button": "Log out",
 | 
			
		||||
        "alert": {
 | 
			
		||||
@@ -125,13 +201,6 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "settings": {
 | 
			
		||||
      "push": {
 | 
			
		||||
        "heading": "$t(me.stacks.push.name)",
 | 
			
		||||
        "content": {
 | 
			
		||||
          "enabled": "Enabled",
 | 
			
		||||
          "disabled": "Disabled"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      "fontsize": {
 | 
			
		||||
        "heading": "$t(me.stacks.fontSize.name)",
 | 
			
		||||
        "content": {
 | 
			
		||||
@@ -158,7 +227,7 @@
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      "browser": {
 | 
			
		||||
        "heading": "Opening link",
 | 
			
		||||
        "heading": "Opening Link",
 | 
			
		||||
        "options": {
 | 
			
		||||
          "internal": "Inside app",
 | 
			
		||||
          "external": "Use system browser",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,13 @@
 | 
			
		||||
import * as ImagePicker from 'expo-image-picker'
 | 
			
		||||
import * as Crypto from 'expo-crypto'
 | 
			
		||||
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types'
 | 
			
		||||
import * as VideoThumbnails from 'expo-video-thumbnails'
 | 
			
		||||
import { Dispatch } from 'react'
 | 
			
		||||
import { Alert, Linking } from 'react-native'
 | 
			
		||||
import { Alert } from 'react-native'
 | 
			
		||||
import { ComposeAction } from '../../utils/types'
 | 
			
		||||
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
 | 
			
		||||
import i18next from 'i18next'
 | 
			
		||||
import analytics from '@components/analytics'
 | 
			
		||||
import apiInstance from '@api/instance'
 | 
			
		||||
import mediaSelector from '@components/mediaSelector'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  composeDispatch: Dispatch<ComposeAction>
 | 
			
		||||
@@ -22,35 +21,33 @@ const addAttachment = async ({
 | 
			
		||||
  composeDispatch,
 | 
			
		||||
  showActionSheetWithOptions
 | 
			
		||||
}: Props): Promise<any> => {
 | 
			
		||||
  const uploadAttachment = async (result: ImageInfo) => {
 | 
			
		||||
  const uploader = async (imageInfo: ImageInfo) => {
 | 
			
		||||
    const hash = await Crypto.digestStringAsync(
 | 
			
		||||
      Crypto.CryptoDigestAlgorithm.SHA256,
 | 
			
		||||
      result.uri + Math.random()
 | 
			
		||||
      imageInfo.uri + Math.random()
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    let attachmentType: string
 | 
			
		||||
    // https://github.com/expo/expo/issues/11214
 | 
			
		||||
    const attachmentUri = result.uri.replace('file:/data', 'file:///data')
 | 
			
		||||
 | 
			
		||||
    switch (result.type) {
 | 
			
		||||
    switch (imageInfo.type) {
 | 
			
		||||
      case 'image':
 | 
			
		||||
        attachmentType = `image/${attachmentUri.split('.')[1]}`
 | 
			
		||||
        attachmentType = `image/${imageInfo.uri.split('.')[1]}`
 | 
			
		||||
        composeDispatch({
 | 
			
		||||
          type: 'attachment/upload/start',
 | 
			
		||||
          payload: {
 | 
			
		||||
            local: { ...result, local_thumbnail: attachmentUri, hash },
 | 
			
		||||
            local: { ...imageInfo, local_thumbnail: imageInfo.uri, hash },
 | 
			
		||||
            uploading: true
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        break
 | 
			
		||||
      case 'video':
 | 
			
		||||
        attachmentType = `video/${attachmentUri.split('.')[1]}`
 | 
			
		||||
        VideoThumbnails.getThumbnailAsync(attachmentUri)
 | 
			
		||||
        attachmentType = `video/${imageInfo.uri.split('.')[1]}`
 | 
			
		||||
        VideoThumbnails.getThumbnailAsync(imageInfo.uri)
 | 
			
		||||
          .then(({ uri }) =>
 | 
			
		||||
            composeDispatch({
 | 
			
		||||
              type: 'attachment/upload/start',
 | 
			
		||||
              payload: {
 | 
			
		||||
                local: { ...result, local_thumbnail: uri, hash },
 | 
			
		||||
                local: { ...imageInfo, local_thumbnail: uri, hash },
 | 
			
		||||
                uploading: true
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
@@ -59,7 +56,7 @@ const addAttachment = async ({
 | 
			
		||||
            composeDispatch({
 | 
			
		||||
              type: 'attachment/upload/start',
 | 
			
		||||
              payload: {
 | 
			
		||||
                local: { ...result, hash },
 | 
			
		||||
                local: { ...imageInfo, hash },
 | 
			
		||||
                uploading: true
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
@@ -70,7 +67,7 @@ const addAttachment = async ({
 | 
			
		||||
        composeDispatch({
 | 
			
		||||
          type: 'attachment/upload/start',
 | 
			
		||||
          payload: {
 | 
			
		||||
            local: { ...result, hash },
 | 
			
		||||
            local: { ...imageInfo, hash },
 | 
			
		||||
            uploading: true
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
@@ -101,7 +98,7 @@ const addAttachment = async ({
 | 
			
		||||
    const formData = new FormData()
 | 
			
		||||
    formData.append('file', {
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      uri: attachmentUri,
 | 
			
		||||
      uri: imageInfo.uri,
 | 
			
		||||
      name: attachmentType,
 | 
			
		||||
      type: attachmentType
 | 
			
		||||
    })
 | 
			
		||||
@@ -115,7 +112,7 @@ const addAttachment = async ({
 | 
			
		||||
        if (res.body.id) {
 | 
			
		||||
          composeDispatch({
 | 
			
		||||
            type: 'attachment/upload/end',
 | 
			
		||||
            payload: { remote: res.body, local: result }
 | 
			
		||||
            payload: { remote: res.body, local: imageInfo }
 | 
			
		||||
          })
 | 
			
		||||
        } else {
 | 
			
		||||
          uploadFailed()
 | 
			
		||||
@@ -126,119 +123,7 @@ const addAttachment = async ({
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  showActionSheetWithOptions(
 | 
			
		||||
    {
 | 
			
		||||
      options: [
 | 
			
		||||
        i18next.t(
 | 
			
		||||
          'screenCompose:content.root.actions.attachment.actions.options.library'
 | 
			
		||||
        ),
 | 
			
		||||
        i18next.t(
 | 
			
		||||
          'screenCompose:content.root.actions.attachment.actions.options.photo'
 | 
			
		||||
        ),
 | 
			
		||||
        i18next.t(
 | 
			
		||||
          'screenCompose:content.root.actions.attachment.actions.options.cancel'
 | 
			
		||||
        )
 | 
			
		||||
      ],
 | 
			
		||||
      cancelButtonIndex: 2
 | 
			
		||||
    },
 | 
			
		||||
    async buttonIndex => {
 | 
			
		||||
      if (buttonIndex === 0) {
 | 
			
		||||
        const {
 | 
			
		||||
          status
 | 
			
		||||
        } = await ImagePicker.requestMediaLibraryPermissionsAsync()
 | 
			
		||||
        if (status !== 'granted') {
 | 
			
		||||
          Alert.alert(
 | 
			
		||||
            i18next.t(
 | 
			
		||||
              'screenCompose:content.root.actions.attachment.actions.library.alert.title'
 | 
			
		||||
            ),
 | 
			
		||||
            i18next.t(
 | 
			
		||||
              'screenCompose:content.root.actions.attachment.actions.library.alert.message'
 | 
			
		||||
            ),
 | 
			
		||||
            [
 | 
			
		||||
              {
 | 
			
		||||
                text: i18next.t(
 | 
			
		||||
                  'screenCompose:content.root.actions.attachment.actions.library.alert.buttons.cancel'
 | 
			
		||||
                ),
 | 
			
		||||
                style: 'cancel',
 | 
			
		||||
                onPress: () => {
 | 
			
		||||
                  analytics('compose_addattachment_medialibrary_nopermission', {
 | 
			
		||||
                    action: 'cancel'
 | 
			
		||||
                  })
 | 
			
		||||
                }
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                text: i18next.t(
 | 
			
		||||
                  'screenCompose:content.root.actions.attachment.actions.library.alert.buttons.settings'
 | 
			
		||||
                ),
 | 
			
		||||
                style: 'default',
 | 
			
		||||
                onPress: () => {
 | 
			
		||||
                  analytics('compose_addattachment_medialibrary_nopermission', {
 | 
			
		||||
                    action: 'settings'
 | 
			
		||||
                  })
 | 
			
		||||
                  Linking.openURL('app-settings:')
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            ]
 | 
			
		||||
          )
 | 
			
		||||
        } else {
 | 
			
		||||
          const result = await ImagePicker.launchImageLibraryAsync({
 | 
			
		||||
            mediaTypes: ImagePicker.MediaTypeOptions.All,
 | 
			
		||||
            exif: false
 | 
			
		||||
          })
 | 
			
		||||
 | 
			
		||||
          if (!result.cancelled) {
 | 
			
		||||
            uploadAttachment(result)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } else if (buttonIndex === 1) {
 | 
			
		||||
        const { status } = await ImagePicker.requestCameraPermissionsAsync()
 | 
			
		||||
        if (status !== 'granted') {
 | 
			
		||||
          Alert.alert(
 | 
			
		||||
            i18next.t(
 | 
			
		||||
              'screenCompose:content.root.actions.attachment.actions.photo.alert.title'
 | 
			
		||||
            ),
 | 
			
		||||
            i18next.t(
 | 
			
		||||
              'screenCompose:content.root.actions.attachment.actions.photo.alert.message'
 | 
			
		||||
            ),
 | 
			
		||||
            [
 | 
			
		||||
              {
 | 
			
		||||
                text: i18next.t(
 | 
			
		||||
                  'screenCompose:content.root.actions.attachment.actions.photo.alert.buttons.cancel'
 | 
			
		||||
                ),
 | 
			
		||||
                style: 'cancel',
 | 
			
		||||
                onPress: () => {
 | 
			
		||||
                  analytics('compose_addattachment_camera_nopermission', {
 | 
			
		||||
                    action: 'cancel'
 | 
			
		||||
                  })
 | 
			
		||||
                }
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                text: i18next.t(
 | 
			
		||||
                  'screenCompose:content.root.actions.attachment.actions.photo.alert.buttons.settings'
 | 
			
		||||
                ),
 | 
			
		||||
                style: 'default',
 | 
			
		||||
                onPress: () => {
 | 
			
		||||
                  analytics('compose_addattachment_camera_nopermission', {
 | 
			
		||||
                    action: 'settings'
 | 
			
		||||
                  })
 | 
			
		||||
                  Linking.openURL('app-settings:')
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            ]
 | 
			
		||||
          )
 | 
			
		||||
        } else {
 | 
			
		||||
          const result = await ImagePicker.launchCameraAsync({
 | 
			
		||||
            mediaTypes: ImagePicker.MediaTypeOptions.All,
 | 
			
		||||
            exif: false
 | 
			
		||||
          })
 | 
			
		||||
 | 
			
		||||
          if (!result.cancelled) {
 | 
			
		||||
            uploadAttachment(result)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
  mediaSelector({ uploader, showActionSheetWithOptions })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default addAttachment
 | 
			
		||||
 
 | 
			
		||||
@@ -12,10 +12,14 @@ import {
 | 
			
		||||
  getInstanceAccount,
 | 
			
		||||
  getInstanceActive
 | 
			
		||||
} from '@utils/slices/instancesSlice'
 | 
			
		||||
import {
 | 
			
		||||
  getVersionUpdate,
 | 
			
		||||
  retriveVersionLatest
 | 
			
		||||
} from '@utils/slices/versionSlice'
 | 
			
		||||
import { useTheme } from '@utils/styles/ThemeManager'
 | 
			
		||||
import React, { useCallback, useMemo } from 'react'
 | 
			
		||||
import { Image, Platform } from 'react-native'
 | 
			
		||||
import { useSelector } from 'react-redux'
 | 
			
		||||
import React, { useCallback, useEffect, useMemo } from 'react'
 | 
			
		||||
import { Platform } from 'react-native'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import TabLocal from './Tabs/Local'
 | 
			
		||||
import TabMe from './Tabs/Me'
 | 
			
		||||
import TabNotifications from './Tabs/Notifications'
 | 
			
		||||
@@ -114,6 +118,17 @@ const ScreenTabs = React.memo(
 | 
			
		||||
 | 
			
		||||
    const previousTab = useSelector(getPreviousTab, () => true)
 | 
			
		||||
 | 
			
		||||
    const versionUpdate = useSelector(getVersionUpdate)
 | 
			
		||||
    const dispatch = useDispatch()
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
      dispatch(retriveVersionLatest())
 | 
			
		||||
    }, [])
 | 
			
		||||
    const tabMeOptions = useMemo(() => {
 | 
			
		||||
      if (versionUpdate) {
 | 
			
		||||
        return { tabBarBadge: 1 }
 | 
			
		||||
      }
 | 
			
		||||
    }, [versionUpdate])
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Tab.Navigator
 | 
			
		||||
        initialRouteName={instanceActive !== -1 ? previousTab : 'Tab-Me'}
 | 
			
		||||
@@ -128,7 +143,7 @@ const ScreenTabs = React.memo(
 | 
			
		||||
          listeners={composeListeners}
 | 
			
		||||
        />
 | 
			
		||||
        <Tab.Screen name='Tab-Notifications' component={TabNotifications} />
 | 
			
		||||
        <Tab.Screen name='Tab-Me' component={TabMe} />
 | 
			
		||||
        <Tab.Screen name='Tab-Me' component={TabMe} options={tabMeOptions} />
 | 
			
		||||
      </Tab.Navigator>
 | 
			
		||||
    )
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,20 @@
 | 
			
		||||
import { HeaderCenter, HeaderLeft } from '@components/Header'
 | 
			
		||||
import ScreenMeBookmarks from '@screens/Tabs/Me/Bookmarks'
 | 
			
		||||
import ScreenMeConversations from '@screens/Tabs/Me/Cconversations'
 | 
			
		||||
import ScreenMeFavourites from '@screens/Tabs/Me/Favourites'
 | 
			
		||||
import ScreenMeLists from '@screens/Tabs/Me/Lists'
 | 
			
		||||
import ScreenMeRoot from '@screens/Tabs/Me/Root'
 | 
			
		||||
import ScreenMeListsList from '@screens/Tabs/Me/Root/Lists/List'
 | 
			
		||||
import ScreenMeSettings from '@screens/Tabs/Me/Settings'
 | 
			
		||||
import ScreenMeSwitch from '@screens/Tabs/Me/Switch'
 | 
			
		||||
import sharedScreens from '@screens/Tabs/Shared/sharedScreens'
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { useTranslation } from 'react-i18next'
 | 
			
		||||
import { Platform } from 'react-native'
 | 
			
		||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
 | 
			
		||||
import ScreenMeSettingsFontsize from './Me/Fontsize'
 | 
			
		||||
import ScreenMeSettingsPush from './Me/Push'
 | 
			
		||||
import TabMeBookmarks from './Me/Bookmarks'
 | 
			
		||||
import TabMeConversations from './Me/Cconversations'
 | 
			
		||||
import TabMeFavourites from './Me/Favourites'
 | 
			
		||||
import TabMeLists from './Me/Lists'
 | 
			
		||||
import TabMeListsList from './Me/ListsList'
 | 
			
		||||
import TabMeProfile from './Me/Profile'
 | 
			
		||||
import TabMePush from './Me/Push'
 | 
			
		||||
import TabMeRoot from './Me/Root'
 | 
			
		||||
import TabMeSettings from './Me/Settings'
 | 
			
		||||
import TabMeSettingsFontsize from './Me/SettingsFontsize'
 | 
			
		||||
import TabMeSwitch from './Me/Switch'
 | 
			
		||||
import sharedScreens from './Shared/sharedScreens'
 | 
			
		||||
 | 
			
		||||
const Stack = createNativeStackNavigator<Nav.TabMeStackParamList>()
 | 
			
		||||
 | 
			
		||||
@@ -27,7 +28,7 @@ const TabMe = React.memo(
 | 
			
		||||
      >
 | 
			
		||||
        <Stack.Screen
 | 
			
		||||
          name='Tab-Me-Root'
 | 
			
		||||
          component={ScreenMeRoot}
 | 
			
		||||
          component={TabMeRoot}
 | 
			
		||||
          options={{
 | 
			
		||||
            headerTranslucent: true,
 | 
			
		||||
            headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' },
 | 
			
		||||
@@ -36,7 +37,7 @@ const TabMe = React.memo(
 | 
			
		||||
        />
 | 
			
		||||
        <Stack.Screen
 | 
			
		||||
          name='Tab-Me-Bookmarks'
 | 
			
		||||
          component={ScreenMeBookmarks}
 | 
			
		||||
          component={TabMeBookmarks}
 | 
			
		||||
          options={({ navigation }: any) => ({
 | 
			
		||||
            headerTitle: t('me.stacks.bookmarks.name'),
 | 
			
		||||
            ...(Platform.OS === 'android' && {
 | 
			
		||||
@@ -49,7 +50,7 @@ const TabMe = React.memo(
 | 
			
		||||
        />
 | 
			
		||||
        <Stack.Screen
 | 
			
		||||
          name='Tab-Me-Conversations'
 | 
			
		||||
          component={ScreenMeConversations}
 | 
			
		||||
          component={TabMeConversations}
 | 
			
		||||
          options={({ navigation }: any) => ({
 | 
			
		||||
            headerTitle: t('me.stacks.conversations.name'),
 | 
			
		||||
            ...(Platform.OS === 'android' && {
 | 
			
		||||
@@ -62,7 +63,7 @@ const TabMe = React.memo(
 | 
			
		||||
        />
 | 
			
		||||
        <Stack.Screen
 | 
			
		||||
          name='Tab-Me-Favourites'
 | 
			
		||||
          component={ScreenMeFavourites}
 | 
			
		||||
          component={TabMeFavourites}
 | 
			
		||||
          options={({ navigation }: any) => ({
 | 
			
		||||
            headerTitle: t('me.stacks.favourites.name'),
 | 
			
		||||
            ...(Platform.OS === 'android' && {
 | 
			
		||||
@@ -75,7 +76,7 @@ const TabMe = React.memo(
 | 
			
		||||
        />
 | 
			
		||||
        <Stack.Screen
 | 
			
		||||
          name='Tab-Me-Lists'
 | 
			
		||||
          component={ScreenMeLists}
 | 
			
		||||
          component={TabMeLists}
 | 
			
		||||
          options={({ navigation }: any) => ({
 | 
			
		||||
            headerTitle: t('me.stacks.lists.name'),
 | 
			
		||||
            ...(Platform.OS === 'android' && {
 | 
			
		||||
@@ -88,7 +89,7 @@ const TabMe = React.memo(
 | 
			
		||||
        />
 | 
			
		||||
        <Stack.Screen
 | 
			
		||||
          name='Tab-Me-Lists-List'
 | 
			
		||||
          component={ScreenMeListsList}
 | 
			
		||||
          component={TabMeListsList}
 | 
			
		||||
          options={({ route, navigation }: any) => ({
 | 
			
		||||
            headerTitle: t('me.stacks.list.name', { list: route.params.title }),
 | 
			
		||||
            ...(Platform.OS === 'android' && {
 | 
			
		||||
@@ -103,9 +104,30 @@ const TabMe = React.memo(
 | 
			
		||||
            headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
 | 
			
		||||
          })}
 | 
			
		||||
        />
 | 
			
		||||
        <Stack.Screen
 | 
			
		||||
          name='Tab-Me-Profile'
 | 
			
		||||
          component={TabMeProfile}
 | 
			
		||||
          options={{
 | 
			
		||||
            stackPresentation: 'modal',
 | 
			
		||||
            headerShown: false
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        <Stack.Screen
 | 
			
		||||
          name='Tab-Me-Push'
 | 
			
		||||
          component={TabMePush}
 | 
			
		||||
          options={({ navigation }: any) => ({
 | 
			
		||||
            headerTitle: t('me.stacks.push.name'),
 | 
			
		||||
            ...(Platform.OS === 'android' && {
 | 
			
		||||
              headerCenter: () => (
 | 
			
		||||
                <HeaderCenter content={t('me.stacks.push.name')} />
 | 
			
		||||
              )
 | 
			
		||||
            }),
 | 
			
		||||
            headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
 | 
			
		||||
          })}
 | 
			
		||||
        />
 | 
			
		||||
        <Stack.Screen
 | 
			
		||||
          name='Tab-Me-Settings'
 | 
			
		||||
          component={ScreenMeSettings}
 | 
			
		||||
          component={TabMeSettings}
 | 
			
		||||
          options={({ navigation }: any) => ({
 | 
			
		||||
            headerTitle: t('me.stacks.settings.name'),
 | 
			
		||||
            ...(Platform.OS === 'android' && {
 | 
			
		||||
@@ -118,7 +140,7 @@ const TabMe = React.memo(
 | 
			
		||||
        />
 | 
			
		||||
        <Stack.Screen
 | 
			
		||||
          name='Tab-Me-Settings-Fontsize'
 | 
			
		||||
          component={ScreenMeSettingsFontsize}
 | 
			
		||||
          component={TabMeSettingsFontsize}
 | 
			
		||||
          options={({ navigation }: any) => ({
 | 
			
		||||
            headerTitle: t('me.stacks.fontSize.name'),
 | 
			
		||||
            ...(Platform.OS === 'android' && {
 | 
			
		||||
@@ -129,22 +151,9 @@ const TabMe = React.memo(
 | 
			
		||||
            headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
 | 
			
		||||
          })}
 | 
			
		||||
        />
 | 
			
		||||
        <Stack.Screen
 | 
			
		||||
          name='Tab-Me-Settings-Push'
 | 
			
		||||
          component={ScreenMeSettingsPush}
 | 
			
		||||
          options={({ navigation }: any) => ({
 | 
			
		||||
            headerTitle: t('me.stacks.push.name'),
 | 
			
		||||
            ...(Platform.OS === 'android' && {
 | 
			
		||||
              headerCenter: () => (
 | 
			
		||||
                <HeaderCenter content={t('me.stacks.push.name')} />
 | 
			
		||||
              )
 | 
			
		||||
            }),
 | 
			
		||||
            headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
 | 
			
		||||
          })}
 | 
			
		||||
        />
 | 
			
		||||
        <Stack.Screen
 | 
			
		||||
          name='Tab-Me-Switch'
 | 
			
		||||
          component={ScreenMeSwitch}
 | 
			
		||||
          component={TabMeSwitch}
 | 
			
		||||
          options={{
 | 
			
		||||
            stackPresentation: 'modal',
 | 
			
		||||
            headerShown: false
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import TimelineDefault from '@components/Timeline/Default'
 | 
			
		||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
 | 
			
		||||
import React, { useCallback } from 'react'
 | 
			
		||||
 | 
			
		||||
const ScreenMeBookmarks = React.memo(
 | 
			
		||||
const TabMeBookmarks = React.memo(
 | 
			
		||||
  () => {
 | 
			
		||||
    const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Bookmarks' }]
 | 
			
		||||
    const renderItem = useCallback(
 | 
			
		||||
@@ -15,4 +15,4 @@ const ScreenMeBookmarks = React.memo(
 | 
			
		||||
  () => true
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export default ScreenMeBookmarks
 | 
			
		||||
export default TabMeBookmarks
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import TimelineConversation from '@components/Timeline/Conversation'
 | 
			
		||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
 | 
			
		||||
import React, { useCallback } from 'react'
 | 
			
		||||
 | 
			
		||||
const ScreenMeConversations = React.memo(
 | 
			
		||||
const TabMeConversations = React.memo(
 | 
			
		||||
  () => {
 | 
			
		||||
    const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Conversations' }]
 | 
			
		||||
    const renderItem = useCallback(
 | 
			
		||||
@@ -18,4 +18,4 @@ const ScreenMeConversations = React.memo(
 | 
			
		||||
  () => true
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export default ScreenMeConversations
 | 
			
		||||
export default TabMeConversations
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import TimelineDefault from '@components/Timeline/Default'
 | 
			
		||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
 | 
			
		||||
import React, { useCallback } from 'react'
 | 
			
		||||
 | 
			
		||||
const ScreenMeFavourites = React.memo(
 | 
			
		||||
const TabMeFavourites = React.memo(
 | 
			
		||||
  () => {
 | 
			
		||||
    const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Favourites' }]
 | 
			
		||||
    const renderItem = useCallback(
 | 
			
		||||
@@ -16,4 +16,4 @@ const ScreenMeFavourites = React.memo(
 | 
			
		||||
  () => true
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export default ScreenMeFavourites
 | 
			
		||||
export default TabMeFavourites
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import { StackScreenProps } from '@react-navigation/stack'
 | 
			
		||||
import { useListsQuery } from '@utils/queryHooks/lists'
 | 
			
		||||
import React from 'react'
 | 
			
		||||
 | 
			
		||||
const ScreenMeLists: React.FC<StackScreenProps<
 | 
			
		||||
const TabMeLists: React.FC<StackScreenProps<
 | 
			
		||||
  Nav.TabMeStackParamList,
 | 
			
		||||
  'Tab-Me-Lists'
 | 
			
		||||
>> = ({ navigation }) => {
 | 
			
		||||
@@ -28,4 +28,4 @@ const ScreenMeLists: React.FC<StackScreenProps<
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ScreenMeLists
 | 
			
		||||
export default TabMeLists
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import { StackScreenProps } from '@react-navigation/stack'
 | 
			
		||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
 | 
			
		||||
import React, { useCallback } from 'react'
 | 
			
		||||
 | 
			
		||||
const ScreenMeListsList: React.FC<StackScreenProps<
 | 
			
		||||
const TabMeListsList: React.FC<StackScreenProps<
 | 
			
		||||
  Nav.TabMeStackParamList,
 | 
			
		||||
  'Tab-Me-Lists-List'
 | 
			
		||||
>> = ({
 | 
			
		||||
@@ -21,4 +21,4 @@ const ScreenMeListsList: React.FC<StackScreenProps<
 | 
			
		||||
  return <Timeline queryKey={queryKey} customProps={{ renderItem }} />
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ScreenMeListsList
 | 
			
		||||
export default TabMeListsList
 | 
			
		||||
							
								
								
									
										116
									
								
								src/screens/Tabs/Me/Profile.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								src/screens/Tabs/Me/Profile.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,116 @@
 | 
			
		||||
import { HeaderCenter, HeaderLeft } from '@components/Header'
 | 
			
		||||
import { Message } from '@components/Message'
 | 
			
		||||
import { StackScreenProps } from '@react-navigation/stack'
 | 
			
		||||
import React, { useRef } from 'react'
 | 
			
		||||
import { useTranslation } from 'react-i18next'
 | 
			
		||||
import { KeyboardAvoidingView, Platform } from 'react-native'
 | 
			
		||||
import FlashMessage from 'react-native-flash-message'
 | 
			
		||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
 | 
			
		||||
import ScreenMeProfileFields from './Profile/Fields'
 | 
			
		||||
import ScreenMeProfileName from './Profile/Name'
 | 
			
		||||
import ScreenMeProfileNote from './Profile/Note'
 | 
			
		||||
import ScreenMeProfileRoot from './Profile/Root'
 | 
			
		||||
 | 
			
		||||
const Stack = createNativeStackNavigator<Nav.TabMeProfileStackParamList>()
 | 
			
		||||
 | 
			
		||||
const TabMeProfile: React.FC<StackScreenProps<
 | 
			
		||||
  Nav.TabMeStackParamList,
 | 
			
		||||
  'Tab-Me-Switch'
 | 
			
		||||
>> = ({ navigation }) => {
 | 
			
		||||
  const { t } = useTranslation('screenTabs')
 | 
			
		||||
  const messageRef = useRef<FlashMessage>(null)
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <KeyboardAvoidingView
 | 
			
		||||
      style={{ flex: 1 }}
 | 
			
		||||
      behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
 | 
			
		||||
    >
 | 
			
		||||
      <Stack.Navigator
 | 
			
		||||
        screenOptions={{
 | 
			
		||||
          headerHideShadow: true,
 | 
			
		||||
          headerTopInsetEnabled: false
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Stack.Screen
 | 
			
		||||
          name='Tab-Me-Profile-Root'
 | 
			
		||||
          component={ScreenMeProfileRoot}
 | 
			
		||||
          options={{
 | 
			
		||||
            headerTitle: t('me.stacks.profile.name'),
 | 
			
		||||
            ...(Platform.OS === 'android' && {
 | 
			
		||||
              headerCenter: () => (
 | 
			
		||||
                <HeaderCenter content={t('me.stacks.profile.name')} />
 | 
			
		||||
              )
 | 
			
		||||
            }),
 | 
			
		||||
            headerLeft: () => (
 | 
			
		||||
              <HeaderLeft
 | 
			
		||||
                content='ChevronDown'
 | 
			
		||||
                onPress={() => navigation.goBack()}
 | 
			
		||||
              />
 | 
			
		||||
            )
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        <Stack.Screen
 | 
			
		||||
          name='Tab-Me-Profile-Name'
 | 
			
		||||
          options={{
 | 
			
		||||
            headerTitle: t('me.stacks.profileName.name'),
 | 
			
		||||
            ...(Platform.OS === 'android' && {
 | 
			
		||||
              headerCenter: () => (
 | 
			
		||||
                <HeaderCenter content={t('me.stacks.profileName.name')} />
 | 
			
		||||
              )
 | 
			
		||||
            })
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {({ route, navigation }) => (
 | 
			
		||||
            <ScreenMeProfileName
 | 
			
		||||
              messageRef={messageRef}
 | 
			
		||||
              route={route}
 | 
			
		||||
              navigation={navigation}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
        </Stack.Screen>
 | 
			
		||||
        <Stack.Screen
 | 
			
		||||
          name='Tab-Me-Profile-Note'
 | 
			
		||||
          options={{
 | 
			
		||||
            headerTitle: t('me.stacks.profileNote.name'),
 | 
			
		||||
            ...(Platform.OS === 'android' && {
 | 
			
		||||
              headerCenter: () => (
 | 
			
		||||
                <HeaderCenter content={t('me.stacks.profileNote.name')} />
 | 
			
		||||
              )
 | 
			
		||||
            })
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {({ route, navigation }) => (
 | 
			
		||||
            <ScreenMeProfileNote
 | 
			
		||||
              messageRef={messageRef}
 | 
			
		||||
              route={route}
 | 
			
		||||
              navigation={navigation}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
        </Stack.Screen>
 | 
			
		||||
        <Stack.Screen
 | 
			
		||||
          name='Tab-Me-Profile-Fields'
 | 
			
		||||
          options={{
 | 
			
		||||
            headerTitle: t('me.stacks.profileFields.name'),
 | 
			
		||||
            ...(Platform.OS === 'android' && {
 | 
			
		||||
              headerCenter: () => (
 | 
			
		||||
                <HeaderCenter content={t('me.stacks.profileFields.name')} />
 | 
			
		||||
              )
 | 
			
		||||
            })
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {({ route, navigation }) => (
 | 
			
		||||
            <ScreenMeProfileFields
 | 
			
		||||
              messageRef={messageRef}
 | 
			
		||||
              route={route}
 | 
			
		||||
              navigation={navigation}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
        </Stack.Screen>
 | 
			
		||||
      </Stack.Navigator>
 | 
			
		||||
 | 
			
		||||
      <Message ref={messageRef} />
 | 
			
		||||
    </KeyboardAvoidingView>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default TabMeProfile
 | 
			
		||||
							
								
								
									
										168
									
								
								src/screens/Tabs/Me/Profile/Fields.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								src/screens/Tabs/Me/Profile/Fields.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,168 @@
 | 
			
		||||
import { HeaderLeft, HeaderRight } from '@components/Header'
 | 
			
		||||
import Input from '@components/Input'
 | 
			
		||||
import { displayMessage } from '@components/Message'
 | 
			
		||||
import { StackScreenProps } from '@react-navigation/stack'
 | 
			
		||||
import { useProfileMutation } from '@utils/queryHooks/profile'
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import { useTheme } from '@utils/styles/ThemeManager'
 | 
			
		||||
import { isEqual } from 'lodash'
 | 
			
		||||
import React, { RefObject, useEffect, useState } from 'react'
 | 
			
		||||
import { useTranslation } from 'react-i18next'
 | 
			
		||||
import { Alert, StyleSheet, Text, View } from 'react-native'
 | 
			
		||||
import FlashMessage from 'react-native-flash-message'
 | 
			
		||||
import { ScrollView } from 'react-native-gesture-handler'
 | 
			
		||||
 | 
			
		||||
const prepareFields = (
 | 
			
		||||
  fields: Mastodon.Field[] | undefined
 | 
			
		||||
): Mastodon.Field[] => {
 | 
			
		||||
  return Array.from(Array(4).keys()).map(index => {
 | 
			
		||||
    if (fields && fields[index]) {
 | 
			
		||||
      return fields[index]
 | 
			
		||||
    } else {
 | 
			
		||||
      return { name: '', value: '', verified_at: null }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ScreenMeProfileFields: React.FC<StackScreenProps<
 | 
			
		||||
  Nav.TabMeProfileStackParamList,
 | 
			
		||||
  'Tab-Me-Profile-Fields'
 | 
			
		||||
> & { messageRef: RefObject<FlashMessage> }> = ({
 | 
			
		||||
  messageRef,
 | 
			
		||||
  route: {
 | 
			
		||||
    params: { fields }
 | 
			
		||||
  },
 | 
			
		||||
  navigation
 | 
			
		||||
}) => {
 | 
			
		||||
  const { mode, theme } = useTheme()
 | 
			
		||||
  const { t, i18n } = useTranslation('screenTabs')
 | 
			
		||||
  const { mutateAsync, status } = useProfileMutation()
 | 
			
		||||
 | 
			
		||||
  const [newFields, setNewFields] = useState(prepareFields(fields))
 | 
			
		||||
 | 
			
		||||
  const [dirty, setDirty] = useState(false)
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setDirty(!isEqual(prepareFields(fields), newFields))
 | 
			
		||||
  }, [newFields])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    navigation.setOptions({
 | 
			
		||||
      headerLeft: () => (
 | 
			
		||||
        <HeaderLeft
 | 
			
		||||
          onPress={() => {
 | 
			
		||||
            if (dirty) {
 | 
			
		||||
              Alert.alert(
 | 
			
		||||
                t('me.profile.cancellation.title'),
 | 
			
		||||
                t('me.profile.cancellation.message'),
 | 
			
		||||
                [
 | 
			
		||||
                  {
 | 
			
		||||
                    text: t('me.profile.cancellation.buttons.cancel'),
 | 
			
		||||
                    style: 'default'
 | 
			
		||||
                  },
 | 
			
		||||
                  {
 | 
			
		||||
                    text: t('me.profile.cancellation.buttons.discard'),
 | 
			
		||||
                    style: 'destructive',
 | 
			
		||||
                    onPress: () => navigation.navigate('Tab-Me-Profile-Root')
 | 
			
		||||
                  }
 | 
			
		||||
                ]
 | 
			
		||||
              )
 | 
			
		||||
            } else {
 | 
			
		||||
              navigation.navigate('Tab-Me-Profile-Root')
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      ),
 | 
			
		||||
      headerRight: () => (
 | 
			
		||||
        <HeaderRight
 | 
			
		||||
          disabled={!dirty}
 | 
			
		||||
          loading={status === 'loading'}
 | 
			
		||||
          content='Save'
 | 
			
		||||
          onPress={async () => {
 | 
			
		||||
            mutateAsync({
 | 
			
		||||
              type: 'fields_attributes',
 | 
			
		||||
              data: newFields
 | 
			
		||||
                .filter(field => field.name.length && field.value.length)
 | 
			
		||||
                .map(field => ({ name: field.name, value: field.value }))
 | 
			
		||||
            })
 | 
			
		||||
              .then(() => {
 | 
			
		||||
                navigation.navigate('Tab-Me-Profile-Root')
 | 
			
		||||
                displayMessage({
 | 
			
		||||
                  ref: messageRef,
 | 
			
		||||
                  message: t('me.profile.feedback.succeed', {
 | 
			
		||||
                    type: t('me.profile.root.note.title')
 | 
			
		||||
                  }),
 | 
			
		||||
                  mode,
 | 
			
		||||
                  type: 'success'
 | 
			
		||||
                })
 | 
			
		||||
              })
 | 
			
		||||
              .catch(() => {
 | 
			
		||||
                displayMessage({
 | 
			
		||||
                  ref: messageRef,
 | 
			
		||||
                  message: t('me.profile.feedback.failed', {
 | 
			
		||||
                    type: t('me.profile.root.note.title')
 | 
			
		||||
                  }),
 | 
			
		||||
                  mode,
 | 
			
		||||
                  type: 'error'
 | 
			
		||||
                })
 | 
			
		||||
              })
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
  }, [mode, i18n.language, dirty, status, newFields])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ScrollView style={styles.base}>
 | 
			
		||||
      {Array.from(Array(4).keys()).map(index => (
 | 
			
		||||
        <View key={index} style={styles.group}>
 | 
			
		||||
          <Text style={[styles.headline, { color: theme.primaryDefault }]}>
 | 
			
		||||
            {t('me.profile.fields.group', { index: index + 1 })}
 | 
			
		||||
          </Text>
 | 
			
		||||
          <Input
 | 
			
		||||
            title={t('me.profile.fields.label')}
 | 
			
		||||
            autoFocus={false}
 | 
			
		||||
            maxLength={255}
 | 
			
		||||
            value={newFields[index].name}
 | 
			
		||||
            setValue={(v: any) =>
 | 
			
		||||
              setNewFields(
 | 
			
		||||
                newFields.map((field, i) =>
 | 
			
		||||
                  i === index ? { ...field, name: v } : field
 | 
			
		||||
                )
 | 
			
		||||
              )
 | 
			
		||||
            }
 | 
			
		||||
            emoji
 | 
			
		||||
          />
 | 
			
		||||
          <Input
 | 
			
		||||
            title={t('me.profile.fields.content')}
 | 
			
		||||
            autoFocus={false}
 | 
			
		||||
            maxLength={255}
 | 
			
		||||
            value={newFields[index].value}
 | 
			
		||||
            setValue={(v: any) =>
 | 
			
		||||
              setNewFields(
 | 
			
		||||
                newFields.map((field, i) =>
 | 
			
		||||
                  i === index ? { ...field, value: v } : field
 | 
			
		||||
                )
 | 
			
		||||
              )
 | 
			
		||||
            }
 | 
			
		||||
            emoji
 | 
			
		||||
          />
 | 
			
		||||
        </View>
 | 
			
		||||
      ))}
 | 
			
		||||
    </ScrollView>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  base: {
 | 
			
		||||
    padding: StyleConstants.Spacing.Global.PagePadding
 | 
			
		||||
  },
 | 
			
		||||
  group: {
 | 
			
		||||
    marginBottom: StyleConstants.Spacing.M
 | 
			
		||||
  },
 | 
			
		||||
  headline: {
 | 
			
		||||
    ...StyleConstants.FontStyle.S,
 | 
			
		||||
    marginBottom: StyleConstants.Spacing.XS
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default ScreenMeProfileFields
 | 
			
		||||
							
								
								
									
										109
									
								
								src/screens/Tabs/Me/Profile/Name.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/screens/Tabs/Me/Profile/Name.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,109 @@
 | 
			
		||||
import { HeaderLeft, HeaderRight } from '@components/Header'
 | 
			
		||||
import Input from '@components/Input'
 | 
			
		||||
import { displayMessage } from '@components/Message'
 | 
			
		||||
import { StackScreenProps } from '@react-navigation/stack'
 | 
			
		||||
import { useProfileMutation } from '@utils/queryHooks/profile'
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import { useTheme } from '@utils/styles/ThemeManager'
 | 
			
		||||
import React, { RefObject, useEffect, useState } from 'react'
 | 
			
		||||
import { useTranslation } from 'react-i18next'
 | 
			
		||||
import { Alert, StyleSheet } from 'react-native'
 | 
			
		||||
import FlashMessage from 'react-native-flash-message'
 | 
			
		||||
import { ScrollView } from 'react-native-gesture-handler'
 | 
			
		||||
 | 
			
		||||
const ScreenMeProfileName: React.FC<StackScreenProps<
 | 
			
		||||
  Nav.TabMeProfileStackParamList,
 | 
			
		||||
  'Tab-Me-Profile-Name'
 | 
			
		||||
> & { messageRef: RefObject<FlashMessage> }> = ({
 | 
			
		||||
  messageRef,
 | 
			
		||||
  route: {
 | 
			
		||||
    params: { display_name }
 | 
			
		||||
  },
 | 
			
		||||
  navigation
 | 
			
		||||
}) => {
 | 
			
		||||
  const { mode } = useTheme()
 | 
			
		||||
  const { t, i18n } = useTranslation('screenTabs')
 | 
			
		||||
  const { mutateAsync, status } = useProfileMutation()
 | 
			
		||||
 | 
			
		||||
  const [displayName, setDisplayName] = useState(display_name)
 | 
			
		||||
  
 | 
			
		||||
  const [dirty, setDirty] = useState(false)
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setDirty(display_name !== displayName)
 | 
			
		||||
  }, [displayName])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    navigation.setOptions({
 | 
			
		||||
      headerLeft: () => (
 | 
			
		||||
        <HeaderLeft
 | 
			
		||||
          onPress={() => {
 | 
			
		||||
            if (dirty) {
 | 
			
		||||
              Alert.alert(
 | 
			
		||||
                t('me.profile.cancellation.title'),
 | 
			
		||||
                t('me.profile.cancellation.message'),
 | 
			
		||||
                [
 | 
			
		||||
                  {
 | 
			
		||||
                    text: t('me.profile.cancellation.buttons.cancel'),
 | 
			
		||||
                    style: 'default'
 | 
			
		||||
                  },
 | 
			
		||||
                  {
 | 
			
		||||
                    text: t('me.profile.cancellation.buttons.discard'),
 | 
			
		||||
                    style: 'destructive',
 | 
			
		||||
                    onPress: () => navigation.navigate('Tab-Me-Profile-Root')
 | 
			
		||||
                  }
 | 
			
		||||
                ]
 | 
			
		||||
              )
 | 
			
		||||
            } else {
 | 
			
		||||
              navigation.navigate('Tab-Me-Profile-Root')
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      ),
 | 
			
		||||
      headerRight: () => (
 | 
			
		||||
        <HeaderRight
 | 
			
		||||
          disabled={!dirty}
 | 
			
		||||
          loading={status === 'loading'}
 | 
			
		||||
          content='Save'
 | 
			
		||||
          onPress={async () => {
 | 
			
		||||
            mutateAsync({ type: 'display_name', data: displayName })
 | 
			
		||||
              .then(() => {
 | 
			
		||||
                navigation.navigate('Tab-Me-Profile-Root')
 | 
			
		||||
                displayMessage({
 | 
			
		||||
                  ref: messageRef,
 | 
			
		||||
                  message: t('me.profile.feedback.succeed', {
 | 
			
		||||
                    type: t('me.profile.root.name.title')
 | 
			
		||||
                  }),
 | 
			
		||||
                  mode,
 | 
			
		||||
                  type: 'success'
 | 
			
		||||
                })
 | 
			
		||||
              })
 | 
			
		||||
              .catch(() => {
 | 
			
		||||
                displayMessage({
 | 
			
		||||
                  ref: messageRef,
 | 
			
		||||
                  message: t('me.profile.feedback.failed', {
 | 
			
		||||
                    type: t('me.profile.root.name.title')
 | 
			
		||||
                  }),
 | 
			
		||||
                  mode,
 | 
			
		||||
                  type: 'error'
 | 
			
		||||
                })
 | 
			
		||||
              })
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
  }, [mode, i18n.language, dirty, status, displayName])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ScrollView style={styles.base}>
 | 
			
		||||
      <Input value={displayName} setValue={setDisplayName} emoji />
 | 
			
		||||
    </ScrollView>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  base: {
 | 
			
		||||
    padding: StyleConstants.Spacing.Global.PagePadding
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default ScreenMeProfileName
 | 
			
		||||
							
								
								
									
										109
									
								
								src/screens/Tabs/Me/Profile/Note.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/screens/Tabs/Me/Profile/Note.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,109 @@
 | 
			
		||||
import { HeaderLeft, HeaderRight } from '@components/Header'
 | 
			
		||||
import Input from '@components/Input'
 | 
			
		||||
import { displayMessage } from '@components/Message'
 | 
			
		||||
import { StackScreenProps } from '@react-navigation/stack'
 | 
			
		||||
import { useProfileMutation } from '@utils/queryHooks/profile'
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import { useTheme } from '@utils/styles/ThemeManager'
 | 
			
		||||
import React, { RefObject, useEffect, useState } from 'react'
 | 
			
		||||
import { useTranslation } from 'react-i18next'
 | 
			
		||||
import { Alert, StyleSheet } from 'react-native'
 | 
			
		||||
import FlashMessage from 'react-native-flash-message'
 | 
			
		||||
import { ScrollView } from 'react-native-gesture-handler'
 | 
			
		||||
 | 
			
		||||
const ScreenMeProfileNote: React.FC<StackScreenProps<
 | 
			
		||||
  Nav.TabMeProfileStackParamList,
 | 
			
		||||
  'Tab-Me-Profile-Note'
 | 
			
		||||
> & { messageRef: RefObject<FlashMessage> }> = ({
 | 
			
		||||
  messageRef,
 | 
			
		||||
  route: {
 | 
			
		||||
    params: { note }
 | 
			
		||||
  },
 | 
			
		||||
  navigation
 | 
			
		||||
}) => {
 | 
			
		||||
  const { mode } = useTheme()
 | 
			
		||||
  const { t, i18n } = useTranslation('screenTabs')
 | 
			
		||||
  const { mutateAsync, status } = useProfileMutation()
 | 
			
		||||
 | 
			
		||||
  const [newNote, setNewNote] = useState(note)
 | 
			
		||||
  
 | 
			
		||||
  const [dirty, setDirty] = useState(false)
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setDirty(note !== newNote)
 | 
			
		||||
  }, [newNote])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    navigation.setOptions({
 | 
			
		||||
      headerLeft: () => (
 | 
			
		||||
        <HeaderLeft
 | 
			
		||||
          onPress={() => {
 | 
			
		||||
            if (dirty) {
 | 
			
		||||
              Alert.alert(
 | 
			
		||||
                t('me.profile.cancellation.title'),
 | 
			
		||||
                t('me.profile.cancellation.message'),
 | 
			
		||||
                [
 | 
			
		||||
                  {
 | 
			
		||||
                    text: t('me.profile.cancellation.buttons.cancel'),
 | 
			
		||||
                    style: 'default'
 | 
			
		||||
                  },
 | 
			
		||||
                  {
 | 
			
		||||
                    text: t('me.profile.cancellation.buttons.discard'),
 | 
			
		||||
                    style: 'destructive',
 | 
			
		||||
                    onPress: () => navigation.navigate('Tab-Me-Profile-Root')
 | 
			
		||||
                  }
 | 
			
		||||
                ]
 | 
			
		||||
              )
 | 
			
		||||
            } else {
 | 
			
		||||
              navigation.navigate('Tab-Me-Profile-Root')
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      ),
 | 
			
		||||
      headerRight: () => (
 | 
			
		||||
        <HeaderRight
 | 
			
		||||
          disabled={!dirty}
 | 
			
		||||
          loading={status === 'loading'}
 | 
			
		||||
          content='Save'
 | 
			
		||||
          onPress={async () => {
 | 
			
		||||
            mutateAsync({ type: 'note', data: newNote })
 | 
			
		||||
              .then(() => {
 | 
			
		||||
                navigation.navigate('Tab-Me-Profile-Root')
 | 
			
		||||
                displayMessage({
 | 
			
		||||
                  ref: messageRef,
 | 
			
		||||
                  message: t('me.profile.feedback.succeed', {
 | 
			
		||||
                    type: t('me.profile.root.note.title')
 | 
			
		||||
                  }),
 | 
			
		||||
                  mode,
 | 
			
		||||
                  type: 'success'
 | 
			
		||||
                })
 | 
			
		||||
              })
 | 
			
		||||
              .catch(() => {
 | 
			
		||||
                displayMessage({
 | 
			
		||||
                  ref: messageRef,
 | 
			
		||||
                  message: t('me.profile.feedback.failed', {
 | 
			
		||||
                    type: t('me.profile.root.note.title')
 | 
			
		||||
                  }),
 | 
			
		||||
                  mode,
 | 
			
		||||
                  type: 'error'
 | 
			
		||||
                })
 | 
			
		||||
              })
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
  }, [mode, i18n.language, dirty, status, newNote])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ScrollView style={styles.base}>
 | 
			
		||||
      <Input value={newNote} setValue={setNewNote} multiline emoji />
 | 
			
		||||
    </ScrollView>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  base: {
 | 
			
		||||
    padding: StyleConstants.Spacing.Global.PagePadding
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default ScreenMeProfileNote
 | 
			
		||||
							
								
								
									
										187
									
								
								src/screens/Tabs/Me/Profile/Root.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								src/screens/Tabs/Me/Profile/Root.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,187 @@
 | 
			
		||||
import { MenuContainer, MenuRow } from '@components/Menu'
 | 
			
		||||
import { useActionSheet } from '@expo/react-native-action-sheet'
 | 
			
		||||
import { StackScreenProps } from '@react-navigation/stack'
 | 
			
		||||
import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile'
 | 
			
		||||
import React, { useCallback } from 'react'
 | 
			
		||||
import { useTranslation } from 'react-i18next'
 | 
			
		||||
import { ScrollView } from 'react-native-gesture-handler'
 | 
			
		||||
 | 
			
		||||
const ScreenMeProfileRoot: React.FC<StackScreenProps<
 | 
			
		||||
  Nav.TabMeProfileStackParamList,
 | 
			
		||||
  'Tab-Me-Profile-Root'
 | 
			
		||||
>> = ({ navigation }) => {
 | 
			
		||||
  const { t } = useTranslation('screenTabs')
 | 
			
		||||
 | 
			
		||||
  const { showActionSheetWithOptions } = useActionSheet()
 | 
			
		||||
 | 
			
		||||
  const { data, isLoading } = useProfileQuery({})
 | 
			
		||||
  const { mutate } = useProfileMutation()
 | 
			
		||||
 | 
			
		||||
  const onPressVisibility = useCallback(() => {
 | 
			
		||||
    showActionSheetWithOptions(
 | 
			
		||||
      {
 | 
			
		||||
        title: t('me.profile.root.visibility.title'),
 | 
			
		||||
        options: [
 | 
			
		||||
          t('me.profile.root.visibility.options.public'),
 | 
			
		||||
          t('me.profile.root.visibility.options.unlisted'),
 | 
			
		||||
          t('me.profile.root.visibility.options.private'),
 | 
			
		||||
          t('me.profile.root.visibility.options.direct'),
 | 
			
		||||
          t('me.profile.root.visibility.options.cancel')
 | 
			
		||||
        ],
 | 
			
		||||
        cancelButtonIndex: 4
 | 
			
		||||
      },
 | 
			
		||||
      async buttonIndex => {
 | 
			
		||||
        switch (buttonIndex) {
 | 
			
		||||
          case 0:
 | 
			
		||||
            mutate({ type: 'source[privacy]', data: 'public' })
 | 
			
		||||
            break
 | 
			
		||||
          case 1:
 | 
			
		||||
            mutate({ type: 'source[privacy]', data: 'unlisted' })
 | 
			
		||||
            break
 | 
			
		||||
          case 2:
 | 
			
		||||
            mutate({ type: 'source[privacy]', data: 'private' })
 | 
			
		||||
            break
 | 
			
		||||
          case 3:
 | 
			
		||||
            mutate({ type: 'source[privacy]', data: 'direct' })
 | 
			
		||||
            break
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const onPressSensitive = useCallback(() => {
 | 
			
		||||
    if (data?.source.sensitive === undefined) {
 | 
			
		||||
      mutate({ type: 'source[sensitive]', data: true })
 | 
			
		||||
    } else {
 | 
			
		||||
      mutate({ type: 'source[sensitive]', data: !data.source.sensitive })
 | 
			
		||||
    }
 | 
			
		||||
  }, [data?.source.sensitive])
 | 
			
		||||
 | 
			
		||||
  const onPressLock = useCallback(() => {
 | 
			
		||||
    if (data?.locked === undefined) {
 | 
			
		||||
      mutate({ type: 'locked', data: true })
 | 
			
		||||
    } else {
 | 
			
		||||
      mutate({ type: 'locked', data: !data.locked })
 | 
			
		||||
    }
 | 
			
		||||
  }, [data?.locked])
 | 
			
		||||
 | 
			
		||||
  const onPressBot = useCallback(() => {
 | 
			
		||||
    if (data?.bot === undefined) {
 | 
			
		||||
      mutate({ type: 'bot', data: true })
 | 
			
		||||
    } else {
 | 
			
		||||
      mutate({ type: 'bot', data: !data?.bot })
 | 
			
		||||
    }
 | 
			
		||||
  }, [data?.bot])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ScrollView>
 | 
			
		||||
      <MenuContainer>
 | 
			
		||||
        <MenuRow
 | 
			
		||||
          title={t('me.profile.root.name.title')}
 | 
			
		||||
          content={data?.display_name}
 | 
			
		||||
          loading={isLoading}
 | 
			
		||||
          iconBack='ChevronRight'
 | 
			
		||||
          onPress={() => {
 | 
			
		||||
            data &&
 | 
			
		||||
              navigation.navigate('Tab-Me-Profile-Name', {
 | 
			
		||||
                display_name: data.display_name
 | 
			
		||||
              })
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        <MenuRow
 | 
			
		||||
          title={t('me.profile.root.avatar.title')}
 | 
			
		||||
          description={t('me.profile.root.avatar.description')}
 | 
			
		||||
          // content={
 | 
			
		||||
          //   <GracefullyImage
 | 
			
		||||
          //     style={{ flex: 1 }}
 | 
			
		||||
          //     uri={{
 | 
			
		||||
          //       original: data?.avatar_static
 | 
			
		||||
          //     }}
 | 
			
		||||
          //   />
 | 
			
		||||
          // }
 | 
			
		||||
          // loading={isLoading}
 | 
			
		||||
          // iconBack='ChevronRight'
 | 
			
		||||
        />
 | 
			
		||||
        <MenuRow
 | 
			
		||||
          title={t('me.profile.root.banner.title')}
 | 
			
		||||
          description={t('me.profile.root.banner.description')}
 | 
			
		||||
          // content={
 | 
			
		||||
          //   <GracefullyImage
 | 
			
		||||
          //     style={{ flex: 1 }}
 | 
			
		||||
          //     uri={{
 | 
			
		||||
          //       original: data?.header_static
 | 
			
		||||
          //     }}
 | 
			
		||||
          //   />
 | 
			
		||||
          // }
 | 
			
		||||
          // loading={isLoading}
 | 
			
		||||
          // iconBack='ChevronRight'
 | 
			
		||||
        />
 | 
			
		||||
        <MenuRow
 | 
			
		||||
          title={t('me.profile.root.note.title')}
 | 
			
		||||
          content={data?.source.note}
 | 
			
		||||
          loading={isLoading}
 | 
			
		||||
          iconBack='ChevronRight'
 | 
			
		||||
          onPress={() => {
 | 
			
		||||
            navigation.navigate('Tab-Me-Profile-Note', {
 | 
			
		||||
              note: data?.source?.note || ''
 | 
			
		||||
            })
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        <MenuRow
 | 
			
		||||
          title={t('me.profile.root.fields.title')}
 | 
			
		||||
          content={
 | 
			
		||||
            data?.source.fields && data.source.fields.length
 | 
			
		||||
              ? t('me.profile.root.fields.total', {
 | 
			
		||||
                  count: data.source.fields.length
 | 
			
		||||
                })
 | 
			
		||||
              : undefined
 | 
			
		||||
          }
 | 
			
		||||
          loading={isLoading}
 | 
			
		||||
          iconBack='ChevronRight'
 | 
			
		||||
          onPress={() => {
 | 
			
		||||
            navigation.navigate('Tab-Me-Profile-Fields', {
 | 
			
		||||
              fields: data?.source.fields
 | 
			
		||||
            })
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      </MenuContainer>
 | 
			
		||||
      <MenuContainer>
 | 
			
		||||
        <MenuRow
 | 
			
		||||
          title={t('me.profile.root.visibility.title')}
 | 
			
		||||
          content={
 | 
			
		||||
            data?.source.privacy
 | 
			
		||||
              ? t(`me.profile.root.visibility.options.${data?.source.privacy}`)
 | 
			
		||||
              : undefined
 | 
			
		||||
          }
 | 
			
		||||
          loading={isLoading}
 | 
			
		||||
          iconBack='ChevronRight'
 | 
			
		||||
          onPress={onPressVisibility}
 | 
			
		||||
        />
 | 
			
		||||
        <MenuRow
 | 
			
		||||
          title={t('me.profile.root.sensitive.title')}
 | 
			
		||||
          switchValue={data?.source.sensitive}
 | 
			
		||||
          switchOnValueChange={onPressSensitive}
 | 
			
		||||
          loading={isLoading}
 | 
			
		||||
        />
 | 
			
		||||
      </MenuContainer>
 | 
			
		||||
      <MenuContainer>
 | 
			
		||||
        <MenuRow
 | 
			
		||||
          title={t('me.profile.root.lock.title')}
 | 
			
		||||
          description={t('me.profile.root.lock.description')}
 | 
			
		||||
          switchValue={data?.locked}
 | 
			
		||||
          switchOnValueChange={onPressLock}
 | 
			
		||||
          loading={isLoading}
 | 
			
		||||
        />
 | 
			
		||||
        <MenuRow
 | 
			
		||||
          title={t('me.profile.root.bot.title')}
 | 
			
		||||
          description={t('me.profile.root.bot.description')}
 | 
			
		||||
          switchValue={data?.bot}
 | 
			
		||||
          switchOnValueChange={onPressBot}
 | 
			
		||||
          loading={isLoading}
 | 
			
		||||
        />
 | 
			
		||||
      </MenuContainer>
 | 
			
		||||
    </ScrollView>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ScreenMeProfileRoot
 | 
			
		||||
@@ -2,7 +2,12 @@ import { MenuContainer, MenuRow } from '@components/Menu'
 | 
			
		||||
import { updateInstancePush } from '@utils/slices/instances/updatePush'
 | 
			
		||||
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
 | 
			
		||||
import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode'
 | 
			
		||||
import { clearPushLoading, getInstancePush } from '@utils/slices/instancesSlice'
 | 
			
		||||
import {
 | 
			
		||||
  clearPushLoading,
 | 
			
		||||
  getInstanceAccount,
 | 
			
		||||
  getInstancePush,
 | 
			
		||||
  getInstanceUri
 | 
			
		||||
} from '@utils/slices/instancesSlice'
 | 
			
		||||
import * as WebBrowser from 'expo-web-browser'
 | 
			
		||||
import * as Notifications from 'expo-notifications'
 | 
			
		||||
import React, { useEffect, useMemo, useState } from 'react'
 | 
			
		||||
@@ -13,9 +18,18 @@ import layoutAnimation from '@utils/styles/layoutAnimation'
 | 
			
		||||
import Button from '@components/Button'
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import { AppState, Linking } from 'react-native'
 | 
			
		||||
import { StackScreenProps } from '@react-navigation/stack'
 | 
			
		||||
 | 
			
		||||
const ScreenMeSettingsPush: React.FC = () => {
 | 
			
		||||
const TabMePush: React.FC<StackScreenProps<
 | 
			
		||||
  Nav.TabMeStackParamList,
 | 
			
		||||
  'Tab-Me-Push'
 | 
			
		||||
>> = () => {
 | 
			
		||||
  const { t } = useTranslation('screenTabs')
 | 
			
		||||
  const instanceAccount = useSelector(
 | 
			
		||||
    getInstanceAccount,
 | 
			
		||||
    (prev, next) => prev?.acct === next?.acct
 | 
			
		||||
  )
 | 
			
		||||
  const instanceUri = useSelector(getInstanceUri)
 | 
			
		||||
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
  const instancePush = useSelector(getInstancePush)
 | 
			
		||||
@@ -106,7 +120,9 @@ const ScreenMeSettingsPush: React.FC = () => {
 | 
			
		||||
      ) : null}
 | 
			
		||||
      <MenuContainer>
 | 
			
		||||
        <MenuRow
 | 
			
		||||
          title={t('me.push.global.heading')}
 | 
			
		||||
          title={t('me.push.global.heading', {
 | 
			
		||||
            acct: `@${instanceAccount?.acct}@${instanceUri}`
 | 
			
		||||
          })}
 | 
			
		||||
          description={t('me.push.global.description')}
 | 
			
		||||
          loading={instancePush?.global.loading}
 | 
			
		||||
          switchDisabled={!pushEnabled || isLoading}
 | 
			
		||||
@@ -144,4 +160,4 @@ const ScreenMeSettingsPush: React.FC = () => {
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ScreenMeSettingsPush
 | 
			
		||||
export default TabMePush
 | 
			
		||||
 
 | 
			
		||||
@@ -4,31 +4,25 @@ import Collections from '@screens/Tabs/Me/Root/Collections'
 | 
			
		||||
import Logout from '@screens/Tabs/Me/Root/Logout'
 | 
			
		||||
import MyInfo from '@screens/Tabs/Me/Root/MyInfo'
 | 
			
		||||
import Settings from '@screens/Tabs/Me/Root/Settings'
 | 
			
		||||
import AccountInformationSwitch from '@screens/Tabs/Me/Root/Switch'
 | 
			
		||||
import AccountNav from '@screens/Tabs/Shared/Account/Nav'
 | 
			
		||||
import AccountContext from '@screens/Tabs/Shared/Account/utils/createContext'
 | 
			
		||||
import accountInitialState from '@screens/Tabs/Shared/Account/utils/initialState'
 | 
			
		||||
import accountReducer from '@screens/Tabs/Shared/Account/utils/reducer'
 | 
			
		||||
import { useAccountQuery } from '@utils/queryHooks/account'
 | 
			
		||||
import {
 | 
			
		||||
  getInstanceAccount,
 | 
			
		||||
  getInstanceActive
 | 
			
		||||
} from '@utils/slices/instancesSlice'
 | 
			
		||||
import { useProfileQuery } from '@utils/queryHooks/profile'
 | 
			
		||||
import { getInstanceActive } from '@utils/slices/instancesSlice'
 | 
			
		||||
import React, { useReducer, useRef } from 'react'
 | 
			
		||||
import Animated, {
 | 
			
		||||
  useAnimatedScrollHandler,
 | 
			
		||||
  useSharedValue
 | 
			
		||||
} from 'react-native-reanimated'
 | 
			
		||||
import { useSelector } from 'react-redux'
 | 
			
		||||
import Update from './Root/Update'
 | 
			
		||||
 | 
			
		||||
const ScreenMeRoot: React.FC = () => {
 | 
			
		||||
const TabMeRoot: React.FC = () => {
 | 
			
		||||
  const instanceActive = useSelector(getInstanceActive)
 | 
			
		||||
  const instanceAccount = useSelector(
 | 
			
		||||
    getInstanceAccount,
 | 
			
		||||
    (prev, next) => prev?.id === next?.id
 | 
			
		||||
  )
 | 
			
		||||
  const { data } = useAccountQuery({
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    id: instanceAccount?.id,
 | 
			
		||||
 | 
			
		||||
  const { data } = useProfileQuery({
 | 
			
		||||
    options: { enabled: instanceActive !== -1, keepPreviousData: false }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
@@ -62,11 +56,13 @@ const ScreenMeRoot: React.FC = () => {
 | 
			
		||||
          <ComponentInstance />
 | 
			
		||||
        )}
 | 
			
		||||
        {instanceActive !== -1 ? <Collections /> : null}
 | 
			
		||||
        <Update />
 | 
			
		||||
        <Settings />
 | 
			
		||||
        {instanceActive !== -1 ? <AccountInformationSwitch /> : null}
 | 
			
		||||
        {instanceActive !== -1 ? <Logout /> : null}
 | 
			
		||||
      </Animated.ScrollView>
 | 
			
		||||
    </AccountContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ScreenMeRoot
 | 
			
		||||
export default TabMeRoot
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ const Logout: React.FC = () => {
 | 
			
		||||
      content={t('me.root.logout.button')}
 | 
			
		||||
      style={{
 | 
			
		||||
        marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2,
 | 
			
		||||
        marginBottom: StyleConstants.Spacing.Global.PagePadding * 2
 | 
			
		||||
        marginTop: StyleConstants.Spacing.Global.PagePadding
 | 
			
		||||
      }}
 | 
			
		||||
      destructive
 | 
			
		||||
      onPress={() =>
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ export interface Props {
 | 
			
		||||
const MyInfo: React.FC<Props> = ({ account }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <AccountHeader account={account} limitHeight />
 | 
			
		||||
      <AccountHeader account={account} />
 | 
			
		||||
      <AccountInformation account={account} myInfo />
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import Button from '@components/Button'
 | 
			
		||||
import { useNavigation } from '@react-navigation/native'
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { useTranslation } from 'react-i18next'
 | 
			
		||||
 | 
			
		||||
@@ -11,6 +12,10 @@ const AccountInformationSwitch: React.FC = () => {
 | 
			
		||||
    <Button
 | 
			
		||||
      type='text'
 | 
			
		||||
      content={t('me.stacks.switch.name')}
 | 
			
		||||
      style={{
 | 
			
		||||
        marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2,
 | 
			
		||||
        marginTop: StyleConstants.Spacing.Global.PagePadding
 | 
			
		||||
      }}
 | 
			
		||||
      onPress={() => navigation.navigate('Tab-Me-Switch')}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
							
								
								
									
										32
									
								
								src/screens/Tabs/Me/Root/Update.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/screens/Tabs/Me/Root/Update.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
import { MenuContainer, MenuRow } from '@components/Menu'
 | 
			
		||||
import { getVersionUpdate } from '@utils/slices/versionSlice'
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { useTranslation } from 'react-i18next'
 | 
			
		||||
import { Linking, Platform } from 'react-native'
 | 
			
		||||
import { useSelector } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
const Update: React.FC = () => {
 | 
			
		||||
  const { t } = useTranslation('screenTabs')
 | 
			
		||||
 | 
			
		||||
  const versionUpdate = useSelector(getVersionUpdate)
 | 
			
		||||
 | 
			
		||||
  return versionUpdate ? (
 | 
			
		||||
    <MenuContainer>
 | 
			
		||||
      <MenuRow
 | 
			
		||||
        iconFront='ChevronsUp'
 | 
			
		||||
        iconBack='ExternalLink'
 | 
			
		||||
        title={t('me.root.update.title')}
 | 
			
		||||
        badge
 | 
			
		||||
        onPress={() => {
 | 
			
		||||
          if (Platform.OS === 'ios') {
 | 
			
		||||
            Linking.openURL('itms-appss://itunes.apple.com/app/id1549772269')
 | 
			
		||||
          } else {
 | 
			
		||||
            Linking.openURL('https://tooot.app')
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    </MenuContainer>
 | 
			
		||||
  ) : null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Update
 | 
			
		||||
@@ -6,7 +6,7 @@ import SettingsApp from './Settings/App'
 | 
			
		||||
import SettingsDev from './Settings/Dev'
 | 
			
		||||
import SettingsTooot from './Settings/Tooot'
 | 
			
		||||
 | 
			
		||||
const ScreenMeSettings: React.FC = () => {
 | 
			
		||||
const TabMeSettings: React.FC = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <ScrollView>
 | 
			
		||||
      <SettingsApp />
 | 
			
		||||
@@ -23,4 +23,4 @@ const ScreenMeSettings: React.FC = () => {
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ScreenMeSettings
 | 
			
		||||
export default TabMeSettings
 | 
			
		||||
 
 | 
			
		||||
@@ -5,10 +5,10 @@ import {
 | 
			
		||||
} from '@utils/slices/settingsSlice'
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import { useTheme } from '@utils/styles/ThemeManager'
 | 
			
		||||
import Constants from 'expo-constants'
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { useTranslation } from 'react-i18next'
 | 
			
		||||
import { StyleSheet, Text } from 'react-native'
 | 
			
		||||
import { Constants } from 'react-native-unimodules'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
const SettingsAnalytics: React.FC = () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,11 +5,7 @@ import { useActionSheet } from '@expo/react-native-action-sheet'
 | 
			
		||||
import { useNavigation } from '@react-navigation/native'
 | 
			
		||||
import { LOCALES } from '@root/i18n/locales'
 | 
			
		||||
import androidDefaults from '@utils/slices/instances/push/androidDefaults'
 | 
			
		||||
import {
 | 
			
		||||
  getInstanceActive,
 | 
			
		||||
  getInstancePush,
 | 
			
		||||
  getInstances
 | 
			
		||||
} from '@utils/slices/instancesSlice'
 | 
			
		||||
import { getInstances } from '@utils/slices/instancesSlice'
 | 
			
		||||
import {
 | 
			
		||||
  changeBrowser,
 | 
			
		||||
  changeLanguage,
 | 
			
		||||
@@ -24,7 +20,7 @@ import React from 'react'
 | 
			
		||||
import { useTranslation } from 'react-i18next'
 | 
			
		||||
import { Platform } from 'react-native'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import { mapFontsizeToName } from '../Fontsize'
 | 
			
		||||
import { mapFontsizeToName } from '../SettingsFontsize'
 | 
			
		||||
 | 
			
		||||
const SettingsApp: React.FC = () => {
 | 
			
		||||
  const navigation = useNavigation()
 | 
			
		||||
@@ -34,43 +30,22 @@ const SettingsApp: React.FC = () => {
 | 
			
		||||
  const { t, i18n } = useTranslation('screenTabs')
 | 
			
		||||
 | 
			
		||||
  const instances = useSelector(getInstances, () => true)
 | 
			
		||||
  const instanceActive = useSelector(getInstanceActive)
 | 
			
		||||
  const settingsFontsize = useSelector(getSettingsFontsize)
 | 
			
		||||
  const settingsTheme = useSelector(getSettingsTheme)
 | 
			
		||||
  const settingsBrowser = useSelector(getSettingsBrowser)
 | 
			
		||||
  const instancePush = useSelector(
 | 
			
		||||
    getInstancePush,
 | 
			
		||||
    (prev, next) => prev?.global.value === next?.global.value
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <MenuContainer>
 | 
			
		||||
      {instanceActive !== -1 ? (
 | 
			
		||||
        <>
 | 
			
		||||
          <MenuRow
 | 
			
		||||
            title={t('me.settings.push.heading')}
 | 
			
		||||
            content={
 | 
			
		||||
              instancePush?.global.value
 | 
			
		||||
                ? t('me.settings.push.content.enabled')
 | 
			
		||||
                : t('me.settings.push.content.disabled')
 | 
			
		||||
            }
 | 
			
		||||
            iconBack='ChevronRight'
 | 
			
		||||
            onPress={() => {
 | 
			
		||||
              navigation.navigate('Tab-Me-Settings-Push')
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
          <MenuRow
 | 
			
		||||
            title={t('me.settings.fontsize.heading')}
 | 
			
		||||
            content={t(
 | 
			
		||||
              `me.settings.fontsize.content.${mapFontsizeToName(settingsFontsize)}`
 | 
			
		||||
            )}
 | 
			
		||||
            iconBack='ChevronRight'
 | 
			
		||||
            onPress={() => {
 | 
			
		||||
              navigation.navigate('Tab-Me-Settings-Fontsize')
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </>
 | 
			
		||||
      ) : null}
 | 
			
		||||
      <MenuRow
 | 
			
		||||
        title={t('me.settings.fontsize.heading')}
 | 
			
		||||
        content={t(
 | 
			
		||||
          `me.settings.fontsize.content.${mapFontsizeToName(settingsFontsize)}`
 | 
			
		||||
        )}
 | 
			
		||||
        iconBack='ChevronRight'
 | 
			
		||||
        onPress={() => {
 | 
			
		||||
          navigation.navigate('Tab-Me-Settings-Fontsize')
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
      <MenuRow
 | 
			
		||||
        title={t('me.settings.language.heading')}
 | 
			
		||||
        // @ts-ignore
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import Button from '@components/Button'
 | 
			
		||||
import { MenuContainer, MenuRow } from '@components/Menu'
 | 
			
		||||
import { displayMessage } from '@components/Message'
 | 
			
		||||
import { useActionSheet } from '@expo/react-native-action-sheet'
 | 
			
		||||
import { persistor } from '@root/store'
 | 
			
		||||
import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
 | 
			
		||||
@@ -50,12 +51,21 @@ const SettingsDev: React.FC = () => {
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      />
 | 
			
		||||
      <Button
 | 
			
		||||
        type='text'
 | 
			
		||||
        content={'Test flash message'}
 | 
			
		||||
        style={{
 | 
			
		||||
          marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2,
 | 
			
		||||
          marginBottom: StyleConstants.Spacing.Global.PagePadding
 | 
			
		||||
        }}
 | 
			
		||||
        onPress={() => displayMessage({ message: 'This is a testing message' })}
 | 
			
		||||
      />
 | 
			
		||||
      <Button
 | 
			
		||||
        type='text'
 | 
			
		||||
        content={'Purge secure storage'}
 | 
			
		||||
        style={{
 | 
			
		||||
          marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2,
 | 
			
		||||
          marginBottom: StyleConstants.Spacing.Global.PagePadding * 2
 | 
			
		||||
          marginBottom: StyleConstants.Spacing.Global.PagePadding
 | 
			
		||||
        }}
 | 
			
		||||
        destructive
 | 
			
		||||
        onPress={() => persistor.purge()}
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@ export const mapFontsizeToName = (size: SettingsState['fontsize']) => {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ScreenMeSettingsFontsize: React.FC<StackScreenProps<
 | 
			
		||||
const TabMeSettingsFontsize: React.FC<StackScreenProps<
 | 
			
		||||
  Nav.TabMeStackParamList,
 | 
			
		||||
  'Tab-Me-Settings-Fontsize'
 | 
			
		||||
>> = () => {
 | 
			
		||||
@@ -183,4 +183,4 @@ const styles = StyleSheet.create({
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default ScreenMeSettingsFontsize
 | 
			
		||||
export default TabMeSettingsFontsize
 | 
			
		||||
@@ -8,7 +8,7 @@ import ScreenMeSwitchRoot from './Switch/Root'
 | 
			
		||||
 | 
			
		||||
const Stack = createNativeStackNavigator()
 | 
			
		||||
 | 
			
		||||
const ScreenMeSwitch: React.FC<StackScreenProps<
 | 
			
		||||
const TabMeSwitch: React.FC<StackScreenProps<
 | 
			
		||||
  Nav.TabMeStackParamList,
 | 
			
		||||
  'Tab-Me-Switch'
 | 
			
		||||
>> = ({ navigation }) => {
 | 
			
		||||
@@ -44,4 +44,4 @@ const ScreenMeSwitch: React.FC<StackScreenProps<
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ScreenMeSwitch
 | 
			
		||||
export default TabMeSwitch
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ import TimelineDefault from '@components/Timeline/Default'
 | 
			
		||||
import { useAccountQuery } from '@utils/queryHooks/account'
 | 
			
		||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
 | 
			
		||||
import { useTheme } from '@utils/styles/ThemeManager'
 | 
			
		||||
import React, { useCallback, useEffect, useMemo, useReducer } from 'react'
 | 
			
		||||
import React, { useCallback, useEffect, useMemo } from 'react'
 | 
			
		||||
import { useTranslation } from 'react-i18next'
 | 
			
		||||
import { StyleSheet, View } from 'react-native'
 | 
			
		||||
import { useSharedValue } from 'react-native-reanimated'
 | 
			
		||||
@@ -13,9 +13,6 @@ import AccountAttachments from './Account/Attachments'
 | 
			
		||||
import AccountHeader from './Account/Header'
 | 
			
		||||
import AccountInformation from './Account/Information'
 | 
			
		||||
import AccountNav from './Account/Nav'
 | 
			
		||||
import AccountContext from './Account/utils/createContext'
 | 
			
		||||
import accountInitialState from './Account/utils/initialState'
 | 
			
		||||
import accountReducer from './Account/utils/reducer'
 | 
			
		||||
import { SharedAccountProp } from './sharedScreens'
 | 
			
		||||
 | 
			
		||||
const TabSharedAccount: React.FC<SharedAccountProp> = ({
 | 
			
		||||
@@ -30,10 +27,6 @@ const TabSharedAccount: React.FC<SharedAccountProp> = ({
 | 
			
		||||
  const { data } = useAccountQuery({ id: account.id })
 | 
			
		||||
 | 
			
		||||
  const scrollY = useSharedValue(0)
 | 
			
		||||
  const [accountState, accountDispatch] = useReducer(
 | 
			
		||||
    accountReducer,
 | 
			
		||||
    accountInitialState
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const updateHeaderRight = () =>
 | 
			
		||||
@@ -86,7 +79,7 @@ const TabSharedAccount: React.FC<SharedAccountProp> = ({
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <AccountContext.Provider value={{ accountState, accountDispatch }}>
 | 
			
		||||
    <>
 | 
			
		||||
      <AccountNav scrollY={scrollY} account={data} />
 | 
			
		||||
 | 
			
		||||
      <Timeline
 | 
			
		||||
@@ -98,7 +91,7 @@ const TabSharedAccount: React.FC<SharedAccountProp> = ({
 | 
			
		||||
          ListHeaderComponent
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    </AccountContext.Provider>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,36 +1,50 @@
 | 
			
		||||
import Button from '@components/Button'
 | 
			
		||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
 | 
			
		||||
import { useTheme } from '@utils/styles/ThemeManager'
 | 
			
		||||
import React, { useContext } from 'react'
 | 
			
		||||
import { Dimensions, Image } from 'react-native'
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Dimensions, Image, View } from 'react-native'
 | 
			
		||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
 | 
			
		||||
import AccountContext from './utils/createContext'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  account?: Mastodon.Account
 | 
			
		||||
  limitHeight?: boolean
 | 
			
		||||
  edit?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AccountHeader: React.FC<Props> = ({ account }) => {
 | 
			
		||||
  const { accountState } = useContext(AccountContext)
 | 
			
		||||
  const { reduceMotionEnabled } = useAccessibility()
 | 
			
		||||
  const { theme } = useTheme()
 | 
			
		||||
  const topInset = useSafeAreaInsets().top
 | 
			
		||||
const AccountHeader = React.memo(
 | 
			
		||||
  ({ account, edit }: Props) => {
 | 
			
		||||
    const { reduceMotionEnabled } = useAccessibility()
 | 
			
		||||
    const { theme } = useTheme()
 | 
			
		||||
    const topInset = useSafeAreaInsets().top
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Image
 | 
			
		||||
      source={{
 | 
			
		||||
        uri: reduceMotionEnabled ? account?.header_static : account?.header
 | 
			
		||||
      }}
 | 
			
		||||
      style={{
 | 
			
		||||
        height:
 | 
			
		||||
          Dimensions.get('screen').width * accountState.headerRatio + topInset,
 | 
			
		||||
        backgroundColor: theme.disabled
 | 
			
		||||
      }}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default React.memo(
 | 
			
		||||
  AccountHeader,
 | 
			
		||||
    return (
 | 
			
		||||
      <View>
 | 
			
		||||
        <Image
 | 
			
		||||
          source={{
 | 
			
		||||
            uri: reduceMotionEnabled ? account?.header_static : account?.header
 | 
			
		||||
          }}
 | 
			
		||||
          style={{
 | 
			
		||||
            height: Dimensions.get('screen').width / 3 + topInset,
 | 
			
		||||
            backgroundColor: theme.disabled
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        {edit ? (
 | 
			
		||||
          <View
 | 
			
		||||
            style={{
 | 
			
		||||
              position: 'absolute',
 | 
			
		||||
              width: '100%',
 | 
			
		||||
              height: '100%',
 | 
			
		||||
              alignContent: 'center',
 | 
			
		||||
              justifyContent: 'center',
 | 
			
		||||
              alignItems: 'center'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Button type='icon' content='Edit' round onPress={() => {}} />
 | 
			
		||||
          </View>
 | 
			
		||||
        ) : null}
 | 
			
		||||
      </View>
 | 
			
		||||
    )
 | 
			
		||||
  },
 | 
			
		||||
  (_, next) => next.account === undefined
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export default AccountHeader
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,7 @@
 | 
			
		||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import { useTheme } from '@utils/styles/ThemeManager'
 | 
			
		||||
import React, { useCallback } from 'react'
 | 
			
		||||
import { StyleSheet, View } from 'react-native'
 | 
			
		||||
import { useSelector } from 'react-redux'
 | 
			
		||||
import { Placeholder, Fade } from 'rn-placeholder'
 | 
			
		||||
import AccountInformationAccount from './Information/Account'
 | 
			
		||||
import AccountInformationActions from './Information/Actions'
 | 
			
		||||
@@ -11,69 +9,59 @@ import AccountInformationAvatar from './Information/Avatar'
 | 
			
		||||
import AccountInformationCreated from './Information/Created'
 | 
			
		||||
import AccountInformationFields from './Information/Fields'
 | 
			
		||||
import AccountInformationName from './Information/Name'
 | 
			
		||||
import AccountInformationNotes from './Information/Notes'
 | 
			
		||||
import AccountInformationNote from './Information/Note'
 | 
			
		||||
import AccountInformationStats from './Information/Stats'
 | 
			
		||||
import AccountInformationSwitch from './Information/Switch'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  account: Mastodon.Account | undefined
 | 
			
		||||
  myInfo?: boolean // Showing from my info page
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AccountInformation: React.FC<Props> = ({ account, myInfo = false }) => {
 | 
			
		||||
  const ownAccount =
 | 
			
		||||
    account?.id ===
 | 
			
		||||
    useSelector(getInstanceAccount, (prev, next) => prev?.id === next?.id)?.id
 | 
			
		||||
  const { mode, theme } = useTheme()
 | 
			
		||||
const AccountInformation = React.memo(
 | 
			
		||||
  ({ account, myInfo = false }: Props) => {
 | 
			
		||||
    const { mode, theme } = useTheme()
 | 
			
		||||
 | 
			
		||||
  const animation = useCallback(
 | 
			
		||||
    props => (
 | 
			
		||||
      <Fade {...props} style={{ backgroundColor: theme.shimmerHighlight }} />
 | 
			
		||||
    ),
 | 
			
		||||
    [mode]
 | 
			
		||||
  )
 | 
			
		||||
    const animation = useCallback(
 | 
			
		||||
      props => (
 | 
			
		||||
        <Fade {...props} style={{ backgroundColor: theme.shimmerHighlight }} />
 | 
			
		||||
      ),
 | 
			
		||||
      [mode]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <View style={styles.base}>
 | 
			
		||||
      <Placeholder Animation={animation}>
 | 
			
		||||
        <View style={styles.avatarAndActions}>
 | 
			
		||||
          <AccountInformationAvatar account={account} myInfo={myInfo} />
 | 
			
		||||
          <View style={styles.actions}>
 | 
			
		||||
            {myInfo ? (
 | 
			
		||||
              <AccountInformationSwitch />
 | 
			
		||||
            ) : (
 | 
			
		||||
              <AccountInformationActions
 | 
			
		||||
                account={account}
 | 
			
		||||
                ownAccount={ownAccount}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
    return (
 | 
			
		||||
      <View style={styles.base}>
 | 
			
		||||
        <Placeholder Animation={animation}>
 | 
			
		||||
          <View style={styles.avatarAndActions}>
 | 
			
		||||
            <AccountInformationAvatar account={account} myInfo={myInfo} />
 | 
			
		||||
            <AccountInformationActions account={account} myInfo={myInfo} />
 | 
			
		||||
          </View>
 | 
			
		||||
        </View>
 | 
			
		||||
 | 
			
		||||
        <AccountInformationName account={account} />
 | 
			
		||||
          <AccountInformationName account={account} />
 | 
			
		||||
 | 
			
		||||
        <AccountInformationAccount account={account} myInfo={myInfo} />
 | 
			
		||||
          <AccountInformationAccount account={account} localInstance={myInfo} />
 | 
			
		||||
 | 
			
		||||
        {!myInfo ? (
 | 
			
		||||
          <>
 | 
			
		||||
            {account?.fields && account.fields.length > 0 ? (
 | 
			
		||||
              <AccountInformationFields account={account} />
 | 
			
		||||
            ) : null}
 | 
			
		||||
            {account?.note &&
 | 
			
		||||
            account.note.length > 0 &&
 | 
			
		||||
            account.note !== '<p></p>' ? (
 | 
			
		||||
              // Empty notes might generate empty p tag
 | 
			
		||||
              <AccountInformationNotes account={account} />
 | 
			
		||||
            ) : null}
 | 
			
		||||
            <AccountInformationCreated account={account} />
 | 
			
		||||
          </>
 | 
			
		||||
        ) : null}
 | 
			
		||||
          <AccountInformationFields account={account} myInfo={myInfo} />
 | 
			
		||||
 | 
			
		||||
        <AccountInformationStats account={account} myInfo={myInfo} />
 | 
			
		||||
      </Placeholder>
 | 
			
		||||
    </View>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
          <AccountInformationNote account={account} myInfo={myInfo} />
 | 
			
		||||
 | 
			
		||||
          <AccountInformationCreated account={account} hidden={myInfo} />
 | 
			
		||||
 | 
			
		||||
          <AccountInformationStats account={account} />
 | 
			
		||||
        </Placeholder>
 | 
			
		||||
      </View>
 | 
			
		||||
    )
 | 
			
		||||
  },
 | 
			
		||||
  (prev, next) => {
 | 
			
		||||
    let skipUpdate = true
 | 
			
		||||
    if (prev.account?.id !== next.account?.id) {
 | 
			
		||||
      skipUpdate = false
 | 
			
		||||
    }
 | 
			
		||||
    if (prev.account?.acct === next.account?.acct) {
 | 
			
		||||
      skipUpdate = false
 | 
			
		||||
    }
 | 
			
		||||
    return skipUpdate
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  base: {
 | 
			
		||||
@@ -90,13 +78,4 @@ const styles = StyleSheet.create({
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default React.memo(AccountInformation, (prev, next) => {
 | 
			
		||||
  let skipUpdate = true
 | 
			
		||||
  if (prev.account?.id !== next.account?.id) {
 | 
			
		||||
    skipUpdate = false
 | 
			
		||||
  }
 | 
			
		||||
  if (prev.account?.acct === next.account?.acct) {
 | 
			
		||||
    skipUpdate = false
 | 
			
		||||
  }
 | 
			
		||||
  return skipUpdate
 | 
			
		||||
})
 | 
			
		||||
export default AccountInformation
 | 
			
		||||
 
 | 
			
		||||
@@ -12,10 +12,10 @@ import { PlaceholderLine } from 'rn-placeholder'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  account: Mastodon.Account | undefined
 | 
			
		||||
  myInfo: boolean
 | 
			
		||||
  localInstance: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => {
 | 
			
		||||
const AccountInformationAccount: React.FC<Props> = ({ account, localInstance }) => {
 | 
			
		||||
  const { theme } = useTheme()
 | 
			
		||||
  const instanceAccount = useSelector(
 | 
			
		||||
    getInstanceAccount,
 | 
			
		||||
@@ -48,7 +48,7 @@ const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => {
 | 
			
		||||
    }
 | 
			
		||||
  }, [account?.moved])
 | 
			
		||||
 | 
			
		||||
  if (account || (myInfo && instanceAccount)) {
 | 
			
		||||
  if (account || (localInstance && instanceAccount)) {
 | 
			
		||||
    return (
 | 
			
		||||
      <View
 | 
			
		||||
        style={[styles.base, { flexDirection: 'row', alignItems: 'center' }]}
 | 
			
		||||
@@ -63,8 +63,8 @@ const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => {
 | 
			
		||||
          ]}
 | 
			
		||||
          selectable
 | 
			
		||||
        >
 | 
			
		||||
          @{myInfo ? instanceAccount?.acct : account?.acct}
 | 
			
		||||
          {myInfo ? `@${instanceUri}` : null}
 | 
			
		||||
          @{localInstance ? instanceAccount?.acct : account?.acct}
 | 
			
		||||
          {localInstance ? `@${instanceUri}` : null}
 | 
			
		||||
        </Text>
 | 
			
		||||
        {movedContent}
 | 
			
		||||
        {account?.locked ? (
 | 
			
		||||
 
 | 
			
		||||
@@ -2,34 +2,21 @@ import analytics from '@components/analytics'
 | 
			
		||||
import Button from '@components/Button'
 | 
			
		||||
import { RelationshipOutgoing } from '@components/Relationship'
 | 
			
		||||
import { useNavigation } from '@react-navigation/native'
 | 
			
		||||
import { StackNavigationProp } from '@react-navigation/stack'
 | 
			
		||||
import { useRelationshipQuery } from '@utils/queryHooks/relationship'
 | 
			
		||||
import {
 | 
			
		||||
  getInstanceAccount,
 | 
			
		||||
  getInstancePush,
 | 
			
		||||
  getInstanceUri
 | 
			
		||||
} from '@utils/slices/instancesSlice'
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { useTranslation } from 'react-i18next'
 | 
			
		||||
import { StyleSheet } from 'react-native'
 | 
			
		||||
import { StyleSheet, View } from 'react-native'
 | 
			
		||||
import { useSelector } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  account: Mastodon.Account | undefined
 | 
			
		||||
  ownAccount: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const GoToMoved = ({ accountMoved }: { accountMoved: Mastodon.Account }) => {
 | 
			
		||||
  const { t } = useTranslation('screenTabs')
 | 
			
		||||
  const navigation = useNavigation<
 | 
			
		||||
    StackNavigationProp<Nav.TabLocalStackParamList>
 | 
			
		||||
  >()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Button
 | 
			
		||||
      type='text'
 | 
			
		||||
      content={t('shared.account.moved')}
 | 
			
		||||
      onPress={() => {
 | 
			
		||||
        analytics('account_gotomoved_press')
 | 
			
		||||
        navigation.push('Tab-Shared-Account', { account: accountMoved })
 | 
			
		||||
      }}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
  myInfo?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Conversation = ({ account }: { account: Mastodon.Account }) => {
 | 
			
		||||
@@ -41,7 +28,7 @@ const Conversation = ({ account }: { account: Mastodon.Account }) => {
 | 
			
		||||
      round
 | 
			
		||||
      type='icon'
 | 
			
		||||
      content='Mail'
 | 
			
		||||
      style={styles.actionConversation}
 | 
			
		||||
      style={styles.actionLeft}
 | 
			
		||||
      onPress={() => {
 | 
			
		||||
        analytics('account_DM_press')
 | 
			
		||||
        navigation.navigate('Screen-Compose', {
 | 
			
		||||
@@ -53,24 +40,76 @@ const Conversation = ({ account }: { account: Mastodon.Account }) => {
 | 
			
		||||
  ) : null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AccountInformationActions: React.FC<Props> = ({
 | 
			
		||||
  account,
 | 
			
		||||
  ownAccount
 | 
			
		||||
}) => {
 | 
			
		||||
  return account && account.id ? (
 | 
			
		||||
    account.moved ? (
 | 
			
		||||
      <GoToMoved accountMoved={account.moved} />
 | 
			
		||||
    ) : !ownAccount ? (
 | 
			
		||||
      <>
 | 
			
		||||
const AccountInformationActions: React.FC<Props> = ({ account, myInfo }) => {
 | 
			
		||||
  const { t } = useTranslation('screenTabs')
 | 
			
		||||
  const navigation = useNavigation()
 | 
			
		||||
 | 
			
		||||
  if (account?.moved) {
 | 
			
		||||
    const accountMoved = account.moved
 | 
			
		||||
    return (
 | 
			
		||||
      <View style={styles.base}>
 | 
			
		||||
        <Button
 | 
			
		||||
          type='text'
 | 
			
		||||
          content={t('shared.account.moved')}
 | 
			
		||||
          onPress={() => {
 | 
			
		||||
            analytics('account_gotomoved_press')
 | 
			
		||||
            // @ts-ignore
 | 
			
		||||
            navigation.push('Tab-Shared-Account', { account: accountMoved })
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      </View>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const instancePush = useSelector(
 | 
			
		||||
    getInstancePush,
 | 
			
		||||
    (prev, next) => prev?.global.value === next?.global.value
 | 
			
		||||
  )
 | 
			
		||||
  const instanceUri = useSelector(getInstanceUri)
 | 
			
		||||
 | 
			
		||||
  if (myInfo) {
 | 
			
		||||
    return (
 | 
			
		||||
      <View style={styles.base}>
 | 
			
		||||
        <Button
 | 
			
		||||
          round
 | 
			
		||||
          type='icon'
 | 
			
		||||
          content={instancePush?.global.value ? 'Bell' : 'BellOff'}
 | 
			
		||||
          style={styles.actionLeft}
 | 
			
		||||
          onPress={() => navigation.navigate('Tab-Me-Push')}
 | 
			
		||||
        />
 | 
			
		||||
        <Button
 | 
			
		||||
          type='text'
 | 
			
		||||
          disabled={account === undefined}
 | 
			
		||||
          content={t('me.stacks.profile.name')}
 | 
			
		||||
          onPress={() => navigation.navigate('Tab-Me-Profile')}
 | 
			
		||||
        />
 | 
			
		||||
      </View>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const instanceAccount = useSelector(getInstanceAccount, () => true)
 | 
			
		||||
  const ownAccount =
 | 
			
		||||
    account?.id === instanceAccount?.id &&
 | 
			
		||||
    account?.acct === instanceAccount?.acct
 | 
			
		||||
 | 
			
		||||
  if (!ownAccount && account) {
 | 
			
		||||
    return (
 | 
			
		||||
      <View style={styles.base}>
 | 
			
		||||
        <Conversation account={account} />
 | 
			
		||||
        <RelationshipOutgoing id={account.id} />
 | 
			
		||||
      </>
 | 
			
		||||
    ) : null
 | 
			
		||||
  ) : null
 | 
			
		||||
      </View>
 | 
			
		||||
    )
 | 
			
		||||
  } else {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  actionConversation: { marginRight: StyleConstants.Spacing.S }
 | 
			
		||||
  base: {
 | 
			
		||||
    alignSelf: 'flex-end',
 | 
			
		||||
    flexDirection: 'row'
 | 
			
		||||
  },
 | 
			
		||||
  actionLeft: { marginRight: StyleConstants.Spacing.S }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default AccountInformationActions
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,24 @@
 | 
			
		||||
import analytics from '@components/analytics'
 | 
			
		||||
import Button from '@components/Button'
 | 
			
		||||
import GracefullyImage from '@components/GracefullyImage'
 | 
			
		||||
import { useNavigation } from '@react-navigation/native'
 | 
			
		||||
import { StackNavigationProp } from '@react-navigation/stack'
 | 
			
		||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Pressable, StyleSheet } from 'react-native'
 | 
			
		||||
import { Pressable, StyleSheet, View } from 'react-native'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  account: Mastodon.Account | undefined
 | 
			
		||||
  myInfo: boolean
 | 
			
		||||
  edit?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AccountInformationAvatar: React.FC<Props> = ({ account, myInfo }) => {
 | 
			
		||||
const AccountInformationAvatar: React.FC<Props> = ({
 | 
			
		||||
  account,
 | 
			
		||||
  myInfo,
 | 
			
		||||
  edit
 | 
			
		||||
}) => {
 | 
			
		||||
  const navigation = useNavigation<
 | 
			
		||||
    StackNavigationProp<Nav.TabLocalStackParamList>
 | 
			
		||||
  >()
 | 
			
		||||
@@ -36,6 +42,20 @@ const AccountInformationAvatar: React.FC<Props> = ({ account, myInfo }) => {
 | 
			
		||||
            : account?.avatar
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
      {edit ? (
 | 
			
		||||
        <View
 | 
			
		||||
          style={{
 | 
			
		||||
            position: 'absolute',
 | 
			
		||||
            width: '100%',
 | 
			
		||||
            height: '100%',
 | 
			
		||||
            alignContent: 'center',
 | 
			
		||||
            justifyContent: 'center',
 | 
			
		||||
            alignItems: 'center'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Button type='icon' content='Edit' round onPress={() => {}} />
 | 
			
		||||
        </View>
 | 
			
		||||
      ) : null}
 | 
			
		||||
    </Pressable>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,55 +8,63 @@ import { PlaceholderLine } from 'rn-placeholder'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  account: Mastodon.Account | undefined
 | 
			
		||||
  hidden?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AccountInformationCreated: React.FC<Props> = ({ account }) => {
 | 
			
		||||
  const { i18n } = useTranslation()
 | 
			
		||||
  const { theme } = useTheme()
 | 
			
		||||
  const { t } = useTranslation('screenTabs')
 | 
			
		||||
const AccountInformationCreated = React.memo(
 | 
			
		||||
  ({ account, hidden = false }: Props) => {
 | 
			
		||||
    if (hidden) {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  if (account) {
 | 
			
		||||
    return (
 | 
			
		||||
      <View
 | 
			
		||||
        style={[styles.base, { flexDirection: 'row', alignItems: 'center' }]}
 | 
			
		||||
      >
 | 
			
		||||
        <Icon
 | 
			
		||||
          name='Calendar'
 | 
			
		||||
          size={StyleConstants.Font.Size.S}
 | 
			
		||||
          color={theme.secondary}
 | 
			
		||||
          style={styles.icon}
 | 
			
		||||
        />
 | 
			
		||||
        <Text
 | 
			
		||||
          style={{
 | 
			
		||||
            color: theme.secondary,
 | 
			
		||||
            ...StyleConstants.FontStyle.S
 | 
			
		||||
          }}
 | 
			
		||||
    const { i18n } = useTranslation()
 | 
			
		||||
    const { theme } = useTheme()
 | 
			
		||||
    const { t } = useTranslation('screenTabs')
 | 
			
		||||
 | 
			
		||||
    if (account) {
 | 
			
		||||
      return (
 | 
			
		||||
        <View
 | 
			
		||||
          style={[styles.base, { flexDirection: 'row', alignItems: 'center' }]}
 | 
			
		||||
        >
 | 
			
		||||
          {t('shared.account.created_at', {
 | 
			
		||||
            date: new Date(account.created_at || '').toLocaleDateString(
 | 
			
		||||
              i18n.language,
 | 
			
		||||
              {
 | 
			
		||||
                year: 'numeric',
 | 
			
		||||
                month: 'long',
 | 
			
		||||
                day: 'numeric'
 | 
			
		||||
              }
 | 
			
		||||
            )
 | 
			
		||||
          })}
 | 
			
		||||
        </Text>
 | 
			
		||||
      </View>
 | 
			
		||||
    )
 | 
			
		||||
  } else {
 | 
			
		||||
    return (
 | 
			
		||||
      <PlaceholderLine
 | 
			
		||||
        width={StyleConstants.Font.Size.S * 4}
 | 
			
		||||
        height={StyleConstants.Font.LineHeight.S}
 | 
			
		||||
        color={theme.shimmerDefault}
 | 
			
		||||
        noMargin
 | 
			
		||||
        style={styles.base}
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
          <Icon
 | 
			
		||||
            name='Calendar'
 | 
			
		||||
            size={StyleConstants.Font.Size.S}
 | 
			
		||||
            color={theme.secondary}
 | 
			
		||||
            style={styles.icon}
 | 
			
		||||
          />
 | 
			
		||||
          <Text
 | 
			
		||||
            style={{
 | 
			
		||||
              color: theme.secondary,
 | 
			
		||||
              ...StyleConstants.FontStyle.S
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            {t('shared.account.created_at', {
 | 
			
		||||
              date: new Date(account.created_at || '').toLocaleDateString(
 | 
			
		||||
                i18n.language,
 | 
			
		||||
                {
 | 
			
		||||
                  year: 'numeric',
 | 
			
		||||
                  month: 'long',
 | 
			
		||||
                  day: 'numeric'
 | 
			
		||||
                }
 | 
			
		||||
              )
 | 
			
		||||
            })}
 | 
			
		||||
          </Text>
 | 
			
		||||
        </View>
 | 
			
		||||
      )
 | 
			
		||||
    } else {
 | 
			
		||||
      return (
 | 
			
		||||
        <PlaceholderLine
 | 
			
		||||
          width={StyleConstants.Font.Size.S * 4}
 | 
			
		||||
          height={StyleConstants.Font.LineHeight.S}
 | 
			
		||||
          color={theme.shimmerDefault}
 | 
			
		||||
          noMargin
 | 
			
		||||
          style={styles.base}
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  (_, next) => next.account === undefined
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  base: {
 | 
			
		||||
@@ -68,7 +76,4 @@ const styles = StyleSheet.create({
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default React.memo(
 | 
			
		||||
  AccountInformationCreated,
 | 
			
		||||
  (_, next) => next.account === undefined
 | 
			
		||||
)
 | 
			
		||||
export default AccountInformationCreated
 | 
			
		||||
 
 | 
			
		||||
@@ -6,11 +6,16 @@ import React from 'react'
 | 
			
		||||
import { StyleSheet, View } from 'react-native'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  account: Mastodon.Account
 | 
			
		||||
  account: Mastodon.Account | undefined
 | 
			
		||||
  myInfo?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AccountInformationFields = React.memo(
 | 
			
		||||
  ({ account }: Props) => {
 | 
			
		||||
  ({ account, myInfo }: Props) => {
 | 
			
		||||
    if (myInfo || !account?.fields || account.fields.length === 0) {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { theme } = useTheme()
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
@@ -88,3 +93,6 @@ const styles = StyleSheet.create({
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default AccountInformationFields
 | 
			
		||||
function htmlToText (note: string): any {
 | 
			
		||||
  throw new Error('Function not implemented.')
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,26 +1,19 @@
 | 
			
		||||
import Input from '@components/Input'
 | 
			
		||||
import { ParseEmojis } from '@components/Parse'
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import { useTheme } from '@utils/styles/ThemeManager'
 | 
			
		||||
import React, { useMemo } from 'react'
 | 
			
		||||
import React, { useMemo, useState } from 'react'
 | 
			
		||||
import { StyleSheet, Text, View } from 'react-native'
 | 
			
		||||
import { PlaceholderLine } from 'rn-placeholder'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  account: Mastodon.Account | undefined
 | 
			
		||||
  edit?: boolean // Editing mode
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AccountInformationName: React.FC<Props> = ({ account }) => {
 | 
			
		||||
const AccountInformationName: React.FC<Props> = ({ account, edit }) => {
 | 
			
		||||
  const { theme } = useTheme()
 | 
			
		||||
 | 
			
		||||
  const movedStyle = useMemo(
 | 
			
		||||
    () =>
 | 
			
		||||
      StyleSheet.create({
 | 
			
		||||
        base: {
 | 
			
		||||
          textDecorationLine: account?.moved ? 'line-through' : undefined
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
    [account?.moved]
 | 
			
		||||
  )
 | 
			
		||||
  const movedContent = useMemo(() => {
 | 
			
		||||
    if (account?.moved) {
 | 
			
		||||
      return (
 | 
			
		||||
@@ -36,20 +29,30 @@ const AccountInformationName: React.FC<Props> = ({ account }) => {
 | 
			
		||||
    }
 | 
			
		||||
  }, [account?.moved])
 | 
			
		||||
 | 
			
		||||
  const [displatName, setDisplayName] = useState(account?.display_name)
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <View style={[styles.base, { flexDirection: 'row' }]}>
 | 
			
		||||
      {account ? (
 | 
			
		||||
        <>
 | 
			
		||||
          <Text style={movedStyle.base}>
 | 
			
		||||
            <ParseEmojis
 | 
			
		||||
              content={account.display_name || account.username}
 | 
			
		||||
              emojis={account.emojis}
 | 
			
		||||
              size='L'
 | 
			
		||||
              fontBold
 | 
			
		||||
            />
 | 
			
		||||
          </Text>
 | 
			
		||||
          {movedContent}
 | 
			
		||||
        </>
 | 
			
		||||
        edit ? (
 | 
			
		||||
          <Input title='昵称' value={displatName} setValue={setDisplayName} />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <>
 | 
			
		||||
            <Text
 | 
			
		||||
              style={{
 | 
			
		||||
                textDecorationLine: account?.moved ? 'line-through' : undefined
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <ParseEmojis
 | 
			
		||||
                content={account.display_name || account.username}
 | 
			
		||||
                emojis={account.emojis}
 | 
			
		||||
                size='L'
 | 
			
		||||
                fontBold
 | 
			
		||||
              />
 | 
			
		||||
            </Text>
 | 
			
		||||
            {movedContent}
 | 
			
		||||
          </>
 | 
			
		||||
        )
 | 
			
		||||
      ) : (
 | 
			
		||||
        <PlaceholderLine
 | 
			
		||||
          width={StyleConstants.Font.Size.L * 2}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										44
									
								
								src/screens/Tabs/Shared/Account/Information/Note.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/screens/Tabs/Shared/Account/Information/Note.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
import Input from '@components/Input'
 | 
			
		||||
import { ParseHTML } from '@components/Parse'
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import React, { useState } from 'react'
 | 
			
		||||
import { StyleSheet, View } from 'react-native'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  account: Mastodon.Account | undefined
 | 
			
		||||
  myInfo?: boolean
 | 
			
		||||
  edit?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AccountInformationNote = React.memo(
 | 
			
		||||
  ({ account, myInfo, edit }: Props) => {
 | 
			
		||||
    const [note, setNote] = useState(account?.source?.note)
 | 
			
		||||
    if (edit) {
 | 
			
		||||
      return <Input title='简介' value={note} setValue={setNote} multiline />
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      myInfo ||
 | 
			
		||||
      !account?.note ||
 | 
			
		||||
      account.note.length === 0 ||
 | 
			
		||||
      account.note === '<p></p>'
 | 
			
		||||
    ) {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <View style={styles.note}>
 | 
			
		||||
        <ParseHTML content={account.note!} size={'M'} emojis={account.emojis} />
 | 
			
		||||
      </View>
 | 
			
		||||
    )
 | 
			
		||||
  },
 | 
			
		||||
  () => true
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  note: {
 | 
			
		||||
    marginBottom: StyleConstants.Spacing.L
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default AccountInformationNote
 | 
			
		||||
@@ -1,27 +0,0 @@
 | 
			
		||||
import { ParseHTML } from '@components/Parse'
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { StyleSheet, View } from 'react-native'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  account: Mastodon.Account
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AccountInformationNotes = React.memo(
 | 
			
		||||
  ({ account }: Props) => {
 | 
			
		||||
    return (
 | 
			
		||||
      <View style={styles.note}>
 | 
			
		||||
        <ParseHTML content={account.note!} size={'M'} emojis={account.emojis} />
 | 
			
		||||
      </View>
 | 
			
		||||
    )
 | 
			
		||||
  },
 | 
			
		||||
  () => true
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  note: {
 | 
			
		||||
    marginBottom: StyleConstants.Spacing.L
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default AccountInformationNotes
 | 
			
		||||
@@ -10,10 +10,9 @@ import { PlaceholderLine } from 'rn-placeholder'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  account: Mastodon.Account | undefined
 | 
			
		||||
  myInfo: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => {
 | 
			
		||||
const AccountInformationStats: React.FC<Props> = ({ account }) => {
 | 
			
		||||
  const navigation = useNavigation<
 | 
			
		||||
    StackNavigationProp<Nav.TabLocalStackParamList>
 | 
			
		||||
  >()
 | 
			
		||||
@@ -28,12 +27,6 @@ const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => {
 | 
			
		||||
          children={t('shared.account.summary.statuses_count', {
 | 
			
		||||
            count: account.statuses_count || 0
 | 
			
		||||
          })}
 | 
			
		||||
          onPress={() => {
 | 
			
		||||
            analytics('account_stats_toots_press', {
 | 
			
		||||
              count: account.statuses_count
 | 
			
		||||
            })
 | 
			
		||||
            myInfo && navigation.push('Tab-Shared-Account', { account })
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <PlaceholderLine
 | 
			
		||||
@@ -46,7 +39,10 @@ const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => {
 | 
			
		||||
      )}
 | 
			
		||||
      {account ? (
 | 
			
		||||
        <Text
 | 
			
		||||
          style={[styles.stat, { color: theme.primaryDefault, textAlign: 'right' }]}
 | 
			
		||||
          style={[
 | 
			
		||||
            styles.stat,
 | 
			
		||||
            { color: theme.primaryDefault, textAlign: 'right' }
 | 
			
		||||
          ]}
 | 
			
		||||
          children={t('shared.account.summary.following_count', {
 | 
			
		||||
            count: account.following_count
 | 
			
		||||
          })}
 | 
			
		||||
@@ -73,7 +69,10 @@ const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => {
 | 
			
		||||
      )}
 | 
			
		||||
      {account ? (
 | 
			
		||||
        <Text
 | 
			
		||||
          style={[styles.stat, { color: theme.primaryDefault, textAlign: 'center' }]}
 | 
			
		||||
          style={[
 | 
			
		||||
            styles.stat,
 | 
			
		||||
            { color: theme.primaryDefault, textAlign: 'center' }
 | 
			
		||||
          ]}
 | 
			
		||||
          children={t('shared.account.summary.followers_count', {
 | 
			
		||||
            count: account.followers_count
 | 
			
		||||
          })}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { ParseEmojis } from '@components/Parse'
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import { useTheme } from '@utils/styles/ThemeManager'
 | 
			
		||||
import React, { useContext } from 'react'
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Dimensions, StyleSheet, Text, View } from 'react-native'
 | 
			
		||||
import Animated, {
 | 
			
		||||
  Extrapolate,
 | 
			
		||||
@@ -9,73 +9,74 @@ import Animated, {
 | 
			
		||||
  useAnimatedStyle
 | 
			
		||||
} from 'react-native-reanimated'
 | 
			
		||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
 | 
			
		||||
import AccountContext from './utils/createContext'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  scrollY: Animated.SharedValue<number>
 | 
			
		||||
  account: Mastodon.Account | undefined
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AccountNav: React.FC<Props> = ({ scrollY, account }) => {
 | 
			
		||||
  const { accountState } = useContext(AccountContext)
 | 
			
		||||
  const { theme } = useTheme()
 | 
			
		||||
  const headerHeight = useSafeAreaInsets().top + 44
 | 
			
		||||
const AccountNav = React.memo(
 | 
			
		||||
  ({ scrollY, account }: Props) => {
 | 
			
		||||
    const { theme } = useTheme()
 | 
			
		||||
    const headerHeight = useSafeAreaInsets().top + 44
 | 
			
		||||
 | 
			
		||||
  const nameY =
 | 
			
		||||
    Dimensions.get('screen').width * accountState.headerRatio +
 | 
			
		||||
    StyleConstants.Avatar.L -
 | 
			
		||||
    StyleConstants.Spacing.Global.PagePadding * 2 +
 | 
			
		||||
    StyleConstants.Spacing.M -
 | 
			
		||||
    headerHeight
 | 
			
		||||
    const nameY =
 | 
			
		||||
      Dimensions.get('screen').width / 3 +
 | 
			
		||||
      StyleConstants.Avatar.L -
 | 
			
		||||
      StyleConstants.Spacing.Global.PagePadding * 2 +
 | 
			
		||||
      StyleConstants.Spacing.M -
 | 
			
		||||
      headerHeight
 | 
			
		||||
 | 
			
		||||
  const styleOpacity = useAnimatedStyle(() => {
 | 
			
		||||
    return {
 | 
			
		||||
      opacity: interpolate(scrollY.value, [0, 200], [0, 1], Extrapolate.CLAMP)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  const styleMarginTop = useAnimatedStyle(() => {
 | 
			
		||||
    return {
 | 
			
		||||
      marginTop: interpolate(
 | 
			
		||||
        scrollY.value,
 | 
			
		||||
        [nameY, nameY + 20],
 | 
			
		||||
        [50, 0],
 | 
			
		||||
        Extrapolate.CLAMP
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
    const styleOpacity = useAnimatedStyle(() => {
 | 
			
		||||
      return {
 | 
			
		||||
        opacity: interpolate(scrollY.value, [0, 200], [0, 1], Extrapolate.CLAMP)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    const styleMarginTop = useAnimatedStyle(() => {
 | 
			
		||||
      return {
 | 
			
		||||
        marginTop: interpolate(
 | 
			
		||||
          scrollY.value,
 | 
			
		||||
          [nameY, nameY + 20],
 | 
			
		||||
          [50, 0],
 | 
			
		||||
          Extrapolate.CLAMP
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Animated.View
 | 
			
		||||
      style={[
 | 
			
		||||
        styles.base,
 | 
			
		||||
        styleOpacity,
 | 
			
		||||
        { backgroundColor: theme.backgroundDefault, height: headerHeight }
 | 
			
		||||
      ]}
 | 
			
		||||
    >
 | 
			
		||||
      <View
 | 
			
		||||
    return (
 | 
			
		||||
      <Animated.View
 | 
			
		||||
        style={[
 | 
			
		||||
          styles.content,
 | 
			
		||||
          {
 | 
			
		||||
            marginTop:
 | 
			
		||||
              useSafeAreaInsets().top + (44 - StyleConstants.Font.Size.L) / 2
 | 
			
		||||
          }
 | 
			
		||||
          styles.base,
 | 
			
		||||
          styleOpacity,
 | 
			
		||||
          { backgroundColor: theme.backgroundDefault, height: headerHeight }
 | 
			
		||||
        ]}
 | 
			
		||||
      >
 | 
			
		||||
        <Animated.View style={[styles.display_name, styleMarginTop]}>
 | 
			
		||||
          {account ? (
 | 
			
		||||
            <Text numberOfLines={1}>
 | 
			
		||||
              <ParseEmojis
 | 
			
		||||
                content={account.display_name || account.username}
 | 
			
		||||
                emojis={account.emojis}
 | 
			
		||||
                fontBold
 | 
			
		||||
              />
 | 
			
		||||
            </Text>
 | 
			
		||||
          ) : null}
 | 
			
		||||
        </Animated.View>
 | 
			
		||||
      </View>
 | 
			
		||||
    </Animated.View>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
        <View
 | 
			
		||||
          style={[
 | 
			
		||||
            styles.content,
 | 
			
		||||
            {
 | 
			
		||||
              marginTop:
 | 
			
		||||
                useSafeAreaInsets().top + (44 - StyleConstants.Font.Size.L) / 2
 | 
			
		||||
            }
 | 
			
		||||
          ]}
 | 
			
		||||
        >
 | 
			
		||||
          <Animated.View style={[styles.display_name, styleMarginTop]}>
 | 
			
		||||
            {account ? (
 | 
			
		||||
              <Text numberOfLines={1}>
 | 
			
		||||
                <ParseEmojis
 | 
			
		||||
                  content={account.display_name || account.username}
 | 
			
		||||
                  emojis={account.emojis}
 | 
			
		||||
                  fontBold
 | 
			
		||||
                />
 | 
			
		||||
              </Text>
 | 
			
		||||
            ) : null}
 | 
			
		||||
          </Animated.View>
 | 
			
		||||
        </View>
 | 
			
		||||
      </Animated.View>
 | 
			
		||||
    )
 | 
			
		||||
  },
 | 
			
		||||
  (_, next) => next.account === undefined
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  base: {
 | 
			
		||||
@@ -92,4 +93,4 @@ const styles = StyleSheet.create({
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default React.memo(AccountNav, (_, next) => next.account === undefined)
 | 
			
		||||
export default AccountNav
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										29
									
								
								src/store.ts
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								src/store.ts
									
									
									
									
									
								
							@@ -1,14 +1,11 @@
 | 
			
		||||
import createSecureStore from '@neverdull-agency/expo-unlimited-secure-store'
 | 
			
		||||
import AsyncStorage from '@react-native-async-storage/async-storage'
 | 
			
		||||
import {
 | 
			
		||||
  combineReducers,
 | 
			
		||||
  configureStore,
 | 
			
		||||
  getDefaultMiddleware
 | 
			
		||||
} from '@reduxjs/toolkit'
 | 
			
		||||
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
 | 
			
		||||
import instancesMigration from '@utils/migrations/instances/migration'
 | 
			
		||||
import contextsSlice from '@utils/slices/contextsSlice'
 | 
			
		||||
import instancesSlice from '@utils/slices/instancesSlice'
 | 
			
		||||
import settingsSlice from '@utils/slices/settingsSlice'
 | 
			
		||||
import versionSlice from '@utils/slices/versionSlice'
 | 
			
		||||
import { createMigrate, persistReducer, persistStore } from 'redux-persist'
 | 
			
		||||
 | 
			
		||||
const secureStorage = createSecureStore()
 | 
			
		||||
@@ -27,7 +24,7 @@ const instancesPersistConfig = {
 | 
			
		||||
  storage: secureStorage,
 | 
			
		||||
  version: 5,
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  migrate: createMigrate(instancesMigration, { debug: true })
 | 
			
		||||
  migrate: createMigrate(instancesMigration)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const settingsPersistConfig = {
 | 
			
		||||
@@ -36,21 +33,13 @@ const settingsPersistConfig = {
 | 
			
		||||
  storage: AsyncStorage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const rootPersistConfig = {
 | 
			
		||||
  key: 'root',
 | 
			
		||||
  prefix,
 | 
			
		||||
  version: 0,
 | 
			
		||||
  storage: AsyncStorage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const rootReducer = combineReducers({
 | 
			
		||||
  contexts: persistReducer(contextsPersistConfig, contextsSlice),
 | 
			
		||||
  instances: persistReducer(instancesPersistConfig, instancesSlice),
 | 
			
		||||
  settings: persistReducer(settingsPersistConfig, settingsSlice)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const store = configureStore({
 | 
			
		||||
  reducer: persistReducer(rootPersistConfig, rootReducer),
 | 
			
		||||
  reducer: {
 | 
			
		||||
    contexts: persistReducer(contextsPersistConfig, contextsSlice),
 | 
			
		||||
    instances: persistReducer(instancesPersistConfig, instancesSlice),
 | 
			
		||||
    settings: persistReducer(settingsPersistConfig, settingsSlice),
 | 
			
		||||
    version: versionSlice
 | 
			
		||||
  },
 | 
			
		||||
  middleware: getDefaultMiddleware({
 | 
			
		||||
    serializableCheck: {
 | 
			
		||||
      ignoredActions: ['persist/PERSIST']
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,9 @@ import apiInstance from '@api/instance'
 | 
			
		||||
import { AxiosError } from 'axios'
 | 
			
		||||
import { useQuery, UseQueryOptions } from 'react-query'
 | 
			
		||||
 | 
			
		||||
export type QueryKey = ['Account', { id: Mastodon.Account['id'] }]
 | 
			
		||||
export type QueryKeyAccount = ['Account', { id: Mastodon.Account['id'] }]
 | 
			
		||||
 | 
			
		||||
const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
 | 
			
		||||
const queryFunction = ({ queryKey }: { queryKey: QueryKeyAccount }) => {
 | 
			
		||||
  const { id } = queryKey[1]
 | 
			
		||||
 | 
			
		||||
  return apiInstance<Mastodon.Account>({
 | 
			
		||||
@@ -16,10 +16,10 @@ const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
 | 
			
		||||
const useAccountQuery = <TData = Mastodon.Account>({
 | 
			
		||||
  options,
 | 
			
		||||
  ...queryKeyParams
 | 
			
		||||
}: QueryKey[1] & {
 | 
			
		||||
}: QueryKeyAccount[1] & {
 | 
			
		||||
  options?: UseQueryOptions<Mastodon.Account, AxiosError, TData>
 | 
			
		||||
}) => {
 | 
			
		||||
  const queryKey: QueryKey = ['Account', { ...queryKeyParams }]
 | 
			
		||||
  const queryKey: QueryKeyAccount = ['Account', { ...queryKeyParams }]
 | 
			
		||||
  return useQuery(queryKey, queryFunction, options)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										121
									
								
								src/utils/queryHooks/profile.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								src/utils/queryHooks/profile.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,121 @@
 | 
			
		||||
import apiInstance from '@api/instance'
 | 
			
		||||
import { displayMessage } from '@components/Message'
 | 
			
		||||
import { queryClient } from '@root/App'
 | 
			
		||||
import { AxiosError } from 'axios'
 | 
			
		||||
import { useMutation, useQuery, UseQueryOptions } from 'react-query'
 | 
			
		||||
import { QueryKeyAccount } from './account'
 | 
			
		||||
 | 
			
		||||
type AccountWithSource = Mastodon.Account &
 | 
			
		||||
  Required<Pick<Mastodon.Account, 'source'>>
 | 
			
		||||
 | 
			
		||||
type QueryKeyProfile = ['Profile']
 | 
			
		||||
const queryKey: QueryKeyProfile = ['Profile']
 | 
			
		||||
 | 
			
		||||
const queryFunction = () => {
 | 
			
		||||
  return apiInstance<AccountWithSource>({
 | 
			
		||||
    method: 'get',
 | 
			
		||||
    url: `accounts/verify_credentials`
 | 
			
		||||
  }).then(res => res.body)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const useProfileQuery = <TData = AccountWithSource>({
 | 
			
		||||
  options
 | 
			
		||||
}: {
 | 
			
		||||
  options?: UseQueryOptions<AccountWithSource, AxiosError, TData>
 | 
			
		||||
}) => {
 | 
			
		||||
  return useQuery(queryKey, queryFunction, options)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MutationVarsProfile =
 | 
			
		||||
  | { type: 'display_name'; data: string }
 | 
			
		||||
  | { type: 'note'; data: string }
 | 
			
		||||
  | { type: 'avatar'; data: string }
 | 
			
		||||
  | { type: 'header'; data: string }
 | 
			
		||||
  | { type: 'locked'; data: boolean }
 | 
			
		||||
  | { type: 'bot'; data: boolean }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'source[privacy]'
 | 
			
		||||
      data: Mastodon.Preferences['posting:default:visibility']
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'source[sensitive]'
 | 
			
		||||
      data: Mastodon.Preferences['posting:default:sensitive']
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'fields_attributes'
 | 
			
		||||
      data: { name: string; value: string }[]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
const mutationFunction = async ({ type, data }: MutationVarsProfile) => {
 | 
			
		||||
  const formData = new FormData()
 | 
			
		||||
  if (type === 'fields_attributes') {
 | 
			
		||||
    const tempData = data as { name: string; value: string }[]
 | 
			
		||||
    tempData.forEach((d, index) => {
 | 
			
		||||
      formData.append(`fields_attributes[${index}][name]`, d.name)
 | 
			
		||||
      formData.append(`fields_attributes[${index}][value]`, d.value)
 | 
			
		||||
    })
 | 
			
		||||
  } else if (type === 'avatar' || type === 'header') {
 | 
			
		||||
    formData.append(type, {
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      uri: data,
 | 
			
		||||
      name: 'image/jpeg',
 | 
			
		||||
      type: 'image/jpeg'
 | 
			
		||||
    })
 | 
			
		||||
  } else {
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    formData.append(type, data)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return apiInstance<AccountWithSource>({
 | 
			
		||||
    method: 'patch',
 | 
			
		||||
    url: 'accounts/update_credentials',
 | 
			
		||||
    body: formData
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const useProfileMutation = () => {
 | 
			
		||||
  return useMutation<
 | 
			
		||||
    { body: AccountWithSource },
 | 
			
		||||
    AxiosError,
 | 
			
		||||
    MutationVarsProfile
 | 
			
		||||
  >(mutationFunction, {
 | 
			
		||||
    onMutate: async variables => {
 | 
			
		||||
      await queryClient.cancelQueries(queryKey)
 | 
			
		||||
 | 
			
		||||
      const oldData = queryClient.getQueryData<AccountWithSource>(queryKey)
 | 
			
		||||
 | 
			
		||||
      queryClient.setQueryData<AccountWithSource | undefined>(queryKey, old => {
 | 
			
		||||
        if (old) {
 | 
			
		||||
          switch (variables.type) {
 | 
			
		||||
            case 'source[privacy]':
 | 
			
		||||
              return {
 | 
			
		||||
                ...old,
 | 
			
		||||
                source: { ...old.source, privacy: variables.data }
 | 
			
		||||
              }
 | 
			
		||||
            case 'source[sensitive]':
 | 
			
		||||
              return {
 | 
			
		||||
                ...old,
 | 
			
		||||
                source: { ...old.source, sensitive: variables.data }
 | 
			
		||||
              }
 | 
			
		||||
            case 'locked':
 | 
			
		||||
              return { ...old, locked: variables.data }
 | 
			
		||||
            case 'bot':
 | 
			
		||||
              return { ...old, bot: variables.data }
 | 
			
		||||
            default:
 | 
			
		||||
              return old
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      return oldData
 | 
			
		||||
    },
 | 
			
		||||
    onError: (_, variables, context) => {
 | 
			
		||||
      queryClient.setQueryData(queryKey, context)
 | 
			
		||||
    },
 | 
			
		||||
    onSettled: () => {
 | 
			
		||||
      queryClient.invalidateQueries(queryKey)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { useProfileQuery, useProfileMutation }
 | 
			
		||||
							
								
								
									
										43
									
								
								src/utils/slices/versionSlice.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/utils/slices/versionSlice.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
 | 
			
		||||
import { RootState } from '@root/store'
 | 
			
		||||
import apiGeneral from '@api/general'
 | 
			
		||||
import { Constants } from 'react-native-unimodules'
 | 
			
		||||
 | 
			
		||||
export const retriveVersionLatest = createAsyncThunk(
 | 
			
		||||
  'version/latest',
 | 
			
		||||
  async () => {
 | 
			
		||||
    const res = await apiGeneral<{ latest: string }>({
 | 
			
		||||
      method: 'get',
 | 
			
		||||
      domain: 'tooot.app',
 | 
			
		||||
      url: 'version.json'
 | 
			
		||||
    })
 | 
			
		||||
    return res.body.latest
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export type VersionState = {
 | 
			
		||||
  update: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const versionInitialState = {
 | 
			
		||||
  update: false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const versionSlice = createSlice({
 | 
			
		||||
  name: 'version',
 | 
			
		||||
  initialState: versionInitialState,
 | 
			
		||||
  reducers: {},
 | 
			
		||||
  extraReducers: builder => {
 | 
			
		||||
    builder.addCase(retriveVersionLatest.fulfilled, (state, action) => {
 | 
			
		||||
      if (action.payload && Constants.manifest.version) {
 | 
			
		||||
        if (parseInt(action.payload) > parseInt(Constants.manifest.version)) {
 | 
			
		||||
          state.update = true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const getVersionUpdate = (state: RootState) => state.version.update
 | 
			
		||||
 | 
			
		||||
export default versionSlice.reducer
 | 
			
		||||
@@ -10,6 +10,7 @@ export type ColorDefinitions =
 | 
			
		||||
  | 'green'
 | 
			
		||||
  | 'yellow'
 | 
			
		||||
  | 'backgroundDefault'
 | 
			
		||||
  | 'backgroundDefaultTransparent'
 | 
			
		||||
  | 'backgroundOverlayDefault'
 | 
			
		||||
  | 'backgroundOverlayInvert'
 | 
			
		||||
  | 'border'
 | 
			
		||||
@@ -59,6 +60,10 @@ const themeColors: {
 | 
			
		||||
    light: 'rgb(250, 250, 250)',
 | 
			
		||||
    dark: 'rgb(18, 18, 18)'
 | 
			
		||||
  },
 | 
			
		||||
  backgroundDefaultTransparent: {
 | 
			
		||||
    light: 'rgba(250, 250, 250, 0)',
 | 
			
		||||
    dark: 'rgba(18, 18, 18, 0)'
 | 
			
		||||
  },
 | 
			
		||||
  backgroundOverlayDefault: {
 | 
			
		||||
    light: 'rgba(250, 250, 250, 0.5)',
 | 
			
		||||
    dark: 'rgba(0, 0, 0, 0.5)'
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user