Partially fixed #113

This commit is contained in:
Zhiyuan Zheng 2021-05-09 21:59:03 +02:00
parent 006edd5c87
commit 0b659913dc
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
62 changed files with 2308 additions and 703 deletions

View File

@ -28,7 +28,7 @@ declare namespace Mastodon {
moved?: Account moved?: Account
fields: Field[] fields: Field[]
bot: boolean bot: boolean
source: Source source?: Source
} }
type Announcement = { type Announcement = {
@ -258,7 +258,7 @@ declare namespace Mastodon {
type Field = { type Field = {
name: string name: string
value: string value: string
verified_at?: string verified_at: string | null
} }
type List = { type List = {

View File

@ -132,9 +132,23 @@ declare namespace Nav {
list: Mastodon.List['id'] list: Mastodon.List['id']
title: Mastodon.List['title'] title: Mastodon.List['title']
} }
'Tab-Me-Profile': undefined
'Tab-Me-Push': undefined
'Tab-Me-Settings': undefined 'Tab-Me-Settings': undefined
'Tab-Me-Settings-Fontsize': undefined 'Tab-Me-Settings-Fontsize': undefined
'Tab-Me-Settings-Push': undefined
'Tab-Me-Switch': undefined 'Tab-Me-Switch': undefined
} & TabSharedStackParamList } & TabSharedStackParamList
type TabMeProfileStackParamList = {
'Tab-Me-Profile-Root': undefined
'Tab-Me-Profile-Name': {
display_name: Mastodon.Account['display_name']
}
'Tab-Me-Profile-Note': {
note: Mastodon.Source['note']
}
'Tab-Me-Profile-Fields': {
fields?: Mastodon.Source['fields']
}
}
} }

View File

@ -168,7 +168,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
options={{ options={{
stackPresentation: 'transparentModal', stackPresentation: 'transparentModal',
stackAnimation: 'fade', stackAnimation: 'fade',
headerShown: false // Android headerShown: false
}} }}
/> />
<Stack.Screen <Stack.Screen
@ -177,7 +177,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
options={{ options={{
stackPresentation: 'transparentModal', stackPresentation: 'transparentModal',
stackAnimation: 'fade', stackAnimation: 'fade',
headerShown: false // Android headerShown: false
}} }}
/> />
<Stack.Screen <Stack.Screen
@ -185,7 +185,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
component={ScreenCompose} component={ScreenCompose}
options={{ options={{
stackPresentation: 'fullScreenModal', stackPresentation: 'fullScreenModal',
headerShown: false // Android headerShown: false
}} }}
/> />
<Stack.Screen <Stack.Screen
@ -194,7 +194,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
options={{ options={{
stackPresentation: 'fullScreenModal', stackPresentation: 'fullScreenModal',
stackAnimation: 'fade', stackAnimation: 'fade',
headerShown: false // Android headerShown: false
}} }}
/> />
</Stack.Navigator> </Stack.Navigator>
@ -206,6 +206,3 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
} }
export default React.memo(Screens, () => true) export default React.memo(Screens, () => true)
function toast (arg0: { type: string; content: string; autoHide: boolean }) {
throw new Error('Function not implemented.')
}

View File

@ -6,7 +6,7 @@ const ctx = new chalk.Instance({ level: 3 })
export type Params = { export type Params = {
method: 'get' | 'post' | 'put' | 'delete' method: 'get' | 'post' | 'put' | 'delete'
domain?: string domain: string
url: string url: string
params?: { params?: {
[key: string]: string | number | boolean | string[] | number[] | boolean[] [key: string]: string | number | boolean | string[] | number[] | boolean[]
@ -25,10 +25,6 @@ const apiGeneral = async <T = unknown>({
body, body,
sentry = false sentry = false
}: Params): Promise<{ body: T }> => { }: Params): Promise<{ body: T }> => {
if (!domain) {
return Promise.reject()
}
console.log( console.log(
ctx.bgGreen.bold(' API general ') + ctx.bgGreen.bold(' API general ') +
' ' + ' ' +

View File

@ -6,7 +6,7 @@ import li from 'li'
const ctx = new chalk.Instance({ level: 3 }) const ctx = new chalk.Instance({ level: 3 })
export type Params = { export type Params = {
method: 'get' | 'post' | 'put' | 'delete' method: 'get' | 'post' | 'put' | 'delete' | 'patch'
version?: 'v1' | 'v2' version?: 'v1' | 'v2'
url: string url: string
params?: { params?: {

View File

@ -2,7 +2,7 @@ import Icon from '@components/Icon'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation' import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useMemo, useRef } from 'react' import React, { useEffect, useMemo, useRef, useState } from 'react'
import { import {
AccessibilityProps, AccessibilityProps,
Pressable, Pressable,
@ -121,9 +121,6 @@ const Button: React.FC<Props> = ({
color: mainColor, color: mainColor,
fontSize: fontSize:
StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1), StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1),
fontWeight: destructive
? StyleConstants.Font.Weight.Bold
: undefined,
opacity: loading ? 0 : 1 opacity: loading ? 0 : 1
}} }}
children={content} children={content}
@ -135,12 +132,7 @@ const Button: React.FC<Props> = ({
} }
}, [mode, content, loading, disabled]) }, [mode, content, loading, disabled])
enum spacingMapping { const [layoutHeight, setLayoutHeight] = useState<number | undefined>()
XS = 'S',
S = 'M',
M = 'L',
L = 'XL'
}
return ( return (
<Pressable <Pressable
@ -161,10 +153,15 @@ const Button: React.FC<Props> = ({
backgroundColor: colorBackground, backgroundColor: colorBackground,
paddingVertical: StyleConstants.Spacing[spacing], paddingVertical: StyleConstants.Spacing[spacing],
paddingHorizontal: paddingHorizontal:
StyleConstants.Spacing[round ? spacing : spacingMapping[spacing]] StyleConstants.Spacing[spacing] + StyleConstants.Spacing.XS,
width: round && layoutHeight ? layoutHeight : undefined
}, },
customStyle customStyle
]} ]}
{...(round && {
onLayout: ({ nativeEvent }) =>
setLayoutHeight(nativeEvent.layout.height)
})}
testID='base' testID='base'
onPress={onPress} onPress={onPress}
children={children} children={children}
@ -176,7 +173,6 @@ const Button: React.FC<Props> = ({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
borderRadius: 100, borderRadius: 100,
flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center' alignItems: 'center'
} }

161
src/components/Emojis.tsx Normal file
View 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 }

View 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

View 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
View 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

View File

@ -7,16 +7,13 @@ export interface Props {
} }
const MenuContainer: React.FC<Props> = ({ children }) => { const MenuContainer: React.FC<Props> = ({ children }) => {
return ( return <View style={styles.base}>{children}</View>
<View style={styles.base}>
{children}
</View>
)
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
base: { base: {
marginBottom: StyleConstants.Spacing.L paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
marginBottom: StyleConstants.Spacing.Global.PagePadding
} }
}) })

View File

