mirror of
https://github.com/tooot-app/app
synced 2025-04-27 16:38:52 +02:00
commit
11b84eb139
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -7,7 +7,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: macos-latest
|
runs-on: macos-10.15
|
||||||
steps:
|
steps:
|
||||||
- name: -- Step 0 -- Extract branch name
|
- name: -- Step 0 -- Extract branch name
|
||||||
shell: bash
|
shell: bash
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"versions": {
|
"versions": {
|
||||||
"native": "210317",
|
"native": "210317",
|
||||||
"major": 1,
|
"major": 1,
|
||||||
"minor": 0,
|
"minor": 1,
|
||||||
"patch": 0,
|
"patch": 0,
|
||||||
"expo": "40.0.0"
|
"expo": "40.0.0"
|
||||||
},
|
},
|
||||||
@ -20,7 +20,8 @@
|
|||||||
"ios": "react-native run-ios",
|
"ios": "react-native run-ios",
|
||||||
"app:build": "bundle exec fastlane build",
|
"app:build": "bundle exec fastlane build",
|
||||||
"test": "jest --watchAll",
|
"test": "jest --watchAll",
|
||||||
"release": "scripts/release.sh"
|
"release": "scripts/release.sh",
|
||||||
|
"clean": "react-native-clean-project"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/react-native-action-sheet": "^3.9.0",
|
"@expo/react-native-action-sheet": "^3.9.0",
|
||||||
@ -114,6 +115,7 @@
|
|||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"jest-expo": "^40.0.2",
|
"jest-expo": "^40.0.2",
|
||||||
"nock": "^13.0.11",
|
"nock": "^13.0.11",
|
||||||
|
"react-native-clean-project": "^3.6.3",
|
||||||
"react-navigation": "^4.4.4",
|
"react-navigation": "^4.4.4",
|
||||||
"react-navigation-stack": "^2.10.4",
|
"react-navigation-stack": "^2.10.4",
|
||||||
"react-test-renderer": "^17.0.1",
|
"react-test-renderer": "^17.0.1",
|
||||||
|
4
src/@types/mastodon.d.ts
vendored
4
src/@types/mastodon.d.ts
vendored
@ -28,7 +28,7 @@ declare namespace Mastodon {
|
|||||||
moved?: Account
|
moved?: Account
|
||||||
fields: Field[]
|
fields: Field[]
|
||||||
bot: boolean
|
bot: boolean
|
||||||
source: Source
|
source?: Source
|
||||||
}
|
}
|
||||||
|
|
||||||
type Announcement = {
|
type Announcement = {
|
||||||
@ -258,7 +258,7 @@ declare namespace Mastodon {
|
|||||||
type Field = {
|
type Field = {
|
||||||
name: string
|
name: string
|
||||||
value: string
|
value: string
|
||||||
verified_at?: string
|
verified_at: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
type List = {
|
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']
|
list: Mastodon.List['id']
|
||||||
title: Mastodon.List['title']
|
title: Mastodon.List['title']
|
||||||
}
|
}
|
||||||
|
'Tab-Me-Profile': undefined
|
||||||
|
'Tab-Me-Push': undefined
|
||||||
'Tab-Me-Settings': undefined
|
'Tab-Me-Settings': undefined
|
||||||
'Tab-Me-Settings-Fontsize': undefined
|
'Tab-Me-Settings-Fontsize': undefined
|
||||||
'Tab-Me-Settings-Push': undefined
|
|
||||||
'Tab-Me-Switch': undefined
|
'Tab-Me-Switch': undefined
|
||||||
} & TabSharedStackParamList
|
} & 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={{
|
options={{
|
||||||
stackPresentation: 'transparentModal',
|
stackPresentation: 'transparentModal',
|
||||||
stackAnimation: 'fade',
|
stackAnimation: 'fade',
|
||||||
headerShown: false // Android
|
headerShown: false
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@ -177,7 +177,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
options={{
|
options={{
|
||||||
stackPresentation: 'transparentModal',
|
stackPresentation: 'transparentModal',
|
||||||
stackAnimation: 'fade',
|
stackAnimation: 'fade',
|
||||||
headerShown: false // Android
|
headerShown: false
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@ -185,7 +185,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
component={ScreenCompose}
|
component={ScreenCompose}
|
||||||
options={{
|
options={{
|
||||||
stackPresentation: 'fullScreenModal',
|
stackPresentation: 'fullScreenModal',
|
||||||
headerShown: false // Android
|
headerShown: false
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@ -194,7 +194,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
options={{
|
options={{
|
||||||
stackPresentation: 'fullScreenModal',
|
stackPresentation: 'fullScreenModal',
|
||||||
stackAnimation: 'fade',
|
stackAnimation: 'fade',
|
||||||
headerShown: false // Android
|
headerShown: false
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
@ -206,6 +206,3 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default React.memo(Screens, () => true)
|
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 = {
|
export type Params = {
|
||||||
method: 'get' | 'post' | 'put' | 'delete'
|
method: 'get' | 'post' | 'put' | 'delete'
|
||||||
domain?: string
|
domain: string
|
||||||
url: string
|
url: string
|
||||||
params?: {
|
params?: {
|
||||||
[key: string]: string | number | boolean | string[] | number[] | boolean[]
|
[key: string]: string | number | boolean | string[] | number[] | boolean[]
|
||||||
@ -25,10 +25,6 @@ const apiGeneral = async <T = unknown>({
|
|||||||
body,
|
body,
|
||||||
sentry = false
|
sentry = false
|
||||||
}: Params): Promise<{ body: T }> => {
|
}: Params): Promise<{ body: T }> => {
|
||||||
if (!domain) {
|
|
||||||
return Promise.reject()
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
ctx.bgGreen.bold(' API general ') +
|
ctx.bgGreen.bold(' API general ') +
|
||||||
' ' +
|
' ' +
|
||||||
|
@ -6,7 +6,7 @@ import li from 'li'
|
|||||||
const ctx = new chalk.Instance({ level: 3 })
|
const ctx = new chalk.Instance({ level: 3 })
|
||||||
|
|
||||||
export type Params = {
|
export type Params = {
|
||||||
method: 'get' | 'post' | 'put' | 'delete'
|
method: 'get' | 'post' | 'put' | 'delete' | 'patch'
|
||||||
version?: 'v1' | 'v2'
|
version?: 'v1' | 'v2'
|
||||||
url: string
|
url: string
|
||||||
params?: {
|
params?: {
|
||||||
|
@ -2,7 +2,7 @@ import Icon from '@components/Icon'
|
|||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useEffect, useMemo, useRef } from 'react'
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
AccessibilityProps,
|
AccessibilityProps,
|
||||||
Pressable,
|
Pressable,
|
||||||
@ -121,9 +121,6 @@ const Button: React.FC<Props> = ({
|
|||||||
color: mainColor,
|
color: mainColor,
|
||||||
fontSize:
|
fontSize:
|
||||||
StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1),
|
StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1),
|
||||||
fontWeight: destructive
|
|
||||||
? StyleConstants.Font.Weight.Bold
|
|
||||||
: undefined,
|
|
||||||
opacity: loading ? 0 : 1
|
opacity: loading ? 0 : 1
|
||||||
}}
|
}}
|
||||||
children={content}
|
children={content}
|
||||||
@ -135,12 +132,7 @@ const Button: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
}, [mode, content, loading, disabled])
|
}, [mode, content, loading, disabled])
|
||||||
|
|
||||||
enum spacingMapping {
|
const [layoutHeight, setLayoutHeight] = useState<number | undefined>()
|
||||||
XS = 'S',
|
|
||||||
S = 'M',
|
|
||||||
M = 'L',
|
|
||||||
L = 'XL'
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@ -161,10 +153,15 @@ const Button: React.FC<Props> = ({
|
|||||||
backgroundColor: colorBackground,
|
backgroundColor: colorBackground,
|
||||||
paddingVertical: StyleConstants.Spacing[spacing],
|
paddingVertical: StyleConstants.Spacing[spacing],
|
||||||
paddingHorizontal:
|
paddingHorizontal:
|
||||||
StyleConstants.Spacing[round ? spacing : spacingMapping[spacing]]
|
StyleConstants.Spacing[spacing] + StyleConstants.Spacing.XS,
|
||||||
|
width: round && layoutHeight ? layoutHeight : undefined
|
||||||
},
|
},
|
||||||
customStyle
|
customStyle
|
||||||
]}
|
]}
|
||||||
|
{...(round && {
|
||||||
|
onLayout: ({ nativeEvent }) =>
|
||||||
|
setLayoutHeight(nativeEvent.layout.height)
|
||||||
|
})}
|
||||||
testID='base'
|
testID='base'
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
children={children}
|
children={children}
|
||||||
@ -176,7 +173,6 @@ const Button: React.FC<Props> = ({
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
button: {
|
button: {
|
||||||
borderRadius: 100,
|
borderRadius: 100,
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center'
|
alignItems: 'center'
|
||||||
}
|
}
|
||||||
|
160
src/components/Emojis.tsx
Normal file
160
src/components/Emojis.tsx
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
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) => {
|
||||||
|
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 }) => {
|
const MenuContainer: React.FC<Props> = ({ children }) => {
|
||||||
return (
|
return <View style={styles.base}>{children}</View>
|
||||||
<View style={styles.base}>
|
|
||||||
{children}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
base: {
|
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({
|
const styles = StyleSheet.create({
|
||||||
base: {
|
base: {
|
||||||
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
paddingRight: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
paddingBottom: StyleConstants.Spacing.S
|
paddingBottom: StyleConstants.Spacing.S
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
|
@ -15,6 +15,7 @@ export interface Props {
|
|||||||
title: string
|
title: string
|
||||||
description?: string
|
description?: string
|
||||||
content?: string | React.ReactNode
|
content?: string | React.ReactNode
|
||||||
|
badge?: boolean
|
||||||
|
|
||||||
switchValue?: boolean
|
switchValue?: boolean
|
||||||
switchDisabled?: boolean
|
switchDisabled?: boolean
|
||||||
@ -33,6 +34,7 @@ const MenuRow: React.FC<Props> = ({
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
content,
|
content,
|
||||||
|
badge = false,
|
||||||
switchValue,
|
switchValue,
|
||||||
switchDisabled,
|
switchDisabled,
|
||||||
switchOnValueChange,
|
switchOnValueChange,
|
||||||
@ -84,6 +86,17 @@ const MenuRow: React.FC<Props> = ({
|
|||||||
style={styles.iconFront}
|
style={styles.iconFront}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{badge ? (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
backgroundColor: theme.red,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginRight: StyleConstants.Spacing.S
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<View style={styles.main}>
|
<View style={styles.main}>
|
||||||
<Text
|
<Text
|
||||||
style={[styles.title, { color: theme.primaryDefault }]}
|
style={[styles.title, { color: theme.primaryDefault }]}
|
||||||
@ -147,12 +160,12 @@ const MenuRow: React.FC<Props> = ({
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
base: {
|
base: {
|
||||||
minHeight: 50
|
minHeight: 46,
|
||||||
|
paddingVertical: StyleConstants.Spacing.S
|
||||||
},
|
},
|
||||||
core: {
|
core: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row'
|
||||||
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
|
|
||||||
},
|
},
|
||||||
front: {
|
front: {
|
||||||
flex: 2,
|
flex: 2,
|
||||||
@ -167,7 +180,7 @@ const styles = StyleSheet.create({
|
|||||||
marginLeft: StyleConstants.Spacing.M
|
marginLeft: StyleConstants.Spacing.M
|
||||||
},
|
},
|
||||||
iconFront: {
|
iconFront: {
|
||||||
marginRight: 8
|
marginRight: StyleConstants.Spacing.S
|
||||||
},
|
},
|
||||||
main: {
|
main: {
|
||||||
flex: 1
|
flex: 1
|
||||||
@ -176,9 +189,7 @@ const styles = StyleSheet.create({
|
|||||||
...StyleConstants.FontStyle.M
|
...StyleConstants.FontStyle.M
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
...StyleConstants.FontStyle.S,
|
...StyleConstants.FontStyle.S
|
||||||
marginTop: StyleConstants.Spacing.XS,
|
|
||||||
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
|
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
...StyleConstants.FontStyle.M
|
...StyleConstants.FontStyle.M
|
||||||
|
@ -2,7 +2,7 @@ import Icon from '@components/Icon'
|
|||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { getTheme } from '@utils/styles/themes'
|
import { getTheme } from '@utils/styles/themes'
|
||||||
import React from 'react'
|
import React, { RefObject } from 'react'
|
||||||
import { AccessibilityInfo } from 'react-native'
|
import { AccessibilityInfo } from 'react-native'
|
||||||
import FlashMessage, {
|
import FlashMessage, {
|
||||||
hideMessage,
|
hideMessage,
|
||||||
@ -11,6 +11,7 @@ import FlashMessage, {
|
|||||||
import haptics from './haptics'
|
import haptics from './haptics'
|
||||||
|
|
||||||
const displayMessage = ({
|
const displayMessage = ({
|
||||||
|
ref,
|
||||||
duration = 'short',
|
duration = 'short',
|
||||||
autoHide = true,
|
autoHide = true,
|
||||||
message,
|
message,
|
||||||
@ -20,6 +21,7 @@ const displayMessage = ({
|
|||||||
type
|
type
|
||||||
}:
|
}:
|
||||||
| {
|
| {
|
||||||
|
ref?: RefObject<FlashMessage>
|
||||||
duration?: 'short' | 'long'
|
duration?: 'short' | 'long'
|
||||||
autoHide?: boolean
|
autoHide?: boolean
|
||||||
message: string
|
message: string
|
||||||
@ -29,6 +31,7 @@ const displayMessage = ({
|
|||||||
type?: undefined
|
type?: undefined
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
|
ref?: RefObject<FlashMessage>
|
||||||
duration?: 'short' | 'long'
|
duration?: 'short' | 'long'
|
||||||
autoHide?: boolean
|
autoHide?: boolean
|
||||||
message: string
|
message: string
|
||||||
@ -54,63 +57,88 @@ const displayMessage = ({
|
|||||||
haptics('Error')
|
haptics('Error')
|
||||||
}
|
}
|
||||||
|
|
||||||
showMessage({
|
if (ref) {
|
||||||
duration: type === 'error' ? 5000 : duration === 'short' ? 1500 : 3000,
|
ref.current?.showMessage({
|
||||||
autoHide,
|
duration: type === 'error' ? 5000 : duration === 'short' ? 1500 : 3000,
|
||||||
message,
|
autoHide,
|
||||||
description,
|
message,
|
||||||
onPress,
|
description,
|
||||||
...(mode &&
|
onPress,
|
||||||
type && {
|
...(mode &&
|
||||||
renderFlashMessageIcon: () => {
|
type && {
|
||||||
return (
|
renderFlashMessageIcon: () => {
|
||||||
<Icon
|
return (
|
||||||
name={iconMapping[type]}
|
<Icon
|
||||||
size={StyleConstants.Font.LineHeight.M}
|
name={iconMapping[type]}
|
||||||
color={getTheme(mode)[colorMapping[type]]}
|
size={StyleConstants.Font.LineHeight.M}
|
||||||
style={{ marginRight: StyleConstants.Spacing.S }}
|
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 = () => {
|
const removeMessage = () => {
|
||||||
|
// if (ref) {
|
||||||
|
// ref.current?.hideMessage()
|
||||||
|
// } else {
|
||||||
hideMessage()
|
hideMessage()
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
const Message = React.memo(
|
const Message = React.forwardRef<FlashMessage>((_, ref) => {
|
||||||
() => {
|
const { mode, theme } = useTheme()
|
||||||
const { mode, theme } = useTheme()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlashMessage
|
<FlashMessage
|
||||||
icon='auto'
|
ref={ref}
|
||||||
position='top'
|
icon='auto'
|
||||||
floating
|
position='top'
|
||||||
style={{
|
floating
|
||||||
backgroundColor: theme.backgroundDefault,
|
style={{
|
||||||
shadowColor: theme.primaryDefault,
|
backgroundColor: theme.backgroundDefault,
|
||||||
shadowOffset: { width: 0, height: 0 },
|
shadowColor: theme.primaryDefault,
|
||||||
shadowOpacity: mode === 'light' ? 0.16 : 0.24,
|
shadowOffset: { width: 0, height: 0 },
|
||||||
shadowRadius: 4
|
shadowOpacity: mode === 'light' ? 0.16 : 0.24,
|
||||||
}}
|
shadowRadius: 4
|
||||||
titleStyle={{
|
}}
|
||||||
color: theme.primaryDefault,
|
titleStyle={{
|
||||||
...StyleConstants.FontStyle.M,
|
color: theme.primaryDefault,
|
||||||
fontWeight: StyleConstants.Font.Weight.Bold
|
...StyleConstants.FontStyle.M,
|
||||||
}}
|
fontWeight: StyleConstants.Font.Weight.Bold
|
||||||
textStyle={{
|
}}
|
||||||
color: theme.primaryDefault,
|
textStyle={{
|
||||||
...StyleConstants.FontStyle.S
|
color: theme.primaryDefault,
|
||||||
}}
|
...StyleConstants.FontStyle.S
|
||||||
// @ts-ignore
|
}}
|
||||||
textProps={{ numberOfLines: 2 }}
|
// @ts-ignore
|
||||||
/>
|
textProps={{ numberOfLines: 2 }}
|
||||||
)
|
/>
|
||||||
},
|
)
|
||||||
() => true
|
})
|
||||||
)
|
|
||||||
|
|
||||||
export { Message, displayMessage, removeMessage }
|
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'),
|
screenTabs: require('./screens/tabs'),
|
||||||
|
|
||||||
componentInstance: require('./components/instance'),
|
componentInstance: require('./components/instance'),
|
||||||
|
componentMediaSelector: require('./components/mediaSelector'),
|
||||||
componentParse: require('./components/parse'),
|
componentParse: require('./components/parse'),
|
||||||
componentRelationship: require('./components/relationship'),
|
componentRelationship: require('./components/relationship'),
|
||||||
componentRelativeTime: require('./components/relativeTime'),
|
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": {
|
"attachment": {
|
||||||
"accessibilityLabel": "Upload attachment",
|
"accessibilityLabel": "Upload attachment",
|
||||||
"accessibilityHint": "Poll function will be disabled when there is any 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": {
|
"failed": {
|
||||||
"alert": {
|
"alert": {
|
||||||
"title": "Upload failed",
|
"title": "Upload failed",
|
||||||
|
@ -10,8 +10,8 @@
|
|||||||
"cancel": "$t(common:buttons.cancel)"
|
"cancel": "$t(common:buttons.cancel)"
|
||||||
},
|
},
|
||||||
"save": {
|
"save": {
|
||||||
"function": "Saving image",
|
"succeed": "Image saved",
|
||||||
"success": "Image saved"
|
"failed": "Saving image failed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -52,8 +52,20 @@
|
|||||||
"push": {
|
"push": {
|
||||||
"name": "Push Notification"
|
"name": "Push Notification"
|
||||||
},
|
},
|
||||||
|
"profile": {
|
||||||
|
"name": "Edit Profile"
|
||||||
|
},
|
||||||
|
"profileName": {
|
||||||
|
"name": "Edit Display Name"
|
||||||
|
},
|
||||||
|
"profileNote": {
|
||||||
|
"name": "Edit Description"
|
||||||
|
},
|
||||||
|
"profileFields": {
|
||||||
|
"name": "Edit Metadata"
|
||||||
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"name": "Settings"
|
"name": "App Settings"
|
||||||
},
|
},
|
||||||
"switch": {
|
"switch": {
|
||||||
"name": "Switch Account"
|
"name": "Switch Account"
|
||||||
@ -71,13 +83,73 @@
|
|||||||
"XXL": "XXL"
|
"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",
|
||||||
|
"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": {
|
"push": {
|
||||||
"enable": {
|
"enable": {
|
||||||
"direct": "Enable push notification",
|
"direct": "Enable push notification",
|
||||||
"settings": "Enable in settings"
|
"settings": "Enable in settings"
|
||||||
},
|
},
|
||||||
"global": {
|
"global": {
|
||||||
"heading": "Enable push notification",
|
"heading": "Enable for {{acct}}",
|
||||||
"description": "Messages are routed through tooot's server"
|
"description": "Messages are routed through tooot's server"
|
||||||
},
|
},
|
||||||
"decode": {
|
"decode": {
|
||||||
@ -112,6 +184,9 @@
|
|||||||
"empty": "None"
|
"empty": "None"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"update": {
|
||||||
|
"title": "Update to latest version"
|
||||||
|
},
|
||||||
"logout": {
|
"logout": {
|
||||||
"button": "Log out",
|
"button": "Log out",
|
||||||
"alert": {
|
"alert": {
|
||||||
@ -125,13 +200,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"push": {
|
|
||||||
"heading": "$t(me.stacks.push.name)",
|
|
||||||
"content": {
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"disabled": "Disabled"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fontsize": {
|
"fontsize": {
|
||||||
"heading": "$t(me.stacks.fontSize.name)",
|
"heading": "$t(me.stacks.fontSize.name)",
|
||||||
"content": {
|
"content": {
|
||||||
@ -158,7 +226,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"browser": {
|
"browser": {
|
||||||
"heading": "Opening link",
|
"heading": "Opening Link",
|
||||||
"options": {
|
"options": {
|
||||||
"internal": "Inside app",
|
"internal": "Inside app",
|
||||||
"external": "Use system browser",
|
"external": "Use system browser",
|
||||||
|
28
src/i18n/zh-Hans/components/mediaSelector.json
Normal file
28
src/i18n/zh-Hans/components/mediaSelector.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"title": "选择媒体",
|
||||||
|
"options": {
|
||||||
|
"library": "从相册上传",
|
||||||
|
"photo": "拍摄照片",
|
||||||
|
"cancel": "$t(common:buttons.cancel)"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"alert": {
|
||||||
|
"title": "无权限",
|
||||||
|
"message": "需要读取相册权限才能上传附件",
|
||||||
|
"buttons": {
|
||||||
|
"settings": "去更新设置",
|
||||||
|
"cancel": "$t(common:buttons.cancel)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"photo": {
|
||||||
|
"alert": {
|
||||||
|
"title": "无权限",
|
||||||
|
"message": "需要使用相机权限才能上传附件",
|
||||||
|
"buttons": {
|
||||||
|
"settings": "去更新设置",
|
||||||
|
"cancel": "$t(common:buttons.cancel)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -62,7 +62,7 @@
|
|||||||
"poll": {
|
"poll": {
|
||||||
"option": {
|
"option": {
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"accessibilityLabel": "选项{{index}}",
|
"accessibilityLabel": "投票选项 {{index}}",
|
||||||
"single": "单选项",
|
"single": "单选项",
|
||||||
"multiple": "多选项"
|
"multiple": "多选项"
|
||||||
}
|
}
|
||||||
@ -104,33 +104,6 @@
|
|||||||
"attachment": {
|
"attachment": {
|
||||||
"accessibilityLabel": "上传附件",
|
"accessibilityLabel": "上传附件",
|
||||||
"accessibilityHint": "当有任何附件时,投票功能将被禁用",
|
"accessibilityHint": "当有任何附件时,投票功能将被禁用",
|
||||||
"actions": {
|
|
||||||
"options": {
|
|
||||||
"library": "从相册上传",
|
|
||||||
"photo": "拍摄上传",
|
|
||||||
"cancel": "$t(common:buttons.cancel)"
|
|
||||||
},
|
|
||||||
"library": {
|
|
||||||
"alert": {
|
|
||||||
"title": "无读取权限",
|
|
||||||
"message": "需要读取相册权限才能上传附件",
|
|
||||||
"buttons": {
|
|
||||||
"settings": "去更新设置",
|
|
||||||
"cancel": "取消上传"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"photo": {
|
|
||||||
"alert": {
|
|
||||||
"title": "无拍照权限",
|
|
||||||
"message": "需要使用相机权限才能上传附件",
|
|
||||||
"buttons": {
|
|
||||||
"settings": "去更新设置",
|
|
||||||
"cancel": "取消上传"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"alert": {
|
"alert": {
|
||||||
"title": "上传失败",
|
"title": "上传失败",
|
||||||
|
@ -10,8 +10,8 @@
|
|||||||
"cancel": "$t(common:buttons.cancel)"
|
"cancel": "$t(common:buttons.cancel)"
|
||||||
},
|
},
|
||||||
"save": {
|
"save": {
|
||||||
"function": "保存图片",
|
"succeed": "图片保存成功",
|
||||||
"success": "图片保存成功"
|
"failed": "保存图片失败"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -52,8 +52,20 @@
|
|||||||
"push": {
|
"push": {
|
||||||
"name": "推送通知"
|
"name": "推送通知"
|
||||||
},
|
},
|
||||||
|
"profile": {
|
||||||
|
"name": "修改个人资料"
|
||||||
|
},
|
||||||
|
"profileName": {
|
||||||
|
"name": "修改昵称"
|
||||||
|
},
|
||||||
|
"profileNote": {
|
||||||
|
"name": "修改简介"
|
||||||
|
},
|
||||||
|
"profileFields": {
|
||||||
|
"name": "修改附加信息"
|
||||||
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"name": "设置"
|
"name": "应用设置"
|
||||||
},
|
},
|
||||||
"switch": {
|
"switch": {
|
||||||
"name": "切换账号"
|
"name": "切换账号"
|
||||||
@ -71,13 +83,73 @@
|
|||||||
"XXL": "超大号"
|
"XXL": "超大号"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"profile": {
|
||||||
|
"cancellation": {
|
||||||
|
"title": "更改尚未保存",
|
||||||
|
"message": "您的更改尚未保存。是否放弃保存更改?",
|
||||||
|
"buttons": {
|
||||||
|
"cancel": "$t(common:buttons.cancel)",
|
||||||
|
"discard": "不保存"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"feedback": {
|
||||||
|
"succeed": "{{type}}已更新",
|
||||||
|
"failed": "{{type}}更新失败,请重试"
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"name": {
|
||||||
|
"title": "昵称"
|
||||||
|
},
|
||||||
|
"avatar": {
|
||||||
|
"title": "头像",
|
||||||
|
"description": "将在下一版中启用"
|
||||||
|
},
|
||||||
|
"banner": {
|
||||||
|
"title": "横幅",
|
||||||
|
"description": "将在下一版中启用"
|
||||||
|
},
|
||||||
|
"note": {
|
||||||
|
"title": "简介"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"title": "附加信息",
|
||||||
|
"total": "{{count}} 项",
|
||||||
|
"total_plural": "{{count}} 项"
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"title": "嘟文默认可见范围",
|
||||||
|
"options": {
|
||||||
|
"public": "公开",
|
||||||
|
"unlisted": "不公开",
|
||||||
|
"private": "仅关注者",
|
||||||
|
"cancel": "$t(common:buttons.cancel)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sensitive": {
|
||||||
|
"title": "媒体默认设为敏感"
|
||||||
|
},
|
||||||
|
"lock": {
|
||||||
|
"title": "锁嘟",
|
||||||
|
"description": "你需要手动审核所有关注请求"
|
||||||
|
},
|
||||||
|
"bot": {
|
||||||
|
"title": "机器人帐户",
|
||||||
|
"description": "来自这个帐户的绝大多数操作都是自动进行的,并且可能无人监控"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"group": "第 {{index}} 组",
|
||||||
|
"label": "标签",
|
||||||
|
"content": "内容"
|
||||||
|
}
|
||||||
|
},
|
||||||
"push": {
|
"push": {
|
||||||
"enable": {
|
"enable": {
|
||||||
"direct": "启用推送通知",
|
"direct": "启用推送通知",
|
||||||
"settings": "在系统设置中启用"
|
"settings": "在系统设置中启用"
|
||||||
},
|
},
|
||||||
"global": {
|
"global": {
|
||||||
"heading": "启用tooot推送通知",
|
"heading": "启用 {{acct}}",
|
||||||
"description": "通知消息将经由tooot服务器转发"
|
"description": "通知消息将经由tooot服务器转发"
|
||||||
},
|
},
|
||||||
"decode": {
|
"decode": {
|
||||||
@ -112,6 +184,9 @@
|
|||||||
"empty": "无公告"
|
"empty": "无公告"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"update": {
|
||||||
|
"title": "更新至最新版本"
|
||||||
|
},
|
||||||
"logout": {
|
"logout": {
|
||||||
"button": "退出当前账号",
|
"button": "退出当前账号",
|
||||||
"alert": {
|
"alert": {
|
||||||
@ -125,13 +200,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"push": {
|
|
||||||
"heading": "$t(me.stacks.push.name)",
|
|
||||||
"content": {
|
|
||||||
"enabled": "已启用",
|
|
||||||
"disabled": "已禁用"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fontsize": {
|
"fontsize": {
|
||||||
"heading": "$t(me.stacks.fontSize.name)",
|
"heading": "$t(me.stacks.fontSize.name)",
|
||||||
"content": {
|
"content": {
|
||||||
|
@ -84,7 +84,7 @@ const ComposePoll: React.FC = () => {
|
|||||||
<View style={styles.controlAmount}>
|
<View style={styles.controlAmount}>
|
||||||
<View style={styles.firstButton}>
|
<View style={styles.firstButton}>
|
||||||
<Button
|
<Button
|
||||||
{...((total > 2)
|
{...(total > 2
|
||||||
? {
|
? {
|
||||||
accessibilityLabel: t(
|
accessibilityLabel: t(
|
||||||
'content.root.footer.poll.quantity.reduce.accessibilityLabel',
|
'content.root.footer.poll.quantity.reduce.accessibilityLabel',
|
||||||
@ -139,78 +139,80 @@ const ComposePoll: React.FC = () => {
|
|||||||
disabled={!(total < 4)}
|
disabled={!(total < 4)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<MenuRow
|
<View style={styles.controlOptions}>
|
||||||
title={t('content.root.footer.poll.multiple.heading')}
|
<MenuRow
|
||||||
content={
|
title={t('content.root.footer.poll.multiple.heading')}
|
||||||
multiple
|
content={
|
||||||
? t('content.root.footer.poll.multiple.options.multiple')
|
multiple
|
||||||
: t('content.root.footer.poll.multiple.options.single')
|
? t('content.root.footer.poll.multiple.options.multiple')
|
||||||
}
|
: t('content.root.footer.poll.multiple.options.single')
|
||||||
onPress={() =>
|
}
|
||||||
showActionSheetWithOptions(
|
onPress={() =>
|
||||||
{
|
showActionSheetWithOptions(
|
||||||
options: [
|
{
|
||||||
t('content.root.footer.poll.multiple.options.single'),
|
options: [
|
||||||
t('content.root.footer.poll.multiple.options.multiple'),
|
t('content.root.footer.poll.multiple.options.single'),
|
||||||
t('content.root.footer.poll.multiple.options.cancel')
|
t('content.root.footer.poll.multiple.options.multiple'),
|
||||||
],
|
t('content.root.footer.poll.multiple.options.cancel')
|
||||||
cancelButtonIndex: 2
|
],
|
||||||
},
|
cancelButtonIndex: 2
|
||||||
index => {
|
},
|
||||||
if (index < 2) {
|
index => {
|
||||||
analytics('compose_poll_expiration_press', {
|
if (index < 2) {
|
||||||
current: multiple,
|
analytics('compose_poll_expiration_press', {
|
||||||
new: index === 1
|
current: multiple,
|
||||||
})
|
new: index === 1
|
||||||
composeDispatch({
|
})
|
||||||
type: 'poll',
|
composeDispatch({
|
||||||
payload: { multiple: index === 1 }
|
type: 'poll',
|
||||||
})
|
payload: { multiple: index === 1 }
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
}
|
iconBack='ChevronRight'
|
||||||
iconBack='ChevronRight'
|
/>
|
||||||
/>
|
<MenuRow
|
||||||
<MenuRow
|
title={t('content.root.footer.poll.expiration.heading')}
|
||||||
title={t('content.root.footer.poll.expiration.heading')}
|
content={t(`content.root.footer.poll.expiration.options.${expire}`)}
|
||||||
content={t(`content.root.footer.poll.expiration.options.${expire}`)}
|
onPress={() => {
|
||||||
onPress={() => {
|
const expirations: [
|
||||||
const expirations: [
|
'300',
|
||||||
'300',
|
'1800',
|
||||||
'1800',
|
'3600',
|
||||||
'3600',
|
'21600',
|
||||||
'21600',
|
'86400',
|
||||||
'86400',
|
'259200',
|
||||||
'259200',
|
'604800'
|
||||||
'604800'
|
] = ['300', '1800', '3600', '21600', '86400', '259200', '604800']
|
||||||
] = ['300', '1800', '3600', '21600', '86400', '259200', '604800']
|
showActionSheetWithOptions(
|
||||||
showActionSheetWithOptions(
|
{
|
||||||
{
|
options: [
|
||||||
options: [
|
...expirations.map(e =>
|
||||||
...expirations.map(e =>
|
t(`content.root.footer.poll.expiration.options.${e}`)
|
||||||
t(`content.root.footer.poll.expiration.options.${e}`)
|
),
|
||||||
),
|
t('content.root.footer.poll.expiration.options.cancel')
|
||||||
t('content.root.footer.poll.expiration.options.cancel')
|
],
|
||||||
],
|
cancelButtonIndex: 7
|
||||||
cancelButtonIndex: 7
|
},
|
||||||
},
|
index => {
|
||||||
index => {
|
if (index < 7) {
|
||||||
if (index < 7) {
|
analytics('compose_poll_expiration_press', {
|
||||||
analytics('compose_poll_expiration_press', {
|
current: expire,
|
||||||
current: expire,
|
new: expirations[index]
|
||||||
new: expirations[index]
|
})
|
||||||
})
|
composeDispatch({
|
||||||
composeDispatch({
|
type: 'poll',
|
||||||
type: 'poll',
|
payload: { expire: expirations[index] }
|
||||||
payload: { expire: expirations[index] }
|
})
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}}
|
||||||
}}
|
iconBack='ChevronRight'
|
||||||
iconBack='ChevronRight'
|
/>
|
||||||
/>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -247,6 +249,9 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
marginRight: StyleConstants.Spacing.M
|
marginRight: StyleConstants.Spacing.M
|
||||||
},
|
},
|
||||||
|
controlOptions: {
|
||||||
|
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
|
||||||
|
},
|
||||||
firstButton: {
|
firstButton: {
|
||||||
marginRight: StyleConstants.Spacing.S
|
marginRight: StyleConstants.Spacing.S
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import * as ImagePicker from 'expo-image-picker'
|
|
||||||
import * as Crypto from 'expo-crypto'
|
import * as Crypto from 'expo-crypto'
|
||||||
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types'
|
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types'
|
||||||
import * as VideoThumbnails from 'expo-video-thumbnails'
|
import * as VideoThumbnails from 'expo-video-thumbnails'
|
||||||
import { Dispatch } from 'react'
|
import { Dispatch } from 'react'
|
||||||
import { Alert, Linking } from 'react-native'
|
import { Alert } from 'react-native'
|
||||||
import { ComposeAction } from '../../utils/types'
|
import { ComposeAction } from '../../utils/types'
|
||||||
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
|
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
|
||||||
import i18next from 'i18next'
|
import i18next from 'i18next'
|
||||||
import analytics from '@components/analytics'
|
|
||||||
import apiInstance from '@api/instance'
|
import apiInstance from '@api/instance'
|
||||||
|
import mediaSelector from '@components/mediaSelector'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
composeDispatch: Dispatch<ComposeAction>
|
composeDispatch: Dispatch<ComposeAction>
|
||||||
@ -22,35 +21,33 @@ const addAttachment = async ({
|
|||||||
composeDispatch,
|
composeDispatch,
|
||||||
showActionSheetWithOptions
|
showActionSheetWithOptions
|
||||||
}: Props): Promise<any> => {
|
}: Props): Promise<any> => {
|
||||||
const uploadAttachment = async (result: ImageInfo) => {
|
const uploader = async (imageInfo: ImageInfo) => {
|
||||||
const hash = await Crypto.digestStringAsync(
|
const hash = await Crypto.digestStringAsync(
|
||||||
Crypto.CryptoDigestAlgorithm.SHA256,
|
Crypto.CryptoDigestAlgorithm.SHA256,
|
||||||
result.uri + Math.random()
|
imageInfo.uri + Math.random()
|
||||||
)
|
)
|
||||||
|
|
||||||
let attachmentType: string
|
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':
|
case 'image':
|
||||||
attachmentType = `image/${attachmentUri.split('.')[1]}`
|
attachmentType = `image/${imageInfo.uri.split('.')[1]}`
|
||||||
composeDispatch({
|
composeDispatch({
|
||||||
type: 'attachment/upload/start',
|
type: 'attachment/upload/start',
|
||||||
payload: {
|
payload: {
|
||||||
local: { ...result, local_thumbnail: attachmentUri, hash },
|
local: { ...imageInfo, local_thumbnail: imageInfo.uri, hash },
|
||||||
uploading: true
|
uploading: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'video':
|
case 'video':
|
||||||
attachmentType = `video/${attachmentUri.split('.')[1]}`
|
attachmentType = `video/${imageInfo.uri.split('.')[1]}`
|
||||||
VideoThumbnails.getThumbnailAsync(attachmentUri)
|
VideoThumbnails.getThumbnailAsync(imageInfo.uri)
|
||||||
.then(({ uri }) =>
|
.then(({ uri }) =>
|
||||||
composeDispatch({
|
composeDispatch({
|
||||||
type: 'attachment/upload/start',
|
type: 'attachment/upload/start',
|
||||||
payload: {
|
payload: {
|
||||||
local: { ...result, local_thumbnail: uri, hash },
|
local: { ...imageInfo, local_thumbnail: uri, hash },
|
||||||
uploading: true
|
uploading: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -59,7 +56,7 @@ const addAttachment = async ({
|
|||||||
composeDispatch({
|
composeDispatch({
|
||||||
type: 'attachment/upload/start',
|
type: 'attachment/upload/start',
|
||||||
payload: {
|
payload: {
|
||||||
local: { ...result, hash },
|
local: { ...imageInfo, hash },
|
||||||
uploading: true
|
uploading: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -70,7 +67,7 @@ const addAttachment = async ({
|
|||||||
composeDispatch({
|
composeDispatch({
|
||||||
type: 'attachment/upload/start',
|
type: 'attachment/upload/start',
|
||||||
payload: {
|
payload: {
|
||||||
local: { ...result, hash },
|
local: { ...imageInfo, hash },
|
||||||
uploading: true
|
uploading: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -101,7 +98,7 @@ const addAttachment = async ({
|
|||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', {
|
formData.append('file', {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
uri: attachmentUri,
|
uri: imageInfo.uri,
|
||||||
name: attachmentType,
|
name: attachmentType,
|
||||||
type: attachmentType
|
type: attachmentType
|
||||||
})
|
})
|
||||||
@ -115,7 +112,7 @@ const addAttachment = async ({
|
|||||||
if (res.body.id) {
|
if (res.body.id) {
|
||||||
composeDispatch({
|
composeDispatch({
|
||||||
type: 'attachment/upload/end',
|
type: 'attachment/upload/end',
|
||||||
payload: { remote: res.body, local: result }
|
payload: { remote: res.body, local: imageInfo }
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
uploadFailed()
|
uploadFailed()
|
||||||
@ -126,119 +123,7 @@ const addAttachment = async ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
showActionSheetWithOptions(
|
mediaSelector({ uploader, 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default addAttachment
|
export default addAttachment
|
||||||
|
@ -1,28 +1,63 @@
|
|||||||
import haptics from '@components/haptics'
|
import haptics from '@components/haptics'
|
||||||
|
import { displayMessage } from '@components/Message'
|
||||||
import CameraRoll from '@react-native-community/cameraroll'
|
import CameraRoll from '@react-native-community/cameraroll'
|
||||||
|
import i18next from 'i18next'
|
||||||
|
import { RefObject } from 'react'
|
||||||
|
import { Platform } from 'react-native'
|
||||||
|
import FlashMessage from 'react-native-flash-message'
|
||||||
import { FileSystem, Permissions } from 'react-native-unimodules'
|
import { FileSystem, Permissions } from 'react-native-unimodules'
|
||||||
|
|
||||||
const saveIos = async (
|
type CommonProps = {
|
||||||
|
messageRef: RefObject<FlashMessage>
|
||||||
|
mode: 'light' | 'dark'
|
||||||
image: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
|
image: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
|
||||||
) => {
|
}
|
||||||
|
|
||||||
|
const saveIos = async ({ messageRef, mode, image }: CommonProps) => {
|
||||||
CameraRoll.save(image.url)
|
CameraRoll.save(image.url)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
haptics('Success')
|
haptics('Success')
|
||||||
|
displayMessage({
|
||||||
|
ref: messageRef,
|
||||||
|
mode,
|
||||||
|
type: 'success',
|
||||||
|
message: i18next.t('screenImageViewer:content.save.succeed')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (image.remote_url) {
|
if (image.remote_url) {
|
||||||
CameraRoll.save(image.remote_url)
|
CameraRoll.save(image.remote_url)
|
||||||
.then(() => haptics('Success'))
|
.then(() => {
|
||||||
.catch(() => haptics('Error'))
|
haptics('Success')
|
||||||
|
displayMessage({
|
||||||
|
ref: messageRef,
|
||||||
|
mode,
|
||||||
|
type: 'success',
|
||||||
|
message: i18next.t('screenImageViewer:content.save.succeed')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
haptics('Error')
|
||||||
|
displayMessage({
|
||||||
|
ref: messageRef,
|
||||||
|
mode,
|
||||||
|
type: 'error',
|
||||||
|
message: i18next.t('screenImageViewer:content.save.failed')
|
||||||
|
})
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
haptics('Error')
|
haptics('Error')
|
||||||
|
displayMessage({
|
||||||
|
ref: messageRef,
|
||||||
|
mode,
|
||||||
|
type: 'error',
|
||||||
|
message: i18next.t('screenImageViewer:content.save.failed')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveAndroid = async (
|
const saveAndroid = async ({ messageRef, mode, image }: CommonProps) => {
|
||||||
image: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
|
|
||||||
) => {
|
|
||||||
const fileUri: string = `${FileSystem.documentDirectory}test.jpg`
|
const fileUri: string = `${FileSystem.documentDirectory}test.jpg`
|
||||||
const downloadedFile: FileSystem.FileSystemDownloadResult = await FileSystem.downloadAsync(
|
const downloadedFile: FileSystem.FileSystemDownloadResult = await FileSystem.downloadAsync(
|
||||||
image.url,
|
image.url,
|
||||||
@ -39,8 +74,35 @@ const saveAndroid = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
CameraRoll.save(downloadedFile.uri)
|
CameraRoll.save(downloadedFile.uri)
|
||||||
.then(() => haptics('Success'))
|
.then(() => {
|
||||||
.catch(() => haptics('Error'))
|
haptics('Success')
|
||||||
|
displayMessage({
|
||||||
|
ref: messageRef,
|
||||||
|
mode,
|
||||||
|
type: 'success',
|
||||||
|
message: 'test'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
haptics('Error')
|
||||||
|
displayMessage({
|
||||||
|
ref: messageRef,
|
||||||
|
mode,
|
||||||
|
type: 'error',
|
||||||
|
message: i18next.t('screenImageViewer:content.save.failed')
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export { saveIos, saveAndroid }
|
const saveImage = async (props: CommonProps) => {
|
||||||
|
switch (Platform.OS) {
|
||||||
|
case 'ios':
|
||||||
|
saveIos(props)
|
||||||
|
break
|
||||||
|
case 'android':
|
||||||
|
saveAndroid(props)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default saveImage
|
||||||
|
@ -1,42 +1,35 @@
|
|||||||
import analytics from '@components/analytics'
|
import analytics from '@components/analytics'
|
||||||
import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header'
|
import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header'
|
||||||
|
import { Message } from '@components/Message'
|
||||||
import { useActionSheet } from '@expo/react-native-action-sheet'
|
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||||
import { StackScreenProps } from '@react-navigation/stack'
|
import { StackScreenProps } from '@react-navigation/stack'
|
||||||
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { findIndex } from 'lodash'
|
import { findIndex } from 'lodash'
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { RefObject, useCallback, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Platform, Share, StatusBar, View } from 'react-native'
|
import { Platform, Share, StatusBar, View } from 'react-native'
|
||||||
|
import FlashMessage from 'react-native-flash-message'
|
||||||
import {
|
import {
|
||||||
SafeAreaProvider,
|
SafeAreaProvider,
|
||||||
useSafeAreaInsets
|
useSafeAreaInsets
|
||||||
} from 'react-native-safe-area-context'
|
} from 'react-native-safe-area-context'
|
||||||
import ImageViewer from './ImageViewer/Root'
|
import ImageViewer from './ImageViewer/Root'
|
||||||
import { saveAndroid, saveIos } from './ImageViewer/save'
|
import saveImage from './ImageViewer/save'
|
||||||
|
|
||||||
const saveImage = async (
|
|
||||||
image: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
|
|
||||||
) => {
|
|
||||||
switch (Platform.OS) {
|
|
||||||
case 'ios':
|
|
||||||
saveIos(image)
|
|
||||||
break
|
|
||||||
case 'android':
|
|
||||||
saveAndroid(image)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const HeaderComponent = React.memo(
|
const HeaderComponent = React.memo(
|
||||||
({
|
({
|
||||||
|
messageRef,
|
||||||
navigation,
|
navigation,
|
||||||
currentIndex,
|
currentIndex,
|
||||||
imageUrls
|
imageUrls
|
||||||
}: {
|
}: {
|
||||||
|
messageRef: RefObject<FlashMessage>
|
||||||
navigation: ScreenImagesViewerProp['navigation']
|
navigation: ScreenImagesViewerProp['navigation']
|
||||||
currentIndex: number
|
currentIndex: number
|
||||||
imageUrls: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls']
|
imageUrls: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls']
|
||||||
}) => {
|
}) => {
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
|
const { mode } = useTheme()
|
||||||
const { t } = useTranslation('screenImageViewer')
|
const { t } = useTranslation('screenImageViewer')
|
||||||
const { showActionSheetWithOptions } = useActionSheet()
|
const { showActionSheetWithOptions } = useActionSheet()
|
||||||
|
|
||||||
@ -55,7 +48,7 @@ const HeaderComponent = React.memo(
|
|||||||
switch (buttonIndex) {
|
switch (buttonIndex) {
|
||||||
case 0:
|
case 0:
|
||||||
analytics('imageviewer_more_save_press')
|
analytics('imageviewer_more_save_press')
|
||||||
saveImage(imageUrls[currentIndex])
|
saveImage({ messageRef, mode, image: imageUrls[currentIndex] })
|
||||||
break
|
break
|
||||||
case 1:
|
case 1:
|
||||||
analytics('imageviewer_more_share_press')
|
analytics('imageviewer_more_share_press')
|
||||||
@ -121,9 +114,13 @@ const ScreenImagesViewer = ({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { mode } = useTheme()
|
||||||
|
|
||||||
const initialIndex = findIndex(imageUrls, ['id', id])
|
const initialIndex = findIndex(imageUrls, ['id', id])
|
||||||
const [currentIndex, setCurrentIndex] = useState(initialIndex)
|
const [currentIndex, setCurrentIndex] = useState(initialIndex)
|
||||||
|
|
||||||
|
const messageRef = useRef<FlashMessage>(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<StatusBar backgroundColor='rgb(0,0,0)' />
|
<StatusBar backgroundColor='rgb(0,0,0)' />
|
||||||
@ -132,15 +129,17 @@ const ScreenImagesViewer = ({
|
|||||||
imageIndex={initialIndex}
|
imageIndex={initialIndex}
|
||||||
onImageIndexChange={index => setCurrentIndex(index)}
|
onImageIndexChange={index => setCurrentIndex(index)}
|
||||||
onRequestClose={() => navigation.goBack()}
|
onRequestClose={() => navigation.goBack()}
|
||||||
onLongPress={saveImage}
|
onLongPress={image => saveImage({ messageRef, mode, image })}
|
||||||
HeaderComponent={() => (
|
HeaderComponent={() => (
|
||||||
<HeaderComponent
|
<HeaderComponent
|
||||||
|
messageRef={messageRef}
|
||||||
navigation={navigation}
|
navigation={navigation}
|
||||||
currentIndex={currentIndex}
|
currentIndex={currentIndex}
|
||||||
imageUrls={imageUrls}
|
imageUrls={imageUrls}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Message ref={messageRef} />
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -12,10 +12,14 @@ import {
|
|||||||
getInstanceAccount,
|
getInstanceAccount,
|
||||||
getInstanceActive
|
getInstanceActive
|
||||||
} from '@utils/slices/instancesSlice'
|
} from '@utils/slices/instancesSlice'
|
||||||
|
import {
|
||||||
|
getVersionUpdate,
|
||||||
|
retriveVersionLatest
|
||||||
|
} from '@utils/slices/versionSlice'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useMemo } from 'react'
|
import React, { useCallback, useEffect, useMemo } from 'react'
|
||||||
import { Image, Platform } from 'react-native'
|
import { Platform } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import TabLocal from './Tabs/Local'
|
import TabLocal from './Tabs/Local'
|
||||||
import TabMe from './Tabs/Me'
|
import TabMe from './Tabs/Me'
|
||||||
import TabNotifications from './Tabs/Notifications'
|
import TabNotifications from './Tabs/Notifications'
|
||||||
@ -114,6 +118,17 @@ const ScreenTabs = React.memo(
|
|||||||
|
|
||||||
const previousTab = useSelector(getPreviousTab, () => true)
|
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 (
|
return (
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
initialRouteName={instanceActive !== -1 ? previousTab : 'Tab-Me'}
|
initialRouteName={instanceActive !== -1 ? previousTab : 'Tab-Me'}
|
||||||
@ -128,7 +143,7 @@ const ScreenTabs = React.memo(
|
|||||||
listeners={composeListeners}
|
listeners={composeListeners}
|
||||||
/>
|
/>
|
||||||
<Tab.Screen name='Tab-Notifications' component={TabNotifications} />
|
<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>
|
</Tab.Navigator>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
import { HeaderCenter, HeaderLeft } from '@components/Header'
|
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 React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Platform } from 'react-native'
|
import { Platform } from 'react-native'
|
||||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||||
import ScreenMeSettingsFontsize from './Me/Fontsize'
|
import TabMeBookmarks from './Me/Bookmarks'
|
||||||
import ScreenMeSettingsPush from './Me/Push'
|
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>()
|
const Stack = createNativeStackNavigator<Nav.TabMeStackParamList>()
|
||||||
|
|
||||||
@ -27,7 +28,7 @@ const TabMe = React.memo(
|
|||||||
>
|
>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='Tab-Me-Root'
|
name='Tab-Me-Root'
|
||||||
component={ScreenMeRoot}
|
component={TabMeRoot}
|
||||||
options={{
|
options={{
|
||||||
headerTranslucent: true,
|
headerTranslucent: true,
|
||||||
headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' },
|
headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' },
|
||||||
@ -36,7 +37,7 @@ const TabMe = React.memo(
|
|||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='Tab-Me-Bookmarks'
|
name='Tab-Me-Bookmarks'
|
||||||
component={ScreenMeBookmarks}
|
component={TabMeBookmarks}
|
||||||
options={({ navigation }: any) => ({
|
options={({ navigation }: any) => ({
|
||||||
headerTitle: t('me.stacks.bookmarks.name'),
|
headerTitle: t('me.stacks.bookmarks.name'),
|
||||||
...(Platform.OS === 'android' && {
|
...(Platform.OS === 'android' && {
|
||||||
@ -49,7 +50,7 @@ const TabMe = React.memo(
|
|||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='Tab-Me-Conversations'
|
name='Tab-Me-Conversations'
|
||||||
component={ScreenMeConversations}
|
component={TabMeConversations}
|
||||||
options={({ navigation }: any) => ({
|
options={({ navigation }: any) => ({
|
||||||
headerTitle: t('me.stacks.conversations.name'),
|
headerTitle: t('me.stacks.conversations.name'),
|
||||||
...(Platform.OS === 'android' && {
|
...(Platform.OS === 'android' && {
|
||||||
@ -62,7 +63,7 @@ const TabMe = React.memo(
|
|||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='Tab-Me-Favourites'
|
name='Tab-Me-Favourites'
|
||||||
component={ScreenMeFavourites}
|
component={TabMeFavourites}
|
||||||
options={({ navigation }: any) => ({
|
options={({ navigation }: any) => ({
|
||||||
headerTitle: t('me.stacks.favourites.name'),
|
headerTitle: t('me.stacks.favourites.name'),
|
||||||
...(Platform.OS === 'android' && {
|
...(Platform.OS === 'android' && {
|
||||||
@ -75,7 +76,7 @@ const TabMe = React.memo(
|
|||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='Tab-Me-Lists'
|
name='Tab-Me-Lists'
|
||||||
component={ScreenMeLists}
|
component={TabMeLists}
|
||||||
options={({ navigation }: any) => ({
|
options={({ navigation }: any) => ({
|
||||||
headerTitle: t('me.stacks.lists.name'),
|
headerTitle: t('me.stacks.lists.name'),
|
||||||
...(Platform.OS === 'android' && {
|
...(Platform.OS === 'android' && {
|
||||||
@ -88,7 +89,7 @@ const TabMe = React.memo(
|
|||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='Tab-Me-Lists-List'
|
name='Tab-Me-Lists-List'
|
||||||
component={ScreenMeListsList}
|
component={TabMeListsList}
|
||||||
options={({ route, navigation }: any) => ({
|
options={({ route, navigation }: any) => ({
|
||||||
headerTitle: t('me.stacks.list.name', { list: route.params.title }),
|
headerTitle: t('me.stacks.list.name', { list: route.params.title }),
|
||||||
...(Platform.OS === 'android' && {
|
...(Platform.OS === 'android' && {
|
||||||
@ -103,9 +104,30 @@ const TabMe = React.memo(
|
|||||||
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
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
|
<Stack.Screen
|
||||||
name='Tab-Me-Settings'
|
name='Tab-Me-Settings'
|
||||||
component={ScreenMeSettings}
|
component={TabMeSettings}
|
||||||
options={({ navigation }: any) => ({
|
options={({ navigation }: any) => ({
|
||||||
headerTitle: t('me.stacks.settings.name'),
|
headerTitle: t('me.stacks.settings.name'),
|
||||||
...(Platform.OS === 'android' && {
|
...(Platform.OS === 'android' && {
|
||||||
@ -118,7 +140,7 @@ const TabMe = React.memo(
|
|||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='Tab-Me-Settings-Fontsize'
|
name='Tab-Me-Settings-Fontsize'
|
||||||
component={ScreenMeSettingsFontsize}
|
component={TabMeSettingsFontsize}
|
||||||
options={({ navigation }: any) => ({
|
options={({ navigation }: any) => ({
|
||||||
headerTitle: t('me.stacks.fontSize.name'),
|
headerTitle: t('me.stacks.fontSize.name'),
|
||||||
...(Platform.OS === 'android' && {
|
...(Platform.OS === 'android' && {
|
||||||
@ -129,22 +151,9 @@ const TabMe = React.memo(
|
|||||||
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
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
|
<Stack.Screen
|
||||||
name='Tab-Me-Switch'
|
name='Tab-Me-Switch'
|
||||||
component={ScreenMeSwitch}
|
component={TabMeSwitch}
|
||||||
options={{
|
options={{
|
||||||
stackPresentation: 'modal',
|
stackPresentation: 'modal',
|
||||||
headerShown: false
|
headerShown: false
|
||||||
|
@ -3,7 +3,7 @@ import TimelineDefault from '@components/Timeline/Default'
|
|||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
|
|
||||||
const ScreenMeBookmarks = React.memo(
|
const TabMeBookmarks = React.memo(
|
||||||
() => {
|
() => {
|
||||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Bookmarks' }]
|
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Bookmarks' }]
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
@ -15,4 +15,4 @@ const ScreenMeBookmarks = React.memo(
|
|||||||
() => true
|
() => true
|
||||||
)
|
)
|
||||||
|
|
||||||
export default ScreenMeBookmarks
|
export default TabMeBookmarks
|
||||||
|
@ -3,7 +3,7 @@ import TimelineConversation from '@components/Timeline/Conversation'
|
|||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
|
|
||||||
const ScreenMeConversations = React.memo(
|
const TabMeConversations = React.memo(
|
||||||
() => {
|
() => {
|
||||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Conversations' }]
|
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Conversations' }]
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
@ -18,4 +18,4 @@ const ScreenMeConversations = React.memo(
|
|||||||
() => true
|
() => true
|
||||||
)
|
)
|
||||||
|
|
||||||
export default ScreenMeConversations
|
export default TabMeConversations
|
||||||
|
@ -3,7 +3,7 @@ import TimelineDefault from '@components/Timeline/Default'
|
|||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
|
|
||||||
const ScreenMeFavourites = React.memo(
|
const TabMeFavourites = React.memo(
|
||||||
() => {
|
() => {
|
||||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Favourites' }]
|
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Favourites' }]
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
@ -16,4 +16,4 @@ const ScreenMeFavourites = React.memo(
|
|||||||
() => true
|
() => true
|
||||||
)
|
)
|
||||||
|
|
||||||
export default ScreenMeFavourites
|
export default TabMeFavourites
|
||||||
|
@ -1,20 +1,21 @@
|
|||||||
import { MenuRow } from '@components/Menu'
|
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||||
import { StackScreenProps } from '@react-navigation/stack'
|
import { StackScreenProps } from '@react-navigation/stack'
|
||||||
import { useListsQuery } from '@utils/queryHooks/lists'
|
import { useListsQuery } from '@utils/queryHooks/lists'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const ScreenMeLists: React.FC<StackScreenProps<
|
const TabMeLists: React.FC<StackScreenProps<
|
||||||
Nav.TabMeStackParamList,
|
Nav.TabMeStackParamList,
|
||||||
'Tab-Me-Lists'
|
'Tab-Me-Lists'
|
||||||
>> = ({ navigation }) => {
|
>> = ({ navigation }) => {
|
||||||
const { data } = useListsQuery({})
|
const { data } = useListsQuery({})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<MenuContainer>
|
||||||
{data?.map((d: Mastodon.List, i: number) => (
|
{data?.map((d: Mastodon.List, i: number) => (
|
||||||
<MenuRow
|
<MenuRow
|
||||||
key={i}
|
key={i}
|
||||||
iconFront='List'
|
iconFront='List'
|
||||||
|
iconBack='ChevronRight'
|
||||||
title={d.title}
|
title={d.title}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigation.navigate('Tab-Me-Lists-List', {
|
navigation.navigate('Tab-Me-Lists-List', {
|
||||||
@ -24,8 +25,8 @@ const ScreenMeLists: React.FC<StackScreenProps<
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</MenuContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ScreenMeLists
|
export default TabMeLists
|
||||||
|
@ -4,7 +4,7 @@ import { StackScreenProps } from '@react-navigation/stack'
|
|||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
|
|
||||||
const ScreenMeListsList: React.FC<StackScreenProps<
|
const TabMeListsList: React.FC<StackScreenProps<
|
||||||
Nav.TabMeStackParamList,
|
Nav.TabMeStackParamList,
|
||||||
'Tab-Me-Lists-List'
|
'Tab-Me-Lists-List'
|
||||||
>> = ({
|
>> = ({
|
||||||
@ -21,4 +21,4 @@ const ScreenMeListsList: React.FC<StackScreenProps<
|
|||||||
return <Timeline queryKey={queryKey} customProps={{ renderItem }} />
|
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
|
183
src/screens/Tabs/Me/Profile/Root.tsx
Normal file
183
src/screens/Tabs/Me/Profile/Root.tsx
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
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.cancel')
|
||||||
|
],
|
||||||
|
cancelButtonIndex: 3
|
||||||
|
},
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
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 { updateInstancePush } from '@utils/slices/instances/updatePush'
|
||||||
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
|
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
|
||||||
import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode'
|
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 WebBrowser from 'expo-web-browser'
|
||||||
import * as Notifications from 'expo-notifications'
|
import * as Notifications from 'expo-notifications'
|
||||||
import React, { useEffect, useMemo, useState } from 'react'
|
import React, { useEffect, useMemo, useState } from 'react'
|
||||||
@ -13,9 +18,18 @@ import layoutAnimation from '@utils/styles/layoutAnimation'
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { AppState, Linking } from 'react-native'
|
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 { t } = useTranslation('screenTabs')
|
||||||
|
const instanceAccount = useSelector(
|
||||||
|
getInstanceAccount,
|
||||||
|
(prev, next) => prev?.acct === next?.acct
|
||||||
|
)
|
||||||
|
const instanceUri = useSelector(getInstanceUri)
|
||||||
|
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const instancePush = useSelector(getInstancePush)
|
const instancePush = useSelector(getInstancePush)
|
||||||
@ -106,7 +120,9 @@ const ScreenMeSettingsPush: React.FC = () => {
|
|||||||
) : null}
|
) : null}
|
||||||
<MenuContainer>
|
<MenuContainer>
|
||||||
<MenuRow
|
<MenuRow
|
||||||
title={t('me.push.global.heading')}
|
title={t('me.push.global.heading', {
|
||||||
|
acct: `@${instanceAccount?.acct}@${instanceUri}`
|
||||||
|
})}
|
||||||
description={t('me.push.global.description')}
|
description={t('me.push.global.description')}
|
||||||
loading={instancePush?.global.loading}
|
loading={instancePush?.global.loading}
|
||||||
switchDisabled={!pushEnabled || isLoading}
|
switchDisabled={!pushEnabled || isLoading}
|
||||||
@ -144,4 +160,4 @@ const ScreenMeSettingsPush: React.FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ScreenMeSettingsPush
|
export default TabMePush
|
||||||
|
@ -4,26 +4,31 @@ import Collections from '@screens/Tabs/Me/Root/Collections'
|
|||||||
import Logout from '@screens/Tabs/Me/Root/Logout'
|
import Logout from '@screens/Tabs/Me/Root/Logout'
|
||||||
import MyInfo from '@screens/Tabs/Me/Root/MyInfo'
|
import MyInfo from '@screens/Tabs/Me/Root/MyInfo'
|
||||||
import Settings from '@screens/Tabs/Me/Root/Settings'
|
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 AccountNav from '@screens/Tabs/Shared/Account/Nav'
|
||||||
import AccountContext from '@screens/Tabs/Shared/Account/utils/createContext'
|
import AccountContext from '@screens/Tabs/Shared/Account/utils/createContext'
|
||||||
import accountInitialState from '@screens/Tabs/Shared/Account/utils/initialState'
|
import accountInitialState from '@screens/Tabs/Shared/Account/utils/initialState'
|
||||||
import accountReducer from '@screens/Tabs/Shared/Account/utils/reducer'
|
import accountReducer from '@screens/Tabs/Shared/Account/utils/reducer'
|
||||||
|
import { useProfileQuery } from '@utils/queryHooks/profile'
|
||||||
import { getInstanceActive } from '@utils/slices/instancesSlice'
|
import { getInstanceActive } from '@utils/slices/instancesSlice'
|
||||||
import React, { useReducer, useRef, useState } from 'react'
|
import React, { useReducer, useRef } from 'react'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
useAnimatedScrollHandler,
|
useAnimatedScrollHandler,
|
||||||
useSharedValue
|
useSharedValue
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
import Update from './Root/Update'
|
||||||
|
|
||||||
const ScreenMeRoot: React.FC = () => {
|
const TabMeRoot: React.FC = () => {
|
||||||
const instanceActive = useSelector(getInstanceActive)
|
const instanceActive = useSelector(getInstanceActive)
|
||||||
|
|
||||||
|
const { data } = useProfileQuery({
|
||||||
|
options: { enabled: instanceActive !== -1, keepPreviousData: false }
|
||||||
|
})
|
||||||
|
|
||||||
const scrollRef = useRef<Animated.ScrollView>(null)
|
const scrollRef = useRef<Animated.ScrollView>(null)
|
||||||
useScrollToTop(scrollRef)
|
useScrollToTop(scrollRef)
|
||||||
|
|
||||||
const [data, setData] = useState<Mastodon.Account>()
|
|
||||||
|
|
||||||
const [accountState, accountDispatch] = useReducer(
|
const [accountState, accountDispatch] = useReducer(
|
||||||
accountReducer,
|
accountReducer,
|
||||||
accountInitialState
|
accountInitialState
|
||||||
@ -46,16 +51,18 @@ const ScreenMeRoot: React.FC = () => {
|
|||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
>
|
>
|
||||||
{instanceActive !== -1 ? (
|
{instanceActive !== -1 ? (
|
||||||
<MyInfo setData={setData} />
|
<MyInfo account={data} />
|
||||||
) : (
|
) : (
|
||||||
<ComponentInstance />
|
<ComponentInstance />
|
||||||
)}
|
)}
|
||||||
{instanceActive !== -1 ? <Collections /> : null}
|
{instanceActive !== -1 ? <Collections /> : null}
|
||||||
|
<Update />
|
||||||
<Settings />
|
<Settings />
|
||||||
|
{instanceActive !== -1 ? <AccountInformationSwitch /> : null}
|
||||||
{instanceActive !== -1 ? <Logout /> : null}
|
{instanceActive !== -1 ? <Logout /> : null}
|
||||||
</Animated.ScrollView>
|
</Animated.ScrollView>
|
||||||
</AccountContext.Provider>
|
</AccountContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ScreenMeRoot
|
export default TabMeRoot
|
||||||
|
@ -21,7 +21,7 @@ const Logout: React.FC = () => {
|
|||||||
content={t('me.root.logout.button')}
|
content={t('me.root.logout.button')}
|
||||||
style={{
|
style={{
|
||||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2,
|
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2,
|
||||||
marginBottom: StyleConstants.Spacing.Global.PagePadding * 2
|
marginTop: StyleConstants.Spacing.Global.PagePadding
|
||||||
}}
|
}}
|
||||||
destructive
|
destructive
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
|
@ -1,31 +1,16 @@
|
|||||||
import AccountHeader from '@screens/Tabs/Shared/Account/Header'
|
import AccountHeader from '@screens/Tabs/Shared/Account/Header'
|
||||||
import AccountInformation from '@screens/Tabs/Shared/Account/Information'
|
import AccountInformation from '@screens/Tabs/Shared/Account/Information'
|
||||||
import { useAccountQuery } from '@utils/queryHooks/account'
|
import React from 'react'
|
||||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
|
||||||
import React, { useEffect } from 'react'
|
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
setData: React.Dispatch<React.SetStateAction<Mastodon.Account | undefined>>
|
account: Mastodon.Account | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const MyInfo: React.FC<Props> = ({ setData }) => {
|
const MyInfo: React.FC<Props> = ({ account }) => {
|
||||||
const instanceAccount = useSelector(
|
|
||||||
getInstanceAccount,
|
|
||||||
(prev, next) => prev?.id === next?.id
|
|
||||||
)
|
|
||||||
const { data } = useAccountQuery({ id: instanceAccount!.id })
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
setData(data)
|
|
||||||
}
|
|
||||||
}, [data])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AccountHeader account={data} limitHeight />
|
<AccountHeader account={account} />
|
||||||
<AccountInformation account={data} myInfo />
|
<AccountInformation account={account} myInfo />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
@ -11,6 +12,10 @@ const AccountInformationSwitch: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
type='text'
|
type='text'
|
||||||
content={t('me.stacks.switch.name')}
|
content={t('me.stacks.switch.name')}
|
||||||
|
style={{
|
||||||
|
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2,
|
||||||
|
marginTop: StyleConstants.Spacing.Global.PagePadding
|
||||||
|
}}
|
||||||
onPress={() => navigation.navigate('Tab-Me-Switch')}
|
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 SettingsDev from './Settings/Dev'
|
||||||
import SettingsTooot from './Settings/Tooot'
|
import SettingsTooot from './Settings/Tooot'
|
||||||
|
|
||||||
const ScreenMeSettings: React.FC = () => {
|
const TabMeSettings: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<SettingsApp />
|
<SettingsApp />
|
||||||
@ -23,4 +23,4 @@ const ScreenMeSettings: React.FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ScreenMeSettings
|
export default TabMeSettings
|
||||||
|
@ -5,10 +5,10 @@ import {
|
|||||||
} from '@utils/slices/settingsSlice'
|
} from '@utils/slices/settingsSlice'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import Constants from 'expo-constants'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet, Text } from 'react-native'
|
import { StyleSheet, Text } from 'react-native'
|
||||||
|
import { Constants } from 'react-native-unimodules'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
const SettingsAnalytics: React.FC = () => {
|
const SettingsAnalytics: React.FC = () => {
|
||||||
|
@ -5,11 +5,7 @@ import { useActionSheet } from '@expo/react-native-action-sheet'
|
|||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { LOCALES } from '@root/i18n/locales'
|
import { LOCALES } from '@root/i18n/locales'
|
||||||
import androidDefaults from '@utils/slices/instances/push/androidDefaults'
|
import androidDefaults from '@utils/slices/instances/push/androidDefaults'
|
||||||
import {
|
import { getInstances } from '@utils/slices/instancesSlice'
|
||||||
getInstanceActive,
|
|
||||||
getInstancePush,
|
|
||||||
getInstances
|
|
||||||
} from '@utils/slices/instancesSlice'
|
|
||||||
import {
|
import {
|
||||||
changeBrowser,
|
changeBrowser,
|
||||||
changeLanguage,
|
changeLanguage,
|
||||||
@ -24,7 +20,7 @@ import React from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Platform } from 'react-native'
|
import { Platform } from 'react-native'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { mapFontsizeToName } from '../Fontsize'
|
import { mapFontsizeToName } from '../SettingsFontsize'
|
||||||
|
|
||||||
const SettingsApp: React.FC = () => {
|
const SettingsApp: React.FC = () => {
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
@ -34,43 +30,22 @@ const SettingsApp: React.FC = () => {
|
|||||||
const { t, i18n } = useTranslation('screenTabs')
|
const { t, i18n } = useTranslation('screenTabs')
|
||||||
|
|
||||||
const instances = useSelector(getInstances, () => true)
|
const instances = useSelector(getInstances, () => true)
|
||||||
const instanceActive = useSelector(getInstanceActive)
|
|
||||||
const settingsFontsize = useSelector(getSettingsFontsize)
|
const settingsFontsize = useSelector(getSettingsFontsize)
|
||||||
const settingsTheme = useSelector(getSettingsTheme)
|
const settingsTheme = useSelector(getSettingsTheme)
|
||||||
const settingsBrowser = useSelector(getSettingsBrowser)
|
const settingsBrowser = useSelector(getSettingsBrowser)
|
||||||
const instancePush = useSelector(
|
|
||||||
getInstancePush,
|
|
||||||
(prev, next) => prev?.global.value === next?.global.value
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuContainer>
|
<MenuContainer>
|
||||||
{instanceActive !== -1 ? (
|
<MenuRow
|
||||||
<>
|
title={t('me.settings.fontsize.heading')}
|
||||||
<MenuRow
|
content={t(
|
||||||
title={t('me.settings.push.heading')}
|
`me.settings.fontsize.content.${mapFontsizeToName(settingsFontsize)}`
|
||||||
content={
|
)}
|
||||||
instancePush?.global.value
|
iconBack='ChevronRight'
|
||||||
? t('me.settings.push.content.enabled')
|
onPress={() => {
|
||||||
: t('me.settings.push.content.disabled')
|
navigation.navigate('Tab-Me-Settings-Fontsize')
|
||||||
}
|
}}
|
||||||
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
|
<MenuRow
|
||||||
title={t('me.settings.language.heading')}
|
title={t('me.settings.language.heading')}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import { MenuContainer, MenuRow } from '@components/Menu'
|
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||||
|
import { displayMessage } from '@components/Message'
|
||||||
import { useActionSheet } from '@expo/react-native-action-sheet'
|
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||||
import { persistor } from '@root/store'
|
import { persistor } from '@root/store'
|
||||||
import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
|
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
|
<Button
|
||||||
type='text'
|
type='text'
|
||||||
content={'Purge secure storage'}
|
content={'Purge secure storage'}
|
||||||
style={{
|
style={{
|
||||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2,
|
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2,
|
||||||
marginBottom: StyleConstants.Spacing.Global.PagePadding * 2
|
marginBottom: StyleConstants.Spacing.Global.PagePadding
|
||||||
}}
|
}}
|
||||||
destructive
|
destructive
|
||||||
onPress={() => persistor.purge()}
|
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,
|
Nav.TabMeStackParamList,
|
||||||
'Tab-Me-Settings-Fontsize'
|
'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 Stack = createNativeStackNavigator()
|
||||||
|
|
||||||
const ScreenMeSwitch: React.FC<StackScreenProps<
|
const TabMeSwitch: React.FC<StackScreenProps<
|
||||||
Nav.TabMeStackParamList,
|
Nav.TabMeStackParamList,
|
||||||
'Tab-Me-Switch'
|
'Tab-Me-Switch'
|
||||||
>> = ({ navigation }) => {
|
>> = ({ 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 { useAccountQuery } from '@utils/queryHooks/account'
|
||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
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 { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet, View } from 'react-native'
|
import { StyleSheet, View } from 'react-native'
|
||||||
import { useSharedValue } from 'react-native-reanimated'
|
import { useSharedValue } from 'react-native-reanimated'
|
||||||
@ -13,9 +13,6 @@ import AccountAttachments from './Account/Attachments'
|
|||||||
import AccountHeader from './Account/Header'
|
import AccountHeader from './Account/Header'
|
||||||
import AccountInformation from './Account/Information'
|
import AccountInformation from './Account/Information'
|
||||||
import AccountNav from './Account/Nav'
|
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'
|
import { SharedAccountProp } from './sharedScreens'
|
||||||
|
|
||||||
const TabSharedAccount: React.FC<SharedAccountProp> = ({
|
const TabSharedAccount: React.FC<SharedAccountProp> = ({
|
||||||
@ -30,10 +27,6 @@ const TabSharedAccount: React.FC<SharedAccountProp> = ({
|
|||||||
const { data } = useAccountQuery({ id: account.id })
|
const { data } = useAccountQuery({ id: account.id })
|
||||||
|
|
||||||
const scrollY = useSharedValue(0)
|
const scrollY = useSharedValue(0)
|
||||||
const [accountState, accountDispatch] = useReducer(
|
|
||||||
accountReducer,
|
|
||||||
accountInitialState
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateHeaderRight = () =>
|
const updateHeaderRight = () =>
|
||||||
@ -86,7 +79,7 @@ const TabSharedAccount: React.FC<SharedAccountProp> = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccountContext.Provider value={{ accountState, accountDispatch }}>
|
<>
|
||||||
<AccountNav scrollY={scrollY} account={data} />
|
<AccountNav scrollY={scrollY} account={data} />
|
||||||
|
|
||||||
<Timeline
|
<Timeline
|
||||||
@ -98,7 +91,7 @@ const TabSharedAccount: React.FC<SharedAccountProp> = ({
|
|||||||
ListHeaderComponent
|
ListHeaderComponent
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</AccountContext.Provider>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,36 +1,50 @@
|
|||||||
|
import Button from '@components/Button'
|
||||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext } from 'react'
|
import React from 'react'
|
||||||
import { Dimensions, Image } from 'react-native'
|
import { Dimensions, Image, View } from 'react-native'
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
import AccountContext from './utils/createContext'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
account?: Mastodon.Account
|
account?: Mastodon.Account
|
||||||
limitHeight?: boolean
|
edit?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccountHeader: React.FC<Props> = ({ account }) => {
|
const AccountHeader = React.memo(
|
||||||
const { accountState } = useContext(AccountContext)
|
({ account, edit }: Props) => {
|
||||||
const { reduceMotionEnabled } = useAccessibility()
|
const { reduceMotionEnabled } = useAccessibility()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const topInset = useSafeAreaInsets().top
|
const topInset = useSafeAreaInsets().top
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Image
|
<View>
|
||||||
source={{
|
<Image
|
||||||
uri: reduceMotionEnabled ? account?.header_static : account?.header
|
source={{
|
||||||
}}
|
uri: reduceMotionEnabled ? account?.header_static : account?.header
|
||||||
style={{
|
}}
|
||||||
height:
|
style={{
|
||||||
Dimensions.get('screen').width * accountState.headerRatio + topInset,
|
height: Dimensions.get('screen').width / 3 + topInset,
|
||||||
backgroundColor: theme.disabled
|
backgroundColor: theme.disabled
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
{edit ? (
|
||||||
}
|
<View
|
||||||
|
style={{
|
||||||
export default React.memo(
|
position: 'absolute',
|
||||||
AccountHeader,
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
alignContent: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type='icon' content='Edit' round onPress={() => {}} />
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
},
|
||||||
(_, next) => next.account === undefined
|
(_, next) => next.account === undefined
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export default AccountHeader
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { StyleSheet, View } from 'react-native'
|
import { StyleSheet, View } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import { Placeholder, Fade } from 'rn-placeholder'
|
import { Placeholder, Fade } from 'rn-placeholder'
|
||||||
import AccountInformationAccount from './Information/Account'
|
import AccountInformationAccount from './Information/Account'
|
||||||
import AccountInformationActions from './Information/Actions'
|
import AccountInformationActions from './Information/Actions'
|
||||||
@ -11,69 +9,59 @@ import AccountInformationAvatar from './Information/Avatar'
|
|||||||
import AccountInformationCreated from './Information/Created'
|
import AccountInformationCreated from './Information/Created'
|
||||||
import AccountInformationFields from './Information/Fields'
|
import AccountInformationFields from './Information/Fields'
|
||||||
import AccountInformationName from './Information/Name'
|
import AccountInformationName from './Information/Name'
|
||||||
import AccountInformationNotes from './Information/Notes'
|
import AccountInformationNote from './Information/Note'
|
||||||
import AccountInformationStats from './Information/Stats'
|
import AccountInformationStats from './Information/Stats'
|
||||||
import AccountInformationSwitch from './Information/Switch'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
account: Mastodon.Account | undefined
|
account: Mastodon.Account | undefined
|
||||||
myInfo?: boolean // Showing from my info page
|
myInfo?: boolean // Showing from my info page
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccountInformation: React.FC<Props> = ({ account, myInfo = false }) => {
|
const AccountInformation = React.memo(
|
||||||
const ownAccount =
|
({ account, myInfo = false }: Props) => {
|
||||||
account?.id ===
|
const { mode, theme } = useTheme()
|
||||||
useSelector(getInstanceAccount, (prev, next) => prev?.id === next?.id)?.id
|
|
||||||
const { mode, theme } = useTheme()
|
|
||||||
|
|
||||||
const animation = useCallback(
|
const animation = useCallback(
|
||||||
props => (
|
props => (
|
||||||
<Fade {...props} style={{ backgroundColor: theme.shimmerHighlight }} />
|
<Fade {...props} style={{ backgroundColor: theme.shimmerHighlight }} />
|
||||||
),
|
),
|
||||||
[mode]
|
[mode]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.base}>
|
<View style={styles.base}>
|
||||||
<Placeholder Animation={animation}>
|
<Placeholder Animation={animation}>
|
||||||
<View style={styles.avatarAndActions}>
|
<View style={styles.avatarAndActions}>
|
||||||
<AccountInformationAvatar account={account} myInfo={myInfo} />
|
<AccountInformationAvatar account={account} myInfo={myInfo} />
|
||||||
<View style={styles.actions}>
|
<AccountInformationActions account={account} myInfo={myInfo} />
|
||||||
{myInfo ? (
|
|
||||||
<AccountInformationSwitch />
|
|
||||||
) : (
|
|
||||||
<AccountInformationActions
|
|
||||||
account={account}
|
|
||||||
ownAccount={ownAccount}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
|
||||||
|
|
||||||
<AccountInformationName account={account} />
|
<AccountInformationName account={account} />
|
||||||
|
|
||||||
<AccountInformationAccount account={account} myInfo={myInfo} />
|
<AccountInformationAccount account={account} localInstance={myInfo} />
|
||||||
|
|
||||||
{!myInfo ? (
|
<AccountInformationFields account={account} 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}
|
|
||||||
|
|
||||||
<AccountInformationStats account={account} myInfo={myInfo} />
|
<AccountInformationNote account={account} myInfo={myInfo} />
|
||||||
</Placeholder>
|
|
||||||
</View>
|
<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({
|
const styles = StyleSheet.create({
|
||||||
base: {
|
base: {
|
||||||
@ -90,13 +78,4 @@ const styles = StyleSheet.create({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default React.memo(AccountInformation, (prev, next) => {
|
export default AccountInformation
|
||||||
let skipUpdate = true
|
|
||||||
if (prev.account?.id !== next.account?.id) {
|
|
||||||
skipUpdate = false
|
|
||||||
}
|
|
||||||
if (prev.account?.acct === next.account?.acct) {
|
|
||||||
skipUpdate = false
|
|
||||||
}
|
|
||||||
return skipUpdate
|
|
||||||
})
|
|
||||||
|
@ -12,10 +12,10 @@ import { PlaceholderLine } from 'rn-placeholder'
|
|||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
account: Mastodon.Account | undefined
|
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 { theme } = useTheme()
|
||||||
const instanceAccount = useSelector(
|
const instanceAccount = useSelector(
|
||||||
getInstanceAccount,
|
getInstanceAccount,
|
||||||
@ -48,7 +48,7 @@ const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => {
|
|||||||
}
|
}
|
||||||
}, [account?.moved])
|
}, [account?.moved])
|
||||||
|
|
||||||
if (account || (myInfo && instanceAccount)) {
|
if (account || (localInstance && instanceAccount)) {
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[styles.base, { flexDirection: 'row', alignItems: 'center' }]}
|
style={[styles.base, { flexDirection: 'row', alignItems: 'center' }]}
|
||||||
@ -63,8 +63,8 @@ const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => {
|
|||||||
]}
|
]}
|
||||||
selectable
|
selectable
|
||||||
>
|
>
|
||||||
@{myInfo ? instanceAccount?.acct : account?.acct}
|
@{localInstance ? instanceAccount?.acct : account?.acct}
|
||||||
{myInfo ? `@${instanceUri}` : null}
|
{localInstance ? `@${instanceUri}` : null}
|
||||||
</Text>
|
</Text>
|
||||||
{movedContent}
|
{movedContent}
|
||||||
{account?.locked ? (
|
{account?.locked ? (
|
||||||
@ -88,7 +88,7 @@ const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => {
|
|||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<PlaceholderLine
|
<PlaceholderLine
|
||||||
width={StyleConstants.Font.Size.M * 2}
|
width={StyleConstants.Font.Size.M * 3}
|
||||||
height={StyleConstants.Font.LineHeight.M}
|
height={StyleConstants.Font.LineHeight.M}
|
||||||
color={theme.shimmerDefault}
|
color={theme.shimmerDefault}
|
||||||
noMargin
|
noMargin
|
||||||
|
@ -2,34 +2,21 @@ import analytics from '@components/analytics'
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import { RelationshipOutgoing } from '@components/Relationship'
|
import { RelationshipOutgoing } from '@components/Relationship'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { StackNavigationProp } from '@react-navigation/stack'
|
|
||||||
import { useRelationshipQuery } from '@utils/queryHooks/relationship'
|
import { useRelationshipQuery } from '@utils/queryHooks/relationship'
|
||||||
|
import {
|
||||||
|
getInstanceAccount,
|
||||||
|
getInstancePush,
|
||||||
|
getInstanceUri
|
||||||
|
} from '@utils/slices/instancesSlice'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet } from 'react-native'
|
import { StyleSheet, View } from 'react-native'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
account: Mastodon.Account | undefined
|
account: Mastodon.Account | undefined
|
||||||
ownAccount: boolean
|
myInfo?: 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 })
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Conversation = ({ account }: { account: Mastodon.Account }) => {
|
const Conversation = ({ account }: { account: Mastodon.Account }) => {
|
||||||
@ -41,7 +28,7 @@ const Conversation = ({ account }: { account: Mastodon.Account }) => {
|
|||||||
round
|
round
|
||||||
type='icon'
|
type='icon'
|
||||||
content='Mail'
|
content='Mail'
|
||||||
style={styles.actionConversation}
|
style={styles.actionLeft}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
analytics('account_DM_press')
|
analytics('account_DM_press')
|
||||||
navigation.navigate('Screen-Compose', {
|
navigation.navigate('Screen-Compose', {
|
||||||
@ -53,24 +40,76 @@ const Conversation = ({ account }: { account: Mastodon.Account }) => {
|
|||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccountInformationActions: React.FC<Props> = ({
|
const AccountInformationActions: React.FC<Props> = ({ account, myInfo }) => {
|
||||||
account,
|
const { t } = useTranslation('screenTabs')
|
||||||
ownAccount
|
const navigation = useNavigation()
|
||||||
}) => {
|
|
||||||
return account && account.id ? (
|
if (account?.moved) {
|
||||||
account.moved ? (
|
const accountMoved = account.moved
|
||||||
<GoToMoved accountMoved={account.moved} />
|
return (
|
||||||
) : !ownAccount ? (
|
<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} />
|
<Conversation account={account} />
|
||||||
<RelationshipOutgoing id={account.id} />
|
<RelationshipOutgoing id={account.id} />
|
||||||
</>
|
</View>
|
||||||
) : null
|
)
|
||||||
) : null
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
actionConversation: { marginRight: StyleConstants.Spacing.S }
|
base: {
|
||||||
|
alignSelf: 'flex-end',
|
||||||
|
flexDirection: 'row'
|
||||||
|
},
|
||||||
|
actionLeft: { marginRight: StyleConstants.Spacing.S }
|
||||||
})
|
})
|
||||||
|
|
||||||
export default AccountInformationActions
|
export default AccountInformationActions
|
||||||
|
@ -1,18 +1,24 @@
|
|||||||
import analytics from '@components/analytics'
|
import analytics from '@components/analytics'
|
||||||
|
import Button from '@components/Button'
|
||||||
import GracefullyImage from '@components/GracefullyImage'
|
import GracefullyImage from '@components/GracefullyImage'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { StackNavigationProp } from '@react-navigation/stack'
|
import { StackNavigationProp } from '@react-navigation/stack'
|
||||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Pressable, StyleSheet } from 'react-native'
|
import { Pressable, StyleSheet, View } from 'react-native'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
account: Mastodon.Account | undefined
|
account: Mastodon.Account | undefined
|
||||||
myInfo: boolean
|
myInfo: boolean
|
||||||
|
edit?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccountInformationAvatar: React.FC<Props> = ({ account, myInfo }) => {
|
const AccountInformationAvatar: React.FC<Props> = ({
|
||||||
|
account,
|
||||||
|
myInfo,
|
||||||
|
edit
|
||||||
|
}) => {
|
||||||
const navigation = useNavigation<
|
const navigation = useNavigation<
|
||||||
StackNavigationProp<Nav.TabLocalStackParamList>
|
StackNavigationProp<Nav.TabLocalStackParamList>
|
||||||
>()
|
>()
|
||||||
@ -36,6 +42,20 @@ const AccountInformationAvatar: React.FC<Props> = ({ account, myInfo }) => {
|
|||||||
: account?.avatar
|
: 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>
|
</Pressable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -8,55 +8,63 @@ import { PlaceholderLine } from 'rn-placeholder'
|
|||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
account: Mastodon.Account | undefined
|
account: Mastodon.Account | undefined
|
||||||
|
hidden?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccountInformationCreated: React.FC<Props> = ({ account }) => {
|
const AccountInformationCreated = React.memo(
|
||||||
const { i18n } = useTranslation()
|
({ account, hidden = false }: Props) => {
|
||||||
const { theme } = useTheme()
|
if (hidden) {
|
||||||
const { t } = useTranslation('screenTabs')
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
if (account) {
|
const { i18n } = useTranslation()
|
||||||
return (
|
const { theme } = useTheme()
|
||||||
<View
|
const { t } = useTranslation('screenTabs')
|
||||||
style={[styles.base, { flexDirection: 'row', alignItems: 'center' }]}
|
|
||||||
>
|
if (account) {
|
||||||
<Icon
|
return (
|
||||||
name='Calendar'
|
<View
|
||||||
size={StyleConstants.Font.Size.S}
|
style={[styles.base, { flexDirection: 'row', alignItems: 'center' }]}
|
||||||
color={theme.secondary}
|
|
||||||
style={styles.icon}
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
color: theme.secondary,
|
|
||||||
...StyleConstants.FontStyle.S
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{t('shared.account.created_at', {
|
<Icon
|
||||||
date: new Date(account.created_at || '').toLocaleDateString(
|
name='Calendar'
|
||||||
i18n.language,
|
size={StyleConstants.Font.Size.S}
|
||||||
{
|
color={theme.secondary}
|
||||||
year: 'numeric',
|
style={styles.icon}
|
||||||
month: 'long',
|
/>
|
||||||
day: 'numeric'
|
<Text
|
||||||
}
|
style={{
|
||||||
)
|
color: theme.secondary,
|
||||||
})}
|
...StyleConstants.FontStyle.S
|
||||||
</Text>
|
}}
|
||||||
</View>
|
>
|
||||||
)
|
{t('shared.account.created_at', {
|
||||||
} else {
|
date: new Date(account.created_at || '').toLocaleDateString(
|
||||||
return (
|
i18n.language,
|
||||||
<PlaceholderLine
|
{
|
||||||
width={StyleConstants.Font.Size.S * 3}
|
year: 'numeric',
|
||||||
height={StyleConstants.Font.LineHeight.S}
|
month: 'long',
|
||||||
color={theme.shimmerDefault}
|
day: 'numeric'
|
||||||
noMargin
|
}
|
||||||
style={styles.base}
|
)
|
||||||
/>
|
})}
|
||||||
)
|
</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({
|
const styles = StyleSheet.create({
|
||||||
base: {
|
base: {
|
||||||
@ -68,7 +76,4 @@ const styles = StyleSheet.create({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default React.memo(
|
export default AccountInformationCreated
|
||||||
AccountInformationCreated,
|
|
||||||
(_, next) => next.account === undefined
|
|
||||||
)
|
|
||||||
|
@ -6,11 +6,16 @@ import React from 'react'
|
|||||||
import { StyleSheet, View } from 'react-native'
|
import { StyleSheet, View } from 'react-native'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
account: Mastodon.Account
|
account: Mastodon.Account | undefined
|
||||||
|
myInfo?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccountInformationFields = React.memo(
|
const AccountInformationFields = React.memo(
|
||||||
({ account }: Props) => {
|
({ account, myInfo }: Props) => {
|
||||||
|
if (myInfo || !account?.fields || account.fields.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -88,3 +93,6 @@ const styles = StyleSheet.create({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export default AccountInformationFields
|
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 { ParseEmojis } from '@components/Parse'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo, useState } from 'react'
|
||||||
import { StyleSheet, Text, View } from 'react-native'
|
import { StyleSheet, Text, View } from 'react-native'
|
||||||
import { PlaceholderLine } from 'rn-placeholder'
|
import { PlaceholderLine } from 'rn-placeholder'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
account: Mastodon.Account | undefined
|
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 { theme } = useTheme()
|
||||||
|
|
||||||
const movedStyle = useMemo(
|
|
||||||
() =>
|
|
||||||
StyleSheet.create({
|
|
||||||
base: {
|
|
||||||
textDecorationLine: account?.moved ? 'line-through' : undefined
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
[account?.moved]
|
|
||||||
)
|
|
||||||
const movedContent = useMemo(() => {
|
const movedContent = useMemo(() => {
|
||||||
if (account?.moved) {
|
if (account?.moved) {
|
||||||
return (
|
return (
|
||||||
@ -36,31 +29,41 @@ const AccountInformationName: React.FC<Props> = ({ account }) => {
|
|||||||
}
|
}
|
||||||
}, [account?.moved])
|
}, [account?.moved])
|
||||||
|
|
||||||
if (account) {
|
const [displatName, setDisplayName] = useState(account?.display_name)
|
||||||
return (
|
|
||||||
<View style={[styles.base, { flexDirection: 'row' }]}>
|
return (
|
||||||
<Text style={movedStyle.base}>
|
<View style={[styles.base, { flexDirection: 'row' }]}>
|
||||||
<ParseEmojis
|
{account ? (
|
||||||
content={account.display_name || account.username}
|
edit ? (
|
||||||
emojis={account.emojis}
|
<Input title='昵称' value={displatName} setValue={setDisplayName} />
|
||||||
size='L'
|
) : (
|
||||||
fontBold
|
<>
|
||||||
/>
|
<Text
|
||||||
</Text>
|
style={{
|
||||||
{movedContent}
|
textDecorationLine: account?.moved ? 'line-through' : undefined
|
||||||
</View>
|
}}
|
||||||
)
|
>
|
||||||
} else {
|
<ParseEmojis
|
||||||
return (
|
content={account.display_name || account.username}
|
||||||
<PlaceholderLine
|
emojis={account.emojis}
|
||||||
width={StyleConstants.Font.Size.L * 2}
|
size='L'
|
||||||
height={StyleConstants.Font.LineHeight.L}
|
fontBold
|
||||||
color={theme.shimmerDefault}
|
/>
|
||||||
noMargin
|
</Text>
|
||||||
style={styles.base}
|
{movedContent}
|
||||||
/>
|
</>
|
||||||
)
|
)
|
||||||
}
|
) : (
|
||||||
|
<PlaceholderLine
|
||||||
|
width={StyleConstants.Font.Size.L * 2}
|
||||||
|
height={StyleConstants.Font.LineHeight.L}
|
||||||
|
color={theme.shimmerDefault}
|
||||||
|
noMargin
|
||||||
|
style={{ borderRadius: 0 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
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 {
|
export interface Props {
|
||||||
account: Mastodon.Account | undefined
|
account: Mastodon.Account | undefined
|
||||||
myInfo: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => {
|
const AccountInformationStats: React.FC<Props> = ({ account }) => {
|
||||||
const navigation = useNavigation<
|
const navigation = useNavigation<
|
||||||
StackNavigationProp<Nav.TabLocalStackParamList>
|
StackNavigationProp<Nav.TabLocalStackParamList>
|
||||||
>()
|
>()
|
||||||
@ -28,12 +27,6 @@ const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => {
|
|||||||
children={t('shared.account.summary.statuses_count', {
|
children={t('shared.account.summary.statuses_count', {
|
||||||
count: account.statuses_count || 0
|
count: account.statuses_count || 0
|
||||||
})}
|
})}
|
||||||
onPress={() => {
|
|
||||||
analytics('account_stats_toots_press', {
|
|
||||||
count: account.statuses_count
|
|
||||||
})
|
|
||||||
myInfo && navigation.push('Tab-Shared-Account', { account })
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PlaceholderLine
|
<PlaceholderLine
|
||||||
@ -46,7 +39,10 @@ const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => {
|
|||||||
)}
|
)}
|
||||||
{account ? (
|
{account ? (
|
||||||
<Text
|
<Text
|
||||||
style={[styles.stat, { color: theme.primaryDefault, textAlign: 'right' }]}
|
style={[
|
||||||
|
styles.stat,
|
||||||
|
{ color: theme.primaryDefault, textAlign: 'right' }
|
||||||
|
]}
|
||||||
children={t('shared.account.summary.following_count', {
|
children={t('shared.account.summary.following_count', {
|
||||||
count: account.following_count
|
count: account.following_count
|
||||||
})}
|
})}
|
||||||
@ -73,7 +69,10 @@ const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => {
|
|||||||
)}
|
)}
|
||||||
{account ? (
|
{account ? (
|
||||||
<Text
|
<Text
|
||||||
style={[styles.stat, { color: theme.primaryDefault, textAlign: 'center' }]}
|
style={[
|
||||||
|
styles.stat,
|
||||||
|
{ color: theme.primaryDefault, textAlign: 'center' }
|
||||||
|
]}
|
||||||
children={t('shared.account.summary.followers_count', {
|
children={t('shared.account.summary.followers_count', {
|
||||||
count: account.followers_count
|
count: account.followers_count
|
||||||
})}
|
})}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ParseEmojis } from '@components/Parse'
|
import { ParseEmojis } from '@components/Parse'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext } from 'react'
|
import React from 'react'
|
||||||
import { Dimensions, StyleSheet, Text, View } from 'react-native'
|
import { Dimensions, StyleSheet, Text, View } from 'react-native'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
Extrapolate,
|
Extrapolate,
|
||||||
@ -9,73 +9,74 @@ import Animated, {
|
|||||||
useAnimatedStyle
|
useAnimatedStyle
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
import AccountContext from './utils/createContext'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
scrollY: Animated.SharedValue<number>
|
scrollY: Animated.SharedValue<number>
|
||||||
account: Mastodon.Account | undefined
|
account: Mastodon.Account | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccountNav: React.FC<Props> = ({ scrollY, account }) => {
|
const AccountNav = React.memo(
|
||||||
const { accountState } = useContext(AccountContext)
|
({ scrollY, account }: Props) => {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const headerHeight = useSafeAreaInsets().top + 44
|
const headerHeight = useSafeAreaInsets().top + 44
|
||||||
|
|
||||||
const nameY =
|
const nameY =
|
||||||
Dimensions.get('screen').width * accountState.headerRatio +
|
Dimensions.get('screen').width / 3 +
|
||||||
StyleConstants.Avatar.L -
|
StyleConstants.Avatar.L -
|
||||||
StyleConstants.Spacing.Global.PagePadding * 2 +
|
StyleConstants.Spacing.Global.PagePadding * 2 +
|
||||||
StyleConstants.Spacing.M -
|
StyleConstants.Spacing.M -
|
||||||
headerHeight
|
headerHeight
|
||||||
|
|
||||||
const styleOpacity = useAnimatedStyle(() => {
|
const styleOpacity = useAnimatedStyle(() => {
|
||||||
return {
|
return {
|
||||||
opacity: interpolate(scrollY.value, [0, 200], [0, 1], Extrapolate.CLAMP)
|
opacity: interpolate(scrollY.value, [0, 200], [0, 1], Extrapolate.CLAMP)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const styleMarginTop = useAnimatedStyle(() => {
|
const styleMarginTop = useAnimatedStyle(() => {
|
||||||
return {
|
return {
|
||||||
marginTop: interpolate(
|
marginTop: interpolate(
|
||||||
scrollY.value,
|
scrollY.value,
|
||||||
[nameY, nameY + 20],
|
[nameY, nameY + 20],
|
||||||
[50, 0],
|
[50, 0],
|
||||||
Extrapolate.CLAMP
|
Extrapolate.CLAMP
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
|
||||||
styles.base,
|
|
||||||
styleOpacity,
|
|
||||||
{ backgroundColor: theme.backgroundDefault, height: headerHeight }
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={[
|
style={[
|
||||||
styles.content,
|
styles.base,
|
||||||
{
|
styleOpacity,
|
||||||
marginTop:
|
{ backgroundColor: theme.backgroundDefault, height: headerHeight }
|
||||||
useSafeAreaInsets().top + (44 - StyleConstants.Font.Size.L) / 2
|
|
||||||
}
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Animated.View style={[styles.display_name, styleMarginTop]}>
|
<View
|
||||||
{account ? (
|
style={[
|
||||||
<Text numberOfLines={1}>
|
styles.content,
|
||||||
<ParseEmojis
|
{
|
||||||
content={account.display_name || account.username}
|
marginTop:
|
||||||
emojis={account.emojis}
|
useSafeAreaInsets().top + (44 - StyleConstants.Font.Size.L) / 2
|
||||||
fontBold
|
}
|
||||||
/>
|
]}
|
||||||
</Text>
|
>
|
||||||
) : null}
|
<Animated.View style={[styles.display_name, styleMarginTop]}>
|
||||||
</Animated.View>
|
{account ? (
|
||||||
</View>
|
<Text numberOfLines={1}>
|
||||||
</Animated.View>
|
<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({
|
const styles = StyleSheet.create({
|
||||||
base: {
|
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 createSecureStore from '@neverdull-agency/expo-unlimited-secure-store'
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||||
import {
|
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
|
||||||
combineReducers,
|
|
||||||
configureStore,
|
|
||||||
getDefaultMiddleware
|
|
||||||
} from '@reduxjs/toolkit'
|
|
||||||
import instancesMigration from '@utils/migrations/instances/migration'
|
import instancesMigration from '@utils/migrations/instances/migration'
|
||||||
import contextsSlice from '@utils/slices/contextsSlice'
|
import contextsSlice from '@utils/slices/contextsSlice'
|
||||||
import instancesSlice from '@utils/slices/instancesSlice'
|
import instancesSlice from '@utils/slices/instancesSlice'
|
||||||
import settingsSlice from '@utils/slices/settingsSlice'
|
import settingsSlice from '@utils/slices/settingsSlice'
|
||||||
|
import versionSlice from '@utils/slices/versionSlice'
|
||||||
import { createMigrate, persistReducer, persistStore } from 'redux-persist'
|
import { createMigrate, persistReducer, persistStore } from 'redux-persist'
|
||||||
|
|
||||||
const secureStorage = createSecureStore()
|
const secureStorage = createSecureStore()
|
||||||
@ -27,7 +24,7 @@ const instancesPersistConfig = {
|
|||||||
storage: secureStorage,
|
storage: secureStorage,
|
||||||
version: 5,
|
version: 5,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
migrate: createMigrate(instancesMigration, { debug: true })
|
migrate: createMigrate(instancesMigration)
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsPersistConfig = {
|
const settingsPersistConfig = {
|
||||||
@ -36,21 +33,13 @@ const settingsPersistConfig = {
|
|||||||
storage: AsyncStorage
|
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({
|
const store = configureStore({
|
||||||
reducer: persistReducer(rootPersistConfig, rootReducer),
|
reducer: {
|
||||||
|
contexts: persistReducer(contextsPersistConfig, contextsSlice),
|
||||||
|
instances: persistReducer(instancesPersistConfig, instancesSlice),
|
||||||
|
settings: persistReducer(settingsPersistConfig, settingsSlice),
|
||||||
|
version: versionSlice
|
||||||
|
},
|
||||||
middleware: getDefaultMiddleware({
|
middleware: getDefaultMiddleware({
|
||||||
serializableCheck: {
|
serializableCheck: {
|
||||||
ignoredActions: ['persist/PERSIST']
|
ignoredActions: ['persist/PERSIST']
|
||||||
|
@ -2,9 +2,9 @@ import apiInstance from '@api/instance'
|
|||||||
import { AxiosError } from 'axios'
|
import { AxiosError } from 'axios'
|
||||||
import { useQuery, UseQueryOptions } from 'react-query'
|
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]
|
const { id } = queryKey[1]
|
||||||
|
|
||||||
return apiInstance<Mastodon.Account>({
|
return apiInstance<Mastodon.Account>({
|
||||||
@ -16,10 +16,10 @@ const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
|
|||||||
const useAccountQuery = <TData = Mastodon.Account>({
|
const useAccountQuery = <TData = Mastodon.Account>({
|
||||||
options,
|
options,
|
||||||
...queryKeyParams
|
...queryKeyParams
|
||||||
}: QueryKey[1] & {
|
}: QueryKeyAccount[1] & {
|
||||||
options?: UseQueryOptions<Mastodon.Account, AxiosError, TData>
|
options?: UseQueryOptions<Mastodon.Account, AxiosError, TData>
|
||||||
}) => {
|
}) => {
|
||||||
const queryKey: QueryKey = ['Account', { ...queryKeyParams }]
|
const queryKey: QueryKeyAccount = ['Account', { ...queryKeyParams }]
|
||||||
return useQuery(queryKey, queryFunction, options)
|
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'
|
| 'green'
|
||||||
| 'yellow'
|
| 'yellow'
|
||||||
| 'backgroundDefault'
|
| 'backgroundDefault'
|
||||||
|
| 'backgroundDefaultTransparent'
|
||||||
| 'backgroundOverlayDefault'
|
| 'backgroundOverlayDefault'
|
||||||
| 'backgroundOverlayInvert'
|
| 'backgroundOverlayInvert'
|
||||||
| 'border'
|
| 'border'
|
||||||
@ -59,6 +60,10 @@ const themeColors: {
|
|||||||
light: 'rgb(250, 250, 250)',
|
light: 'rgb(250, 250, 250)',
|
||||||
dark: 'rgb(18, 18, 18)'
|
dark: 'rgb(18, 18, 18)'
|
||||||
},
|
},
|
||||||
|
backgroundDefaultTransparent: {
|
||||||
|
light: 'rgba(250, 250, 250, 0)',
|
||||||
|
dark: 'rgba(18, 18, 18, 0)'
|
||||||
|
},
|
||||||
backgroundOverlayDefault: {
|
backgroundOverlayDefault: {
|
||||||
light: 'rgba(250, 250, 250, 0.5)',
|
light: 'rgba(250, 250, 250, 0.5)',
|
||||||
dark: 'rgba(0, 0, 0, 0.5)'
|
dark: 'rgba(0, 0, 0, 0.5)'
|
||||||
|
@ -8731,6 +8731,11 @@ react-native-blurhash@^1.1.4:
|
|||||||
resolved "https://registry.yarnpkg.com/react-native-blurhash/-/react-native-blurhash-1.1.4.tgz#820afd0ef1cea3b1d322d0448f92964286b85843"
|
resolved "https://registry.yarnpkg.com/react-native-blurhash/-/react-native-blurhash-1.1.4.tgz#820afd0ef1cea3b1d322d0448f92964286b85843"
|
||||||
integrity sha512-lQFDVzrRGU0r3kC7I66MEiGKlzMeRVUm5Jt50rGZnXkm+kLK+LQDOwqEGxxn2Y2OMoDylzrccdX7mP9bf5BnLA==
|
integrity sha512-lQFDVzrRGU0r3kC7I66MEiGKlzMeRVUm5Jt50rGZnXkm+kLK+LQDOwqEGxxn2Y2OMoDylzrccdX7mP9bf5BnLA==
|
||||||
|
|
||||||
|
react-native-clean-project@^3.6.3:
|
||||||
|
version "3.6.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-native-clean-project/-/react-native-clean-project-3.6.3.tgz#ad43b8e1491512f285b7f455ac56db3328b5a65f"
|
||||||
|
integrity sha512-sBbv+Zl05O9LfQqamLu2Crb//W/d8+l59TICF8nKxQ0nJsvear06a1CB2+FaO3rCrPNHiSjDDNXZ/D6muHTUkw==
|
||||||
|
|
||||||
react-native-codegen@^0.0.6:
|
react-native-codegen@^0.0.6:
|
||||||
version "0.0.6"
|
version "0.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.0.6.tgz#b3173faa879cf71bfade8d030f9c4698388f6909"
|
resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.0.6.tgz#b3173faa879cf71bfade8d030f9c4698388f6909"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user