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