@ -19,8 +19,6 @@ const MenuHeader: React.FC<Props> = ({ heading }) => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
base: { base: {
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
paddingRight: StyleConstants.Spacing.Global.PagePadding,
paddingBottom: StyleConstants.Spacing.S paddingBottom: StyleConstants.Spacing.S
}, },
text: { text: {

View File

@ -15,6 +15,7 @@ export interface Props {
title: string title: string
description?: string description?: string
content?: string | React.ReactNode content?: string | React.ReactNode
badge?: boolean
switchValue?: boolean switchValue?: boolean
switchDisabled?: boolean switchDisabled?: boolean
@ -33,6 +34,7 @@ const MenuRow: React.FC<Props> = ({
title, title,
description, description,
content, content,
badge = false,
switchValue, switchValue,
switchDisabled, switchDisabled,
switchOnValueChange, switchOnValueChange,
@ -84,6 +86,17 @@ const MenuRow: React.FC<Props> = ({
style={styles.iconFront} style={styles.iconFront}
/> />
)} )}
{badge ? (
<View
style={{
width: 8,
height: 8,
backgroundColor: theme.red,
borderRadius: 8,
marginRight: StyleConstants.Spacing.S
}}
/>
) : null}
<View style={styles.main}> <View style={styles.main}>
<Text <Text
style={[styles.title, { color: theme.primaryDefault }]} style={[styles.title, { color: theme.primaryDefault }]}
@ -147,12 +160,12 @@ const MenuRow: React.FC<Props> = ({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
base: { base: {
minHeight: 50 minHeight: 46,
paddingVertical: StyleConstants.Spacing.S
}, },
core: { core: {
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row'
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
}, },
front: { front: {
flex: 2, flex: 2,
@ -167,7 +180,7 @@ const styles = StyleSheet.create({
marginLeft: StyleConstants.Spacing.M marginLeft: StyleConstants.Spacing.M
}, },
iconFront: { iconFront: {
marginRight: 8 marginRight: StyleConstants.Spacing.S
}, },
main: { main: {
flex: 1 flex: 1
@ -176,9 +189,7 @@ const styles = StyleSheet.create({
...StyleConstants.FontStyle.M ...StyleConstants.FontStyle.M
}, },
description: { description: {
...StyleConstants.FontStyle.S, ...StyleConstants.FontStyle.S
marginTop: StyleConstants.Spacing.XS,
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
}, },
content: { content: {
...StyleConstants.FontStyle.M ...StyleConstants.FontStyle.M

View File

@ -2,7 +2,7 @@ import Icon from '@components/Icon'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { getTheme } from '@utils/styles/themes' import { getTheme } from '@utils/styles/themes'
import React from 'react' import React, { RefObject } from 'react'
import { AccessibilityInfo } from 'react-native' import { AccessibilityInfo } from 'react-native'
import FlashMessage, { import FlashMessage, {
hideMessage, hideMessage,
@ -11,6 +11,7 @@ import FlashMessage, {
import haptics from './haptics' import haptics from './haptics'
const displayMessage = ({ const displayMessage = ({
ref,
duration = 'short', duration = 'short',
autoHide = true, autoHide = true,
message, message,
@ -20,6 +21,7 @@ const displayMessage = ({
type type
}: }:
| { | {
ref?: RefObject<FlashMessage>
duration?: 'short' | 'long' duration?: 'short' | 'long'
autoHide?: boolean autoHide?: boolean
message: string message: string
@ -29,6 +31,7 @@ const displayMessage = ({
type?: undefined type?: undefined
} }
| { | {
ref?: RefObject<FlashMessage>
duration?: 'short' | 'long' duration?: 'short' | 'long'
autoHide?: boolean autoHide?: boolean
message: string message: string
@ -54,63 +57,88 @@ const displayMessage = ({
haptics('Error') haptics('Error')
} }
showMessage({ if (ref) {
duration: type === 'error' ? 5000 : duration === 'short' ? 1500 : 3000, ref.current?.showMessage({
autoHide, duration: type === 'error' ? 5000 : duration === 'short' ? 1500 : 3000,
message, autoHide,
description, message,
onPress, description,
...(mode && onPress,
type && { ...(mode &&
renderFlashMessageIcon: () => { type && {
return ( renderFlashMessageIcon: () => {
<Icon return (
name={iconMapping[type]} <Icon
size={StyleConstants.Font.LineHeight.M} name={iconMapping[type]}
color={getTheme(mode)[colorMapping[type]]} size={StyleConstants.Font.LineHeight.M}
style={{ marginRight: StyleConstants.Spacing.S }} color={getTheme(mode)[colorMapping[type]]}
/> style={{ marginRight: StyleConstants.Spacing.S }}
) />
} )
}) }
}) })
})
} else {
showMessage({
duration: type === 'error' ? 5000 : duration === 'short' ? 1500 : 3000,
autoHide,
message,
description,
onPress,
...(mode &&
type && {
renderFlashMessageIcon: () => {
return (
<Icon
name={iconMapping[type]}
size={StyleConstants.Font.LineHeight.M}
color={getTheme(mode)[colorMapping[type]]}
style={{ marginRight: StyleConstants.Spacing.S }}
/>
)
}
})
})
}
} }
const removeMessage = () => { const removeMessage = () => {
// if (ref) {
// ref.current?.hideMessage()
// } else {
hideMessage() hideMessage()
// }
} }
const Message = React.memo( const Message = React.forwardRef<FlashMessage>((_, ref) => {
() => { const { mode, theme } = useTheme()
const { mode, theme } = useTheme()
return ( return (
<FlashMessage <FlashMessage
icon='auto' ref={ref}
position='top' icon='auto'
floating position='top'
style={{ floating
backgroundColor: theme.backgroundDefault, style={{
shadowColor: theme.primaryDefault, backgroundColor: theme.backgroundDefault,
shadowOffset: { width: 0, height: 0 }, shadowColor: theme.primaryDefault,
shadowOpacity: mode === 'light' ? 0.16 : 0.24, shadowOffset: { width: 0, height: 0 },
shadowRadius: 4 shadowOpacity: mode === 'light' ? 0.16 : 0.24,
}} shadowRadius: 4
titleStyle={{ }}
color: theme.primaryDefault, titleStyle={{
...StyleConstants.FontStyle.M, color: theme.primaryDefault,
fontWeight: StyleConstants.Font.Weight.Bold ...StyleConstants.FontStyle.M,
}} fontWeight: StyleConstants.Font.Weight.Bold
textStyle={{ }}
color: theme.primaryDefault, textStyle={{
...StyleConstants.FontStyle.S color: theme.primaryDefault,
}} ...StyleConstants.FontStyle.S
// @ts-ignore }}
textProps={{ numberOfLines: 2 }} // @ts-ignore
/> textProps={{ numberOfLines: 2 }}
) />
}, )
() => true })
)
export { Message, displayMessage, removeMessage } export { Message, displayMessage, removeMessage }

View 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

View File

@ -9,6 +9,7 @@ export default {
screenTabs: require('./screens/tabs'), screenTabs: require('./screens/tabs'),
componentInstance: require('./components/instance'), componentInstance: require('./components/instance'),
componentMediaSelector: require('./components/mediaSelector'),
componentParse: require('./components/parse'), componentParse: require('./components/parse'),
componentRelationship: require('./components/relationship'), componentRelationship: require('./components/relationship'),
componentRelativeTime: require('./components/relativeTime'), componentRelativeTime: require('./components/relativeTime'),

View 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)"
}
}
}
}

View File

@ -104,33 +104,6 @@
"attachment": { "attachment": {
"accessibilityLabel": "Upload attachment", "accessibilityLabel": "Upload attachment",
"accessibilityHint": "Poll function will be disabled when there is any attachment", "accessibilityHint": "Poll function will be disabled when there is any attachment",
"actions": {
"options": {
"library": "Upload from photo library",
"photo": "Upload with camera",
"cancel": "$t(common:buttons.cancel)"
},
"library": {
"alert": {
"title": "No permission",
"message": "Require photo library read permission to upload",
"buttons": {
"settings": "Update setting",
"cancel": "Cancel"
}
}
},
"photo": {
"alert": {
"title": "No permission",
"message": "Require camera usage permission to upload",
"buttons": {
"settings": "Update setting",
"cancel": "Cancel"
}
}
}
},
"failed": { "failed": {
"alert": { "alert": {
"title": "Upload failed", "title": "Upload failed",

View File

@ -52,8 +52,20 @@
"push": { "push": {
"name": "Push Notification" "name": "Push Notification"
}, },
"profile": {
"name": "Edit Profile"
},
"profileName": {
"name": "Edit Display Name"
},
"profileNote": {
"name": "Edit Description"
},
"profileFields": {
"name": "Edit Metadata"
},
"settings": { "settings": {
"name": "Settings" "name": "App Settings"
}, },
"switch": { "switch": {
"name": "Switch Account" "name": "Switch Account"
@ -71,13 +83,74 @@
"XXL": "XXL" "XXL": "XXL"
} }
}, },
"profile": {
"cancellation": {
"title": "Change Not Saved",
"message": "Your change has not been saved. Would you discard saving the changes?",
"buttons": {
"cancel": "$t(common:buttons.cancel)",
"discard": "Discard"
}
},
"feedback": {
"succeed": "{{type}} updated",
"failed": "{{type}} update failed, please try again"
},
"root": {
"name": {
"title": "Display Name"
},
"avatar": {
"title": "Avatar",
"description": "Available in next version"
},
"banner": {
"title": "Banner",
"description": "Available in next version"
},
"note": {
"title": "Description"
},
"fields": {
"title": "Metadata",
"total": "{{count}} field",
"total_plural": "{{count}} fields"
},
"visibility": {
"title": "Posting Visibility",
"options": {
"public": "Public",
"unlisted": "Unlisted",
"private": "Followers only",
"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": { "push": {
"enable": { "enable": {
"direct": "Enable push notification", "direct": "Enable push notification",
"settings": "Enable in settings" "settings": "Enable in settings"
}, },
"global": { "global": {
"heading": "Enable push notification", "heading": "Enable for {{acct}}",
"description": "Messages are routed through tooot's server" "description": "Messages are routed through tooot's server"
}, },
"decode": { "decode": {
@ -112,6 +185,9 @@
"empty": "None" "empty": "None"
} }
}, },
"update": {
"title": "Update to latest version"
},
"logout": { "logout": {
"button": "Log out", "button": "Log out",
"alert": { "alert": {
@ -125,13 +201,6 @@
} }
}, },
"settings": { "settings": {
"push": {
"heading": "$t(me.stacks.push.name)",
"content": {
"enabled": "Enabled",
"disabled": "Disabled"
}
},
"fontsize": { "fontsize": {
"heading": "$t(me.stacks.fontSize.name)", "heading": "$t(me.stacks.fontSize.name)",
"content": { "content": {
@ -158,7 +227,7 @@
} }
}, },
"browser": { "browser": {
"heading": "Opening link", "heading": "Opening Link",
"options": { "options": {
"internal": "Inside app", "internal": "Inside app",
"external": "Use system browser", "external": "Use system browser",

View File

@ -1,14 +1,13 @@
import * as ImagePicker from 'expo-image-picker'
import * as Crypto from 'expo-crypto' import * as Crypto from 'expo-crypto'
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types' import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types'
import * as VideoThumbnails from 'expo-video-thumbnails' import * as VideoThumbnails from 'expo-video-thumbnails'
import { Dispatch } from 'react' import { Dispatch } from 'react'
import { Alert, Linking } from 'react-native' import { Alert } from 'react-native'
import { ComposeAction } from '../../utils/types' import { ComposeAction } from '../../utils/types'
import { ActionSheetOptions } from '@expo/react-native-action-sheet' import { ActionSheetOptions } from '@expo/react-native-action-sheet'
import i18next from 'i18next' import i18next from 'i18next'
import analytics from '@components/analytics'
import apiInstance from '@api/instance' import apiInstance from '@api/instance'
import mediaSelector from '@components/mediaSelector'
export interface Props { export interface Props {
composeDispatch: Dispatch<ComposeAction> composeDispatch: Dispatch<ComposeAction>
@ -22,35 +21,33 @@ const addAttachment = async ({
composeDispatch, composeDispatch,
showActionSheetWithOptions showActionSheetWithOptions
}: Props): Promise<any> => { }: Props): Promise<any> => {
const uploadAttachment = async (result: ImageInfo) => { const uploader = async (imageInfo: ImageInfo) => {
const hash = await Crypto.digestStringAsync( const hash = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256, Crypto.CryptoDigestAlgorithm.SHA256,
result.uri + Math.random() imageInfo.uri + Math.random()
) )
let attachmentType: string let attachmentType: string
// https://github.com/expo/expo/issues/11214
const attachmentUri = result.uri.replace('file:/data', 'file:///data')
switch (result.type) { switch (imageInfo.type) {
case 'image': case 'image':
attachmentType = `image/${attachmentUri.split('.')[1]}` attachmentType = `image/${imageInfo.uri.split('.')[1]}`
composeDispatch({ composeDispatch({
type: 'attachment/upload/start', type: 'attachment/upload/start',
payload: { payload: {
local: { ...result, local_thumbnail: attachmentUri, hash }, local: { ...imageInfo, local_thumbnail: imageInfo.uri, hash },
uploading: true uploading: true
} }
}) })
break break
case 'video': case 'video':
attachmentType = `video/${attachmentUri.split('.')[1]}` attachmentType = `video/${imageInfo.uri.split('.')[1]}`
VideoThumbnails.getThumbnailAsync(attachmentUri) VideoThumbnails.getThumbnailAsync(imageInfo.uri)
.then(({ uri }) => .then(({ uri }) =>
composeDispatch({ composeDispatch({
type: 'attachment/upload/start', type: 'attachment/upload/start',
payload: { payload: {
local: { ...result, local_thumbnail: uri, hash }, local: { ...imageInfo, local_thumbnail: uri, hash },
uploading: true uploading: true
} }
}) })
@ -59,7 +56,7 @@ const addAttachment = async ({
composeDispatch({ composeDispatch({
type: 'attachment/upload/start', type: 'attachment/upload/start',
payload: { payload: {
local: { ...result, hash }, local: { ...imageInfo, hash },
uploading: true uploading: true
} }
}) })
@ -70,7 +67,7 @@ const addAttachment = async ({
composeDispatch({ composeDispatch({
type: 'attachment/upload/start', type: 'attachment/upload/start',
payload: { payload: {
local: { ...result, hash }, local: { ...imageInfo, hash },
uploading: true uploading: true
} }
}) })
@ -101,7 +98,7 @@ const addAttachment = async ({
const formData = new FormData() const formData = new FormData()
formData.append('file', { formData.append('file', {
// @ts-ignore // @ts-ignore
uri: attachmentUri, uri: imageInfo.uri,
name: attachmentType, name: attachmentType,
type: attachmentType type: attachmentType
}) })
@ -115,7 +112,7 @@ const addAttachment = async ({
if (res.body.id) { if (res.body.id) {
composeDispatch({ composeDispatch({
type: 'attachment/upload/end', type: 'attachment/upload/end',
payload: { remote: res.body, local: result } payload: { remote: res.body, local: imageInfo }
}) })
} else { } else {
uploadFailed() uploadFailed()
@ -126,119 +123,7 @@ const addAttachment = async ({
}) })
} }
showActionSheetWithOptions( mediaSelector({ uploader, showActionSheetWithOptions })
{
options: [
i18next.t(
'screenCompose:content.root.actions.attachment.actions.options.library'
),
i18next.t(
'screenCompose:content.root.actions.attachment.actions.options.photo'
),
i18next.t(
'screenCompose:content.root.actions.attachment.actions.options.cancel'
)
],
cancelButtonIndex: 2
},
async buttonIndex => {
if (buttonIndex === 0) {
const {
status
} = await ImagePicker.requestMediaLibraryPermissionsAsync()
if (status !== 'granted') {
Alert.alert(
i18next.t(
'screenCompose:content.root.actions.attachment.actions.library.alert.title'
),
i18next.t(
'screenCompose:content.root.actions.attachment.actions.library.alert.message'
),
[
{
text: i18next.t(
'screenCompose:content.root.actions.attachment.actions.library.alert.buttons.cancel'
),
style: 'cancel',
onPress: () => {
analytics('compose_addattachment_medialibrary_nopermission', {
action: 'cancel'
})
}
},
{
text: i18next.t(
'screenCompose:content.root.actions.attachment.actions.library.alert.buttons.settings'
),
style: 'default',
onPress: () => {
analytics('compose_addattachment_medialibrary_nopermission', {
action: 'settings'
})
Linking.openURL('app-settings:')
}
}
]
)
} else {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
exif: false
})
if (!result.cancelled) {
uploadAttachment(result)
}
}
} else if (buttonIndex === 1) {
const { status } = await ImagePicker.requestCameraPermissionsAsync()
if (status !== 'granted') {
Alert.alert(
i18next.t(
'screenCompose:content.root.actions.attachment.actions.photo.alert.title'
),
i18next.t(
'screenCompose:content.root.actions.attachment.actions.photo.alert.message'
),
[
{
text: i18next.t(
'screenCompose:content.root.actions.attachment.actions.photo.alert.buttons.cancel'
),
style: 'cancel',
onPress: () => {
analytics('compose_addattachment_camera_nopermission', {
action: 'cancel'
})
}
},
{
text: i18next.t(
'screenCompose:content.root.actions.attachment.actions.photo.alert.buttons.settings'
),
style: 'default',
onPress: () => {
analytics('compose_addattachment_camera_nopermission', {
action: 'settings'
})
Linking.openURL('app-settings:')
}
}
]
)
} else {
const result = await ImagePicker.launchCameraAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
exif: false
})
if (!result.cancelled) {
uploadAttachment(result)
}
}
}
}
)
} }
export default addAttachment export default addAttachment

View File

@ -12,10 +12,14 @@ import {
getInstanceAccount, getInstanceAccount,
getInstanceActive getInstanceActive
} from '@utils/slices/instancesSlice' } from '@utils/slices/instancesSlice'
import {
getVersionUpdate,
retriveVersionLatest
} from '@utils/slices/versionSlice'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useMemo } from 'react' import React, { useCallback, useEffect, useMemo } from 'react'
import { Image, Platform } from 'react-native' import { Platform } from 'react-native'
import { useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import TabLocal from './Tabs/Local' import TabLocal from './Tabs/Local'
import TabMe from './Tabs/Me' import TabMe from './Tabs/Me'
import TabNotifications from './Tabs/Notifications' import TabNotifications from './Tabs/Notifications'
@ -114,6 +118,17 @@ const ScreenTabs = React.memo(
const previousTab = useSelector(getPreviousTab, () => true) const previousTab = useSelector(getPreviousTab, () => true)
const versionUpdate = useSelector(getVersionUpdate)
const dispatch = useDispatch()
useEffect(() => {
dispatch(retriveVersionLatest())
}, [])
const tabMeOptions = useMemo(() => {
if (versionUpdate) {
return { tabBarBadge: 1 }
}
}, [versionUpdate])
return ( return (
<Tab.Navigator <Tab.Navigator
initialRouteName={instanceActive !== -1 ? previousTab : 'Tab-Me'} initialRouteName={instanceActive !== -1 ? previousTab : 'Tab-Me'}
@ -128,7 +143,7 @@ const ScreenTabs = React.memo(
listeners={composeListeners} listeners={composeListeners}
/> />
<Tab.Screen name='Tab-Notifications' component={TabNotifications} /> <Tab.Screen name='Tab-Notifications' component={TabNotifications} />
<Tab.Screen name='Tab-Me' component={TabMe} /> <Tab.Screen name='Tab-Me' component={TabMe} options={tabMeOptions} />
</Tab.Navigator> </Tab.Navigator>
) )
}, },

View File

@ -1,19 +1,20 @@
import { HeaderCenter, HeaderLeft } from '@components/Header' import { HeaderCenter, HeaderLeft } from '@components/Header'
import ScreenMeBookmarks from '@screens/Tabs/Me/Bookmarks'
import ScreenMeConversations from '@screens/Tabs/Me/Cconversations'
import ScreenMeFavourites from '@screens/Tabs/Me/Favourites'
import ScreenMeLists from '@screens/Tabs/Me/Lists'
import ScreenMeRoot from '@screens/Tabs/Me/Root'
import ScreenMeListsList from '@screens/Tabs/Me/Root/Lists/List'
import ScreenMeSettings from '@screens/Tabs/Me/Settings'
import ScreenMeSwitch from '@screens/Tabs/Me/Switch'
import sharedScreens from '@screens/Tabs/Shared/sharedScreens'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native' import { Platform } from 'react-native'
import { createNativeStackNavigator } from 'react-native-screens/native-stack' import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import ScreenMeSettingsFontsize from './Me/Fontsize' import TabMeBookmarks from './Me/Bookmarks'
import ScreenMeSettingsPush from './Me/Push' import TabMeConversations from './Me/Cconversations'
import TabMeFavourites from './Me/Favourites'
import TabMeLists from './Me/Lists'
import TabMeListsList from './Me/ListsList'
import TabMeProfile from './Me/Profile'
import TabMePush from './Me/Push'
import TabMeRoot from './Me/Root'
import TabMeSettings from './Me/Settings'
import TabMeSettingsFontsize from './Me/SettingsFontsize'
import TabMeSwitch from './Me/Switch'
import sharedScreens from './Shared/sharedScreens'
const Stack = createNativeStackNavigator<Nav.TabMeStackParamList>() const Stack = createNativeStackNavigator<Nav.TabMeStackParamList>()
@ -27,7 +28,7 @@ const TabMe = React.memo(
> >
<Stack.Screen <Stack.Screen
name='Tab-Me-Root' name='Tab-Me-Root'
component={ScreenMeRoot} component={TabMeRoot}
options={{ options={{
headerTranslucent: true, headerTranslucent: true,
headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' }, headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' },
@ -36,7 +37,7 @@ const TabMe = React.memo(
/> />
<Stack.Screen <Stack.Screen
name='Tab-Me-Bookmarks' name='Tab-Me-Bookmarks'
component={ScreenMeBookmarks} component={TabMeBookmarks}
options={({ navigation }: any) => ({ options={({ navigation }: any) => ({
headerTitle: t('me.stacks.bookmarks.name'), headerTitle: t('me.stacks.bookmarks.name'),
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {
@ -49,7 +50,7 @@ const TabMe = React.memo(
/> />
<Stack.Screen <Stack.Screen
name='Tab-Me-Conversations' name='Tab-Me-Conversations'
component={ScreenMeConversations} component={TabMeConversations}
options={({ navigation }: any) => ({ options={({ navigation }: any) => ({
headerTitle: t('me.stacks.conversations.name'), headerTitle: t('me.stacks.conversations.name'),
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {
@ -62,7 +63,7 @@ const TabMe = React.memo(
/> />
<Stack.Screen <Stack.Screen
name='Tab-Me-Favourites' name='Tab-Me-Favourites'
component={ScreenMeFavourites} component={TabMeFavourites}
options={({ navigation }: any) => ({ options={({ navigation }: any) => ({
headerTitle: t('me.stacks.favourites.name'), headerTitle: t('me.stacks.favourites.name'),
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {
@ -75,7 +76,7 @@ const TabMe = React.memo(
/> />
<Stack.Screen <Stack.Screen
name='Tab-Me-Lists' name='Tab-Me-Lists'
component={ScreenMeLists} component={TabMeLists}
options={({ navigation }: any) => ({ options={({ navigation }: any) => ({
headerTitle: t('me.stacks.lists.name'), headerTitle: t('me.stacks.lists.name'),
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {
@ -88,7 +89,7 @@ const TabMe = React.memo(
/> />
<Stack.Screen <Stack.Screen
name='Tab-Me-Lists-List' name='Tab-Me-Lists-List'
component={ScreenMeListsList} component={TabMeListsList}
options={({ route, navigation }: any) => ({ options={({ route, navigation }: any) => ({
headerTitle: t('me.stacks.list.name', { list: route.params.title }), headerTitle: t('me.stacks.list.name', { list: route.params.title }),
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {
@ -103,9 +104,30 @@ const TabMe = React.memo(
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})} })}
/> />
<Stack.Screen
name='Tab-Me-Profile'
component={TabMeProfile}
options={{
stackPresentation: 'modal',
headerShown: false
}}
/>
<Stack.Screen
name='Tab-Me-Push'
component={TabMePush}
options={({ navigation }: any) => ({
headerTitle: t('me.stacks.push.name'),
...(Platform.OS === 'android' && {
headerCenter: () => (
<HeaderCenter content={t('me.stacks.push.name')} />
)
}),
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})}
/>
<Stack.Screen <Stack.Screen
name='Tab-Me-Settings' name='Tab-Me-Settings'
component={ScreenMeSettings} component={TabMeSettings}
options={({ navigation }: any) => ({ options={({ navigation }: any) => ({
headerTitle: t('me.stacks.settings.name'), headerTitle: t('me.stacks.settings.name'),
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {
@ -118,7 +140,7 @@ const TabMe = React.memo(
/> />
<Stack.Screen <Stack.Screen
name='Tab-Me-Settings-Fontsize' name='Tab-Me-Settings-Fontsize'
component={ScreenMeSettingsFontsize} component={TabMeSettingsFontsize}
options={({ navigation }: any) => ({ options={({ navigation }: any) => ({
headerTitle: t('me.stacks.fontSize.name'), headerTitle: t('me.stacks.fontSize.name'),
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {
@ -129,22 +151,9 @@ const TabMe = React.memo(
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})} })}
/> />
<Stack.Screen
name='Tab-Me-Settings-Push'
component={ScreenMeSettingsPush}
options={({ navigation }: any) => ({
headerTitle: t('me.stacks.push.name'),
...(Platform.OS === 'android' && {
headerCenter: () => (
<HeaderCenter content={t('me.stacks.push.name')} />
)
}),
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})}
/>
<Stack.Screen <Stack.Screen
name='Tab-Me-Switch' name='Tab-Me-Switch'
component={ScreenMeSwitch} component={TabMeSwitch}
options={{ options={{
stackPresentation: 'modal', stackPresentation: 'modal',
headerShown: false headerShown: false

View File

@ -3,7 +3,7 @@ import TimelineDefault from '@components/Timeline/Default'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
const ScreenMeBookmarks = React.memo( const TabMeBookmarks = React.memo(
() => { () => {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Bookmarks' }] const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Bookmarks' }]
const renderItem = useCallback( const renderItem = useCallback(
@ -15,4 +15,4 @@ const ScreenMeBookmarks = React.memo(
() => true () => true
) )
export default ScreenMeBookmarks export default TabMeBookmarks

View File

@ -3,7 +3,7 @@ import TimelineConversation from '@components/Timeline/Conversation'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
const ScreenMeConversations = React.memo( const TabMeConversations = React.memo(
() => { () => {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Conversations' }] const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Conversations' }]
const renderItem = useCallback( const renderItem = useCallback(
@ -18,4 +18,4 @@ const ScreenMeConversations = React.memo(
() => true () => true
) )
export default ScreenMeConversations export default TabMeConversations

View File

@ -3,7 +3,7 @@ import TimelineDefault from '@components/Timeline/Default'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
const ScreenMeFavourites = React.memo( const TabMeFavourites = React.memo(
() => { () => {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Favourites' }] const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Favourites' }]
const renderItem = useCallback( const renderItem = useCallback(
@ -16,4 +16,4 @@ const ScreenMeFavourites = React.memo(
() => true () => true
) )
export default ScreenMeFavourites export default TabMeFavourites

View File

@ -3,7 +3,7 @@ import { StackScreenProps } from '@react-navigation/stack'
import { useListsQuery } from '@utils/queryHooks/lists' import { useListsQuery } from '@utils/queryHooks/lists'
import React from 'react' import React from 'react'
const ScreenMeLists: React.FC<StackScreenProps< const TabMeLists: React.FC<StackScreenProps<
Nav.TabMeStackParamList, Nav.TabMeStackParamList,
'Tab-Me-Lists' 'Tab-Me-Lists'
>> = ({ navigation }) => { >> = ({ navigation }) => {
@ -28,4 +28,4 @@ const ScreenMeLists: React.FC<StackScreenProps<
) )
} }
export default ScreenMeLists export default TabMeLists

View File

@ -4,7 +4,7 @@ import { StackScreenProps } from '@react-navigation/stack'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
const ScreenMeListsList: React.FC<StackScreenProps< const TabMeListsList: React.FC<StackScreenProps<
Nav.TabMeStackParamList, Nav.TabMeStackParamList,
'Tab-Me-Lists-List' 'Tab-Me-Lists-List'
>> = ({ >> = ({
@ -21,4 +21,4 @@ const ScreenMeListsList: React.FC<StackScreenProps<
return <Timeline queryKey={queryKey} customProps={{ renderItem }} /> return <Timeline queryKey={queryKey} customProps={{ renderItem }} />
} }
export default ScreenMeListsList export default TabMeListsList

View 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

View 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

View 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

View 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

View 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

View File

@ -2,7 +2,12 @@ import { MenuContainer, MenuRow } from '@components/Menu'
import { updateInstancePush } from '@utils/slices/instances/updatePush' import { updateInstancePush } from '@utils/slices/instances/updatePush'
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert' import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode' import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode'
import { clearPushLoading, getInstancePush } from '@utils/slices/instancesSlice' import {
clearPushLoading,
getInstanceAccount,
getInstancePush,
getInstanceUri
} from '@utils/slices/instancesSlice'
import * as WebBrowser from 'expo-web-browser' import * as WebBrowser from 'expo-web-browser'
import * as Notifications from 'expo-notifications' import * as Notifications from 'expo-notifications'
import React, { useEffect, useMemo, useState } from 'react' import React, { useEffect, useMemo, useState } from 'react'
@ -13,9 +18,18 @@ import layoutAnimation from '@utils/styles/layoutAnimation'
import Button from '@components/Button' import Button from '@components/Button'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { AppState, Linking } from 'react-native' import { AppState, Linking } from 'react-native'
import { StackScreenProps } from '@react-navigation/stack'
const ScreenMeSettingsPush: React.FC = () => { const TabMePush: React.FC<StackScreenProps<
Nav.TabMeStackParamList,
'Tab-Me-Push'
>> = () => {
const { t } = useTranslation('screenTabs') const { t } = useTranslation('screenTabs')
const instanceAccount = useSelector(
getInstanceAccount,
(prev, next) => prev?.acct === next?.acct
)
const instanceUri = useSelector(getInstanceUri)
const dispatch = useDispatch() const dispatch = useDispatch()
const instancePush = useSelector(getInstancePush) const instancePush = useSelector(getInstancePush)
@ -106,7 +120,9 @@ const ScreenMeSettingsPush: React.FC = () => {
) : null} ) : null}
<MenuContainer> <MenuContainer>
<MenuRow <MenuRow
title={t('me.push.global.heading')} title={t('me.push.global.heading', {
acct: `@${instanceAccount?.acct}@${instanceUri}`
})}
description={t('me.push.global.description')} description={t('me.push.global.description')}
loading={instancePush?.global.loading} loading={instancePush?.global.loading}
switchDisabled={!pushEnabled || isLoading} switchDisabled={!pushEnabled || isLoading}
@ -144,4 +160,4 @@ const ScreenMeSettingsPush: React.FC = () => {
) )
} }
export default ScreenMeSettingsPush export default TabMePush

View File

@ -4,31 +4,25 @@ import Collections from '@screens/Tabs/Me/Root/Collections'
import Logout from '@screens/Tabs/Me/Root/Logout' import Logout from '@screens/Tabs/Me/Root/Logout'
import MyInfo from '@screens/Tabs/Me/Root/MyInfo' import MyInfo from '@screens/Tabs/Me/Root/MyInfo'
import Settings from '@screens/Tabs/Me/Root/Settings' import Settings from '@screens/Tabs/Me/Root/Settings'
import AccountInformationSwitch from '@screens/Tabs/Me/Root/Switch'
import AccountNav from '@screens/Tabs/Shared/Account/Nav' import AccountNav from '@screens/Tabs/Shared/Account/Nav'
import AccountContext from '@screens/Tabs/Shared/Account/utils/createContext' import AccountContext from '@screens/Tabs/Shared/Account/utils/createContext'
import accountInitialState from '@screens/Tabs/Shared/Account/utils/initialState' import accountInitialState from '@screens/Tabs/Shared/Account/utils/initialState'
import accountReducer from '@screens/Tabs/Shared/Account/utils/reducer' import accountReducer from '@screens/Tabs/Shared/Account/utils/reducer'
import { useAccountQuery } from '@utils/queryHooks/account' import { useProfileQuery } from '@utils/queryHooks/profile'
import { import { getInstanceActive } from '@utils/slices/instancesSlice'
getInstanceAccount,
getInstanceActive
} from '@utils/slices/instancesSlice'
import React, { useReducer, useRef } from 'react' import React, { useReducer, useRef } from 'react'
import Animated, { import Animated, {
useAnimatedScrollHandler, useAnimatedScrollHandler,
useSharedValue useSharedValue
} from 'react-native-reanimated' } from 'react-native-reanimated'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import Update from './Root/Update'
const ScreenMeRoot: React.FC = () => { const TabMeRoot: React.FC = () => {
const instanceActive = useSelector(getInstanceActive) const instanceActive = useSelector(getInstanceActive)
const instanceAccount = useSelector(
getInstanceAccount, const { data } = useProfileQuery({
(prev, next) => prev?.id === next?.id
)
const { data } = useAccountQuery({
// @ts-ignore
id: instanceAccount?.id,
options: { enabled: instanceActive !== -1, keepPreviousData: false } options: { enabled: instanceActive !== -1, keepPreviousData: false }
}) })
@ -62,11 +56,13 @@ const ScreenMeRoot: React.FC = () => {
<ComponentInstance /> <ComponentInstance />
)} )}
{instanceActive !== -1 ? <Collections /> : null} {instanceActive !== -1 ? <Collections /> : null}
<Update />
<Settings /> <Settings />
{instanceActive !== -1 ? <AccountInformationSwitch /> : null}
{instanceActive !== -1 ? <Logout /> : null} {instanceActive !== -1 ? <Logout /> : null}
</Animated.ScrollView> </Animated.ScrollView>
</AccountContext.Provider> </AccountContext.Provider>
) )
} }
export default ScreenMeRoot export default TabMeRoot

View File

@ -21,7 +21,7 @@ const Logout: React.FC = () => {
content={t('me.root.logout.button')} content={t('me.root.logout.button')}
style={{ style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2, marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2,
marginBottom: StyleConstants.Spacing.Global.PagePadding * 2 marginTop: StyleConstants.Spacing.Global.PagePadding
}} }}
destructive destructive
onPress={() => onPress={() =>

View File

@ -9,7 +9,7 @@ export interface Props {
const MyInfo: React.FC<Props> = ({ account }) => { const MyInfo: React.FC<Props> = ({ account }) => {
return ( return (
<> <>
<AccountHeader account={account} limitHeight /> <AccountHeader account={account} />
<AccountInformation account={account} myInfo /> <AccountInformation account={account} myInfo />
</> </>
) )

View File

@ -1,5 +1,6 @@
import Button from '@components/Button' import Button from '@components/Button'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { StyleConstants } from '@utils/styles/constants'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -11,6 +12,10 @@ const AccountInformationSwitch: React.FC = () => {
<Button <Button
type='text' type='text'
content={t('me.stacks.switch.name')} content={t('me.stacks.switch.name')}
style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2,
marginTop: StyleConstants.Spacing.Global.PagePadding
}}
onPress={() => navigation.navigate('Tab-Me-Switch')} onPress={() => navigation.navigate('Tab-Me-Switch')}
/> />
) )

View 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

View File

@ -6,7 +6,7 @@ import SettingsApp from './Settings/App'
import SettingsDev from './Settings/Dev' import SettingsDev from './Settings/Dev'
import SettingsTooot from './Settings/Tooot' import SettingsTooot from './Settings/Tooot'
const ScreenMeSettings: React.FC = () => { const TabMeSettings: React.FC = () => {
return ( return (
<ScrollView> <ScrollView>
<SettingsApp /> <SettingsApp />
@ -23,4 +23,4 @@ const ScreenMeSettings: React.FC = () => {
) )
} }
export default ScreenMeSettings export default TabMeSettings

View File

@ -5,10 +5,10 @@ import {
} from '@utils/slices/settingsSlice' } from '@utils/slices/settingsSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import Constants from 'expo-constants'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleSheet, Text } from 'react-native' import { StyleSheet, Text } from 'react-native'
import { Constants } from 'react-native-unimodules'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
const SettingsAnalytics: React.FC = () => { const SettingsAnalytics: React.FC = () => {

View File

@ -5,11 +5,7 @@ import { useActionSheet } from '@expo/react-native-action-sheet'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { LOCALES } from '@root/i18n/locales' import { LOCALES } from '@root/i18n/locales'
import androidDefaults from '@utils/slices/instances/push/androidDefaults' import androidDefaults from '@utils/slices/instances/push/androidDefaults'
import { import { getInstances } from '@utils/slices/instancesSlice'
getInstanceActive,
getInstancePush,
getInstances
} from '@utils/slices/instancesSlice'
import { import {
changeBrowser, changeBrowser,
changeLanguage, changeLanguage,
@ -24,7 +20,7 @@ import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native' import { Platform } from 'react-native'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { mapFontsizeToName } from '../Fontsize' import { mapFontsizeToName } from '../SettingsFontsize'
const SettingsApp: React.FC = () => { const SettingsApp: React.FC = () => {
const navigation = useNavigation() const navigation = useNavigation()
@ -34,43 +30,22 @@ const SettingsApp: React.FC = () => {
const { t, i18n } = useTranslation('screenTabs') const { t, i18n } = useTranslation('screenTabs')
const instances = useSelector(getInstances, () => true) const instances = useSelector(getInstances, () => true)
const instanceActive = useSelector(getInstanceActive)
const settingsFontsize = useSelector(getSettingsFontsize) const settingsFontsize = useSelector(getSettingsFontsize)
const settingsTheme = useSelector(getSettingsTheme) const settingsTheme = useSelector(getSettingsTheme)
const settingsBrowser = useSelector(getSettingsBrowser) const settingsBrowser = useSelector(getSettingsBrowser)
const instancePush = useSelector(
getInstancePush,
(prev, next) => prev?.global.value === next?.global.value
)
return ( return (
<MenuContainer> <MenuContainer>
{instanceActive !== -1 ? ( <MenuRow
<> title={t('me.settings.fontsize.heading')}
<MenuRow content={t(
title={t('me.settings.push.heading')} `me.settings.fontsize.content.${mapFontsizeToName(settingsFontsize)}`
content={ )}
instancePush?.global.value iconBack='ChevronRight'
? t('me.settings.push.content.enabled') onPress={() => {
: t('me.settings.push.content.disabled') navigation.navigate('Tab-Me-Settings-Fontsize')
} }}
iconBack='ChevronRight' />
onPress={() => {
navigation.navigate('Tab-Me-Settings-Push')
}}
/>
<MenuRow
title={t('me.settings.fontsize.heading')}
content={t(
`me.settings.fontsize.content.${mapFontsizeToName(settingsFontsize)}`
)}
iconBack='ChevronRight'
onPress={() => {
navigation.navigate('Tab-Me-Settings-Fontsize')
}}
/>
</>
) : null}
<MenuRow <MenuRow
title={t('me.settings.language.heading')} title={t('me.settings.language.heading')}
// @ts-ignore // @ts-ignore

View File

@ -1,5 +1,6 @@
import Button from '@components/Button' import Button from '@components/Button'
import { MenuContainer, MenuRow } from '@components/Menu' import { MenuContainer, MenuRow } from '@components/Menu'
import { displayMessage } from '@components/Message'
import { useActionSheet } from '@expo/react-native-action-sheet' import { useActionSheet } from '@expo/react-native-action-sheet'
import { persistor } from '@root/store' import { persistor } from '@root/store'
import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice' import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
@ -50,12 +51,21 @@ const SettingsDev: React.FC = () => {
) )
} }
/> />
<Button
type='text'
content={'Test flash message'}
style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2,
marginBottom: StyleConstants.Spacing.Global.PagePadding
}}
onPress={() => displayMessage({ message: 'This is a testing message' })}
/>
<Button <Button
type='text' type='text'
content={'Purge secure storage'} content={'Purge secure storage'}
style={{ style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2, marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2,
marginBottom: StyleConstants.Spacing.Global.PagePadding * 2 marginBottom: StyleConstants.Spacing.Global.PagePadding
}} }}
destructive destructive
onPress={() => persistor.purge()} onPress={() => persistor.purge()}

View File

@ -32,7 +32,7 @@ export const mapFontsizeToName = (size: SettingsState['fontsize']) => {
} }
} }
const ScreenMeSettingsFontsize: React.FC<StackScreenProps< const TabMeSettingsFontsize: React.FC<StackScreenProps<
Nav.TabMeStackParamList, Nav.TabMeStackParamList,
'Tab-Me-Settings-Fontsize' 'Tab-Me-Settings-Fontsize'
>> = () => { >> = () => {
@ -183,4 +183,4 @@ const styles = StyleSheet.create({
} }
}) })
export default ScreenMeSettingsFontsize export default TabMeSettingsFontsize

View File

@ -8,7 +8,7 @@ import ScreenMeSwitchRoot from './Switch/Root'
const Stack = createNativeStackNavigator() const Stack = createNativeStackNavigator()
const ScreenMeSwitch: React.FC<StackScreenProps< const TabMeSwitch: React.FC<StackScreenProps<
Nav.TabMeStackParamList, Nav.TabMeStackParamList,
'Tab-Me-Switch' 'Tab-Me-Switch'
>> = ({ navigation }) => { >> = ({ navigation }) => {
@ -44,4 +44,4 @@ const ScreenMeSwitch: React.FC<StackScreenProps<
) )
} }
export default ScreenMeSwitch export default TabMeSwitch

View File

@ -5,7 +5,7 @@ import TimelineDefault from '@components/Timeline/Default'
import { useAccountQuery } from '@utils/queryHooks/account' import { useAccountQuery } from '@utils/queryHooks/account'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useEffect, useMemo, useReducer } from 'react' import React, { useCallback, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
import { useSharedValue } from 'react-native-reanimated' import { useSharedValue } from 'react-native-reanimated'
@ -13,9 +13,6 @@ import AccountAttachments from './Account/Attachments'
import AccountHeader from './Account/Header' import AccountHeader from './Account/Header'
import AccountInformation from './Account/Information' import AccountInformation from './Account/Information'
import AccountNav from './Account/Nav' import AccountNav from './Account/Nav'
import AccountContext from './Account/utils/createContext'
import accountInitialState from './Account/utils/initialState'
import accountReducer from './Account/utils/reducer'
import { SharedAccountProp } from './sharedScreens' import { SharedAccountProp } from './sharedScreens'
const TabSharedAccount: React.FC<SharedAccountProp> = ({ const TabSharedAccount: React.FC<SharedAccountProp> = ({
@ -30,10 +27,6 @@ const TabSharedAccount: React.FC<SharedAccountProp> = ({
const { data } = useAccountQuery({ id: account.id }) const { data } = useAccountQuery({ id: account.id })
const scrollY = useSharedValue(0) const scrollY = useSharedValue(0)
const [accountState, accountDispatch] = useReducer(
accountReducer,
accountInitialState
)
useEffect(() => { useEffect(() => {
const updateHeaderRight = () => const updateHeaderRight = () =>
@ -86,7 +79,7 @@ const TabSharedAccount: React.FC<SharedAccountProp> = ({
) )
return ( return (
<AccountContext.Provider value={{ accountState, accountDispatch }}> <>
<AccountNav scrollY={scrollY} account={data} /> <AccountNav scrollY={scrollY} account={data} />
<Timeline <Timeline
@ -98,7 +91,7 @@ const TabSharedAccount: React.FC<SharedAccountProp> = ({
ListHeaderComponent ListHeaderComponent
}} }}
/> />
</AccountContext.Provider> </>
) )
} }

View File

@ -1,36 +1,50 @@
import Button from '@components/Button'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager' import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react' import React from 'react'
import { Dimensions, Image } from 'react-native' import { Dimensions, Image, View } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context' import { useSafeAreaInsets } from 'react-native-safe-area-context'
import AccountContext from './utils/createContext'
export interface Props { export interface Props {
account?: Mastodon.Account account?: Mastodon.Account
limitHeight?: boolean edit?: boolean
} }
const AccountHeader: React.FC<Props> = ({ account }) => { const AccountHeader = React.memo(
const { accountState } = useContext(AccountContext) ({ account, edit }: Props) => {
const { reduceMotionEnabled } = useAccessibility() const { reduceMotionEnabled } = useAccessibility()
const { theme } = useTheme() const { theme } = useTheme()
const topInset = useSafeAreaInsets().top const topInset = useSafeAreaInsets().top
return ( return (
<Image <View>
source={{ <Image
uri: reduceMotionEnabled ? account?.header_static : account?.header source={{
}} uri: reduceMotionEnabled ? account?.header_static : account?.header
style={{ }}
height: style={{
Dimensions.get('screen').width * accountState.headerRatio + topInset, height: Dimensions.get('screen').width / 3 + topInset,
backgroundColor: theme.disabled backgroundColor: theme.disabled
}} }}
/> />
) {edit ? (
} <View
style={{
export default React.memo( position: 'absolute',
AccountHeader, width: '100%',
height: '100%',
alignContent: 'center',
justifyContent: 'center',
alignItems: 'center'
}}
>
<Button type='icon' content='Edit' round onPress={() => {}} />
</View>
) : null}
</View>
)
},
(_, next) => next.account === undefined (_, next) => next.account === undefined
) )
export default AccountHeader

View File

@ -1,9 +1,7 @@
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
import { useSelector } from 'react-redux'
import { Placeholder, Fade } from 'rn-placeholder' import { Placeholder, Fade } from 'rn-placeholder'
import AccountInformationAccount from './Information/Account' import AccountInformationAccount from './Information/Account'
import AccountInformationActions from './Information/Actions' import AccountInformationActions from './Information/Actions'
@ -11,69 +9,59 @@ import AccountInformationAvatar from './Information/Avatar'
import AccountInformationCreated from './Information/Created' import AccountInformationCreated from './Information/Created'
import AccountInformationFields from './Information/Fields' import AccountInformationFields from './Information/Fields'
import AccountInformationName from './Information/Name' import AccountInformationName from './Information/Name'
import AccountInformationNotes from './Information/Notes' import AccountInformationNote from './Information/Note'
import AccountInformationStats from './Information/Stats' import AccountInformationStats from './Information/Stats'
import AccountInformationSwitch from './Information/Switch'
export interface Props { export interface Props {
account: Mastodon.Account | undefined account: Mastodon.Account | undefined
myInfo?: boolean // Showing from my info page myInfo?: boolean // Showing from my info page
} }
const AccountInformation: React.FC<Props> = ({ account, myInfo = false }) => { const AccountInformation = React.memo(
const ownAccount = ({ account, myInfo = false }: Props) => {
account?.id === const { mode, theme } = useTheme()
useSelector(getInstanceAccount, (prev, next) => prev?.id === next?.id)?.id
const { mode, theme } = useTheme()
const animation = useCallback( const animation = useCallback(
props => ( props => (
<Fade {...props} style={{ backgroundColor: theme.shimmerHighlight }} /> <Fade {...props} style={{ backgroundColor: theme.shimmerHighlight }} />
), ),
[mode] [mode]
) )
return ( return (
<View style={styles.base}> <View style={styles.base}>
<Placeholder Animation={animation}> <Placeholder Animation={animation}>
<View style={styles.avatarAndActions}> <View style={styles.avatarAndActions}>
<AccountInformationAvatar account={account} myInfo={myInfo} /> <AccountInformationAvatar account={account} myInfo={myInfo} />
<View style={styles.actions}> <AccountInformationActions account={account} myInfo={myInfo} />
{myInfo ? (
<AccountInformationSwitch />
) : (
<AccountInformationActions
account={account}
ownAccount={ownAccount}
/>
)}
</View> </View>
</View>
<AccountInformationName account={account} /> <AccountInformationName account={account} />
<AccountInformationAccount account={account} myInfo={myInfo} /> <AccountInformationAccount account={account} localInstance={myInfo} />
{!myInfo ? ( <AccountInformationFields account={account} myInfo={myInfo} />
<>
{account?.fields && account.fields.length > 0 ? (
<AccountInformationFields account={account} />
) : null}
{account?.note &&
account.note.length > 0 &&
account.note !== '<p></p>' ? (
// Empty notes might generate empty p tag
<AccountInformationNotes account={account} />
) : null}
<AccountInformationCreated account={account} />
</>
) : null}
<AccountInformationStats account={account} myInfo={myInfo} /> <AccountInformationNote account={account} myInfo={myInfo} />
</Placeholder>
</View> <AccountInformationCreated account={account} hidden={myInfo} />
)
} <AccountInformationStats account={account} />
</Placeholder>
</View>
)
},
(prev, next) => {
let skipUpdate = true
if (prev.account?.id !== next.account?.id) {
skipUpdate = false
}
if (prev.account?.acct === next.account?.acct) {
skipUpdate = false
}
return skipUpdate
}
)
const styles = StyleSheet.create({ const styles = StyleSheet.create({
base: { base: {
@ -90,13 +78,4 @@ const styles = StyleSheet.create({
} }
}) })
export default React.memo(AccountInformation, (prev, next) => { export default AccountInformation
let skipUpdate = true
if (prev.account?.id !== next.account?.id) {
skipUpdate = false
}
if (prev.account?.acct === next.account?.acct) {
skipUpdate = false
}
return skipUpdate
})

View File

@ -12,10 +12,10 @@ import { PlaceholderLine } from 'rn-placeholder'
export interface Props { export interface Props {
account: Mastodon.Account | undefined account: Mastodon.Account | undefined
myInfo: boolean localInstance: boolean
} }
const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => { const AccountInformationAccount: React.FC<Props> = ({ account, localInstance }) => {
const { theme } = useTheme() const { theme } = useTheme()
const instanceAccount = useSelector( const instanceAccount = useSelector(
getInstanceAccount, getInstanceAccount,
@ -48,7 +48,7 @@ const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => {
} }
}, [account?.moved]) }, [account?.moved])
if (account || (myInfo && instanceAccount)) { if (account || (localInstance && instanceAccount)) {
return ( return (
<View <View
style={[styles.base, { flexDirection: 'row', alignItems: 'center' }]} style={[styles.base, { flexDirection: 'row', alignItems: 'center' }]}
@ -63,8 +63,8 @@ const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => {
]} ]}
selectable selectable
> >
@{myInfo ? instanceAccount?.acct : account?.acct} @{localInstance ? instanceAccount?.acct : account?.acct}
{myInfo ? `@${instanceUri}` : null} {localInstance ? `@${instanceUri}` : null}
</Text> </Text>
{movedContent} {movedContent}
{account?.locked ? ( {account?.locked ? (

View File

@ -2,34 +2,21 @@ import analytics from '@components/analytics'
import Button from '@components/Button' import Button from '@components/Button'
import { RelationshipOutgoing } from '@components/Relationship' import { RelationshipOutgoing } from '@components/Relationship'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { useRelationshipQuery } from '@utils/queryHooks/relationship' import { useRelationshipQuery } from '@utils/queryHooks/relationship'
import {
getInstanceAccount,
getInstancePush,
getInstanceUri
} from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleSheet } from 'react-native' import { StyleSheet, View } from 'react-native'
import { useSelector } from 'react-redux'
export interface Props { export interface Props {
account: Mastodon.Account | undefined account: Mastodon.Account | undefined
ownAccount: boolean myInfo?: boolean
}
const GoToMoved = ({ accountMoved }: { accountMoved: Mastodon.Account }) => {
const { t } = useTranslation('screenTabs')
const navigation = useNavigation<
StackNavigationProp<Nav.TabLocalStackParamList>
>()
return (
<Button
type='text'
content={t('shared.account.moved')}
onPress={() => {
analytics('account_gotomoved_press')
navigation.push('Tab-Shared-Account', { account: accountMoved })
}}
/>
)
} }
const Conversation = ({ account }: { account: Mastodon.Account }) => { const Conversation = ({ account }: { account: Mastodon.Account }) => {
@ -41,7 +28,7 @@ const Conversation = ({ account }: { account: Mastodon.Account }) => {
round round
type='icon' type='icon'
content='Mail' content='Mail'
style={styles.actionConversation} style={styles.actionLeft}
onPress={() => { onPress={() => {
analytics('account_DM_press') analytics('account_DM_press')
navigation.navigate('Screen-Compose', { navigation.navigate('Screen-Compose', {
@ -53,24 +40,76 @@ const Conversation = ({ account }: { account: Mastodon.Account }) => {
) : null ) : null
} }
const AccountInformationActions: React.FC<Props> = ({ const AccountInformationActions: React.FC<Props> = ({ account, myInfo }) => {
account, const { t } = useTranslation('screenTabs')
ownAccount const navigation = useNavigation()
}) => {
return account && account.id ? ( if (account?.moved) {
account.moved ? ( const accountMoved = account.moved
<GoToMoved accountMoved={account.moved} /> return (
) : !ownAccount ? ( <View style={styles.base}>
<> <Button
type='text'
content={t('shared.account.moved')}
onPress={() => {
analytics('account_gotomoved_press')
// @ts-ignore
navigation.push('Tab-Shared-Account', { account: accountMoved })
}}
/>
</View>
)
}
const instancePush = useSelector(
getInstancePush,
(prev, next) => prev?.global.value === next?.global.value
)
const instanceUri = useSelector(getInstanceUri)
if (myInfo) {
return (
<View style={styles.base}>
<Button
round
type='icon'
content={instancePush?.global.value ? 'Bell' : 'BellOff'}
style={styles.actionLeft}
onPress={() => navigation.navigate('Tab-Me-Push')}
/>
<Button
type='text'
disabled={account === undefined}
content={t('me.stacks.profile.name')}
onPress={() => navigation.navigate('Tab-Me-Profile')}
/>
</View>
)
}
const instanceAccount = useSelector(getInstanceAccount, () => true)
const ownAccount =
account?.id === instanceAccount?.id &&
account?.acct === instanceAccount?.acct
if (!ownAccount && account) {
return (
<View style={styles.base}>
<Conversation account={account} /> <Conversation account={account} />
<RelationshipOutgoing id={account.id} /> <RelationshipOutgoing id={account.id} />
</> </View>
) : null )
) : null } else {
return null
}
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
actionConversation: { marginRight: StyleConstants.Spacing.S } base: {
alignSelf: 'flex-end',
flexDirection: 'row'
},
actionLeft: { marginRight: StyleConstants.Spacing.S }
}) })
export default AccountInformationActions export default AccountInformationActions

View File

@ -1,18 +1,24 @@
import analytics from '@components/analytics' import analytics from '@components/analytics'
import Button from '@components/Button'
import GracefullyImage from '@components/GracefullyImage' import GracefullyImage from '@components/GracefullyImage'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack' import { StackNavigationProp } from '@react-navigation/stack'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager' import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import React from 'react' import React from 'react'
import { Pressable, StyleSheet } from 'react-native' import { Pressable, StyleSheet, View } from 'react-native'
export interface Props { export interface Props {
account: Mastodon.Account | undefined account: Mastodon.Account | undefined
myInfo: boolean myInfo: boolean
edit?: boolean
} }
const AccountInformationAvatar: React.FC<Props> = ({ account, myInfo }) => { const AccountInformationAvatar: React.FC<Props> = ({
account,
myInfo,
edit
}) => {
const navigation = useNavigation< const navigation = useNavigation<
StackNavigationProp<Nav.TabLocalStackParamList> StackNavigationProp<Nav.TabLocalStackParamList>
>() >()
@ -36,6 +42,20 @@ const AccountInformationAvatar: React.FC<Props> = ({ account, myInfo }) => {
: account?.avatar : account?.avatar
}} }}
/> />
{edit ? (
<View
style={{
position: 'absolute',
width: '100%',
height: '100%',
alignContent: 'center',
justifyContent: 'center',
alignItems: 'center'
}}
>
<Button type='icon' content='Edit' round onPress={() => {}} />
</View>
) : null}
</Pressable> </Pressable>
) )
} }

View File

@ -8,55 +8,63 @@ import { PlaceholderLine } from 'rn-placeholder'
export interface Props { export interface Props {
account: Mastodon.Account | undefined account: Mastodon.Account | undefined
hidden?: boolean
} }
const AccountInformationCreated: React.FC<Props> = ({ account }) => { const AccountInformationCreated = React.memo(
const { i18n } = useTranslation() ({ account, hidden = false }: Props) => {
const { theme } = useTheme() if (hidden) {
const { t } = useTranslation('screenTabs') return null
}
if (account) { const { i18n } = useTranslation()
return ( const { theme } = useTheme()
<View const { t } = useTranslation('screenTabs')
style={[styles.base, { flexDirection: 'row', alignItems: 'center' }]}
> if (account) {
<Icon return (
name='Calendar' <View
size={StyleConstants.Font.Size.S} style={[styles.base, { flexDirection: 'row', alignItems: 'center' }]}
color={theme.secondary}
style={styles.icon}
/>
<Text
style={{
color: theme.secondary,
...StyleConstants.FontStyle.S
}}
> >
{t('shared.account.created_at', { <Icon
date: new Date(account.created_at || '').toLocaleDateString( name='Calendar'
i18n.language, size={StyleConstants.Font.Size.S}
{ color={theme.secondary}
year: 'numeric', style={styles.icon}
month: 'long', />
day: 'numeric' <Text
} style={{
) color: theme.secondary,
})} ...StyleConstants.FontStyle.S
</Text> }}
</View> >
) {t('shared.account.created_at', {
} else { date: new Date(account.created_at || '').toLocaleDateString(
return ( i18n.language,
<PlaceholderLine {
width={StyleConstants.Font.Size.S * 4} year: 'numeric',
height={StyleConstants.Font.LineHeight.S} month: 'long',
color={theme.shimmerDefault} day: 'numeric'
noMargin }
style={styles.base} )
/> })}
) </Text>
} </View>
} )
} else {
return (
<PlaceholderLine
width={StyleConstants.Font.Size.S * 4}
height={StyleConstants.Font.LineHeight.S}
color={theme.shimmerDefault}
noMargin
style={styles.base}
/>
)
}
},
(_, next) => next.account === undefined
)
const styles = StyleSheet.create({ const styles = StyleSheet.create({
base: { base: {
@ -68,7 +76,4 @@ const styles = StyleSheet.create({
} }
}) })
export default React.memo( export default AccountInformationCreated
AccountInformationCreated,
(_, next) => next.account === undefined
)

View File

@ -6,11 +6,16 @@ import React from 'react'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
export interface Props { export interface Props {
account: Mastodon.Account account: Mastodon.Account | undefined
myInfo?: boolean
} }
const AccountInformationFields = React.memo( const AccountInformationFields = React.memo(
({ account }: Props) => { ({ account, myInfo }: Props) => {
if (myInfo || !account?.fields || account.fields.length === 0) {
return null
}
const { theme } = useTheme() const { theme } = useTheme()
return ( return (
@ -88,3 +93,6 @@ const styles = StyleSheet.create({
}) })
export default AccountInformationFields export default AccountInformationFields
function htmlToText (note: string): any {
throw new Error('Function not implemented.')
}

View File

@ -1,26 +1,19 @@
import Input from '@components/Input'
import { ParseEmojis } from '@components/Parse' import { ParseEmojis } from '@components/Parse'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useMemo } from 'react' import React, { useMemo, useState } from 'react'
import { StyleSheet, Text, View } from 'react-native' import { StyleSheet, Text, View } from 'react-native'
import { PlaceholderLine } from 'rn-placeholder' import { PlaceholderLine } from 'rn-placeholder'
export interface Props { export interface Props {
account: Mastodon.Account | undefined account: Mastodon.Account | undefined
edit?: boolean // Editing mode
} }
const AccountInformationName: React.FC<Props> = ({ account }) => { const AccountInformationName: React.FC<Props> = ({ account, edit }) => {
const { theme } = useTheme() const { theme } = useTheme()
const movedStyle = useMemo(
() =>
StyleSheet.create({
base: {
textDecorationLine: account?.moved ? 'line-through' : undefined
}
}),
[account?.moved]
)
const movedContent = useMemo(() => { const movedContent = useMemo(() => {
if (account?.moved) { if (account?.moved) {
return ( return (
@ -36,20 +29,30 @@ const AccountInformationName: React.FC<Props> = ({ account }) => {
} }
}, [account?.moved]) }, [account?.moved])
const [displatName, setDisplayName] = useState(account?.display_name)
return ( return (
<View style={[styles.base, { flexDirection: 'row' }]}> <View style={[styles.base, { flexDirection: 'row' }]}>
{account ? ( {account ? (
<> edit ? (
<Text style={movedStyle.base}> <Input title='昵称' value={displatName} setValue={setDisplayName} />
<ParseEmojis ) : (
content={account.display_name || account.username} <>
emojis={account.emojis} <Text
size='L' style={{
fontBold textDecorationLine: account?.moved ? 'line-through' : undefined
/> }}
</Text> >
{movedContent} <ParseEmojis
</> content={account.display_name || account.username}
emojis={account.emojis}
size='L'
fontBold
/>
</Text>
{movedContent}
</>
)
) : ( ) : (
<PlaceholderLine <PlaceholderLine
width={StyleConstants.Font.Size.L * 2} width={StyleConstants.Font.Size.L * 2}

View 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

View File

@ -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

View File

@ -10,10 +10,9 @@ import { PlaceholderLine } from 'rn-placeholder'
export interface Props { export interface Props {
account: Mastodon.Account | undefined account: Mastodon.Account | undefined
myInfo: boolean
} }
const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => { const AccountInformationStats: React.FC<Props> = ({ account }) => {
const navigation = useNavigation< const navigation = useNavigation<
StackNavigationProp<Nav.TabLocalStackParamList> StackNavigationProp<Nav.TabLocalStackParamList>
>() >()
@ -28,12 +27,6 @@ const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => {
children={t('shared.account.summary.statuses_count', { children={t('shared.account.summary.statuses_count', {
count: account.statuses_count || 0 count: account.statuses_count || 0
})} })}
onPress={() => {
analytics('account_stats_toots_press', {
count: account.statuses_count
})
myInfo && navigation.push('Tab-Shared-Account', { account })
}}
/> />
) : ( ) : (
<PlaceholderLine <PlaceholderLine
@ -46,7 +39,10 @@ const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => {
)} )}
{account ? ( {account ? (
<Text <Text
style={[styles.stat, { color: theme.primaryDefault, textAlign: 'right' }]} style={[
styles.stat,
{ color: theme.primaryDefault, textAlign: 'right' }
]}
children={t('shared.account.summary.following_count', { children={t('shared.account.summary.following_count', {
count: account.following_count count: account.following_count
})} })}
@ -73,7 +69,10 @@ const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => {
)} )}
{account ? ( {account ? (
<Text <Text
style={[styles.stat, { color: theme.primaryDefault, textAlign: 'center' }]} style={[
styles.stat,
{ color: theme.primaryDefault, textAlign: 'center' }
]}
children={t('shared.account.summary.followers_count', { children={t('shared.account.summary.followers_count', {
count: account.followers_count count: account.followers_count
})} })}

View File

@ -1,7 +1,7 @@
import { ParseEmojis } from '@components/Parse' import { ParseEmojis } from '@components/Parse'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react' import React from 'react'
import { Dimensions, StyleSheet, Text, View } from 'react-native' import { Dimensions, StyleSheet, Text, View } from 'react-native'
import Animated, { import Animated, {
Extrapolate, Extrapolate,
@ -9,73 +9,74 @@ import Animated, {
useAnimatedStyle useAnimatedStyle
} from 'react-native-reanimated' } from 'react-native-reanimated'
import { useSafeAreaInsets } from 'react-native-safe-area-context' import { useSafeAreaInsets } from 'react-native-safe-area-context'
import AccountContext from './utils/createContext'
export interface Props { export interface Props {
scrollY: Animated.SharedValue<number> scrollY: Animated.SharedValue<number>
account: Mastodon.Account | undefined account: Mastodon.Account | undefined
} }
const AccountNav: React.FC<Props> = ({ scrollY, account }) => { const AccountNav = React.memo(
const { accountState } = useContext(AccountContext) ({ scrollY, account }: Props) => {
const { theme } = useTheme() const { theme } = useTheme()
const headerHeight = useSafeAreaInsets().top + 44 const headerHeight = useSafeAreaInsets().top + 44
const nameY = const nameY =
Dimensions.get('screen').width * accountState.headerRatio + Dimensions.get('screen').width / 3 +
StyleConstants.Avatar.L - StyleConstants.Avatar.L -
StyleConstants.Spacing.Global.PagePadding * 2 + StyleConstants.Spacing.Global.PagePadding * 2 +
StyleConstants.Spacing.M - StyleConstants.Spacing.M -
headerHeight headerHeight
const styleOpacity = useAnimatedStyle(() => { const styleOpacity = useAnimatedStyle(() => {
return { return {
opacity: interpolate(scrollY.value, [0, 200], [0, 1], Extrapolate.CLAMP) opacity: interpolate(scrollY.value, [0, 200], [0, 1], Extrapolate.CLAMP)
} }
}) })
const styleMarginTop = useAnimatedStyle(() => { const styleMarginTop = useAnimatedStyle(() => {
return { return {
marginTop: interpolate( marginTop: interpolate(
scrollY.value, scrollY.value,
[nameY, nameY + 20], [nameY, nameY + 20],
[50, 0], [50, 0],
Extrapolate.CLAMP Extrapolate.CLAMP
) )
} }
}) })
return ( return (
<Animated.View <Animated.View
style={[
styles.base,
styleOpacity,
{ backgroundColor: theme.backgroundDefault, height: headerHeight }
]}
>
<View
style={[ style={[
styles.content, styles.base,
{ styleOpacity,
marginTop: { backgroundColor: theme.backgroundDefault, height: headerHeight }
useSafeAreaInsets().top + (44 - StyleConstants.Font.Size.L) / 2
}
]} ]}
> >
<Animated.View style={[styles.display_name, styleMarginTop]}> <View
{account ? ( style={[
<Text numberOfLines={1}> styles.content,
<ParseEmojis {
content={account.display_name || account.username} marginTop:
emojis={account.emojis} useSafeAreaInsets().top + (44 - StyleConstants.Font.Size.L) / 2
fontBold }
/> ]}
</Text> >
) : null} <Animated.View style={[styles.display_name, styleMarginTop]}>
</Animated.View> {account ? (
</View> <Text numberOfLines={1}>
</Animated.View> <ParseEmojis
) content={account.display_name || account.username}
} emojis={account.emojis}
fontBold
/>
</Text>
) : null}
</Animated.View>
</View>
</Animated.View>
)
},
(_, next) => next.account === undefined
)
const styles = StyleSheet.create({ const styles = StyleSheet.create({
base: { base: {
@ -92,4 +93,4 @@ const styles = StyleSheet.create({
} }
}) })
export default React.memo(AccountNav, (_, next) => next.account === undefined) export default AccountNav

View File

@ -1,14 +1,11 @@
import createSecureStore from '@neverdull-agency/expo-unlimited-secure-store' import createSecureStore from '@neverdull-agency/expo-unlimited-secure-store'
import AsyncStorage from '@react-native-async-storage/async-storage' import AsyncStorage from '@react-native-async-storage/async-storage'
import { import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
combineReducers,
configureStore,
getDefaultMiddleware
} from '@reduxjs/toolkit'
import instancesMigration from '@utils/migrations/instances/migration' import instancesMigration from '@utils/migrations/instances/migration'
import contextsSlice from '@utils/slices/contextsSlice' import contextsSlice from '@utils/slices/contextsSlice'
import instancesSlice from '@utils/slices/instancesSlice' import instancesSlice from '@utils/slices/instancesSlice'
import settingsSlice from '@utils/slices/settingsSlice' import settingsSlice from '@utils/slices/settingsSlice'
import versionSlice from '@utils/slices/versionSlice'
import { createMigrate, persistReducer, persistStore } from 'redux-persist' import { createMigrate, persistReducer, persistStore } from 'redux-persist'
const secureStorage = createSecureStore() const secureStorage = createSecureStore()
@ -27,7 +24,7 @@ const instancesPersistConfig = {
storage: secureStorage, storage: secureStorage,
version: 5, version: 5,
// @ts-ignore // @ts-ignore
migrate: createMigrate(instancesMigration, { debug: true }) migrate: createMigrate(instancesMigration)
} }
const settingsPersistConfig = { const settingsPersistConfig = {
@ -36,21 +33,13 @@ const settingsPersistConfig = {
storage: AsyncStorage storage: AsyncStorage
} }
const rootPersistConfig = {
key: 'root',
prefix,
version: 0,
storage: AsyncStorage
}
const rootReducer = combineReducers({
contexts: persistReducer(contextsPersistConfig, contextsSlice),
instances: persistReducer(instancesPersistConfig, instancesSlice),
settings: persistReducer(settingsPersistConfig, settingsSlice)
})
const store = configureStore({ const store = configureStore({
reducer: persistReducer(rootPersistConfig, rootReducer), reducer: {
contexts: persistReducer(contextsPersistConfig, contextsSlice),
instances: persistReducer(instancesPersistConfig, instancesSlice),
settings: persistReducer(settingsPersistConfig, settingsSlice),
version: versionSlice
},
middleware: getDefaultMiddleware({ middleware: getDefaultMiddleware({
serializableCheck: { serializableCheck: {
ignoredActions: ['persist/PERSIST'] ignoredActions: ['persist/PERSIST']

View File

@ -2,9 +2,9 @@ import apiInstance from '@api/instance'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { useQuery, UseQueryOptions } from 'react-query' import { useQuery, UseQueryOptions } from 'react-query'
export type QueryKey = ['Account', { id: Mastodon.Account['id'] }] export type QueryKeyAccount = ['Account', { id: Mastodon.Account['id'] }]
const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => { const queryFunction = ({ queryKey }: { queryKey: QueryKeyAccount }) => {
const { id } = queryKey[1] const { id } = queryKey[1]
return apiInstance<Mastodon.Account>({ return apiInstance<Mastodon.Account>({
@ -16,10 +16,10 @@ const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
const useAccountQuery = <TData = Mastodon.Account>({ const useAccountQuery = <TData = Mastodon.Account>({
options, options,
...queryKeyParams ...queryKeyParams
}: QueryKey[1] & { }: QueryKeyAccount[1] & {
options?: UseQueryOptions<Mastodon.Account, AxiosError, TData> options?: UseQueryOptions<Mastodon.Account, AxiosError, TData>
}) => { }) => {
const queryKey: QueryKey = ['Account', { ...queryKeyParams }] const queryKey: QueryKeyAccount = ['Account', { ...queryKeyParams }]
return useQuery(queryKey, queryFunction, options) return useQuery(queryKey, queryFunction, options)
} }

View 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 }

View 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

View File

@ -10,6 +10,7 @@ export type ColorDefinitions =
| 'green' | 'green'
| 'yellow' | 'yellow'
| 'backgroundDefault' | 'backgroundDefault'
| 'backgroundDefaultTransparent'
| 'backgroundOverlayDefault' | 'backgroundOverlayDefault'
| 'backgroundOverlayInvert' | 'backgroundOverlayInvert'
| 'border' | 'border'
@ -59,6 +60,10 @@ const themeColors: {
light: 'rgb(250, 250, 250)', light: 'rgb(250, 250, 250)',
dark: 'rgb(18, 18, 18)' dark: 'rgb(18, 18, 18)'
}, },
backgroundDefaultTransparent: {
light: 'rgba(250, 250, 250, 0)',
dark: 'rgba(18, 18, 18, 0)'
},
backgroundOverlayDefault: { backgroundOverlayDefault: {
light: 'rgba(250, 250, 250, 0.5)', light: 'rgba(250, 250, 250, 0.5)',
dark: 'rgba(0, 0, 0, 0.5)' dark: 'rgba(0, 0, 0, 0.5)'