mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Merge branch 'v2' into main
This commit is contained in:
6
src/@types/react-navigation.d.ts
vendored
6
src/@types/react-navigation.d.ts
vendored
@ -1,6 +1,4 @@
|
||||
declare namespace Nav {
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
|
||||
type RootStackParamList = {
|
||||
'Screen-Tabs': undefined
|
||||
'Screen-Actions':
|
||||
@ -151,8 +149,4 @@ declare namespace Nav {
|
||||
fields?: Mastodon.Source['fields']
|
||||
}
|
||||
}
|
||||
|
||||
type TabMePushStackParamList = {
|
||||
'Tab-Me-Push-Root': undefined
|
||||
}
|
||||
}
|
||||
|
1
src/@types/untyped.d.ts
vendored
1
src/@types/untyped.d.ts
vendored
@ -1,4 +1,5 @@
|
||||
declare module 'gl-react-blurhash'
|
||||
declare module 'htmlparser2-without-node-native'
|
||||
declare module 'li'
|
||||
declare module 'react-native-feather'
|
||||
declare module 'react-native-htmlview'
|
||||
|
10
src/App.tsx
10
src/App.tsx
@ -1,4 +1,5 @@
|
||||
import { ActionSheetProvider } from '@expo/react-native-action-sheet'
|
||||
import queryClient from '@helpers/queryClient'
|
||||
import i18n from '@root/i18n/i18n'
|
||||
import Screens from '@root/Screens'
|
||||
import audio from '@root/startup/audio'
|
||||
@ -14,8 +15,7 @@ import * as Notifications from 'expo-notifications'
|
||||
import * as SplashScreen from 'expo-splash-screen'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { AppState, LogBox, Platform } from 'react-native'
|
||||
import { enableScreens } from 'react-native-screens'
|
||||
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||
import { QueryClientProvider } from 'react-query'
|
||||
import { Provider } from 'react-redux'
|
||||
import { PersistGate } from 'redux-persist/integration/react'
|
||||
import push from './startup/push'
|
||||
@ -29,12 +29,6 @@ sentry()
|
||||
audio()
|
||||
push()
|
||||
|
||||
log('log', 'react-query', 'initializing')
|
||||
export const queryClient = new QueryClient()
|
||||
|
||||
log('log', 'react-native-screens', 'initializing')
|
||||
enableScreens()
|
||||
|
||||
const App: React.FC = () => {
|
||||
log('log', 'App', 'rendering App')
|
||||
const [localCorrupt, setLocalCorrupt] = useState<string>()
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { HeaderCenter, HeaderLeft } from '@components/Header'
|
||||
import { displayMessage, Message, removeMessage } from '@components/Message'
|
||||
import navigationRef from '@helpers/navigationRef'
|
||||
import { useNetInfo } from '@react-native-community/netinfo'
|
||||
import {
|
||||
NavigationContainer,
|
||||
NavigationContainerRef
|
||||
} from '@react-navigation/native'
|
||||
import { NavigationContainer } from '@react-navigation/native'
|
||||
import ScreenActions from '@screens/Actions'
|
||||
import ScreenAnnouncements from '@screens/Announcements'
|
||||
import ScreenCompose from '@screens/Compose'
|
||||
@ -19,7 +18,7 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { themes } from '@utils/styles/themes'
|
||||
import * as Analytics from 'expo-firebase-analytics'
|
||||
import { addScreenshotListener } from 'expo-screen-capture'
|
||||
import React, { createRef, useCallback, useEffect, useRef } from 'react'
|
||||
import React, { useCallback, useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert, Platform, StatusBar } from 'react-native'
|
||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||
@ -28,7 +27,6 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import * as Sentry from 'sentry-expo'
|
||||
|
||||
const Stack = createNativeStackNavigator<Nav.RootStackParamList>()
|
||||
export const navigationRef = createRef<NavigationContainerRef>()
|
||||
|
||||
export interface Props {
|
||||
localCorrupt?: string
|
||||
@ -174,18 +172,30 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||
<Stack.Screen
|
||||
name='Screen-Announcements'
|
||||
component={ScreenAnnouncements}
|
||||
options={{
|
||||
options={({ navigation }) => ({
|
||||
stackPresentation: 'transparentModal',
|
||||
stackAnimation: 'fade',
|
||||
headerShown: false
|
||||
}}
|
||||
headerShown: true,
|
||||
headerHideShadow: true,
|
||||
headerTopInsetEnabled: false,
|
||||
headerStyle: { backgroundColor: 'transparent' },
|
||||
headerLeft: () => (
|
||||
<HeaderLeft content='X' onPress={() => navigation.goBack()} />
|
||||
),
|
||||
headerTitle: t('screenAnnouncements:heading'),
|
||||
...(Platform.OS === 'android' && {
|
||||
headerCenter: () => (
|
||||
<HeaderCenter content={t('screenAnnouncements:heading')} />
|
||||
)
|
||||
})
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Screen-Compose'
|
||||
component={ScreenCompose}
|
||||
options={{
|
||||
stackPresentation: 'fullScreenModal',
|
||||
headerShown: false
|
||||
...(Platform.OS === 'android' && { headerShown: false })
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@ -194,7 +204,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||
options={{
|
||||
stackPresentation: 'fullScreenModal',
|
||||
stackAnimation: 'fade',
|
||||
headerShown: false
|
||||
...(Platform.OS === 'android' && { headerShown: false })
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
|
@ -69,12 +69,16 @@ const apiGeneral = async <T = unknown>({
|
||||
error.response.status,
|
||||
error.response.data.error
|
||||
)
|
||||
return Promise.reject(error.response)
|
||||
return Promise.reject(error.response.data.error)
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
// http.ClientRequest in node.js
|
||||
console.error(ctx.bold(' API general '), ctx.bold('request'), error)
|
||||
console.error(
|
||||
ctx.bold(' API general '),
|
||||
ctx.bold('request'),
|
||||
error.request
|
||||
)
|
||||
return Promise.reject()
|
||||
} else {
|
||||
console.error(
|
||||
|
@ -98,7 +98,7 @@ const apiInstance = async <T = unknown>({
|
||||
error.response.status,
|
||||
error.response.data.error
|
||||
)
|
||||
return Promise.reject(error.response)
|
||||
return Promise.reject(error.response.data.error)
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
|
@ -4,7 +4,6 @@ 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,
|
||||
@ -13,44 +12,7 @@ import React, {
|
||||
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)
|
||||
import EmojisContext, { emojisReducer } from './Emojis/helpers/EmojisContext'
|
||||
|
||||
const prefetchEmojis = (
|
||||
sortedEmojis: { title: string; data: Mastodon.Emoji[][] }[],
|
||||
@ -163,4 +125,4 @@ const ComponentEmojis: React.FC<Props> = ({
|
||||
)
|
||||
}
|
||||
|
||||
export { ComponentEmojis, EmojisContext, EmojisButton, EmojisList }
|
||||
export { ComponentEmojis, EmojisButton, EmojisList }
|
||||
|
@ -1,9 +1,9 @@
|
||||
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'
|
||||
import EmojisContext from './helpers/EmojisContext'
|
||||
|
||||
const EmojisButton = React.memo(
|
||||
() => {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { EmojisContext } from '@components/Emojis'
|
||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||
@ -16,6 +15,7 @@ import {
|
||||
} from 'react-native'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import validUrl from 'valid-url'
|
||||
import EmojisContext from './helpers/EmojisContext'
|
||||
|
||||
const EmojisList = React.memo(
|
||||
() => {
|
||||
|
41
src/components/Emojis/helpers/EmojisContext.tsx
Normal file
41
src/components/Emojis/helpers/EmojisContext.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { createContext, Dispatch } from 'react'
|
||||
|
||||
export type EmojisState = {
|
||||
enabled: boolean
|
||||
active: boolean
|
||||
emojis: { title: string; data: Mastodon.Emoji[][] }[]
|
||||
shortcode: Mastodon.Emoji['shortcode'] | null
|
||||
}
|
||||
|
||||
export type EmojisAction =
|
||||
| {
|
||||
type: 'load'
|
||||
payload: NonNullable<EmojisState['emojis']>
|
||||
}
|
||||
| {
|
||||
type: 'activate'
|
||||
payload: EmojisState['active']
|
||||
}
|
||||
| {
|
||||
type: 'shortcode'
|
||||
payload: EmojisState['shortcode']
|
||||
}
|
||||
|
||||
type ContextType = {
|
||||
emojisState: EmojisState
|
||||
emojisDispatch: Dispatch<EmojisAction>
|
||||
}
|
||||
const EmojisContext = createContext<ContextType>({} as ContextType)
|
||||
|
||||
export 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 }
|
||||
}
|
||||
}
|
||||
|
||||
export default EmojisContext
|
@ -18,12 +18,8 @@ import {
|
||||
View
|
||||
} from 'react-native'
|
||||
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
|
||||
import {
|
||||
ComponentEmojis,
|
||||
EmojisButton,
|
||||
EmojisContext,
|
||||
EmojisList
|
||||
} from './Emojis'
|
||||
import { ComponentEmojis, EmojisButton, EmojisList } from './Emojis'
|
||||
import EmojisContext from './Emojis/helpers/EmojisContext'
|
||||
|
||||
export interface Props {
|
||||
autoFocus?: boolean
|
||||
@ -114,7 +110,8 @@ const Input: React.FC<Props> = ({
|
||||
styles.base,
|
||||
{
|
||||
borderColor: theme.border,
|
||||
flexDirection: multiline ? 'column' : 'row'
|
||||
flexDirection: multiline ? 'column' : 'row',
|
||||
alignItems: 'stretch'
|
||||
}
|
||||
]}
|
||||
>
|
||||
@ -157,7 +154,7 @@ const Input: React.FC<Props> = ({
|
||||
{title}
|
||||
</Animated.Text>
|
||||
) : null}
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<View style={{ flexDirection: 'row', alignSelf: 'flex-end' }}>
|
||||
{options?.maxLength && value?.length ? (
|
||||
<Text style={[styles.maxLength, { color: theme.secondary }]}>
|
||||
{value?.length} / {options.maxLength}
|
||||
|
@ -76,96 +76,98 @@ const MenuRow: React.FC<Props> = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<View style={styles.core}>
|
||||
<View style={styles.front}>
|
||||
{iconFront && (
|
||||
<Icon
|
||||
name={iconFront}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme[iconFrontColor]}
|
||||
style={styles.iconFront}
|
||||
/>
|
||||
)}
|
||||
{badge ? (
|
||||
<View
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
backgroundColor: theme.red,
|
||||
borderRadius: 8,
|
||||
marginRight: StyleConstants.Spacing.S
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<View style={styles.main}>
|
||||
<Text
|
||||
style={[styles.title, { color: theme.primaryDefault }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{content || switchValue !== undefined || iconBack ? (
|
||||
<View style={styles.back}>
|
||||
{content ? (
|
||||
typeof content === 'string' ? (
|
||||
<Text
|
||||
style={[
|
||||
styles.content,
|
||||
{
|
||||
color: theme.secondary,
|
||||
opacity: !iconBack && loading ? 0 : 1
|
||||
}
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{content}
|
||||
</Text>
|
||||
) : (
|
||||
content
|
||||
)
|
||||
) : null}
|
||||
{switchValue !== undefined ? (
|
||||
<Switch
|
||||
value={switchValue}
|
||||
onValueChange={switchOnValueChange}
|
||||
disabled={switchDisabled}
|
||||
trackColor={{ true: theme.blue, false: theme.disabled }}
|
||||
style={{ opacity: loading ? 0 : 1 }}
|
||||
/>
|
||||
) : null}
|
||||
{iconBack ? (
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={styles.core}>
|
||||
<View style={styles.front}>
|
||||
{iconFront && (
|
||||
<Icon
|
||||
name={iconBack}
|
||||
name={iconFront}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme[iconBackColor]}
|
||||
style={[styles.iconBack, { opacity: loading ? 0 : 1 }]}
|
||||
color={theme[iconFrontColor]}
|
||||
style={styles.iconFront}
|
||||
/>
|
||||
)}
|
||||
{badge ? (
|
||||
<View
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
backgroundColor: theme.red,
|
||||
borderRadius: 8,
|
||||
marginRight: StyleConstants.Spacing.S
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{loading && loadingSpinkit}
|
||||
<View style={styles.main}>
|
||||
<Text
|
||||
style={[styles.title, { color: theme.primaryDefault }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{content || switchValue !== undefined || iconBack ? (
|
||||
<View style={styles.back}>
|
||||
{content ? (
|
||||
typeof content === 'string' ? (
|
||||
<Text
|
||||
style={[
|
||||
styles.content,
|
||||
{
|
||||
color: theme.secondary,
|
||||
opacity: !iconBack && loading ? 0 : 1
|
||||
}
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{content}
|
||||
</Text>
|
||||
) : (
|
||||
content
|
||||
)
|
||||
) : null}
|
||||
{switchValue !== undefined ? (
|
||||
<Switch
|
||||
value={switchValue}
|
||||
onValueChange={switchOnValueChange}
|
||||
disabled={switchDisabled}
|
||||
trackColor={{ true: theme.blue, false: theme.disabled }}
|
||||
style={{ opacity: loading ? 0 : 1 }}
|
||||
/>
|
||||
) : null}
|
||||
{iconBack ? (
|
||||
<Icon
|
||||
name={iconBack}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme[iconBackColor]}
|
||||
style={[styles.iconBack, { opacity: loading ? 0 : 1 }]}
|
||||
/>
|
||||
) : null}
|
||||
{loading && loadingSpinkit}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
{description ? (
|
||||
<Text style={[styles.description, { color: theme.secondary }]}>
|
||||
{description}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</TapGestureHandler>
|
||||
{description ? (
|
||||
<Text style={[styles.description, { color: theme.secondary }]}>
|
||||
{description}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
minHeight: 46,
|
||||
paddingVertical: StyleConstants.Spacing.S
|
||||
minHeight: 50
|
||||
},
|
||||
core: {
|
||||
flex: 1,
|
||||
flexDirection: 'row'
|
||||
flexDirection: 'row',
|
||||
paddingVertical: StyleConstants.Spacing.S
|
||||
},
|
||||
front: {
|
||||
flex: 2,
|
||||
|
@ -4,7 +4,6 @@ import { StyleConstants } from '@utils/styles/constants'
|
||||
import { adaptiveScale } from '@utils/styles/scaling'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StyleSheet, Text } from 'react-native'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import { useSelector } from 'react-redux'
|
||||
@ -28,7 +27,6 @@ const ParseEmojis = React.memo(
|
||||
adaptiveSize = false,
|
||||
fontBold = false
|
||||
}: Props) => {
|
||||
const { t } = useTranslation('componentParse')
|
||||
const { reduceMotionEnabled } = useAccessibility()
|
||||
|
||||
const adaptiveFontsize = useSelector(getSettingsFontsize)
|
||||
|
@ -162,7 +162,9 @@ export interface Props {
|
||||
showFullLink?: boolean
|
||||
numberOfLines?: number
|
||||
expandHint?: string
|
||||
highlighted?: boolean
|
||||
disableDetails?: boolean
|
||||
selectable?: boolean
|
||||
}
|
||||
|
||||
const ParseHTML = React.memo(
|
||||
@ -176,7 +178,9 @@ const ParseHTML = React.memo(
|
||||
showFullLink = false,
|
||||
numberOfLines = 10,
|
||||
expandHint,
|
||||
disableDetails = false
|
||||
highlighted = false,
|
||||
disableDetails = false,
|
||||
selectable = false
|
||||
}: Props) => {
|
||||
const adaptiveFontsize = useSelector(getSettingsFontsize)
|
||||
const adaptedFontsize = adaptiveScale(
|
||||
@ -234,7 +238,7 @@ const ParseHTML = React.memo(
|
||||
const { t } = useTranslation('componentParse')
|
||||
|
||||
const [expandAllow, setExpandAllow] = useState(false)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [expanded, setExpanded] = useState(highlighted)
|
||||
|
||||
const onTextLayout = useCallback(({ nativeEvent }) => {
|
||||
if (
|
||||
@ -253,6 +257,7 @@ const ParseHTML = React.memo(
|
||||
numberOfLines={
|
||||
expandAllow ? (expanded ? 999 : numberOfLines) : undefined
|
||||
}
|
||||
selectable={selectable}
|
||||
/>
|
||||
{expandAllow ? (
|
||||
<Pressable
|
||||
|
@ -19,6 +19,7 @@ import { Pressable, StyleSheet, View } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
import TimelineActionsUsers from './Shared/ActionsUsers'
|
||||
import TimelineFullConversation from './Shared/FullConversation'
|
||||
import TimelineTranslate from './Shared/Translate'
|
||||
|
||||
export interface Props {
|
||||
item: Mastodon.Status & { _pinned?: boolean } // For account page, internal property
|
||||
@ -128,11 +129,13 @@ const TimelineDefault: React.FC<Props> = ({
|
||||
{!disableDetails && actualStatus.card && (
|
||||
<TimelineCard card={actualStatus.card} />
|
||||
)}
|
||||
<TimelineFullConversation queryKey={queryKey} status={actualStatus} />
|
||||
{!disableDetails ? (
|
||||
<TimelineFullConversation queryKey={queryKey} status={actualStatus} />
|
||||
) : null}
|
||||
<TimelineTranslate status={actualStatus} highlighted={highlighted} />
|
||||
<TimelineActionsUsers status={actualStatus} highlighted={highlighted} />
|
||||
</View>
|
||||
|
||||
<TimelineActionsUsers status={actualStatus} highlighted={highlighted} />
|
||||
|
||||
{queryKey && !disableDetails && (
|
||||
<TimelineActions
|
||||
queryKey={queryKey}
|
||||
|
@ -340,7 +340,7 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: StyleConstants.Font.Size.L + StyleConstants.Spacing.S * 4,
|
||||
minHeight: StyleConstants.Font.Size.L + StyleConstants.Spacing.S * 3,
|
||||
marginHorizontal: StyleConstants.Spacing.S
|
||||
}
|
||||
})
|
||||
|
@ -38,7 +38,7 @@ const TimelineActionsUsers = React.memo(
|
||||
'shared.actionsUsers.reblogged_by.accessibilityHint'
|
||||
)}
|
||||
accessibilityRole='button'
|
||||
style={[styles.text, { color: theme.secondary }]}
|
||||
style={[styles.text, { color: theme.blue }]}
|
||||
onPress={() => {
|
||||
analytics('timeline_shared_actionsusers_press_boosted', {
|
||||
count: status.reblogs_count
|
||||
@ -68,7 +68,7 @@ const TimelineActionsUsers = React.memo(
|
||||
'shared.actionsUsers.favourited_by.accessibilityHint'
|
||||
)}
|
||||
accessibilityRole='button'
|
||||
style={[styles.text, { color: theme.secondary }]}
|
||||
style={[styles.text, { color: theme.blue }]}
|
||||
onPress={() => {
|
||||
analytics('timeline_shared_actionsusers_press_boosted', {
|
||||
count: status.favourites_count
|
||||
@ -98,10 +98,9 @@ const styles = StyleSheet.create({
|
||||
base: {
|
||||
flexDirection: 'row'
|
||||
},
|
||||
pressable: { margin: StyleConstants.Spacing.M },
|
||||
text: {
|
||||
...StyleConstants.FontStyle.S,
|
||||
padding: StyleConstants.Spacing.S * 1.5,
|
||||
...StyleConstants.FontStyle.M,
|
||||
padding: StyleConstants.Spacing.S,
|
||||
paddingLeft: 0,
|
||||
marginRight: StyleConstants.Spacing.S
|
||||
}
|
||||
|
@ -30,7 +30,9 @@ const TimelineContent = React.memo(
|
||||
mentions={status.mentions}
|
||||
tags={status.tags}
|
||||
numberOfLines={999}
|
||||
highlighted={highlighted}
|
||||
disableDetails={disableDetails}
|
||||
selectable={highlighted}
|
||||
/>
|
||||
<ParseHTML
|
||||
content={status.content}
|
||||
@ -41,7 +43,9 @@ const TimelineContent = React.memo(
|
||||
tags={status.tags}
|
||||
numberOfLines={1}
|
||||
expandHint={t('shared.content.expandHint')}
|
||||
highlighted={highlighted}
|
||||
disableDetails={disableDetails}
|
||||
selectable={highlighted}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
@ -54,6 +58,7 @@ const TimelineContent = React.memo(
|
||||
tags={status.tags}
|
||||
numberOfLines={highlighted ? 999 : numberOfLines}
|
||||
disableDetails={disableDetails}
|
||||
selectable={highlighted}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -26,7 +26,7 @@ const TimelineFullConversation = React.memo(
|
||||
style={{
|
||||
...StyleConstants.FontStyle.S,
|
||||
color: theme.blue,
|
||||
marginTop: StyleConstants.Font.Size.S
|
||||
marginTop: StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
{t('shared.fullConversation')}
|
||||
|
131
src/components/Timeline/Shared/Translate.tsx
Normal file
131
src/components/Timeline/Shared/Translate.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import analytics from '@components/analytics'
|
||||
import { ParseHTML } from '@components/Parse'
|
||||
import { useTranslateQuery } from '@utils/queryHooks/translate'
|
||||
import { getSettingsLanguage } from '@utils/slices/settingsSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Pressable, StyleSheet, Text } from 'react-native'
|
||||
import { Circle } from 'react-native-animated-spinkit'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
export interface Props {
|
||||
highlighted: boolean
|
||||
status: Mastodon.Status
|
||||
}
|
||||
|
||||
const TimelineTranslate = React.memo(
|
||||
({ highlighted, status }: Props) => {
|
||||
if (!highlighted) {
|
||||
return null
|
||||
}
|
||||
if (!status.language) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { theme } = useTheme()
|
||||
|
||||
const tootLanguage = status.language.slice(0, 2)
|
||||
|
||||
const settingsLanguage = useSelector(getSettingsLanguage)
|
||||
|
||||
if (settingsLanguage.includes(tootLanguage)) {
|
||||
return null
|
||||
}
|
||||
|
||||
let text = status.spoiler_text
|
||||
? [status.spoiler_text, status.content]
|
||||
: [status.content]
|
||||
|
||||
for (const i in text) {
|
||||
for (const emoji of status.emojis) {
|
||||
text[i] = text[i].replaceAll(`:${emoji.shortcode}:`, '')
|
||||
}
|
||||
}
|
||||
|
||||
const [enabled, setEnabled] = useState(false)
|
||||
const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({
|
||||
uri: status.uri,
|
||||
source: status.language,
|
||||
target: settingsLanguage,
|
||||
text,
|
||||
options: { enabled }
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Pressable
|
||||
style={[styles.button, { paddingBottom: isSuccess ? 0 : undefined }]}
|
||||
onPress={() => {
|
||||
if (enabled) {
|
||||
if (!isSuccess) {
|
||||
analytics('timeline_shared_translate_retry', {
|
||||
language: status.language
|
||||
})
|
||||
refetch()
|
||||
}
|
||||
} else {
|
||||
analytics('timeline_shared_translate', {
|
||||
language: status.language
|
||||
})
|
||||
setEnabled(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
...StyleConstants.FontStyle.M,
|
||||
color:
|
||||
isLoading || isSuccess
|
||||
? theme.secondary
|
||||
: isError
|
||||
? theme.red
|
||||
: theme.blue
|
||||
}}
|
||||
>
|
||||
{isError
|
||||
? t('shared.translate.failed')
|
||||
: isSuccess
|
||||
? t('shared.translate.succeed', {
|
||||
provider: data?.provider,
|
||||
source: data?.sourceLanguage
|
||||
})
|
||||
: t('shared.translate.default')}
|
||||
{__DEV__ ? ` Source: ${status.language}` : undefined}
|
||||
</Text>
|
||||
{isLoading ? (
|
||||
<Circle
|
||||
size={StyleConstants.Font.Size.M}
|
||||
color={theme.disabled}
|
||||
style={{ marginLeft: StyleConstants.Spacing.S }}
|
||||
/>
|
||||
) : null}
|
||||
</Pressable>
|
||||
{data
|
||||
? data.text.map((d, i) => (
|
||||
<ParseHTML
|
||||
key={i}
|
||||
content={d}
|
||||
size={'M'}
|
||||
numberOfLines={999}
|
||||
selectable
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
</>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: StyleConstants.Spacing.S
|
||||
}
|
||||
})
|
||||
|
||||
export default TimelineTranslate
|
@ -1,13 +1,14 @@
|
||||
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 { ActionSheetOptions } from '@expo/react-native-action-sheet'
|
||||
import * as ImageManipulator from 'expo-image-manipulator'
|
||||
import * as ImagePicker from 'expo-image-picker'
|
||||
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types'
|
||||
import i18next from 'i18next'
|
||||
import { Alert, Linking } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
mediaTypes?: ImagePicker.MediaTypeOptions
|
||||
uploader: (imageInfo: ImageInfo) => void
|
||||
resize?: { width?: number; height?: number } // Resize mode contain
|
||||
showActionSheetWithOptions: (
|
||||
options: ActionSheetOptions,
|
||||
callback: (i: number) => void
|
||||
@ -16,118 +17,134 @@ export interface Props {
|
||||
|
||||
const mediaSelector = async ({
|
||||
mediaTypes = ImagePicker.MediaTypeOptions.All,
|
||||
uploader,
|
||||
resize,
|
||||
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
|
||||
}: Props): Promise<ImageInfo> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const resolveResult = async (result: ImageInfo) => {
|
||||
if (resize && result.type === 'image') {
|
||||
let newResult: ImageManipulator.ImageResult
|
||||
if (resize.width && resize.height) {
|
||||
if (resize.width / resize.height > result.width / result.height) {
|
||||
newResult = await ImageManipulator.manipulateAsync(result.uri, [
|
||||
{ resize: { width: resize.width } }
|
||||
])
|
||||
} else {
|
||||
newResult = await ImageManipulator.manipulateAsync(result.uri, [
|
||||
{ resize: { height: resize.height } }
|
||||
])
|
||||
}
|
||||
}
|
||||
} 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
|
||||
})
|
||||
newResult = await ImageManipulator.manipulateAsync(result.uri, [
|
||||
{ resize }
|
||||
])
|
||||
}
|
||||
resolve(newResult)
|
||||
} else {
|
||||
resolve(result)
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.cancelled) {
|
||||
// https://github.com/expo/expo/issues/11214
|
||||
const fixResult = {
|
||||
...result,
|
||||
uri: result.uri.replace('file:/data', 'file:///data')
|
||||
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) {
|
||||
await resolveResult(result)
|
||||
}
|
||||
}
|
||||
} 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) {
|
||||
await resolveResult(result)
|
||||
}
|
||||
uploader(fixResult)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default mediaSelector
|
||||
|
@ -1,6 +1,6 @@
|
||||
import apiInstance from '@api/instance'
|
||||
import navigationRef from '@helpers/navigationRef'
|
||||
import { NavigationProp, ParamListBase } from '@react-navigation/native'
|
||||
import { navigationRef } from '@root/Screens'
|
||||
import { store } from '@root/store'
|
||||
import { SearchResult } from '@utils/queryHooks/search'
|
||||
import { getInstanceUrl } from '@utils/slices/instancesSlice'
|
||||
|
6
src/helpers/navigationRef.ts
Normal file
6
src/helpers/navigationRef.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { NavigationContainerRef } from '@react-navigation/native'
|
||||
import { createRef } from 'react'
|
||||
|
||||
const navigationRef = createRef<NavigationContainerRef>()
|
||||
|
||||
export default navigationRef
|
5
src/helpers/queryClient.ts
Normal file
5
src/helpers/queryClient.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { QueryClient } from 'react-query'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
export default queryClient
|
@ -74,6 +74,11 @@
|
||||
"expandHint": "hidden content"
|
||||
},
|
||||
"fullConversation": "Read conversations",
|
||||
"translate": {
|
||||
"default": "Translate",
|
||||
"succeed": "Translated by {{provider}} from {{source}}",
|
||||
"failed": "Translation failed"
|
||||
},
|
||||
"header": {
|
||||
"shared": {
|
||||
"account": {
|
||||
|
@ -102,11 +102,11 @@
|
||||
},
|
||||
"avatar": {
|
||||
"title": "Avatar",
|
||||
"description": "Available in next version"
|
||||
"description": "Will be downscaled to 400x400px"
|
||||
},
|
||||
"banner": {
|
||||
"header": {
|
||||
"title": "Banner",
|
||||
"description": "Available in next version"
|
||||
"description": "Will be downscaled to 1500x500px"
|
||||
},
|
||||
"note": {
|
||||
"title": "Description"
|
||||
|
@ -1,7 +1,6 @@
|
||||
import analytics from '@components/analytics'
|
||||
import Button from '@components/Button'
|
||||
import haptics from '@components/haptics'
|
||||
import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header'
|
||||
import { ParseHTML } from '@components/Parse'
|
||||
import RelativeTime from '@components/RelativeTime'
|
||||
import { BlurView } from '@react-native-community/blur'
|
||||
@ -88,6 +87,7 @@ const ScreenAnnouncements: React.FC<ScreenAnnouncementsProp> = ({
|
||||
emojis={item.emojis}
|
||||
mentions={item.mentions}
|
||||
numberOfLines={999}
|
||||
selectable
|
||||
/>
|
||||
</ScrollView>
|
||||
{item.reactions?.length ? (
|
||||
@ -210,28 +210,6 @@ const ScreenAnnouncements: React.FC<ScreenAnnouncementsProp> = ({
|
||||
reducedTransparencyFallbackColor={theme.backgroundDefault}
|
||||
>
|
||||
<SafeAreaView style={styles.base}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexBasis: 44
|
||||
}}
|
||||
>
|
||||
<HeaderLeft
|
||||
content='X'
|
||||
native={false}
|
||||
onPress={() => navigation.goBack()}
|
||||
/>
|
||||
<HeaderCenter content={t('screenAnnouncements:heading')} />
|
||||
<View style={{ opacity: 0 }} accessible={false}>
|
||||
<HeaderRight
|
||||
content='MoreHorizontal'
|
||||
native={false}
|
||||
onPress={() => {}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<FlatList
|
||||
horizontal
|
||||
data={query.data}
|
||||
|
@ -88,6 +88,14 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
|
||||
return {
|
||||
...composeInitialState,
|
||||
timestamp: Date.now(),
|
||||
attachments: {
|
||||
...composeInitialState.attachments,
|
||||
sensitive:
|
||||
localAccount?.preferences &&
|
||||
localAccount?.preferences['posting:default:sensitive']
|
||||
? localAccount?.preferences['posting:default:sensitive']
|
||||
: false
|
||||
},
|
||||
visibility:
|
||||
localAccount?.preferences &&
|
||||
localAccount.preferences['posting:default:visibility']
|
||||
@ -397,12 +405,18 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
|
||||
<Stack.Screen
|
||||
name='Screen-Compose-DraftsList'
|
||||
component={ComposeDraftsList}
|
||||
options={{ stackPresentation: 'modal', headerShown: false }}
|
||||
options={{
|
||||
stackPresentation: 'modal',
|
||||
...(Platform.OS === 'android' && { headerShown: false })
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Screen-Compose-EditAttachment'
|
||||
component={ComposeEditAttachment}
|
||||
options={{ stackPresentation: 'modal', headerShown: false }}
|
||||
options={{
|
||||
stackPresentation: 'modal',
|
||||
...(Platform.OS === 'android' && { headerShown: false })
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</ComposeContext.Provider>
|
||||
|
@ -3,7 +3,7 @@ import { useEmojisQuery } from '@utils/queryHooks/emojis'
|
||||
import { useSearchQuery } from '@utils/queryHooks/search'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { forEach, groupBy, sortBy } from 'lodash'
|
||||
import { chunk, forEach, groupBy, sortBy } from 'lodash'
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
@ -28,23 +28,26 @@ import ComposeContext from './utils/createContext'
|
||||
import ComposeDrafts from './Root/Drafts'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||
import { ComposeState } from './utils/types'
|
||||
|
||||
const prefetchEmojis = (
|
||||
sortedEmojis: { title: string; data: Mastodon.Emoji[] }[],
|
||||
sortedEmojis: NonNullable<ComposeState['emoji']['emojis']>,
|
||||
reduceMotionEnabled: boolean
|
||||
) => {
|
||||
const prefetches: { uri: string }[] = []
|
||||
let requestedIndex = 0
|
||||
sortedEmojis.forEach(sorted => {
|
||||
sorted.data.forEach(emoji => {
|
||||
if (requestedIndex > 40) {
|
||||
return
|
||||
}
|
||||
prefetches.push({
|
||||
uri: reduceMotionEnabled ? emoji.static_url : emoji.url
|
||||
sorted.data.forEach(emojis =>
|
||||
emojis.forEach(emoji => {
|
||||
if (requestedIndex > 40) {
|
||||
return
|
||||
}
|
||||
prefetches.push({
|
||||
uri: reduceMotionEnabled ? emoji.static_url : emoji.url
|
||||
})
|
||||
requestedIndex++
|
||||
})
|
||||
requestedIndex++
|
||||
})
|
||||
)
|
||||
})
|
||||
try {
|
||||
FastImage.preload(prefetches)
|
||||
@ -90,10 +93,11 @@ const ComposeRoot = React.memo(
|
||||
const { data: emojisData } = useEmojisQuery({})
|
||||
useEffect(() => {
|
||||
if (emojisData && emojisData.length) {
|
||||
let sortedEmojis: { title: string; data: Mastodon.Emoji[] }[] = []
|
||||
let sortedEmojis: { title: string; data: Mastodon.Emoji[][] }[] = []
|
||||
forEach(
|
||||
groupBy(sortBy(emojisData, ['category', 'shortcode']), 'category'),
|
||||
(value, key) => sortedEmojis.push({ title: key, data: value })
|
||||
(value, key) =>
|
||||
sortedEmojis.push({ title: key, data: chunk(value, 5) })
|
||||
)
|
||||
composeDispatch({
|
||||
type: 'emoji',
|
||||
|
@ -1,15 +1,8 @@
|
||||
import analytics from '@components/analytics'
|
||||
import haptics from '@components/haptics'
|
||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, {
|
||||
RefObject,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo
|
||||
} from 'react'
|
||||
import React, { RefObject, useCallback, useContext, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
AccessibilityInfo,
|
||||
@ -25,52 +18,15 @@ import validUrl from 'valid-url'
|
||||
import updateText from '../../updateText'
|
||||
import ComposeContext from '../../utils/createContext'
|
||||
|
||||
const SingleEmoji = ({ emoji }: { emoji: Mastodon.Emoji }) => {
|
||||
const { t } = useTranslation()
|
||||
const { reduceMotionEnabled } = useAccessibility()
|
||||
|
||||
const { composeState, composeDispatch } = useContext(ComposeContext)
|
||||
const onPress = useCallback(() => {
|
||||
analytics('compose_emoji_add')
|
||||
updateText({
|
||||
composeState,
|
||||
composeDispatch,
|
||||
newText: `:${emoji.shortcode}:`,
|
||||
type: 'emoji'
|
||||
})
|
||||
haptics('Light')
|
||||
}, [composeState])
|
||||
const children = useMemo(() => {
|
||||
const uri = reduceMotionEnabled ? emoji.static_url : emoji.url
|
||||
if (validUrl.isHttpsUri(uri)) {
|
||||
return (
|
||||
<FastImage
|
||||
accessibilityLabel={t('common:customEmoji.accessibilityLabel', {
|
||||
emoji: emoji.shortcode
|
||||
})}
|
||||
accessibilityHint={t(
|
||||
'screenCompose:content.root.footer.emojis.accessibilityHint'
|
||||
)}
|
||||
source={{ uri: reduceMotionEnabled ? emoji.static_url : emoji.url }}
|
||||
style={styles.emoji}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}, [])
|
||||
return (
|
||||
<Pressable key={emoji.shortcode} onPress={onPress} children={children} />
|
||||
)
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
accessibleRefEmojis: RefObject<SectionList>
|
||||
}
|
||||
|
||||
const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
|
||||
const { composeState } = useContext(ComposeContext)
|
||||
const { composeState, composeDispatch } = useContext(ComposeContext)
|
||||
const { reduceMotionEnabled } = useAccessibility()
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
const tagEmojis = findNodeHandle(accessibleRefEmojis.current)
|
||||
@ -86,21 +42,49 @@ const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
|
||||
[]
|
||||
)
|
||||
|
||||
const emojiList = useCallback(
|
||||
section =>
|
||||
section.data.map((emoji: Mastodon.Emoji) => (
|
||||
<SingleEmoji key={emoji.shortcode} emoji={emoji} />
|
||||
)),
|
||||
[]
|
||||
)
|
||||
const listItem = useCallback(
|
||||
({ section, index }) =>
|
||||
index === 0 ? (
|
||||
<View key={section.title} style={styles.emojis}>
|
||||
{emojiList(section)}
|
||||
({ 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={() => {
|
||||
updateText({
|
||||
composeState,
|
||||
composeDispatch,
|
||||
newText: `:${emoji.shortcode}:`,
|
||||
type: 'emoji'
|
||||
})
|
||||
haptics('Light')
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
) : null,
|
||||
[]
|
||||
)
|
||||
},
|
||||
[composeState]
|
||||
)
|
||||
|
||||
return (
|
||||
@ -111,7 +95,7 @@ const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
|
||||
horizontal
|
||||
keyboardShouldPersistTaps='always'
|
||||
sections={composeState.emoji.emojis || []}
|
||||
keyExtractor={item => item.shortcode}
|
||||
keyExtractor={item => item[0].shortcode}
|
||||
renderSectionHeader={listHeader}
|
||||
renderItem={listItem}
|
||||
windowSize={2}
|
||||
|
@ -123,7 +123,8 @@ const addAttachment = async ({
|
||||
})
|
||||
}
|
||||
|
||||
mediaSelector({ uploader, showActionSheetWithOptions })
|
||||
const result = await mediaSelector({ showActionSheetWithOptions })
|
||||
await uploader(result)
|
||||
}
|
||||
|
||||
export default addAttachment
|
||||
|
@ -31,7 +31,10 @@ const composeInitialState: Omit<ComposeState, 'timestamp'> = {
|
||||
multiple: false,
|
||||
expire: '86400'
|
||||
},
|
||||
attachments: { sensitive: false, uploads: [] },
|
||||
attachments: {
|
||||
sensitive: false,
|
||||
uploads: []
|
||||
},
|
||||
visibility: 'public',
|
||||
visibilityLock: false,
|
||||
replyToStatus: undefined,
|
||||
|
2
src/screens/Compose/utils/types.d.ts
vendored
2
src/screens/Compose/utils/types.d.ts
vendored
@ -40,7 +40,7 @@ export type ComposeState = {
|
||||
}
|
||||
emoji: {
|
||||
active: boolean
|
||||
emojis: { title: string; data: Mastodon.Emoji[] }[] | undefined
|
||||
emojis: { title: string; data: Mastodon.Emoji[][] }[] | undefined
|
||||
}
|
||||
poll: {
|
||||
active: boolean
|
||||
|
@ -109,16 +109,28 @@ const TabMe = React.memo(
|
||||
component={TabMeProfile}
|
||||
options={{
|
||||
stackPresentation: 'modal',
|
||||
headerShown: false
|
||||
...(Platform.OS === 'android' && { headerShown: false })
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Tab-Me-Push'
|
||||
component={TabMePush}
|
||||
options={{
|
||||
options={({ navigation }) => ({
|
||||
stackPresentation: 'modal',
|
||||
headerShown: false
|
||||
}}
|
||||
headerShown: true,
|
||||
headerTitle: t('me.stacks.push.name'),
|
||||
...(Platform.OS === 'android' && {
|
||||
headerCenter: () => (
|
||||
<HeaderCenter content={t('me.stacks.push.name')} />
|
||||
)
|
||||
}),
|
||||
headerLeft: () => (
|
||||
<HeaderLeft
|
||||
content='ChevronDown'
|
||||
onPress={() => navigation.goBack()}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Tab-Me-Settings'
|
||||
@ -149,10 +161,22 @@ const TabMe = React.memo(
|
||||
<Stack.Screen
|
||||
name='Tab-Me-Switch'
|
||||
component={TabMeSwitch}
|
||||
options={{
|
||||
options={({ navigation }) => ({
|
||||
stackPresentation: 'modal',
|
||||
headerShown: false
|
||||
}}
|
||||
headerShown: true,
|
||||
headerTitle: t('me.stacks.switch.name'),
|
||||
...(Platform.OS === 'android' && {
|
||||
headerCenter: () => (
|
||||
<HeaderCenter content={t('me.stacks.switch.name')} />
|
||||
)
|
||||
}),
|
||||
headerLeft: () => (
|
||||
<HeaderLeft
|
||||
content='ChevronDown'
|
||||
onPress={() => navigation.goBack()}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
/>
|
||||
|
||||
{sharedScreens(Stack as any)}
|
||||
|
@ -30,7 +30,6 @@ const TabMeProfile: React.FC<StackScreenProps<
|
||||
>
|
||||
<Stack.Screen
|
||||
name='Tab-Me-Profile-Root'
|
||||
component={TabMeProfileRoot}
|
||||
options={{
|
||||
headerTitle: t('me.stacks.profile.name'),
|
||||
...(Platform.OS === 'android' && {
|
||||
@ -45,7 +44,15 @@ const TabMeProfile: React.FC<StackScreenProps<
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{({ route, navigation }) => (
|
||||
<TabMeProfileRoot
|
||||
messageRef={messageRef}
|
||||
route={route}
|
||||
navigation={navigation}
|
||||
/>
|
||||
)}
|
||||
</Stack.Screen>
|
||||
<Stack.Screen
|
||||
name='Tab-Me-Profile-Name'
|
||||
options={{
|
||||
|
@ -95,12 +95,13 @@ const TabMeProfileFields: React.FC<StackScreenProps<
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
.catch(err => {
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.note.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
|
@ -77,12 +77,13 @@ const TabMeProfileName: React.FC<StackScreenProps<
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
.catch(err => {
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.name.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
|
@ -77,12 +77,13 @@ const TabMeProfileNote: React.FC<StackScreenProps<
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
.catch(err => {
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.note.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
|
@ -1,21 +1,30 @@
|
||||
import analytics from '@components/analytics'
|
||||
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||
import { displayMessage } from '@components/Message'
|
||||
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 { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { RefObject, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import FlashMessage from 'react-native-flash-message'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import ProfileAvatarHeader from './Root/AvatarHeader'
|
||||
|
||||
const TabMeProfileRoot: React.FC<StackScreenProps<
|
||||
Nav.TabMeProfileStackParamList,
|
||||
'Tab-Me-Profile-Root'
|
||||
>> = ({ navigation }) => {
|
||||
> & { messageRef: RefObject<FlashMessage> }> = ({ messageRef, navigation }) => {
|
||||
const { mode } = useTheme()
|
||||
const { t } = useTranslation('screenTabs')
|
||||
|
||||
const { showActionSheetWithOptions } = useActionSheet()
|
||||
|
||||
const { data, isLoading } = useProfileQuery({})
|
||||
const { mutate } = useProfileMutation()
|
||||
const { mutateAsync } = useProfileMutation()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const onPressVisibility = useCallback(() => {
|
||||
showActionSheetWithOptions(
|
||||
@ -32,40 +41,185 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
|
||||
async buttonIndex => {
|
||||
switch (buttonIndex) {
|
||||
case 0:
|
||||
mutate({ type: 'source[privacy]', data: 'public' })
|
||||
analytics('me_profile_visibility', {
|
||||
current: t(
|
||||
`me.profile.root.visibility.options.${data?.source.privacy}`
|
||||
),
|
||||
new: 'public'
|
||||
})
|
||||
mutateAsync({ type: 'source[privacy]', data: 'public' })
|
||||
.then(() => dispatch(updateAccountPreferences()))
|
||||
.catch(err =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.visibility.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
break
|
||||
case 1:
|
||||
mutate({ type: 'source[privacy]', data: 'unlisted' })
|
||||
analytics('me_profile_visibility', {
|
||||
current: t(
|
||||
`me.profile.root.visibility.options.${data?.source.privacy}`
|
||||
),
|
||||
new: 'unlisted'
|
||||
})
|
||||
mutateAsync({ type: 'source[privacy]', data: 'unlisted' })
|
||||
.then(() => dispatch(updateAccountPreferences()))
|
||||
.catch(err =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.visibility.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
break
|
||||
case 2:
|
||||
mutate({ type: 'source[privacy]', data: 'private' })
|
||||
analytics('me_profile_visibility', {
|
||||
current: t(
|
||||
`me.profile.root.visibility.options.${data?.source.privacy}`
|
||||
),
|
||||
new: 'unlisted'
|
||||
})
|
||||
mutateAsync({ type: 'source[privacy]', data: 'private' })
|
||||
.then(() => dispatch(updateAccountPreferences()))
|
||||
.catch(err =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.visibility.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
)
|
||||
}, [])
|
||||
}, [data?.source.privacy])
|
||||
|
||||
const onPressSensitive = useCallback(() => {
|
||||
if (data?.source.sensitive === undefined) {
|
||||
mutate({ type: 'source[sensitive]', data: true })
|
||||
analytics('me_profile_sensitive', {
|
||||
current: undefined,
|
||||
new: true
|
||||
})
|
||||
mutateAsync({ type: 'source[sensitive]', data: true })
|
||||
.then(() => dispatch(updateAccountPreferences()))
|
||||
.catch(err =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.sensitive.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
} else {
|
||||
mutate({ type: 'source[sensitive]', data: !data.source.sensitive })
|
||||
analytics('me_profile_sensitive', {
|
||||
current: data.source.sensitive,
|
||||
new: !data.source.sensitive
|
||||
})
|
||||
mutateAsync({
|
||||
type: 'source[sensitive]',
|
||||
data: !data.source.sensitive
|
||||
})
|
||||
.then(() => dispatch(updateAccountPreferences()))
|
||||
.catch(err =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.sensitive.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
}
|
||||
}, [data?.source.sensitive])
|
||||
|
||||
const onPressLock = useCallback(() => {
|
||||
if (data?.locked === undefined) {
|
||||
mutate({ type: 'locked', data: true })
|
||||
analytics('me_profile_lock', {
|
||||
current: undefined,
|
||||
new: true
|
||||
})
|
||||
mutateAsync({ type: 'locked', data: true }).catch(err =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.lock.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
} else {
|
||||
mutate({ type: 'locked', data: !data.locked })
|
||||
analytics('me_profile_lock', {
|
||||
current: data.locked,
|
||||
new: !data.locked
|
||||
})
|
||||
mutateAsync({ type: 'locked', data: !data.locked }).catch(err =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.lock.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
}
|
||||
}, [data?.locked])
|
||||
|
||||
const onPressBot = useCallback(() => {
|
||||
if (data?.bot === undefined) {
|
||||
mutate({ type: 'bot', data: true })
|
||||
analytics('me_profile_bot', {
|
||||
current: undefined,
|
||||
new: true
|
||||
})
|
||||
mutateAsync({ type: 'bot', data: true }).catch(err =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.bot.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
} else {
|
||||
mutate({ type: 'bot', data: !data?.bot })
|
||||
analytics('me_profile_bot', {
|
||||
current: data.bot,
|
||||
new: !data.bot
|
||||
})
|
||||
mutateAsync({ type: 'bot', data: !data?.bot }).catch(err =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t('me.profile.root.bot.title')
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
}
|
||||
}, [data?.bot])
|
||||
|
||||
@ -84,43 +238,18 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<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'
|
||||
/>
|
||||
<ProfileAvatarHeader type='avatar' messageRef={messageRef} />
|
||||
<ProfileAvatarHeader type='header' messageRef={messageRef} />
|
||||
<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 || ''
|
||||
})
|
||||
data &&
|
||||
navigation.navigate('Tab-Me-Profile-Note', {
|
||||
note: data.source?.note
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<MenuRow
|
||||
|
66
src/screens/Tabs/Me/Profile/Root/AvatarHeader.tsx
Normal file
66
src/screens/Tabs/Me/Profile/Root/AvatarHeader.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import mediaSelector from '@components/mediaSelector'
|
||||
import { MenuRow } from '@components/Menu'
|
||||
import { displayMessage } from '@components/Message'
|
||||
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||
import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import * as ImagePicker from 'expo-image-picker'
|
||||
import React, { RefObject } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import FlashMessage from 'react-native-flash-message'
|
||||
|
||||
export interface Props {
|
||||
type: 'avatar' | 'header'
|
||||
messageRef: RefObject<FlashMessage>
|
||||
}
|
||||
|
||||
const ProfileAvatarHeader: React.FC<Props> = ({ type, messageRef }) => {
|
||||
const { mode } = useTheme()
|
||||
const { t } = useTranslation('screenTabs')
|
||||
|
||||
const { showActionSheetWithOptions } = useActionSheet()
|
||||
|
||||
const query = useProfileQuery({})
|
||||
const mutation = useProfileMutation()
|
||||
|
||||
return (
|
||||
<MenuRow
|
||||
title={t(`me.profile.root.${type}.title`)}
|
||||
description={t(`me.profile.root.${type}.description`)}
|
||||
loading={query.isLoading || mutation.isLoading}
|
||||
iconBack='ChevronRight'
|
||||
onPress={async () => {
|
||||
const image = await mediaSelector({
|
||||
showActionSheetWithOptions,
|
||||
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||
resize: { width: 400, height: 400 }
|
||||
})
|
||||
mutation
|
||||
.mutateAsync({ type, data: image.uri })
|
||||
.then(() =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.succeed', {
|
||||
type: t(`me.profile.root.${type}.title`)
|
||||
}),
|
||||
mode,
|
||||
type: 'success'
|
||||
})
|
||||
)
|
||||
.catch(err =>
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('me.profile.feedback.failed', {
|
||||
type: t(`me.profile.root.${type}.title`)
|
||||
}),
|
||||
...(err && { description: err }),
|
||||
mode,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfileAvatarHeader
|
@ -1,42 +1,173 @@
|
||||
import { HeaderCenter, HeaderLeft } from '@components/Header'
|
||||
import { StackScreenProps } from '@react-navigation/stack'
|
||||
import React from 'react'
|
||||
import analytics from '@components/analytics'
|
||||
import Button from '@components/Button'
|
||||
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||
import { updateInstancePush } from '@utils/slices/instances/updatePush'
|
||||
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
|
||||
import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode'
|
||||
import {
|
||||
clearPushLoading,
|
||||
getInstanceAccount,
|
||||
getInstancePush,
|
||||
getInstanceUri
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import * as WebBrowser from 'expo-web-browser'
|
||||
import React, { useState, useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Platform } from 'react-native'
|
||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||
import TabMePushRoot from './Push/Root'
|
||||
import { AppState, Linking, ScrollView } from 'react-native'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
const Stack = createNativeStackNavigator<Nav.TabMePushStackParamList>()
|
||||
|
||||
const TabMePush: React.FC<StackScreenProps<
|
||||
Nav.TabMeStackParamList,
|
||||
'Tab-Me-Push'
|
||||
>> = ({ navigation }) => {
|
||||
const TabMePush: React.FC = () => {
|
||||
const { t } = useTranslation('screenTabs')
|
||||
const instanceAccount = useSelector(
|
||||
getInstanceAccount,
|
||||
(prev, next) => prev?.acct === next?.acct
|
||||
)
|
||||
const instanceUri = useSelector(getInstanceUri)
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const instancePush = useSelector(getInstancePush)
|
||||
|
||||
const [pushEnabled, setPushEnabled] = useState<boolean>()
|
||||
const [pushCanAskAgain, setPushCanAskAgain] = useState<boolean>()
|
||||
const checkPush = async () => {
|
||||
const settings = await Notifications.getPermissionsAsync()
|
||||
layoutAnimation()
|
||||
setPushEnabled(settings.granted)
|
||||
setPushCanAskAgain(settings.canAskAgain)
|
||||
}
|
||||
useEffect(() => {
|
||||
checkPush()
|
||||
AppState.addEventListener('change', checkPush)
|
||||
return () => {
|
||||
AppState.removeEventListener('change', checkPush)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(clearPushLoading())
|
||||
}, [])
|
||||
|
||||
const isLoading = instancePush?.global.loading || instancePush?.decode.loading
|
||||
|
||||
const alerts = useMemo(() => {
|
||||
return instancePush?.alerts
|
||||
? (['follow', 'favourite', 'reblog', 'mention', 'poll'] as [
|
||||
'follow',
|
||||
'favourite',
|
||||
'reblog',
|
||||
'mention',
|
||||
'poll'
|
||||
]).map(alert => (
|
||||
<MenuRow
|
||||
key={alert}
|
||||
title={t(`me.push.${alert}.heading`)}
|
||||
switchDisabled={
|
||||
!pushEnabled || !instancePush.global.value || isLoading
|
||||
}
|
||||
switchValue={instancePush?.alerts[alert].value}
|
||||
switchOnValueChange={() => {
|
||||
analytics(`me_push_${alert}`, {
|
||||
current: instancePush?.alerts[alert].value,
|
||||
new: !instancePush?.alerts[alert].value
|
||||
})
|
||||
dispatch(
|
||||
updateInstancePushAlert({
|
||||
changed: alert,
|
||||
alerts: {
|
||||
...instancePush?.alerts,
|
||||
[alert]: {
|
||||
...instancePush?.alerts[alert],
|
||||
value: !instancePush?.alerts[alert].value
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))
|
||||
: null
|
||||
}, [pushEnabled, instancePush?.global, instancePush?.alerts, isLoading])
|
||||
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }}
|
||||
>
|
||||
<Stack.Screen
|
||||
name='Tab-Me-Push-Root'
|
||||
component={TabMePushRoot}
|
||||
options={{
|
||||
headerTitle: t('me.stacks.push.name'),
|
||||
...(Platform.OS === 'android' && {
|
||||
headerCenter: () => (
|
||||
<HeaderCenter content={t('me.stacks.push.name')} />
|
||||
)
|
||||
}),
|
||||
headerLeft: () => (
|
||||
<HeaderLeft
|
||||
content='ChevronDown'
|
||||
onPress={() => navigation.goBack()}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
<ScrollView>
|
||||
{pushEnabled === false ? (
|
||||
<MenuContainer>
|
||||
<Button
|
||||
type='text'
|
||||
content={
|
||||
pushCanAskAgain
|
||||
? t('me.push.enable.direct')
|
||||
: t('me.push.enable.settings')
|
||||
}
|
||||
style={{
|
||||
marginTop: StyleConstants.Spacing.Global.PagePadding,
|
||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (pushCanAskAgain) {
|
||||
analytics('me_push_enabled_dialogue')
|
||||
const result = await Notifications.requestPermissionsAsync()
|
||||
setPushEnabled(result.granted)
|
||||
setPushCanAskAgain(result.canAskAgain)
|
||||
} else {
|
||||
analytics('me_push_enabled_setting')
|
||||
Linking.openSettings()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</MenuContainer>
|
||||
) : null}
|
||||
<MenuContainer>
|
||||
<MenuRow
|
||||
title={t('me.push.global.heading', {
|
||||
acct: `@${instanceAccount?.acct}@${instanceUri}`
|
||||
})}
|
||||
description={t('me.push.global.description')}
|
||||
loading={instancePush?.global.loading}
|
||||
switchDisabled={!pushEnabled || isLoading}
|
||||
switchValue={
|
||||
pushEnabled === false ? false : instancePush?.global.value
|
||||
}
|
||||
switchOnValueChange={() => {
|
||||
analytics('me_push_global', {
|
||||
current: instancePush?.global.value,
|
||||
new: !instancePush?.global.value
|
||||
})
|
||||
dispatch(updateInstancePush(!instancePush?.global.value))
|
||||
}}
|
||||
/>
|
||||
</MenuContainer>
|
||||
<MenuContainer>
|
||||
<MenuRow
|
||||
title={t('me.push.decode.heading')}
|
||||
description={t('me.push.decode.description')}
|
||||
loading={instancePush?.decode.loading}
|
||||
switchDisabled={
|
||||
!pushEnabled || !instancePush?.global.value || isLoading
|
||||
}
|
||||
switchValue={instancePush?.decode.value}
|
||||
switchOnValueChange={() => {
|
||||
analytics('me_push_decode', {
|
||||
current: instancePush?.decode.value,
|
||||
new: !instancePush?.decode.value
|
||||
})
|
||||
dispatch(updateInstancePushDecode(!instancePush?.decode.value))
|
||||
}}
|
||||
/>
|
||||
<MenuRow
|
||||
title={t('me.push.howitworks')}
|
||||
iconBack='ExternalLink'
|
||||
onPress={() => {
|
||||
analytics('me_push_howitworks')
|
||||
WebBrowser.openBrowserAsync('https://tooot.app/how-push-works')
|
||||
}}
|
||||
/>
|
||||
</MenuContainer>
|
||||
<MenuContainer>{alerts}</MenuContainer>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,163 +0,0 @@
|
||||
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||
import { updateInstancePush } from '@utils/slices/instances/updatePush'
|
||||
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
|
||||
import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode'
|
||||
import {
|
||||
clearPushLoading,
|
||||
getInstanceAccount,
|
||||
getInstancePush,
|
||||
getInstanceUri
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import * as WebBrowser from 'expo-web-browser'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||
import Button from '@components/Button'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { AppState, Linking } from 'react-native'
|
||||
import { StackScreenProps } from '@react-navigation/stack'
|
||||
|
||||
const TabMePushRoot: React.FC<StackScreenProps<
|
||||
Nav.TabMeStackParamList,
|
||||
'Tab-Me-Push'
|
||||
>> = () => {
|
||||
const { t } = useTranslation('screenTabs')
|
||||
const instanceAccount = useSelector(
|
||||
getInstanceAccount,
|
||||
(prev, next) => prev?.acct === next?.acct
|
||||
)
|
||||
const instanceUri = useSelector(getInstanceUri)
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const instancePush = useSelector(getInstancePush)
|
||||
|
||||
const [pushEnabled, setPushEnabled] = useState<boolean>()
|
||||
const [pushCanAskAgain, setPushCanAskAgain] = useState<boolean>()
|
||||
const checkPush = async () => {
|
||||
const settings = await Notifications.getPermissionsAsync()
|
||||
layoutAnimation()
|
||||
setPushEnabled(settings.granted)
|
||||
setPushCanAskAgain(settings.canAskAgain)
|
||||
}
|
||||
useEffect(() => {
|
||||
checkPush()
|
||||
AppState.addEventListener('change', checkPush)
|
||||
return () => {
|
||||
AppState.removeEventListener('change', checkPush)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(clearPushLoading())
|
||||
}, [])
|
||||
|
||||
const isLoading = instancePush?.global.loading || instancePush?.decode.loading
|
||||
|
||||
const alerts = useMemo(() => {
|
||||
return instancePush?.alerts
|
||||
? (['follow', 'favourite', 'reblog', 'mention', 'poll'] as [
|
||||
'follow',
|
||||
'favourite',
|
||||
'reblog',
|
||||
'mention',
|
||||
'poll'
|
||||
]).map(alert => (
|
||||
<MenuRow
|
||||
key={alert}
|
||||
title={t(`me.push.${alert}.heading`)}
|
||||
switchDisabled={
|
||||
!pushEnabled || !instancePush.global.value || isLoading
|
||||
}
|
||||
switchValue={instancePush?.alerts[alert].value}
|
||||
switchOnValueChange={() =>
|
||||
dispatch(
|
||||
updateInstancePushAlert({
|
||||
changed: alert,
|
||||
alerts: {
|
||||
...instancePush?.alerts,
|
||||
[alert]: {
|
||||
...instancePush?.alerts[alert],
|
||||
value: !instancePush?.alerts[alert].value
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
))
|
||||
: null
|
||||
}, [pushEnabled, instancePush?.global, instancePush?.alerts, isLoading])
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
{pushEnabled === false ? (
|
||||
<MenuContainer>
|
||||
<Button
|
||||
type='text'
|
||||
content={
|
||||
pushCanAskAgain
|
||||
? t('me.push.enable.direct')
|
||||
: t('me.push.enable.settings')
|
||||
}
|
||||
style={{
|
||||
marginTop: StyleConstants.Spacing.Global.PagePadding,
|
||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (pushCanAskAgain) {
|
||||
const result = await Notifications.requestPermissionsAsync()
|
||||
setPushEnabled(result.granted)
|
||||
setPushCanAskAgain(result.canAskAgain)
|
||||
} else {
|
||||
Linking.openSettings()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</MenuContainer>
|
||||
) : null}
|
||||
<MenuContainer>
|
||||
<MenuRow
|
||||
title={t('me.push.global.heading', {
|
||||
acct: `@${instanceAccount?.acct}@${instanceUri}`
|
||||
})}
|
||||
description={t('me.push.global.description')}
|
||||
loading={instancePush?.global.loading}
|
||||
switchDisabled={!pushEnabled || isLoading}
|
||||
switchValue={
|
||||
pushEnabled === false ? false : instancePush?.global.value
|
||||
}
|
||||
switchOnValueChange={() =>
|
||||
dispatch(updateInstancePush(!instancePush?.global.value))
|
||||
}
|
||||
/>
|
||||
</MenuContainer>
|
||||
<MenuContainer>
|
||||
<MenuRow
|
||||
title={t('me.push.decode.heading')}
|
||||
description={t('me.push.decode.description')}
|
||||
loading={instancePush?.decode.loading}
|
||||
switchDisabled={
|
||||
!pushEnabled || !instancePush?.global.value || isLoading
|
||||
}
|
||||
switchValue={instancePush?.decode.value}
|
||||
switchOnValueChange={() =>
|
||||
dispatch(updateInstancePushDecode(!instancePush?.decode.value))
|
||||
}
|
||||
/>
|
||||
<MenuRow
|
||||
title={t('me.push.howitworks')}
|
||||
iconBack='ExternalLink'
|
||||
onPress={() =>
|
||||
WebBrowser.openBrowserAsync('https://tooot.app/how-push-works')
|
||||
}
|
||||
/>
|
||||
</MenuContainer>
|
||||
<MenuContainer>{alerts}</MenuContainer>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
export default TabMePushRoot
|
@ -2,65 +2,25 @@ import { MenuContainer, MenuRow } from '@components/Menu'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { useAnnouncementQuery } from '@utils/queryHooks/announcement'
|
||||
import { useListsQuery } from '@utils/queryHooks/lists'
|
||||
import React, { useMemo } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const Collections: React.FC = () => {
|
||||
const { t, i18n } = useTranslation('screenTabs')
|
||||
const { t } = useTranslation('screenTabs')
|
||||
const navigation = useNavigation()
|
||||
|
||||
const listsQuery = useListsQuery({
|
||||
options: {
|
||||
notifyOnChangeProps: []
|
||||
notifyOnChangeProps: ['data']
|
||||
}
|
||||
})
|
||||
const rowLists = useMemo(() => {
|
||||
if (listsQuery.isSuccess && listsQuery.data?.length) {
|
||||
return (
|
||||
<MenuRow
|
||||
iconFront='List'
|
||||
iconBack='ChevronRight'
|
||||
title={t('me.stacks.lists.name')}
|
||||
onPress={() => navigation.navigate('Tab-Me-Lists')}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}, [listsQuery.isSuccess, listsQuery.data, i18n.language])
|
||||
|
||||
const announcementsQuery = useAnnouncementQuery({
|
||||
showAll: true,
|
||||
options: {
|
||||
notifyOnChangeProps: []
|
||||
notifyOnChangeProps: ['data']
|
||||
}
|
||||
})
|
||||
const rowAnnouncements = useMemo(() => {
|
||||
if (announcementsQuery.isSuccess && announcementsQuery.data?.length) {
|
||||
const amount = announcementsQuery.data.filter(
|
||||
announcement => !announcement.read
|
||||
).length
|
||||
return (
|
||||
<MenuRow
|
||||
iconFront='Clipboard'
|
||||
iconBack='ChevronRight'
|
||||
title={t('screenAnnouncements:heading')}
|
||||
content={
|
||||
amount
|
||||
? t('me.root.announcements.content.unread', {
|
||||
amount
|
||||
})
|
||||
: t('me.root.announcements.content.read')
|
||||
}
|
||||
onPress={() =>
|
||||
navigation.navigate('Screen-Announcements', { showAll: true })
|
||||
}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}, [announcementsQuery.isSuccess, announcementsQuery.data, i18n.language])
|
||||
|
||||
return (
|
||||
<MenuContainer>
|
||||
@ -82,8 +42,34 @@ const Collections: React.FC = () => {
|
||||
title={t('me.stacks.favourites.name')}
|
||||
onPress={() => navigation.navigate('Tab-Me-Favourites')}
|
||||
/>
|
||||
{rowLists}
|
||||
{rowAnnouncements}
|
||||
{listsQuery.data?.length ? (
|
||||
<MenuRow
|
||||
iconFront='List'
|
||||
iconBack='ChevronRight'
|
||||
title={t('me.stacks.lists.name')}
|
||||
onPress={() => navigation.navigate('Tab-Me-Lists')}
|
||||
/>
|
||||
) : null}
|
||||
{announcementsQuery.data?.length ? (
|
||||
<MenuRow
|
||||
iconFront='Clipboard'
|
||||
iconBack='ChevronRight'
|
||||
title={t('screenAnnouncements:heading')}
|
||||
content={
|
||||
announcementsQuery.data.filter(announcement => !announcement.read)
|
||||
.length
|
||||
? t('me.root.announcements.content.unread', {
|
||||
amount: announcementsQuery.data.filter(
|
||||
announcement => !announcement.read
|
||||
).length
|
||||
})
|
||||
: t('me.root.announcements.content.read')
|
||||
}
|
||||
onPress={() =>
|
||||
navigation.navigate('Screen-Announcements', { showAll: true })
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</MenuContainer>
|
||||
)
|
||||
}
|
||||
|
@ -1,47 +1,139 @@
|
||||
import { HeaderCenter, HeaderLeft } from '@components/Header'
|
||||
import { StackScreenProps } from '@react-navigation/stack'
|
||||
import React from 'react'
|
||||
import analytics from '@components/analytics'
|
||||
import Button from '@components/Button'
|
||||
import haptics from '@components/haptics'
|
||||
import ComponentInstance from '@components/Instance'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import {
|
||||
getInstanceActive,
|
||||
getInstances,
|
||||
Instance,
|
||||
updateInstanceActive
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { KeyboardAvoidingView, Platform } from 'react-native'
|
||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||
import TabMeSwitchRoot from './Switch/Root'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
import { useQueryClient } from 'react-query'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
const Stack = createNativeStackNavigator()
|
||||
interface Props {
|
||||
instance: Instance
|
||||
selected?: boolean
|
||||
}
|
||||
|
||||
const AccountButton: React.FC<Props> = ({ instance, selected = false }) => {
|
||||
const queryClient = useQueryClient()
|
||||
const navigation = useNavigation()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const TabMeSwitch: React.FC<StackScreenProps<
|
||||
Nav.TabMeStackParamList,
|
||||
'Tab-Me-Switch'
|
||||
>> = ({ navigation }) => {
|
||||
const { t } = useTranslation('screenTabs')
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={{ flex: 1 }}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
<Stack.Navigator
|
||||
screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }}
|
||||
>
|
||||
<Stack.Screen
|
||||
name='Screen-Me-Switch-Root'
|
||||
component={TabMeSwitchRoot}
|
||||
options={{
|
||||
headerTitle: t('me.stacks.switch.name'),
|
||||
...(Platform.OS === 'android' && {
|
||||
headerCenter: () => (
|
||||
<HeaderCenter content={t('me.stacks.switch.name')} />
|
||||
)
|
||||
}),
|
||||
headerLeft: () => (
|
||||
<HeaderLeft
|
||||
content='ChevronDown'
|
||||
onPress={() => navigation.goBack()}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</KeyboardAvoidingView>
|
||||
<Button
|
||||
type='text'
|
||||
selected={selected}
|
||||
style={styles.button}
|
||||
content={`@${instance.account.acct}@${instance.uri}${
|
||||
selected ? ' ✓' : ''
|
||||
}`}
|
||||
onPress={() => {
|
||||
haptics('Light')
|
||||
analytics('switch_existing_press')
|
||||
dispatch(updateInstanceActive(instance))
|
||||
queryClient.clear()
|
||||
navigation.goBack()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const TabMeSwitch: React.FC = () => {
|
||||
const { t } = useTranslation('screenTabs')
|
||||
const { theme } = useTheme()
|
||||
const instances = useSelector(getInstances, () => true)
|
||||
const instanceActive = useSelector(getInstanceActive, () => true)
|
||||
|
||||
const scrollViewRef = useRef<ScrollView>(null)
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
style={styles.base}
|
||||
keyboardShouldPersistTaps='always'
|
||||
>
|
||||
<View style={[styles.firstSection, { borderBottomColor: theme.border }]}>
|
||||
<Text style={[styles.header, { color: theme.primaryDefault }]}>
|
||||
{t('me.switch.existing')}
|
||||
</Text>
|
||||
<View style={styles.accountButtons}>
|
||||
{instances.length
|
||||
? instances
|
||||
.slice()
|
||||
.sort((a, b) =>
|
||||
`${a.uri}${a.account.acct}`.localeCompare(
|
||||
`${b.uri}${b.account.acct}`
|
||||
)
|
||||
)
|
||||
.map((instance, index) => {
|
||||
const localAccount = instances[instanceActive!]
|
||||
return (
|
||||
<AccountButton
|
||||
key={index}
|
||||
instance={instance}
|
||||
selected={
|
||||
instance.url === localAccount.url &&
|
||||
instance.token === localAccount.token &&
|
||||
instance.account.id === localAccount.account.id
|
||||
}
|
||||
/>
|
||||
)
|
||||
})
|
||||
: null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.secondSection}>
|
||||
<Text style={[styles.header, { color: theme.primaryDefault }]}>
|
||||
{t('me.switch.new')}
|
||||
</Text>
|
||||
<ComponentInstance
|
||||
scrollViewRef={scrollViewRef}
|
||||
disableHeaderImage
|
||||
goBack
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
marginBottom: StyleConstants.Spacing.L
|
||||
},
|
||||
header: {
|
||||
...StyleConstants.FontStyle.M,
|
||||
textAlign: 'center',
|
||||
paddingVertical: StyleConstants.Spacing.S
|
||||
},
|
||||
firstSection: {
|
||||
marginTop: StyleConstants.Spacing.S,
|
||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
||||
paddingBottom: StyleConstants.Spacing.S,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth
|
||||
},
|
||||
secondSection: {
|
||||
paddingTop: StyleConstants.Spacing.M
|
||||
},
|
||||
accountButtons: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginTop: StyleConstants.Spacing.M
|
||||
},
|
||||
button: {
|
||||
marginBottom: StyleConstants.Spacing.M,
|
||||
marginRight: StyleConstants.Spacing.M
|
||||
}
|
||||
})
|
||||
|
||||
export default TabMeSwitch
|
||||
|
@ -1,139 +0,0 @@
|
||||
import analytics from '@components/analytics'
|
||||
import Button from '@components/Button'
|
||||
import haptics from '@components/haptics'
|
||||
import ComponentInstance from '@components/Instance'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import {
|
||||
getInstanceActive,
|
||||
getInstances,
|
||||
Instance,
|
||||
updateInstanceActive
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
import { useQueryClient } from 'react-query'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
interface Props {
|
||||
instance: Instance
|
||||
selected?: boolean
|
||||
}
|
||||
|
||||
const AccountButton: React.FC<Props> = ({ instance, selected = false }) => {
|
||||
const queryClient = useQueryClient()
|
||||
const navigation = useNavigation()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
return (
|
||||
<Button
|
||||
type='text'
|
||||
selected={selected}
|
||||
style={styles.button}
|
||||
content={`@${instance.account.acct}@${instance.uri}${
|
||||
selected ? ' ✓' : ''
|
||||
}`}
|
||||
onPress={() => {
|
||||
haptics('Light')
|
||||
analytics('switch_existing_press')
|
||||
dispatch(updateInstanceActive(instance))
|
||||
queryClient.clear()
|
||||
navigation.goBack()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const TabMeSwitchRoot: React.FC = () => {
|
||||
const { t } = useTranslation('screenTabs')
|
||||
const { theme } = useTheme()
|
||||
const instances = useSelector(getInstances, () => true)
|
||||
const instanceActive = useSelector(getInstanceActive, () => true)
|
||||
|
||||
const scrollViewRef = useRef<ScrollView>(null)
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
style={styles.base}
|
||||
keyboardShouldPersistTaps='always'
|
||||
>
|
||||
<View style={[styles.firstSection, { borderBottomColor: theme.border }]}>
|
||||
<Text style={[styles.header, { color: theme.primaryDefault }]}>
|
||||
{t('me.switch.existing')}
|
||||
</Text>
|
||||
<View style={styles.accountButtons}>
|
||||
{instances.length
|
||||
? instances
|
||||
.slice()
|
||||
.sort((a, b) =>
|
||||
`${a.uri}${a.account.acct}`.localeCompare(
|
||||
`${b.uri}${b.account.acct}`
|
||||
)
|
||||
)
|
||||
.map((instance, index) => {
|
||||
const localAccount = instances[instanceActive!]
|
||||
return (
|
||||
<AccountButton
|
||||
key={index}
|
||||
instance={instance}
|
||||
selected={
|
||||
instance.url === localAccount.url &&
|
||||
instance.token === localAccount.token &&
|
||||
instance.account.id === localAccount.account.id
|
||||
}
|
||||
/>
|
||||
)
|
||||
})
|
||||
: null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.secondSection}>
|
||||
<Text style={[styles.header, { color: theme.primaryDefault }]}>
|
||||
{t('me.switch.new')}
|
||||
</Text>
|
||||
<ComponentInstance
|
||||
scrollViewRef={scrollViewRef}
|
||||
disableHeaderImage
|
||||
goBack
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
marginBottom: StyleConstants.Spacing.L
|
||||
},
|
||||
header: {
|
||||
...StyleConstants.FontStyle.M,
|
||||
textAlign: 'center',
|
||||
paddingVertical: StyleConstants.Spacing.S
|
||||
},
|
||||
firstSection: {
|
||||
marginTop: StyleConstants.Spacing.S,
|
||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
||||
paddingBottom: StyleConstants.Spacing.S,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth
|
||||
},
|
||||
secondSection: {
|
||||
paddingTop: StyleConstants.Spacing.M
|
||||
},
|
||||
accountButtons: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginTop: StyleConstants.Spacing.M
|
||||
},
|
||||
button: {
|
||||
marginBottom: StyleConstants.Spacing.M,
|
||||
marginRight: StyleConstants.Spacing.M
|
||||
}
|
||||
})
|
||||
|
||||
export default TabMeSwitchRoot
|
@ -34,6 +34,7 @@ const AccountInformationFields = React.memo(
|
||||
emojis={account.emojis}
|
||||
showFullLink
|
||||
numberOfLines={5}
|
||||
selectable
|
||||
/>
|
||||
{field.verified_at ? (
|
||||
<Icon
|
||||
@ -51,6 +52,7 @@ const AccountInformationFields = React.memo(
|
||||
emojis={account.emojis}
|
||||
showFullLink
|
||||
numberOfLines={5}
|
||||
selectable
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@ -58,7 +60,7 @@ const AccountInformationFields = React.memo(
|
||||
</View>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
(_, next) => next.account === undefined
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
@ -28,11 +28,11 @@ const AccountInformationNote = React.memo(
|
||||
|
||||
return (
|
||||
<View style={styles.note}>
|
||||
<ParseHTML content={account.note!} size={'M'} emojis={account.emojis} />
|
||||
<ParseHTML content={account.note!} size={'M'} emojis={account.emojis} selectable />
|
||||
</View>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
(_, next) => next.account === undefined
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
@ -14,11 +14,11 @@ import { debounce } from 'lodash'
|
||||
import React from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Platform, StyleSheet, Text, TextInput, View } from 'react-native'
|
||||
import { NativeStackNavigationOptions } from 'react-native-screens/lib/typescript'
|
||||
import { NativeStackNavigationOptions } from 'react-native-screens/lib/typescript/native-stack'
|
||||
import {
|
||||
NativeStackNavigationEventMap,
|
||||
NativeStackNavigatorProps
|
||||
} from 'react-native-screens/lib/typescript/types'
|
||||
} from 'react-native-screens/lib/typescript/native-stack/types'
|
||||
|
||||
export type BaseScreens =
|
||||
| Nav.TabLocalStackParamList
|
||||
@ -150,17 +150,13 @@ const sharedScreens = (
|
||||
<View style={styles.searchBar}>
|
||||
<TextInput
|
||||
editable={false}
|
||||
children={
|
||||
<Text
|
||||
style={[
|
||||
styles.textInput,
|
||||
{
|
||||
color: theme.primaryDefault
|
||||
}
|
||||
]}
|
||||
children={t('shared.search.header.prefix')}
|
||||
/>
|
||||
}
|
||||
style={[
|
||||
styles.textInput,
|
||||
{
|
||||
color: theme.primaryDefault
|
||||
}
|
||||
]}
|
||||
defaultValue={t('shared.search.header.prefix')}
|
||||
/>
|
||||
<TextInput
|
||||
accessibilityRole='search'
|
||||
|
@ -1,12 +1,12 @@
|
||||
import Constants from 'expo-constants'
|
||||
import * as Updates from 'expo-updates'
|
||||
import { Constants } from 'react-native-unimodules'
|
||||
import * as Sentry from 'sentry-expo'
|
||||
import log from './log'
|
||||
|
||||
const sentry = () => {
|
||||
log('log', 'Sentry', 'initializing')
|
||||
Sentry.init({
|
||||
dsn: Constants.manifest.extra.sentryDSN,
|
||||
dsn: Constants.manifest?.extra?.sentryDSN,
|
||||
enableInExpoDevelopment: false,
|
||||
debug:
|
||||
__DEV__ ||
|
||||
|
@ -1,9 +1,7 @@
|
||||
import apiInstance from '@api/instance'
|
||||
import { displayMessage } from '@components/Message'
|
||||
import { queryClient } from '@root/App'
|
||||
import queryClient from '@helpers/queryClient'
|
||||
import { AxiosError } from 'axios'
|
||||
import { useMutation, useQuery, UseQueryOptions } from 'react-query'
|
||||
import { QueryKeyAccount } from './account'
|
||||
|
||||
type AccountWithSource = Mastodon.Account &
|
||||
Required<Pick<Mastodon.Account, 'source'>>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import apiInstance from '@api/instance'
|
||||
import haptics from '@components/haptics'
|
||||
import { queryClient } from '@root/App'
|
||||
import queryClient from '@helpers/queryClient'
|
||||
import { store } from '@root/store'
|
||||
import { getInstanceNotificationsFilter } from '@utils/slices/instancesSlice'
|
||||
import { AxiosError } from 'axios'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { queryClient } from '@root/App'
|
||||
import queryClient from '@helpers/queryClient'
|
||||
import { InfiniteData } from 'react-query'
|
||||
import { MutationVarsTimelineDeleteItem } from '../timeline'
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { queryClient } from '@root/App'
|
||||
import queryClient from '@helpers/queryClient'
|
||||
import { findIndex } from 'lodash'
|
||||
import { InfiniteData } from 'react-query'
|
||||
import {
|
||||
|
63
src/utils/queryHooks/translate.ts
Normal file
63
src/utils/queryHooks/translate.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import apiGeneral from '@api/general'
|
||||
import { AxiosError } from 'axios'
|
||||
import { Buffer } from 'buffer'
|
||||
import Constants from 'expo-constants'
|
||||
import { useQuery, UseQueryOptions } from 'react-query'
|
||||
|
||||
type Translations = {
|
||||
provider: string
|
||||
sourceLanguage: string
|
||||
text: string[]
|
||||
}
|
||||
|
||||
export type QueryKeyTranslate = [
|
||||
'Translate',
|
||||
{
|
||||
uri: string
|
||||
source: string
|
||||
target: string
|
||||
text: string[]
|
||||
}
|
||||
]
|
||||
|
||||
export const TRANSLATE_SERVER = __DEV__
|
||||
? 'testtranslate.tooot.app'
|
||||
: 'translate.tooot.app'
|
||||
|
||||
const queryFunction = async ({ queryKey }: { queryKey: QueryKeyTranslate }) => {
|
||||
const key = Constants.manifest.extra?.translateKey
|
||||
if (!key) {
|
||||
return Promise.reject()
|
||||
}
|
||||
|
||||
const { uri, source, target, text } = queryKey[1]
|
||||
|
||||
const uriEncoded = Buffer.from(uri.replace(/https?:\/\//, ''))
|
||||
.toString('base64')
|
||||
.replace('+', '-')
|
||||
.replace('/', '_')
|
||||
.replace(/=+$/, '')
|
||||
const original = Buffer.from(JSON.stringify({ source, text })).toString(
|
||||
'base64'
|
||||
)
|
||||
|
||||
const res = await apiGeneral<Translations>({
|
||||
domain: TRANSLATE_SERVER,
|
||||
method: 'get',
|
||||
url: `v1/translate/${uriEncoded}/${target}`,
|
||||
headers: { key, original }
|
||||
})
|
||||
return res.body
|
||||
}
|
||||
|
||||
const useTranslateQuery = ({
|
||||
options,
|
||||
...queryKeyParams
|
||||
}: QueryKeyTranslate[1] & {
|
||||
options?: UseQueryOptions<Translations, AxiosError, Translations>
|
||||
}) => {
|
||||
const queryKey: QueryKeyTranslate = ['Translate', { ...queryKeyParams }]
|
||||
return useQuery(queryKey, queryFunction, options)
|
||||
}
|
||||
|
||||
export { useTranslateQuery }
|
@ -1,6 +1,6 @@
|
||||
import apiGeneral from '@api/general'
|
||||
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(
|
||||
|
Reference in New Issue
Block a user