1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00

619 restructure local storage (#628)

* To MMKV migration working

* POC migrated font size settings

* Moved settings to mmkv

* Fix typos

* Migrated contexts slice

* Migrated app slice

* POC instance emoji update

* Migrated drafts

* Migrated simple instance properties

* All migrated!

* Re-structure files

* Tolerant of undefined settings

* Can properly logging in and out including empty state
This commit is contained in:
xmflsct
2022-12-28 23:41:36 +01:00
committed by GitHub
parent 71ccb4a93c
commit 1ea6aff328
214 changed files with 2151 additions and 3694 deletions

View File

@@ -1,15 +1,14 @@
import AccountButton from '@components/AccountButton'
import CustomText from '@components/Text'
import navigationRef from '@helpers/navigationRef'
import navigationRef from '@utils/navigation/navigationRef'
import { RootStackScreenProps } from '@utils/navigation/navigators'
import { getInstances } from '@utils/slices/instancesSlice'
import { getGlobalStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import * as VideoThumbnails from 'expo-video-thumbnails'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FlatList, Image, ScrollView, View } from 'react-native'
import { useSelector } from 'react-redux'
const Share = ({
text,
@@ -93,7 +92,7 @@ const ScreenAccountSelection = ({
const { colors } = useTheme()
const { t } = useTranslation('screenAccountSelection')
const instances = useSelector(getInstances, () => true)
const accounts = getGlobalStorage.object('accounts')
return (
<ScrollView
@@ -126,27 +125,24 @@ const ScreenAccountSelection = ({
marginTop: StyleConstants.Spacing.M
}}
>
{instances.length
? instances
.slice()
.sort((a, b) =>
`${a.uri}${a.account.acct}`.localeCompare(`${b.uri}${b.account.acct}`)
{accounts &&
accounts
.slice()
.sort((a, b) => a.localeCompare(b))
.map((account, index) => {
return (
<AccountButton
key={index}
account={account}
additionalActions={() =>
navigationRef.navigate('Screen-Compose', {
type: 'share',
...share
})
}
/>
)
.map((instance, index) => {
return (
<AccountButton
key={index}
instance={instance}
additionalActions={() => {
navigationRef.navigate('Screen-Compose', {
type: 'share',
...share
})
}}
/>
)
})
: null}
})}
</View>
</View>
</ScrollView>

View File

@@ -1,12 +1,11 @@
import apiInstance from '@api/instance'
import { HeaderLeft } from '@components/Header'
import Icon from '@components/Icon'
import ComponentSeparator from '@components/Separator'
import CustomText from '@components/Text'
import HeaderSharedCreated from '@components/Timeline/Shared/HeaderShared/Created'
import { useAppDispatch } from '@root/store'
import apiInstance from '@utils/api/instance'
import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators'
import { getInstanceDrafts, removeInstanceDraft } from '@utils/slices/instancesSlice'
import { getAccountStorage, setAccountStorage, useAccountStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext, useEffect, useState } from 'react'
@@ -15,11 +14,19 @@ import { Dimensions, Modal, Platform, Pressable, View } from 'react-native'
import FastImage from 'react-native-fast-image'
import { PanGestureHandler } from 'react-native-gesture-handler'
import { SwipeListView } from 'react-native-swipe-list-view'
import { useSelector } from 'react-redux'
import ComposeContext from './utils/createContext'
import { formatText } from './utils/processText'
import { ComposeStateDraft, ExtendedAttachment } from './utils/types'
export const removeDraft = (timestamp: number) =>
setAccountStorage([
{
key: 'drafts',
value:
getAccountStorage.object('drafts')?.filter(draft => draft.timestamp !== timestamp) || []
}
])
const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-DraftsList'>> = ({
navigation,
route: {
@@ -39,11 +46,8 @@ const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-
}, [])
const { composeDispatch } = useContext(ComposeContext)
const instanceDrafts = useSelector(getInstanceDrafts)?.filter(
draft => draft.timestamp !== timestamp
)
const [drafts] = useAccountStorage.object('drafts')
const [checkingAttachments, setCheckingAttachments] = useState(false)
const dispatch = useAppDispatch()
const actionWidth = StyleConstants.Font.Size.L + StyleConstants.Spacing.Global.PagePadding * 4
@@ -72,7 +76,7 @@ const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-
</View>
<PanGestureHandler enabled={Platform.OS === 'ios'}>
<SwipeListView
data={instanceDrafts}
data={drafts.filter(draft => draft.timestamp !== timestamp)}
renderItem={({ item }: { item: ComposeStateDraft }) => {
return (
<Pressable
@@ -113,7 +117,7 @@ const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-
type: 'loadDraft',
payload: tempDraft
})
dispatch(removeInstanceDraft(item.timestamp))
removeDraft(item.timestamp)
navigation.goBack()
}}
>
@@ -172,7 +176,7 @@ const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-
justifyContent: 'flex-end',
backgroundColor: colors.red
}}
onPress={() => dispatch(removeInstanceDraft(item.timestamp))}
onPress={() => removeDraft(item.timestamp)}
children={
<View
style={{

View File

@@ -1,6 +1,6 @@
import apiInstance from '@api/instance'
import haptics from '@components/haptics'
import { HeaderLeft, HeaderRight } from '@components/Header'
import apiInstance from '@utils/api/instance'
import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators'
import React, { useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'

View File

@@ -1,6 +1,6 @@
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react'
import { Modal, View } from 'react-native'
import { useTheme } from '@utils/styles/ThemeManager'
import ComposeContext from './utils/createContext'
const ComposePosting = () => {

View File

@@ -1,15 +1,14 @@
import { emojis } from '@components/Emojis'
import EmojisContext from '@components/Emojis/helpers/EmojisContext'
import EmojisContext from '@components/Emojis/Context'
import Icon from '@components/Icon'
import { MAX_MEDIA_ATTACHMENTS } from '@components/mediaSelector'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { androidActionSheetStyles } from '@helpers/androidActionSheetStyles'
import { getInstanceConfigurationStatusMaxAttachments } from '@utils/slices/instancesSlice'
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Keyboard, Pressable, StyleSheet, View } from 'react-native'
import { useSelector } from 'react-redux'
import ComposeContext from '../utils/createContext'
import chooseAndUploadAttachment from './Footer/addAttachment'
@@ -18,10 +17,6 @@ const ComposeActions: React.FC = () => {
const { composeState, composeDispatch } = useContext(ComposeContext)
const { t } = useTranslation(['common', 'screenCompose'])
const { colors } = useTheme()
const instanceConfigurationStatusMaxAttachments = useSelector(
getInstanceConfigurationStatusMaxAttachments,
() => true
)
const attachmentColor = useMemo(() => {
if (composeState.poll.active) return colors.disabled
@@ -35,11 +30,8 @@ const ComposeActions: React.FC = () => {
const attachmentOnPress = () => {
if (composeState.poll.active) return
if (composeState.attachments.uploads.length < instanceConfigurationStatusMaxAttachments) {
return chooseAndUploadAttachment({
composeDispatch,
showActionSheetWithOptions
})
if (composeState.attachments.uploads.length < MAX_MEDIA_ATTACHMENTS) {
return chooseAndUploadAttachment({ composeDispatch, showActionSheetWithOptions })
}
}

View File

@@ -1,12 +1,11 @@
import Button from '@components/Button'
import { useNavigation } from '@react-navigation/native'
import { getInstanceDrafts } from '@utils/slices/instancesSlice'
import { useAccountStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
import React, { RefObject, useContext, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, View } from 'react-native'
import { useSelector } from 'react-redux'
import ComposeContext from '../utils/createContext'
export interface Props {
@@ -17,15 +16,14 @@ const ComposeDrafts: React.FC<Props> = ({ accessibleRefDrafts }) => {
const { t } = useTranslation('screenCompose')
const navigation = useNavigation<any>()
const { composeState } = useContext(ComposeContext)
const instanceDrafts = useSelector(getInstanceDrafts)?.filter(
draft => draft.timestamp !== composeState.timestamp
)
const [drafts] = useAccountStorage.object('drafts')
const draftsCount = drafts.filter(draft => draft.timestamp !== composeState.timestamp).length
useEffect(() => {
layoutAnimation()
}, [composeState.dirty])
if (!composeState.dirty && instanceDrafts?.length) {
if (!composeState.dirty && draftsCount) {
return (
<View
accessible
@@ -34,9 +32,7 @@ const ComposeDrafts: React.FC<Props> = ({ accessibleRefDrafts }) => {
children={
<Button
type='text'
content={t('content.root.drafts', {
count: instanceDrafts.length
})}
content={t('content.root.drafts', { count: draftsCount })}
onPress={() =>
navigation.navigate('Screen-Compose-DraftsList', {
timestamp: composeState.timestamp

View File

@@ -1,10 +1,10 @@
import Button from '@components/Button'
import haptics from '@components/haptics'
import Icon from '@components/Icon'
import { MAX_MEDIA_ATTACHMENTS } from '@components/mediaSelector'
import CustomText from '@components/Text'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { useNavigation } from '@react-navigation/native'
import { getInstanceConfigurationStatusMaxAttachments } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager'
@@ -13,7 +13,6 @@ import { useTranslation } from 'react-i18next'
import { FlatList, Pressable, StyleSheet, View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import FastImage from 'react-native-fast-image'
import { useSelector } from 'react-redux'
import ComposeContext from '../../utils/createContext'
import { ExtendedAttachment } from '../../utils/types'
import chooseAndUploadAttachment from './addAttachment'
@@ -31,8 +30,6 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
const { colors } = useTheme()
const navigation = useNavigation<any>()
const maxAttachments = useSelector(getInstanceConfigurationStatusMaxAttachments, () => true)
const flatListRef = useRef<FlatList>(null)
const sensitiveOnPress = useCallback(
@@ -285,7 +282,7 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
data={composeState.attachments.uploads}
keyExtractor={item => item.local?.uri || item.remote?.url || Math.random().toString()}
ListFooterComponent={
composeState.attachments.uploads.length < maxAttachments ? listFooter : null
composeState.attachments.uploads.length < MAX_MEDIA_ATTACHMENTS ? listFooter : null
}
/>
</View>

View File

@@ -3,14 +3,13 @@ import Icon from '@components/Icon'
import { MenuRow } from '@components/Menu'
import CustomText from '@components/Text'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { androidActionSheetStyles } from '@helpers/androidActionSheetStyles'
import { getInstanceConfigurationPoll } from '@utils/slices/instancesSlice'
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
import { useInstanceQuery } from '@utils/queryHooks/instance'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, TextInput, View } from 'react-native'
import { useSelector } from 'react-redux'
import ComposeContext from '../../utils/createContext'
const ComposePoll: React.FC = () => {
@@ -24,11 +23,11 @@ const ComposePoll: React.FC = () => {
const { t } = useTranslation(['common', 'screenCompose'])
const { colors, mode } = useTheme()
const instanceConfigurationPoll = useSelector(getInstanceConfigurationPoll, () => true)
const MAX_OPTIONS = instanceConfigurationPoll.max_options
const MAX_CHARS_PER_OPTION = instanceConfigurationPoll.max_characters_per_option
const MIN_EXPIRATION = instanceConfigurationPoll.min_expiration
const MAX_EXPIRATION = instanceConfigurationPoll.max_expiration
const { data } = useInstanceQuery()
const MAX_OPTIONS = data?.configuration?.polls.max_options || 4
const MAX_CHARS_PER_OPTION = data?.configuration?.polls.max_characters_per_option
const MIN_EXPIRATION = data?.configuration?.polls.min_expiration || 300
const MAX_EXPIRATION = data?.configuration?.polls.max_expiration || 2629746
const [firstRender, setFirstRender] = useState(true)
useEffect(() => {

View File

@@ -1,13 +1,13 @@
import mediaSelector from '@components/mediaSelector'
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
import apiInstance from '@utils/api/instance'
import * as Crypto from 'expo-crypto'
import * as VideoThumbnails from 'expo-video-thumbnails'
import i18next from 'i18next'
import { Dispatch } from 'react'
import { Alert } from 'react-native'
import { ComposeAction } from '../../utils/types'
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
import i18next from 'i18next'
import apiInstance from '@api/instance'
import mediaSelector from '@components/mediaSelector'
import { Asset } from 'react-native-image-picker'
import { ComposeAction } from '../../utils/types'
export interface Props {
composeDispatch: Dispatch<ComposeAction>

View File

@@ -1,36 +0,0 @@
import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import React, { useContext } from 'react'
import { StyleSheet, View } from 'react-native'
import { useSelector } from 'react-redux'
import ComposeContext from '../utils/createContext'
import ComposePostingAs from './Header/PostingAs'
import ComposeSpoilerInput from './Header/SpoilerInput'
import ComposeTextInput from './Header/TextInput'
const ComposeRootHeader: React.FC = () => {
const { composeState } = useContext(ComposeContext)
const instanceActive = useSelector(getInstanceActive)
const localInstances = useSelector(getInstances, (prev, next) => prev.length === next.length)
return (
<View>
{instanceActive !== -1 && localInstances.length > 1 ? (
<View style={styles.postingAs}>
<ComposePostingAs />
</View>
) : null}
{composeState.spoiler.active ? <ComposeSpoilerInput /> : null}
<ComposeTextInput />
</View>
)
}
const styles = StyleSheet.create({
postingAs: {
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
marginTop: StyleConstants.Spacing.S
}
})
export default ComposeRootHeader

View File

@@ -1,24 +1,32 @@
import CustomText from '@components/Text'
import { getInstanceAccount, getInstanceUri } from '@utils/slices/instancesSlice'
import { getAccountStorage, useGlobalStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { View } from 'react-native'
const ComposePostingAs = () => {
const accounts = useGlobalStorage.object('accounts')
if (!accounts.length) return null
const { t } = useTranslation('screenCompose')
const { colors } = useTheme()
const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev?.acct === next?.acct)
const instanceUri = useSelector(getInstanceUri)
return (
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
{t('content.root.header.postingAs', {
acct: instanceAccount?.acct,
domain: instanceUri
})}
</CustomText>
<View
style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
marginTop: StyleConstants.Spacing.S
}}
>
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
{t('content.root.header.postingAs', {
acct: getAccountStorage.string('auth.account.acct'),
domain: getAccountStorage.string('auth.domain')
})}
</CustomText>
</View>
)
}

View File

@@ -1,12 +1,11 @@
import CustomText from '@components/Text'
import { getSettingsFontsize } from '@utils/slices/settingsSlice'
import { useGlobalStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { adaptiveScale } from '@utils/styles/scaling'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { TextInput } from 'react-native'
import { useSelector } from 'react-redux'
import ComposeContext from '../../utils/createContext'
import { formatText } from '../../utils/processText'
@@ -15,7 +14,7 @@ const ComposeSpoilerInput: React.FC = () => {
const { t } = useTranslation('screenCompose')
const { colors, mode } = useTheme()
const adaptiveFontsize = useSelector(getSettingsFontsize)
const [adaptiveFontsize] = useGlobalStorage.number('app.font_size')
const adaptedFontsize = adaptiveScale(StyleConstants.Font.Size.M, adaptiveFontsize)
const adaptedLineheight = adaptiveScale(StyleConstants.Font.LineHeight.M, adaptiveFontsize)

View File

@@ -1,14 +1,13 @@
import { MAX_MEDIA_ATTACHMENTS } from '@components/mediaSelector'
import CustomText from '@components/Text'
import PasteInput, { PastedFile } from '@mattermost/react-native-paste-input'
import { getInstanceConfigurationStatusMaxAttachments } from '@utils/slices/instancesSlice'
import { getSettingsFontsize } from '@utils/slices/settingsSlice'
import { useGlobalStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { adaptiveScale } from '@utils/styles/scaling'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native'
import { useSelector } from 'react-redux'
import ComposeContext from '../../utils/createContext'
import { formatText } from '../../utils/processText'
import { uploadAttachment } from '../Footer/addAttachment'
@@ -18,9 +17,7 @@ const ComposeTextInput: React.FC = () => {
const { t } = useTranslation(['common', 'screenCompose'])
const { colors, mode } = useTheme()
const maxAttachments = useSelector(getInstanceConfigurationStatusMaxAttachments, () => true)
const adaptiveFontsize = useSelector(getSettingsFontsize)
const [adaptiveFontsize] = useGlobalStorage.number('app.font_size')
const adaptedFontsize = adaptiveScale(StyleConstants.Font.Size.M, adaptiveFontsize)
const adaptedLineheight = adaptiveScale(StyleConstants.Font.LineHeight.M, adaptiveFontsize)
@@ -72,7 +69,7 @@ const ComposeTextInput: React.FC = () => {
scrollEnabled={false}
disableCopyPaste={false}
onPaste={(error: string | null | undefined, files: PastedFile[]) => {
if (composeState.attachments.uploads.length + files.length > maxAttachments) {
if (composeState.attachments.uploads.length + files.length > MAX_MEDIA_ATTACHMENTS) {
Alert.alert(
t('screenCompose:content.root.header.textInput.keyboardImage.exceedMaximum.title'),
undefined,

View File

@@ -0,0 +1,20 @@
import React, { useContext } from 'react'
import { View } from 'react-native'
import ComposeContext from '../../utils/createContext'
import ComposePostingAs from './PostingAs'
import ComposeSpoilerInput from './SpoilerInput'
import ComposeTextInput from './TextInput'
const ComposeRootHeader: React.FC = () => {
const { composeState } = useContext(ComposeContext)
return (
<View>
<ComposePostingAs />
{composeState.spoiler.active ? <ComposeSpoilerInput /> : null}
<ComposeTextInput />
</View>
)
}
export default ComposeRootHeader

View File

@@ -5,26 +5,17 @@ import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext, useEffect, useMemo, useRef } from 'react'
import { AccessibilityInfo, findNodeHandle, FlatList, View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import ComposeActions from './Root/Actions'
import ComposePosting from './Posting'
import ComposeRootFooter from './Root/Footer'
import ComposeRootHeader from './Root/Header'
import ComposeRootSuggestion from './Root/Suggestion'
import ComposeContext from './utils/createContext'
import ComposeDrafts from './Root/Drafts'
import { useSelector } from 'react-redux'
import { getInstanceConfigurationStatusCharsURL } from '@utils/slices/instancesSlice'
export let instanceConfigurationStatusCharsURL = 23
import ComposePosting from '../Posting'
import ComposeContext from '../utils/createContext'
import ComposeActions from './Actions'
import ComposeDrafts from './Drafts'
import ComposeRootFooter from './Footer'
import ComposeRootHeader from './Header'
import ComposeRootSuggestion from './Suggestion'
const ComposeRoot = () => {
const { colors } = useTheme()
instanceConfigurationStatusCharsURL = useSelector(
getInstanceConfigurationStatusCharsURL,
() => true
)
const accessibleRefDrafts = useRef(null)
const accessibleRefAttachments = useRef(null)

View File

@@ -1,37 +1,37 @@
import { handleError } from '@api/helpers'
import { ComponentEmojis } from '@components/Emojis'
import { EmojisState } from '@components/Emojis/helpers/EmojisContext'
import { EmojisState } from '@components/Emojis/Context'
import haptics from '@components/haptics'
import { HeaderLeft, HeaderRight } from '@components/Header'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import haptics from '@root/components/haptics'
import { useAppDispatch } from '@root/store'
import ComposeRoot from '@screens/Compose/Root'
import { formatText } from '@screens/Compose/utils/processText'
import { useQueryClient } from '@tanstack/react-query'
import { handleError } from '@utils/api/helpers'
import { RootStackScreenProps } from '@utils/navigation/navigators'
import { useInstanceQuery } from '@utils/queryHooks/instance'
import { usePreferencesQuery } from '@utils/queryHooks/preferences'
import { useTimelineMutation } from '@utils/queryHooks/timeline'
import { updateStoreReview } from '@utils/slices/contextsSlice'
import {
getInstanceAccount,
getInstanceConfigurationStatusMaxChars,
removeInstanceDraft,
updateInstanceDraft
} from '@utils/slices/instancesSlice'
getAccountStorage,
getGlobalStorage,
setAccountStorage,
setGlobalStorage
} from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import * as StoreReview from 'expo-store-review'
import { filter } from 'lodash'
import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert, Keyboard, Platform } from 'react-native'
import { useQueryClient } from '@tanstack/react-query'
import { useSelector } from 'react-redux'
import ComposeDraftsList from './Compose/DraftsList'
import ComposeEditAttachment from './Compose/EditAttachment'
import { uploadAttachment } from './Compose/Root/Footer/addAttachment'
import ComposeContext from './Compose/utils/createContext'
import composeInitialState from './Compose/utils/initialState'
import composeParseState from './Compose/utils/parseState'
import composePost from './Compose/utils/post'
import composeReducer from './Compose/utils/reducer'
import ComposeDraftsList, { removeDraft } from './DraftsList'
import ComposeEditAttachment from './EditAttachment'
import { uploadAttachment } from './Root/Footer/addAttachment'
import ComposeContext from './utils/createContext'
import composeInitialState from './utils/initialState'
import composeParseState from './utils/parseState'
import composePost from './utils/post'
import composeReducer from './utils/reducer'
const Stack = createNativeStackNavigator()
@@ -54,12 +54,8 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
}
}, [])
const localAccount = useSelector(getInstanceAccount, (prev, next) =>
prev?.preferences && next?.preferences
? prev?.preferences['posting:default:visibility'] ===
next?.preferences['posting:default:visibility']
: true
)
const { data: preferences } = usePreferencesQuery()
const initialReducerState = useMemo(() => {
if (params) {
return composeParseState(params)
@@ -70,21 +66,19 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
attachments: {
...composeInitialState.attachments,
sensitive:
localAccount?.preferences && localAccount?.preferences['posting:default:sensitive']
? localAccount?.preferences['posting:default:sensitive']
preferences?.['posting:default:sensitive'] !== undefined
? preferences['posting:default:sensitive']
: false
},
visibility:
localAccount?.preferences && localAccount.preferences['posting:default:visibility']
? localAccount.preferences['posting:default:visibility']
: 'public'
visibility: preferences?.['posting:default:visibility'] || 'public'
}
}
}, [])
const [composeState, composeDispatch] = useReducer(composeReducer, initialReducerState)
const maxTootChars = useSelector(getInstanceConfigurationStatusMaxChars, () => true)
const { data: dataInstance } = useInstanceQuery()
const maxTootChars = dataInstance?.configuration?.statuses.max_characters || 500
const totalTextCount =
(composeState.spoiler.active ? composeState.spoiler.count : 0) + composeState.text.count
@@ -177,29 +171,35 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
}, [params?.type])
const saveDraft = () => {
dispatch(
updateInstanceDraft({
timestamp: composeState.timestamp,
spoiler: composeState.spoiler.raw,
text: composeState.text.raw,
poll: composeState.poll,
attachments: composeState.attachments,
visibility: composeState.visibility,
visibilityLock: composeState.visibilityLock,
replyToStatus: composeState.replyToStatus
})
const payload = {
timestamp: composeState.timestamp,
spoiler: composeState.spoiler.raw,
text: composeState.text.raw,
poll: composeState.poll,
attachments: composeState.attachments,
visibility: composeState.visibility,
visibilityLock: composeState.visibilityLock,
replyToStatus: composeState.replyToStatus
}
const currentDrafts = getAccountStorage.object('drafts') || []
const draftIndex = currentDrafts?.findIndex(
({ timestamp }) => timestamp === composeState.timestamp
)
if (draftIndex === -1) {
currentDrafts?.unshift(payload)
} else {
currentDrafts[draftIndex] = payload
}
setAccountStorage([{ key: 'drafts', value: currentDrafts }])
}
const removeDraft = useCallback(() => {
dispatch(removeInstanceDraft(composeState.timestamp))
}, [composeState.timestamp])
useEffect(() => {
const autoSave = composeState.dirty
? setInterval(() => {
saveDraft()
}, 1000)
: removeDraft()
return () => autoSave && clearInterval(autoSave)
: removeDraft(composeState.timestamp)
return () => (autoSave ? clearInterval(autoSave) : undefined)
}, [composeState])
const headerLeft = useCallback(
@@ -217,7 +217,7 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
text: t('screenCompose:heading.left.alert.buttons.delete'),
style: 'destructive',
onPress: () => {
removeDraft()
removeDraft(composeState.timestamp)
navigation.goBack()
}
},
@@ -239,7 +239,6 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
),
[composeState]
)
const dispatch = useAppDispatch()
const headerRightDisabled = useMemo(() => {
if (totalTextCount > maxTootChars) {
return true
@@ -277,7 +276,14 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
if (Platform.OS === 'ios' && Platform.constants.osVersion === '13.3') {
// https://github.com/tooot-app/app/issues/59
} else {
dispatch(updateStoreReview(1))
const currentCount = getGlobalStorage.number('app.count_till_store_review')
if (currentCount === 10) {
StoreReview?.isAvailableAsync()
.then(() => StoreReview.requestReview())
.catch(() => {})
} else {
setGlobalStorage('app.count_till_store_review', (currentCount || 0) + 1)
}
}
switch (params?.type) {
@@ -296,7 +302,7 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
}
break
}
removeDraft()
removeDraft(composeState.timestamp)
navigation.goBack()
})
.catch(error => {

View File

@@ -1,14 +1,12 @@
import { store } from '@root/store'
import { RootStackParamList } from '@utils/navigation/navigators'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { getAccountStorage } from '@utils/storage/actions'
import composeInitialState from './initialState'
import { ComposeState } from './types'
const assignVisibility = (
target: ComposeState['visibility']
): Pick<ComposeState, 'visibility' | 'visibilityLock'> => {
const accountPreference =
getInstanceAccount(store.getState())?.preferences?.['posting:default:visibility'] || 'public'
const preferences = getAccountStorage.object('preferences')
switch (target) {
case 'direct':
@@ -16,13 +14,13 @@ const assignVisibility = (
case 'private':
return { visibility: 'private', visibilityLock: false }
case 'unlisted':
if (accountPreference === 'private') {
if (preferences?.['posting:default:visibility'] === 'private') {
return { visibility: 'private', visibilityLock: false }
} else {
return { visibility: 'unlisted', visibilityLock: false }
}
case 'public':
switch (accountPreference) {
switch (preferences) {
case 'private':
return { visibility: 'private', visibilityLock: false }
case 'unlisted':

View File

@@ -1,6 +1,6 @@
import apiInstance from '@api/instance'
import detectLanguage from '@helpers/detectLanguage'
import { ComposeState } from '@screens/Compose/utils/types'
import apiInstance from '@utils/api/instance'
import detectLanguage from '@utils/helpers/detectLanguage'
import { RootStackParamList } from '@utils/navigation/navigators'
import * as Crypto from 'expo-crypto'
import { getPureContent } from './processText'

View File

@@ -1,11 +1,12 @@
import { emojis } from '@components/Emojis'
import CustomText from '@components/Text'
import queryClient from '@utils/queryHooks'
import { QueryKeyInstance } from '@utils/queryHooks/instance'
import { useTheme } from '@utils/styles/ThemeManager'
import LinkifyIt from 'linkify-it'
import { debounce, differenceWith, isEqual } from 'lodash'
import React, { Dispatch } from 'react'
import { useTheme } from '@utils/styles/ThemeManager'
import { ComposeAction, ComposeState } from './types'
import { instanceConfigurationStatusCharsURL } from '../Root'
import CustomText from '@components/Text'
import { emojis } from '@components/Emojis'
export interface Params {
textInput: ComposeState['textInputFocus']['current']
@@ -140,7 +141,11 @@ const formatText = ({ textInput, composeDispatch, content, disableDebounce = fal
contentLength = contentLength + main.length
break
default:
contentLength = contentLength + instanceConfigurationStatusCharsURL
const queryKeyInstance: QueryKeyInstance = ['Instance']
contentLength =
contentLength +
(queryClient.getQueryData<Mastodon.Instance<any>>(queryKeyInstance)?.configuration
?.statuses.characters_reserved_per_url || 23)
break
}
_content = next

View File

@@ -1,4 +1,4 @@
import { RefObject } from 'react';
import { RefObject } from 'react'
import { Asset } from 'react-native-image-picker'
export type ExtendedAttachment = {
@@ -67,65 +67,65 @@ export type ComposeState = {
export type ComposeAction =
| {
type: 'loadDraft'
payload: ComposeStateDraft
}
type: 'loadDraft'
payload: ComposeStateDraft
}
| {
type: 'dirty'
payload: ComposeState['dirty']
}
type: 'dirty'
payload: ComposeState['dirty']
}
| {
type: 'posting'
payload: ComposeState['posting']
}
type: 'posting'
payload: ComposeState['posting']
}
| {
type: 'spoiler'
payload: Partial<ComposeState['spoiler']>
}
type: 'spoiler'
payload: Partial<ComposeState['spoiler']>
}
| {
type: 'text'
payload: Partial<ComposeState['text']>
}
type: 'text'
payload: Partial<ComposeState['text']>
}
| {
type: 'tag'
payload: ComposeState['tag']
}
type: 'tag'
payload: ComposeState['tag']
}
| {
type: 'poll'
payload: Partial<ComposeState['poll']>
}
type: 'poll'
payload: Partial<ComposeState['poll']>
}
| {
type: 'attachments/sensitive'
payload: Pick<ComposeState['attachments'], 'sensitive'>
}
type: 'attachments/sensitive'
payload: Pick<ComposeState['attachments'], 'sensitive'>
}
| {
type: 'attachment/upload/start'
payload: Pick<ExtendedAttachment, 'local' | 'uploading'>
}
type: 'attachment/upload/start'
payload: Pick<ExtendedAttachment, 'local' | 'uploading'>
}
| {
type: 'attachment/upload/end'
payload: { remote: Mastodon.Attachment; local: Asset }
}
type: 'attachment/upload/end'
payload: { remote: Mastodon.Attachment; local: Asset }
}
| {
type: 'attachment/upload/fail'
payload: ExtendedAttachment['local']['hash']
}
type: 'attachment/upload/fail'
payload: ExtendedAttachment['local']['hash']
}
| {
type: 'attachment/delete'
payload: NonNullable<ExtendedAttachment['remote']>['id']
}
type: 'attachment/delete'
payload: NonNullable<ExtendedAttachment['remote']>['id']
}
| {
type: 'attachment/edit'
payload: ExtendedAttachment['remote']
}
type: 'attachment/edit'
payload: ExtendedAttachment['remote']
}
| {
type: 'visibility'
payload: ComposeState['visibility']
}
type: 'visibility'
payload: ComposeState['visibility']
}
| {
type: 'textInputFocus'
payload: Partial<ComposeState['textInputFocus']>
}
type: 'textInputFocus'
payload: Partial<ComposeState['textInputFocus']>
}
| {
type: 'removeReply'
}
type: 'removeReply'
}

View File

@@ -1,7 +1,7 @@
import GracefullyImage from '@components/GracefullyImage'
import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { androidActionSheetStyles } from '@helpers/androidActionSheetStyles'
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
import { RootStackScreenProps } from '@utils/navigation/navigators'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useState } from 'react'
@@ -18,9 +18,9 @@ import {
} from 'react-native'
import { Directions, Gesture, LongPressGestureHandler } from 'react-native-gesture-handler'
import { runOnJS, useSharedValue } from 'react-native-reanimated'
import { Zoom, createZoomListComponent } from 'react-native-reanimated-zoom'
import { createZoomListComponent, Zoom } from 'react-native-reanimated-zoom'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import saveImage from './ImageViewer/save'
import saveImage from './save'
const ZoomFlatList = createZoomListComponent(FlatList)

View File

@@ -8,13 +8,12 @@ import { TabLocalStackParamList } from '@utils/navigation/navigators'
import usePopToTop from '@utils/navigation/usePopToTop'
import { useListsQuery } from '@utils/queryHooks/lists'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getInstanceFollowingPage, updateInstanceFollowingPage } from '@utils/slices/instancesSlice'
import { setAccountStorage, useAccountStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { View } from 'react-native'
import { useDispatch, useSelector } from 'react-redux'
import * as DropdownMenu from 'zeego/dropdown-menu'
const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-Root'>> = ({
@@ -25,11 +24,10 @@ const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-R
const { data: lists } = useListsQuery()
const dispatch = useDispatch()
const instanceFollowingPage = useSelector(getInstanceFollowingPage)
const [pageLocal] = useAccountStorage.object('page_local')
const [queryKey, setQueryKey] = useState<QueryKeyTimeline>([
'Timeline',
{ page: 'Following', ...instanceFollowingPage }
{ page: 'Following', ...pageLocal }
])
useEffect(() => {
@@ -59,7 +57,7 @@ const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-R
: t('tabs.local.name')
}
/>
{page.page === 'Following' && !instanceFollowingPage.showBoosts ? (
{page.page === 'Following' && !pageLocal.showBoosts ? (
<Icon
name='Repeat'
size={StyleConstants.Font.Size.M}
@@ -68,7 +66,7 @@ const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-R
crossOut
/>
) : null}
{page.page === 'Following' && !instanceFollowingPage.showReplies ? (
{page.page === 'Following' && !pageLocal.showReplies ? (
<Icon
name='MessageCircle'
size={StyleConstants.Font.Size.M}
@@ -90,9 +88,7 @@ const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-R
<DropdownMenu.Group>
<DropdownMenu.Item
key='default'
onSelect={() =>
setQueryKey(['Timeline', { page: 'Following', ...instanceFollowingPage }])
}
onSelect={() => setQueryKey(['Timeline', { page: 'Following', ...pageLocal }])}
disabled={page.page === 'Following'}
>
<DropdownMenu.ItemTitle children={t('tabs.local.name')} />
@@ -100,19 +96,22 @@ const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-R
</DropdownMenu.Item>
<DropdownMenu.CheckboxItem
key='showBoosts'
value={instanceFollowingPage.showBoosts ? 'on' : 'off'}
value={pageLocal.showBoosts ? 'on' : 'off'}
onValueChange={() => {
setQueryKey([
'Timeline',
{
page: 'Following',
showBoosts: !instanceFollowingPage.showBoosts,
showReplies: instanceFollowingPage.showReplies
showBoosts: !pageLocal.showBoosts,
showReplies: pageLocal.showReplies
}
])
setAccountStorage([
{
key: 'page_local',
value: { ...pageLocal, showBoosts: !pageLocal.showBoosts }
}
])
dispatch(
updateInstanceFollowingPage({ showBoosts: !instanceFollowingPage.showBoosts })
)
}}
>
<DropdownMenu.ItemIndicator />
@@ -120,19 +119,22 @@ const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-R
</DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem
key='showReplies'
value={instanceFollowingPage.showReplies ? 'on' : 'off'}
value={pageLocal.showReplies ? 'on' : 'off'}
onValueChange={() => {
setQueryKey([
'Timeline',
{
page: 'Following',
showBoosts: instanceFollowingPage.showBoosts,
showReplies: !instanceFollowingPage.showReplies
showBoosts: pageLocal.showBoosts,
showReplies: !pageLocal.showReplies
}
])
setAccountStorage([
{
key: 'page_local',
value: { ...pageLocal, showReplies: !pageLocal.showReplies }
}
])
dispatch(
updateInstanceFollowingPage({ showReplies: !instanceFollowingPage.showReplies })
)
}}
>
<DropdownMenu.ItemTitle children={t('tabs.local.options.showReplies')} />
@@ -174,7 +176,7 @@ const Root: React.FC<NativeStackScreenProps<TabLocalStackParamList, 'Tab-Local-R
/>
)
})
}, [mode, queryKey[1], instanceFollowingPage, lists])
}, [mode, queryKey[1], pageLocal, lists])
usePopToTop()

View File

@@ -1,19 +1,19 @@
import { EmojisState } from '@components/Emojis/helpers/EmojisContext'
import { EmojisState } from '@components/Emojis/Context'
import haptics from '@components/haptics'
import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header'
import { HeaderLeft, HeaderRight } from '@components/Header'
import ComponentInput from '@components/Input'
import { displayMessage, Message } from '@components/Message'
import Selections from '@components/Selections'
import CustomText from '@components/Text'
import { CommonActions } from '@react-navigation/native'
import { useQueryClient } from '@tanstack/react-query'
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
import { QueryKeyLists, useListsMutation } from '@utils/queryHooks/lists'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert, Platform, ScrollView, TextInput } from 'react-native'
import { useQueryClient } from '@tanstack/react-query'
import { Alert, ScrollView, TextInput } from 'react-native'
const TabMeListEdit: React.FC<TabMeStackScreenProps<'Tab-Me-List-Edit'>> = ({
navigation,

View File

@@ -2,6 +2,7 @@ import Icon from '@components/Icon'
import { displayMessage } from '@components/Message'
import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default'
import { useQueryClient } from '@tanstack/react-query'
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
import { QueryKeyLists, useListsMutation } from '@utils/queryHooks/lists'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
@@ -9,7 +10,6 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useQueryClient } from '@tanstack/react-query'
import * as DropdownMenu from 'zeego/dropdown-menu'
import { menuListAccounts, menuListDelete, menuListEdit } from './menus'

View File

@@ -1,7 +1,7 @@
import navigationRef from '@helpers/navigationRef'
import { UseMutationResult } from '@tanstack/react-query'
import navigationRef from '@utils/navigation/navigationRef'
import i18next from 'i18next'
import { Alert } from 'react-native'
import { UseMutationResult } from '@tanstack/react-query'
export const menuListAccounts = ({ params }: { params: Mastodon.List }) => ({
key: 'list-accounts',

View File

@@ -1,5 +1,5 @@
import { ComponentEmojis } from '@components/Emojis'
import { EmojisState } from '@components/Emojis/helpers/EmojisContext'
import { EmojisState } from '@components/Emojis/Context'
import { HeaderLeft, HeaderRight } from '@components/Header'
import ComponentInput from '@components/Input'
import CustomText from '@components/Text'

View File

@@ -1,5 +1,5 @@
import { ComponentEmojis } from '@components/Emojis'
import { EmojisState } from '@components/Emojis/helpers/EmojisContext'
import { EmojisState } from '@components/Emojis/Context'
import { HeaderLeft, HeaderRight } from '@components/Header'
import ComponentInput from '@components/Input'
import { TabMeProfileStackScreenProps } from '@utils/navigation/navigators'

View File

@@ -1,5 +1,5 @@
import { ComponentEmojis } from '@components/Emojis'
import { EmojisState } from '@components/Emojis/helpers/EmojisContext'
import { EmojisState } from '@components/Emojis/Context'
import { HeaderLeft, HeaderRight } from '@components/Header'
import ComponentInput from '@components/Input'
import { TabMeProfileStackScreenProps } from '@utils/navigation/navigators'

View File

@@ -1,10 +1,10 @@
import { MenuContainer, MenuRow } from '@components/Menu'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { androidActionSheetStyles } from '@helpers/androidActionSheetStyles'
import { useAppDispatch } from '@root/store'
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
import queryClient from '@utils/queryHooks'
import { TabMeProfileStackScreenProps } from '@utils/navigation/navigators'
import { QueryKeyPreferences } from '@utils/queryHooks/preferences'
import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile'
import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject } from 'react'
import { useTranslation } from 'react-i18next'
@@ -24,7 +24,11 @@ const TabMeProfileRoot: React.FC<
const { data, isFetching } = useProfileQuery()
const { mutateAsync } = useProfileMutation()
const dispatch = useAppDispatch()
const refetchPreferences = () => {
const queryKeyPreferences: QueryKeyPreferences = ['Preferences']
queryClient.refetchQueries(queryKeyPreferences)
}
return (
<ScrollView>
@@ -117,7 +121,7 @@ const TabMeProfileRoot: React.FC<
},
type: 'source[privacy]',
data: indexVisibilityMapping[buttonIndex]
}).then(() => dispatch(updateAccountPreferences()))
}).then(() => refetchPreferences())
}
break
}
@@ -139,7 +143,7 @@ const TabMeProfileRoot: React.FC<
},
type: 'source[sensitive]',
data: data?.source.sensitive === undefined ? true : !data.source.sensitive
}).then(() => dispatch(updateAccountPreferences()))
}).then(() => refetchPreferences())
}
loading={isFetching}
/>

View File

@@ -1,4 +1,4 @@
import { HeaderCenter, HeaderLeft } from '@components/Header'
import { HeaderLeft } from '@components/Header'
import { Message } from '@components/Message'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { TabMeProfileStackParamList, TabMeStackScreenProps } from '@utils/navigation/navigators'
@@ -6,10 +6,10 @@ import React, { useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { KeyboardAvoidingView, Platform } from 'react-native'
import FlashMessage from 'react-native-flash-message'
import TabMeProfileFields from './Profile/Fields'
import TabMeProfileName from './Profile/Name'
import TabMeProfileNote from './Profile/Note'
import TabMeProfileRoot from './Profile/Root'
import TabMeProfileFields from './Fields'
import TabMeProfileName from './Name'
import TabMeProfileNote from './Note'
import TabMeProfileRoot from './Root'
const Stack = createNativeStackNavigator<TabMeProfileStackParamList>()

View File

@@ -1,40 +1,39 @@
import Button from '@components/Button'
import Icon from '@components/Icon'
import { MenuContainer, MenuRow } from '@components/Menu'
import { displayMessage } from '@components/Message'
import CustomText from '@components/Text'
import browserPackage from '@helpers/browserPackage'
import { useAppDispatch } from '@root/store'
import { isDevelopment } from '@utils/checkEnvironment'
import * as Sentry from '@sentry/react-native'
import apiInstance from '@utils/api/instance'
import apiTooot, { TOOOT_API_DOMAIN } from '@utils/api/tooot'
import browserPackage from '@utils/helpers/browserPackage'
import { isDevelopment } from '@utils/helpers/checkEnvironment'
import { PUSH_ADMIN, PUSH_DEFAULT, setChannels } from '@utils/push/constants'
import { updateExpoToken } from '@utils/push/updateExpoToken'
import { useAppsQuery } from '@utils/queryHooks/apps'
import { useProfileQuery } from '@utils/queryHooks/profile'
import { getExpoToken, retrieveExpoToken } from '@utils/slices/appSlice'
import { PUSH_ADMIN, PUSH_DEFAULT, usePushFeatures } from '@utils/slices/instances/push/utils'
import { updateInstancePush } from '@utils/slices/instances/updatePush'
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode'
import { getInstance, getInstancePush } from '@utils/slices/instancesSlice'
import { setAccountStorage, useAccountStorage, useGlobalStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager'
import * as Notifications from 'expo-notifications'
import * as WebBrowser from 'expo-web-browser'
import React, { useState, useEffect } from 'react'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { AppState, Linking, ScrollView, View } from 'react-native'
import { useSelector } from 'react-redux'
import { AppState, Linking, Platform, ScrollView, View } from 'react-native'
const TabMePush: React.FC = () => {
const { colors } = useTheme()
const { t } = useTranslation('screenTabs')
const instance = useSelector(getInstance)
const expoToken = useSelector(getExpoToken)
const [expoToken] = useGlobalStorage.string('app.expo_token')
const [push] = useAccountStorage.object('push')
const [domain] = useAccountStorage.string('auth.domain')
const [accountId] = useAccountStorage.string('auth.account.id')
const [accountAcct] = useAccountStorage.string('auth.account.acct')
const appsQuery = useAppsQuery()
const dispatch = useAppDispatch()
const instancePush = useSelector(getInstancePush)
const [pushAvailable, setPushAvailable] = useState<boolean>()
const [pushEnabled, setPushEnabled] = useState<boolean>()
const [pushCanAskAgain, setPushCanAskAgain] = useState<boolean>()
@@ -45,7 +44,7 @@ const TabMePush: React.FC = () => {
setPushEnabled(permissions.granted)
setPushCanAskAgain(permissions.canAskAgain)
layoutAnimation()
dispatch(retrieveExpoToken())
await updateExpoToken()
}
if (appsQuery.data?.vapid_key) {
@@ -54,7 +53,7 @@ const TabMePush: React.FC = () => {
if (isDevelopment) {
setPushAvailable(true)
} else {
setPushAvailable(!!expoToken)
setPushAvailable(!!expoToken?.length)
}
}
@@ -64,26 +63,29 @@ const TabMePush: React.FC = () => {
}
}, [appsQuery.data?.vapid_key])
const pushFeatures = usePushFeatures()
const alerts = () =>
instancePush?.alerts
? PUSH_DEFAULT(pushFeatures).map(alert => (
push?.alerts
? PUSH_DEFAULT.map(alert => (
<MenuRow
key={alert}
title={t(`me.push.${alert}.heading`)}
switchDisabled={!pushEnabled || !instancePush.global}
switchValue={instancePush?.alerts[alert]}
switchOnValueChange={() =>
dispatch(
updateInstancePushAlert({
alerts: {
...instancePush?.alerts,
[alert]: !instancePush?.alerts[alert]
}
})
)
}
switchDisabled={!pushEnabled || !push.global}
switchValue={push?.alerts[alert]}
switchOnValueChange={async () => {
const alerts = { ...push?.alerts, [alert]: !push?.alerts[alert] }
const formData = new FormData()
for (const [key, value] of Object.entries(alerts)) {
formData.append(`data[alerts][${key}]`, value.toString())
}
await apiInstance<Mastodon.PushSubscription>({
method: 'put',
url: 'push/subscription',
body: formData
})
setAccountStorage([{ key: 'push', value: { ...push, alerts } }])
}}
/>
))
: null
@@ -91,26 +93,34 @@ const TabMePush: React.FC = () => {
const profileQuery = useProfileQuery()
const adminAlerts = () =>
profileQuery.data?.role?.permissions
? PUSH_ADMIN(pushFeatures, profileQuery.data?.role?.permissions).map(({ type }) => (
? PUSH_ADMIN.map(({ type }) => (
<MenuRow
key={type}
title={t(`me.push.${type}.heading`)}
switchDisabled={!pushEnabled || !instancePush.global}
switchValue={instancePush?.alerts[type]}
switchOnValueChange={() =>
dispatch(
updateInstancePushAlert({
alerts: {
...instancePush?.alerts,
[type]: !instancePush?.alerts[type]
}
})
)
}
switchDisabled={!pushEnabled || !push.global}
switchValue={push?.alerts[type]}
switchOnValueChange={async () => {
const alerts = { ...push?.alerts, [type]: !push?.alerts[type] }
const formData = new FormData()
for (const [key, value] of Object.entries(alerts)) {
formData.append(`data[alerts][${key}]`, value.toString())
}
await apiInstance<Mastodon.PushSubscription>({
method: 'put',
url: 'push/subscription',
body: formData
})
setAccountStorage([{ key: 'push', value: { ...push, alerts } }])
}}
/>
))
: null
const pushPath = `${expoToken}/${domain}/${accountId}`
const accountFull = `@${accountAcct}@${domain}`
return (
<ScrollView>
{!!appsQuery.data?.vapid_key ? (
@@ -142,24 +152,103 @@ const TabMePush: React.FC = () => {
) : null}
<MenuContainer>
<MenuRow
title={t('me.push.global.heading', {
acct: `@${instance.account.acct}@${instance.uri}`
})}
title={t('me.push.global.heading', { acct: `@${accountAcct}@${domain}` })}
description={t('me.push.global.description')}
switchDisabled={!pushEnabled}
switchValue={pushEnabled === false ? false : instancePush?.global}
switchOnValueChange={() => dispatch(updateInstancePush(!instancePush?.global))}
switchValue={pushEnabled === false ? false : push?.global}
switchOnValueChange={async () => {
if (push.global) {
// Turning off
await apiInstance({
method: 'delete',
url: 'push/subscription'
})
await apiTooot({
method: 'delete',
url: `push/unsubscribe/${pushPath}`
})
if (Platform.OS === 'android') {
Notifications.deleteNotificationChannelGroupAsync(accountFull)
}
setAccountStorage([{ key: 'push', value: { ...push, global: false } }])
} else {
// Turning on
const randomPath = (Math.random() + 1).toString(36).substring(2)
const endpoint = `https://${TOOOT_API_DOMAIN}/push/send/${pushPath}/${randomPath}`
const formData = new FormData()
formData.append('subscription[endpoint]', endpoint)
formData.append(
'subscription[keys][p256dh]',
'BMn2PLpZrMefG981elzG6SB1EY9gU7QZwmtZ/a/J2vUeWG+zXgeskMPwHh4T/bxsD4l7/8QT94F57CbZqYRRfJo='
)
formData.append('subscription[keys][auth]', push.key)
for (const [key, value] of Object.entries(push.alerts)) {
formData.append(`data[alerts][${key}]`, value.toString())
}
const res = await apiInstance<Mastodon.PushSubscription>({
method: 'post',
url: 'push/subscription',
body: formData
})
if (!res.body.server_key?.length) {
displayMessage({
type: 'danger',
duration: 'long',
message: t('me.push.missingServerKey.message'),
description: t('me.push.missingServerKey.description')
})
Sentry.setContext('Push server key', {
instance: domain,
resBody: res.body
})
Sentry.captureMessage('Push register error')
return Promise.reject()
}
await apiTooot({
method: 'post',
url: `push/subscribe/${pushPath}`,
body: {
accountFull,
serverKey: res.body.server_key,
auth: push.decode === false ? null : push.key
}
})
if (Platform.OS === 'android') {
setChannels(true)
}
setAccountStorage([{ key: 'push', value: { ...push, global: true } }])
}
}}
/>
</MenuContainer>
<MenuContainer>
<MenuRow
title={t('me.push.decode.heading')}
description={t('me.push.decode.description')}
switchDisabled={!pushEnabled || !instancePush?.global}
switchValue={instancePush?.decode}
switchOnValueChange={() =>
dispatch(updateInstancePushDecode(!instancePush?.decode))
}
switchDisabled={!pushEnabled || !push?.global}
switchValue={push?.decode}
switchOnValueChange={async () => {
await apiTooot({
method: 'put',
url: `push/update-decode/${pushPath}`,
body: { auth: push?.decode ? null : push.key }
})
if (Platform.OS === 'android') {
setChannels(true)
}
setAccountStorage([{ key: 'push', value: { ...push, decode: !push.decode } }])
}}
/>
<MenuRow
title={t('me.push.howitworks')}

View File

@@ -1,58 +1,44 @@
import { MenuContainer, MenuRow } from '@components/Menu'
import { useNavigation } from '@react-navigation/native'
import { useAppDispatch } from '@root/store'
import { useAnnouncementQuery } from '@utils/queryHooks/announcement'
import { useListsQuery } from '@utils/queryHooks/lists'
import { useFollowedTagsQuery } from '@utils/queryHooks/tags'
import { getInstanceMePage, updateInstanceMePage } from '@utils/slices/instancesSlice'
import { getInstancePush } from '@utils/slices/instancesSlice'
import { useAccountStorage } from '@utils/storage/actions'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
const Collections: React.FC = () => {
const { t } = useTranslation(['screenAnnouncements', 'screenTabs'])
const navigation = useNavigation<any>()
const dispatch = useAppDispatch()
const mePage = useSelector(getInstanceMePage)
const [pageMe, setPageMe] = useAccountStorage.object('page_me')
useFollowedTagsQuery({
options: {
onSuccess: data =>
dispatch(
updateInstanceMePage({
followedTags: { shown: !!data?.pages?.[0].body?.length }
})
)
setPageMe({ ...pageMe, followedTags: { shown: !!data?.pages?.[0].body?.length } })
}
})
useListsQuery({
options: {
onSuccess: data =>
dispatch(
updateInstanceMePage({
lists: { shown: !!data?.length }
})
)
onSuccess: data => setPageMe({ ...pageMe, lists: { shown: !!data?.length } })
}
})
useAnnouncementQuery({
showAll: true,
options: {
onSuccess: data =>
dispatch(
updateInstanceMePage({
announcements: {
shown: !!data?.length ? true : false,
unread: data?.filter(announcement => !announcement.read).length
}
})
)
setPageMe({
...pageMe,
announcements: {
shown: !!data?.length ? true : false,
unread: data?.filter(announcement => !announcement.read).length
}
})
}
})
const instancePush = useSelector(getInstancePush, (prev, next) => prev?.global === next?.global)
const [instancePush] = useAccountStorage.object('push')
return (
<MenuContainer>
@@ -74,7 +60,7 @@ const Collections: React.FC = () => {
title={t('screenTabs:me.stacks.favourites.name')}
onPress={() => navigation.navigate('Tab-Me-Favourites')}
/>
{mePage.lists.shown ? (
{pageMe.lists.shown ? (
<MenuRow
iconFront='List'
iconBack='ChevronRight'
@@ -82,7 +68,7 @@ const Collections: React.FC = () => {
onPress={() => navigation.navigate('Tab-Me-List-List')}
/>
) : null}
{mePage.followedTags.shown ? (
{pageMe.followedTags.shown ? (
<MenuRow
iconFront='Hash'
iconBack='ChevronRight'
@@ -90,15 +76,15 @@ const Collections: React.FC = () => {
onPress={() => navigation.navigate('Tab-Me-FollowedTags')}
/>
) : null}
{mePage.announcements.shown ? (
{pageMe.announcements.shown ? (
<MenuRow
iconFront='Clipboard'
iconBack='ChevronRight'
title={t('screenAnnouncements:heading')}
content={
mePage.announcements.unread
pageMe.announcements.unread
? t('screenTabs:me.root.announcements.content.unread', {
amount: mePage.announcements.unread
amount: pageMe.announcements.unread
})
: t('screenTabs:me.root.announcements.content.read')
}

View File

@@ -1,20 +1,15 @@
import Button from '@components/Button'
import haptics from '@root/components/haptics'
import { useAppDispatch } from '@root/store'
import removeInstance from '@utils/slices/instances/remove'
import { getInstance } from '@utils/slices/instancesSlice'
import haptics from '@components/haptics'
import { removeAccount, useGlobalStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native'
import { useQueryClient } from '@tanstack/react-query'
import { useSelector } from 'react-redux'
const Logout: React.FC = () => {
const { t } = useTranslation(['common', 'screenTabs'])
const dispatch = useAppDispatch()
const queryClient = useQueryClient()
const instance = useSelector(getInstance)
const [accountActive] = useGlobalStorage.string('account.active')
return (
<Button
@@ -35,10 +30,9 @@ const Logout: React.FC = () => {
text: t('screenTabs:me.root.logout.alert.buttons.logout'),
style: 'destructive',
onPress: () => {
if (instance) {
haptics('Success')
queryClient.clear()
dispatch(removeInstance(instance))
if (accountActive) {
haptics('Light')
removeAccount(accountActive)
}
}
},

View File

@@ -1,17 +1,16 @@
import { MenuContainer, MenuRow } from '@components/Menu'
import browserPackage from '@helpers/browserPackage'
import { useNavigation } from '@react-navigation/native'
import { getInstanceActive, getInstanceUrl } from '@utils/slices/instancesSlice'
import browserPackage from '@utils/helpers/browserPackage'
import { getAccountStorage, useGlobalStorage } from '@utils/storage/actions'
import * as WebBrowser from 'expo-web-browser'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
const Settings: React.FC = () => {
const { t } = useTranslation('screenTabs')
const navigation = useNavigation<any>()
const instanceActive = useSelector(getInstanceActive)
const url = useSelector(getInstanceUrl)
const [accountActive] = useGlobalStorage.string('account.active')
return (
<MenuContainer>
@@ -21,14 +20,14 @@ const Settings: React.FC = () => {
title={t('me.stacks.settings.name')}
onPress={() => navigation.navigate('Tab-Me-Settings')}
/>
{instanceActive !== -1 ? (
{accountActive ? (
<MenuRow
iconFront='Sliders'
iconBack='ExternalLink'
title={t('me.stacks.webSettings.name')}
onPress={async () =>
WebBrowser.openAuthSessionAsync(
`https://${url}/settings/preferences`,
`https://${getAccountStorage.string('auth.domain')}/settings/preferences`,
'tooot://tooot',
{
...(await browserPackage()),

View File

@@ -10,16 +10,15 @@ import AccountContext from '@screens/Tabs/Shared/Account/utils/createContext'
import accountInitialState from '@screens/Tabs/Shared/Account/utils/initialState'
import accountReducer from '@screens/Tabs/Shared/Account/utils/reducer'
import { useProfileQuery } from '@utils/queryHooks/profile'
import { getInstanceActive } from '@utils/slices/instancesSlice'
import { useGlobalStorage } from '@utils/storage/actions'
import React, { useReducer, useRef } from 'react'
import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'
import { useSelector } from 'react-redux'
const TabMeRoot: React.FC = () => {
const instanceActive = useSelector(getInstanceActive)
const [accountActive] = useGlobalStorage.string('account.active')
const { data } = useProfileQuery({
options: { enabled: instanceActive !== -1, keepPreviousData: false }
options: { enabled: !!accountActive, keepPreviousData: false }
})
const scrollRef = useRef<Animated.ScrollView>(null)
@@ -34,18 +33,18 @@ const TabMeRoot: React.FC = () => {
return (
<AccountContext.Provider value={{ accountState, accountDispatch }}>
{instanceActive !== -1 && data ? <AccountNav scrollY={scrollY} account={data} /> : null}
{accountActive && data ? <AccountNav scrollY={scrollY} account={data} /> : null}
<Animated.ScrollView
ref={scrollRef}
keyboardShouldPersistTaps='handled'
onScroll={onScroll}
scrollEventThrottle={16}
>
{instanceActive !== -1 ? <MyInfo account={data} /> : <ComponentInstance />}
{instanceActive !== -1 ? <Collections /> : null}
{accountActive ? <MyInfo account={data} /> : <ComponentInstance />}
{accountActive ? <Collections /> : null}
<Settings />
{instanceActive !== -1 ? <AccountInformationSwitch /> : null}
{instanceActive !== -1 ? <Logout /> : null}
{accountActive ? <AccountInformationSwitch /> : null}
{accountActive ? <Logout /> : null}
</Animated.ScrollView>
</AccountContext.Provider>
)

View File

@@ -1,19 +1,16 @@
import { MenuContainer } from '@components/Menu'
import CustomText from '@components/Text'
import { getInstanceVersion } from '@utils/slices/instancesSlice'
import { getAccountStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import Constants from 'expo-constants'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
const SettingsAnalytics: React.FC = () => {
const { colors } = useTheme()
const { t } = useTranslation('screenTabs')
const instanceVersion = useSelector(getInstanceVersion, () => true)
return (
<>
<MenuContainer>
@@ -34,7 +31,7 @@ const SettingsAnalytics: React.FC = () => {
color: colors.secondary
}}
>
{t('me.settings.instanceVersion', { version: instanceVersion })}
{t('me.settings.instanceVersion', { version: getAccountStorage.string('version') })}
</CustomText>
</MenuContainer>
</>

View File

@@ -1,47 +1,34 @@
import haptics from '@components/haptics'
import { MenuContainer, MenuRow } from '@components/Menu'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { androidActionSheetStyles } from '@helpers/androidActionSheetStyles'
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
import { useNavigation } from '@react-navigation/native'
import { LOCALES } from '@root/i18n/locales'
import { useAppDispatch } from '@root/store'
import {
changeBrowser,
changeTheme,
getSettingsTheme,
getSettingsBrowser,
getSettingsFontsize,
getSettingsDarkTheme,
changeDarkTheme,
getSettingsAutoplayGifv,
changeAutoplayGifv
} from '@utils/slices/settingsSlice'
import { useGlobalStorage } from '@utils/storage/actions'
import { useTheme } from '@utils/styles/ThemeManager'
import * as Localization from 'expo-localization'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Linking, Platform } from 'react-native'
import { useSelector } from 'react-redux'
import { mapFontsizeToName } from '../SettingsFontsize'
import { LOCALES } from '@i18n/locales'
const SettingsApp: React.FC = () => {
const navigation = useNavigation<any>()
const dispatch = useAppDispatch()
const { showActionSheetWithOptions } = useActionSheet()
const { colors } = useTheme()
const { t, i18n } = useTranslation(['common', 'screenTabs'])
const settingsFontsize = useSelector(getSettingsFontsize)
const settingsTheme = useSelector(getSettingsTheme)
const settingsDarkTheme = useSelector(getSettingsDarkTheme)
const settingsBrowser = useSelector(getSettingsBrowser)
const settingsAutoplayGifv = useSelector(getSettingsAutoplayGifv)
const [fontSize] = useGlobalStorage.number('app.font_size')
const [theme, setTheme] = useGlobalStorage.string('app.theme')
const [themeDark, setThemeDark] = useGlobalStorage.string('app.theme.dark')
const [browser, setBrowser] = useGlobalStorage.string('app.browser')
const [autoplayGifv, setAutoplayGifv] = useGlobalStorage.boolean('app.auto_play_gifv')
return (
<MenuContainer>
<MenuRow
title={t('screenTabs:me.stacks.fontSize.name')}
content={t(`screenTabs:me.fontSize.sizes.${mapFontsizeToName(settingsFontsize)}`)}
content={t(`screenTabs:me.fontSize.sizes.${mapFontsizeToName(fontSize || 0)}`)}
iconBack='ChevronRight'
onPress={() => navigation.navigate('Tab-Me-Settings-Fontsize')}
/>
@@ -50,7 +37,9 @@ const SettingsApp: React.FC = () => {
content={
// @ts-ignore
LOCALES[
Platform.OS === 'ios' ? Localization.locale.toLowerCase() : i18n.language.toLowerCase()
Platform.OS === 'ios'
? Localization.locale.toLowerCase().replace(new RegExp(/.*-.*(-.*)/, 'i'), '')
: i18n.language.toLowerCase()
]
}
iconBack='ChevronRight'
@@ -62,7 +51,7 @@ const SettingsApp: React.FC = () => {
/>
<MenuRow
title={t('screenTabs:me.settings.theme.heading')}
content={t(`screenTabs:me.settings.theme.options.${settingsTheme}`)}
content={t(`screenTabs:me.settings.theme.options.${theme || 'auto'}`)}
iconBack='ChevronRight'
onPress={() =>
showActionSheetWithOptions(
@@ -80,16 +69,16 @@ const SettingsApp: React.FC = () => {
buttonIndex => {
switch (buttonIndex) {
case 0:
haptics('Success')
dispatch(changeTheme('auto'))
haptics('Light')
setTheme('auto')
break
case 1:
haptics('Success')
dispatch(changeTheme('light'))
haptics('Light')
setTheme('light')
break
case 2:
haptics('Success')
dispatch(changeTheme('dark'))
haptics('Light')
setTheme('dark')
break
}
}
@@ -98,7 +87,7 @@ const SettingsApp: React.FC = () => {
/>
<MenuRow
title={t('screenTabs:me.settings.darkTheme.heading')}
content={t(`screenTabs:me.settings.darkTheme.options.${settingsDarkTheme}`)}
content={t(`screenTabs:me.settings.darkTheme.options.${themeDark || 'lighter'}`)}
iconBack='ChevronRight'
onPress={() =>
showActionSheetWithOptions(
@@ -115,12 +104,12 @@ const SettingsApp: React.FC = () => {
buttonIndex => {
switch (buttonIndex) {
case 0:
haptics('Success')
dispatch(changeDarkTheme('lighter'))
haptics('Light')
setThemeDark('lighter')
break
case 1:
haptics('Success')
dispatch(changeDarkTheme('darker'))
haptics('Light')
setThemeDark('darker')
break
}
}
@@ -129,7 +118,7 @@ const SettingsApp: React.FC = () => {
/>
<MenuRow
title={t('screenTabs:me.settings.browser.heading')}
content={t(`screenTabs:me.settings.browser.options.${settingsBrowser}`)}
content={t(`screenTabs:me.settings.browser.options.${browser || 'internal'}`)}
iconBack='ChevronRight'
onPress={() =>
showActionSheetWithOptions(
@@ -146,12 +135,12 @@ const SettingsApp: React.FC = () => {
buttonIndex => {
switch (buttonIndex) {
case 0:
haptics('Success')
dispatch(changeBrowser('internal'))
haptics('Light')
setBrowser('internal')
break
case 1:
haptics('Success')
dispatch(changeBrowser('external'))
haptics('Light')
setBrowser('external')
break
}
}
@@ -160,8 +149,8 @@ const SettingsApp: React.FC = () => {
/>
<MenuRow
title={t('screenTabs:me.settings.autoplayGifv.heading')}
switchValue={settingsAutoplayGifv}
switchOnValueChange={() => dispatch(changeAutoplayGifv(!settingsAutoplayGifv))}
switchValue={autoplayGifv}
switchOnValueChange={() => setAutoplayGifv(!autoplayGifv)}
/>
</MenuContainer>
)

View File

@@ -1,64 +1,34 @@
import Button from '@components/Button'
import { MenuContainer, MenuRow } from '@components/Menu'
import { displayMessage } from '@components/Message'
import CustomText from '@components/Text'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { androidActionSheetStyles } from '@helpers/androidActionSheetStyles'
import { persistor } from '@root/store'
import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
import { storage } from '@utils/storage'
import { getGlobalStorage, useGlobalStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import * as Localization from 'expo-localization'
import React from 'react'
import { DevSettings } from 'react-native'
import { useSelector } from 'react-redux'
import { MMKV } from 'react-native-mmkv'
const SettingsDev: React.FC = () => {
const { colors } = useTheme()
const { showActionSheetWithOptions } = useActionSheet()
const instanceActive = useSelector(getInstanceActive)
const instances = useSelector(getInstances, () => true)
const [accounts] = useGlobalStorage.object('accounts')
const [account] = useGlobalStorage.string('account.active')
return (
<MenuContainer>
<CustomText
fontStyle='S'
selectable
style={{
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
color: colors.primaryDefault
}}
>
{JSON.stringify(Localization.locales)}
</CustomText>
<CustomText
fontStyle='S'
selectable
style={{
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
color: colors.primaryDefault
}}
>
{instances[instanceActive]?.token}
</CustomText>
<MenuRow
title={'Local active index'}
content={typeof instanceActive + ' - ' + instanceActive}
onPress={() => {}}
/>
<MenuRow title='Active account' content={account || '-'} onPress={() => {}} />
<MenuRow
title={'Saved local instances'}
content={instances.length.toString()}
content={accounts?.length.toString()}
iconBack='ChevronRight'
onPress={() =>
showActionSheetWithOptions(
{
options: instances
.map(instance => {
return instance.url + ': ' + instance.account.id
})
.concat(['Cancel']),
cancelButtonIndex: instances.length,
options: (accounts || []).concat(['Cancel']),
cancelButtonIndex: accounts?.length,
...androidActionSheetStyles(colors)
},
() => {}
@@ -76,14 +46,24 @@ const SettingsDev: React.FC = () => {
/>
<Button
type='text'
content={'Purge secure storage'}
content={'Purge MMKV'}
style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2,
marginBottom: StyleConstants.Spacing.Global.PagePadding
}}
destructive
onPress={() => {
persistor.purge().then(() => DevSettings.reload())
const accounts = getGlobalStorage.object('accounts')
if (!accounts) return
for (const account of accounts) {
console.log('Clearing', account)
const temp = new MMKV({ id: account })
temp.clearAll()
}
console.log('Clearing', 'global')
storage.global.clearAll()
}}
/>
<Button

View File

@@ -1,27 +1,24 @@
import Icon from '@components/Icon'
import { MenuContainer, MenuRow } from '@components/Menu'
import { useNavigation } from '@react-navigation/native'
import browserPackage from '@utils/helpers/browserPackage'
import { getAccountStorage, useGlobalStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import Constants from 'expo-constants'
import * as Linking from 'expo-linking'
import * as WebBrowser from 'expo-web-browser'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { getInstanceActive, getInstanceVersion } from '@utils/slices/instancesSlice'
import { Platform } from 'react-native'
import Constants from 'expo-constants'
import { getExpoToken } from '@utils/slices/appSlice'
import browserPackage from '@helpers/browserPackage'
const SettingsTooot: React.FC = () => {
const instanceActive = useSelector(getInstanceActive)
const navigation = useNavigation<any>()
const { colors } = useTheme()
const { t } = useTranslation('screenTabs')
const instanceVersion = useSelector(getInstanceVersion, () => true)
const expoToken = useSelector(getExpoToken)
const [accountActive] = useGlobalStorage.string('account.active')
const [expoToken] = useGlobalStorage.string('app.expo_token')
return (
<MenuContainer>
@@ -44,7 +41,7 @@ const SettingsTooot: React.FC = () => {
content={<Icon name='Mail' size={StyleConstants.Font.Size.M} color={colors.secondary} />}
iconBack='ChevronRight'
onPress={async () => {
if (instanceActive !== -1) {
if (accountActive) {
navigation.navigate('Screen-Compose', {
type: 'conversation',
accts: ['tooot@xmflsct.com'],
@@ -55,9 +52,9 @@ const SettingsTooot: React.FC = () => {
' - ' +
(Constants.expoConfig?.version ? `t/${Constants.expoConfig?.version}` : '') +
' - ' +
(instanceVersion ? `m/${instanceVersion}` : '') +
`m/${getAccountStorage.string('version')}` +
' - ' +
(expoToken
(expoToken?.length
? `e/${expoToken.replace(/^ExponentPushToken\[/, '').replace(/\]$/, '')}`
: '') +
']'

View File

@@ -1,10 +1,10 @@
import { isDevelopment } from '@utils/checkEnvironment'
import { isDevelopment } from '@utils/helpers/checkEnvironment'
import React from 'react'
import { ScrollView } from 'react-native-gesture-handler'
import SettingsAnalytics from './Settings/Analytics'
import SettingsApp from './Settings/App'
import SettingsDev from './Settings/Dev'
import SettingsTooot from './Settings/Tooot'
import SettingsAnalytics from './Analytics'
import SettingsApp from './App'
import SettingsDev from './Dev'
import SettingsTooot from './Tooot'
const TabMeSettings: React.FC = () => {
return (

View File

@@ -3,20 +3,18 @@ import haptics from '@components/haptics'
import ComponentSeparator from '@components/Separator'
import CustomText from '@components/Text'
import TimelineDefault from '@components/Timeline/Default'
import { useAppDispatch } from '@root/store'
import { SettingsLatest } from '@utils/migrations/settings/migration'
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
import { changeFontsize, getSettingsFontsize } from '@utils/slices/settingsSlice'
import { useGlobalStorage } from '@utils/storage/actions'
import { StorageGlobal } from '@utils/storage/global'
import { StyleConstants } from '@utils/styles/constants'
import { adaptiveScale } from '@utils/styles/scaling'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useMemo } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, View } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler'
import { useSelector } from 'react-redux'
export const mapFontsizeToName = (size: SettingsLatest['fontsize']) => {
export const mapFontsizeToName = (size: StorageGlobal['app.font_size']) => {
switch (size) {
case -1:
return 'S'
@@ -28,14 +26,16 @@ export const mapFontsizeToName = (size: SettingsLatest['fontsize']) => {
return 'XL'
case 3:
return 'XXL'
default:
return 'M'
}
}
const TabMeSettingsFontsize: React.FC<TabMeStackScreenProps<'Tab-Me-Settings-Fontsize'>> = () => {
const { colors, theme } = useTheme()
const { colors } = useTheme()
const { t } = useTranslation('screenTabs')
const initialSize = useSelector(getSettingsFontsize)
const dispatch = useAppDispatch()
const [fontSize, setFontSize] = useGlobalStorage.number('app.font_size')
const item = {
id: 'demo',
@@ -69,31 +69,6 @@ const TabMeSettingsFontsize: React.FC<TabMeStackScreenProps<'Tab-Me-Settings-Fon
mentions: []
}
const sizesDemo = useMemo(() => {
return (
<>
{([-1, 0, 1, 2, 3] as [-1, 0, 1, 2, 3]).map(size => (
<CustomText
key={size}
style={{
marginHorizontal: StyleConstants.Spacing.XS,
paddingHorizontal: StyleConstants.Spacing.XS,
marginBottom: StyleConstants.Spacing.M,
fontSize: adaptiveScale(StyleConstants.Font.Size.M, size),
lineHeight: adaptiveScale(StyleConstants.Font.LineHeight.M, size),
color: initialSize === size ? colors.primaryDefault : colors.secondary,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.border
}}
fontWeight={initialSize === size ? 'Bold' : undefined}
>
{t(`me.fontSize.sizes.${mapFontsizeToName(size)}`)}
</CustomText>
))}
</>
)
}, [theme, initialSize])
return (
<ScrollView>
<View
@@ -104,7 +79,24 @@ const TabMeSettingsFontsize: React.FC<TabMeStackScreenProps<'Tab-Me-Settings-Fon
marginTop: StyleConstants.Spacing.M
}}
>
{sizesDemo}
{([-1, 0, 1, 2, 3] as [-1, 0, 1, 2, 3]).map(size => (
<CustomText
key={size}
style={{
marginHorizontal: StyleConstants.Spacing.XS,
paddingHorizontal: StyleConstants.Spacing.XS,
marginBottom: StyleConstants.Spacing.M,
fontSize: adaptiveScale(StyleConstants.Font.Size.M, size),
lineHeight: adaptiveScale(StyleConstants.Font.LineHeight.M, size),
color: fontSize === size ? colors.primaryDefault : colors.secondary,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.border
}}
fontWeight={fontSize === size ? 'Bold' : undefined}
>
{t(`me.fontSize.sizes.${mapFontsizeToName(size)}`)}
</CustomText>
))}
</View>
<View
style={{
@@ -115,38 +107,34 @@ const TabMeSettingsFontsize: React.FC<TabMeStackScreenProps<'Tab-Me-Settings-Fon
>
<Button
onPress={() => {
if (initialSize > -1) {
if (fontSize && fontSize > -1) {
haptics('Light')
// @ts-ignore
dispatch(changeFontsize(initialSize - 1))
setFontSize(fontSize - 1)
}
}}
type='icon'
content='Minus'
round
disabled={initialSize <= -1}
disabled={(fontSize || 0) <= -1}
style={{ marginHorizontal: StyleConstants.Spacing.S }}
/>
<Button
onPress={() => {
if (initialSize < 3) {
if (fontSize && fontSize < 3) {
haptics('Light')
// @ts-ignore
dispatch(changeFontsize(initialSize + 1))
setFontSize(fontSize + 1)
}
}}
type='icon'
content='Plus'
round
disabled={initialSize >= 3}
disabled={(fontSize || 0) >= 3}
style={{ marginHorizontal: StyleConstants.Spacing.S }}
/>
</View>
<View
style={{
marginVertical: StyleConstants.Spacing.L
}}
>
<View style={{ marginVertical: StyleConstants.Spacing.L }}>
<ComponentSeparator
extraMarginLeft={-StyleConstants.Spacing.Global.PagePadding}
extraMarginRight={-StyleConstants.Spacing.Global.PagePadding}

View File

@@ -1,33 +1,32 @@
import haptics from '@components/haptics'
import { MenuRow } from '@components/Menu'
import { LOCALES } from '@root/i18n/locales'
import { LOCALES } from '@i18n/locales'
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
import { setChannels } from '@utils/slices/instances/push/utils'
import { getInstances } from '@utils/slices/instancesSlice'
import { changeLanguage } from '@utils/slices/settingsSlice'
import { setChannels } from '@utils/push/constants'
import { getGlobalStorage, useGlobalStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { FlatList, Platform } from 'react-native'
import { useDispatch, useSelector } from 'react-redux'
const TabMeSettingsLanguage: React.FC<TabMeStackScreenProps<'Tab-Me-Settings-Language'>> = ({
navigation
}) => {
const { i18n } = useTranslation('screenTabs')
const languages = Object.entries(LOCALES)
const instances = useSelector(getInstances)
const dispatch = useDispatch()
const [_, setLanguage] = useGlobalStorage.string('app.language')
const change = (lang: string) => {
haptics('Success')
dispatch(changeLanguage(lang))
setLanguage(lang)
i18n.changeLanguage(lang)
// Update Android notification channel language
if (Platform.OS === 'android') {
instances.forEach(instance => setChannels(instance, true))
const accounts = getGlobalStorage.object('accounts')
accounts?.forEach(account => setChannels(true, account))
}
navigation.pop(1)

View File

@@ -1,27 +1,23 @@
import AccountButton from '@components/AccountButton'
import ComponentInstance from '@components/Instance'
import CustomText from '@components/Text'
import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
import { getGlobalStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { KeyboardAvoidingView, Platform, StyleSheet, View } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler'
import { useSelector } from 'react-redux'
const TabMeSwitch: React.FC = () => {
const { t } = useTranslation('screenTabs')
const { colors } = useTheme()
const instances = useSelector(getInstances, () => true)
const instanceActive = useSelector(getInstanceActive, () => true)
const accounts = getGlobalStorage.object('accounts')
const accountActive = getGlobalStorage.string('account.active')
const scrollViewRef = useRef<ScrollView>(null)
useEffect(() => {
setTimeout(
() => scrollViewRef.current?.scrollToEnd({ animated: true }),
150
)
setTimeout(() => scrollViewRef.current?.scrollToEnd({ animated: true }), 150)
}, [scrollViewRef.current])
return (
@@ -45,11 +41,7 @@ const TabMeSwitch: React.FC = () => {
>
{t('me.switch.new')}
</CustomText>
<ComponentInstance
scrollViewRef={scrollViewRef}
disableHeaderImage
goBack
/>
<ComponentInstance scrollViewRef={scrollViewRef} disableHeaderImage goBack />
</View>
<View
@@ -79,29 +71,19 @@ const TabMeSwitch: React.FC = () => {
marginTop: StyleConstants.Spacing.M
}}
>
{instances.length
? instances
.slice()
.sort((a, b) =>
`${a.uri}${a.account.acct}`.localeCompare(
`${b.uri}${b.account.acct}`
)
{accounts &&
accounts
.slice()
.sort((a, b) => a.localeCompare(b))
.map((account, index) => {
return (
<AccountButton
key={index}
account={account}
selected={account === accountActive}
/>
)
.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>
</ScrollView>

View File

@@ -3,6 +3,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { TabMeStackParamList } from '@utils/navigation/navigators'
import React from 'react'
import { useTranslation } from 'react-i18next'
import TabShared from '../Shared'
import TabMeBookmarks from './Bookmarks'
import TabMeConversations from './Cconversations'
import TabMeFavourites from './Favourites'
@@ -18,7 +19,6 @@ import TabMeSettings from './Settings'
import TabMeSettingsFontsize from './SettingsFontsize'
import TabMeSettingsLanguage from './SettingsLanguage'
import TabMeSwitch from './Switch'
import TabShared from '../Shared'
const Stack = createNativeStackNavigator<TabMeStackParamList>()

View File

@@ -1,32 +1,22 @@
import { HeaderLeft, HeaderRight } from '@components/Header'
import { MenuContainer, MenuRow } from '@components/Menu'
import { useAppDispatch } from '@root/store'
import { useQueryClient } from '@tanstack/react-query'
import { TabNotificationsStackScreenProps } from '@utils/navigation/navigators'
import { useProfileQuery } from '@utils/queryHooks/profile'
import { PUSH_ADMIN, PUSH_DEFAULT } from '@utils/push/constants'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { PUSH_ADMIN, PUSH_DEFAULT, usePushFeatures } from '@utils/slices/instances/push/utils'
import {
getInstanceNotificationsFilter,
updateInstanceNotificationsFilter
} from '@utils/slices/instancesSlice'
import { setAccountStorage, useAccountStorage } from '@utils/storage/actions'
import { isEqual } from 'lodash'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler'
import { useSelector } from 'react-redux'
const TabNotificationsFilters: React.FC<
TabNotificationsStackScreenProps<'Tab-Notifications-Filters'>
> = ({ navigation }) => {
const { t } = useTranslation(['common', 'screenTabs'])
const pushFeatures = usePushFeatures()
const dispatch = useAppDispatch()
const instanceNotificationsFilter = useSelector(getInstanceNotificationsFilter)
const [instanceNotificationsFilter] = useAccountStorage.object('notifications')
const [filters, setFilters] = useState(instanceNotificationsFilter)
const queryClient = useQueryClient()
@@ -62,7 +52,7 @@ const TabNotificationsFilters: React.FC<
content={t('common:buttons.apply')}
onPress={() => {
if (changed) {
dispatch(updateInstanceNotificationsFilter(filters))
setAccountStorage([{ key: 'notifications', value: filters }])
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }]
queryClient.invalidateQueries({ queryKey })
}
@@ -73,12 +63,10 @@ const TabNotificationsFilters: React.FC<
})
}, [filters])
const profileQuery = useProfileQuery()
return (
<ScrollView style={{ flex: 1 }}>
<MenuContainer>
{PUSH_DEFAULT(pushFeatures).map((type, index) => (
{PUSH_DEFAULT.map((type, index) => (
<MenuRow
key={index}
title={t(`screenTabs:me.push.${type}.heading`)}
@@ -86,7 +74,7 @@ const TabNotificationsFilters: React.FC<
switchOnValueChange={() => setFilters({ ...filters, [type]: !filters[type] })}
/>
))}
{PUSH_ADMIN(pushFeatures, profileQuery.data?.role?.permissions).map(({ type }) => (
{PUSH_ADMIN.map(({ type }) => (
<MenuRow
key={type}
title={t(`screenTabs:me.push.${type}.heading`)}

View File

@@ -3,18 +3,16 @@ import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default'
import SegmentedControl from '@react-native-community/segmented-control'
import { NativeStackScreenProps } from '@react-navigation/native-stack'
import { useAppDispatch } from '@root/store'
import { ContextsLatest } from '@utils/migrations/contexts/migration'
import { TabPublicStackParamList } from '@utils/navigation/navigators'
import usePopToTop from '@utils/navigation/usePopToTop'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getPreviousSegment, updatePreviousSegment } from '@utils/slices/contextsSlice'
import { getGlobalStorage, setGlobalStorage } from '@utils/storage/actions'
import { StorageGlobal } from '@utils/storage/global'
import { useTheme } from '@utils/styles/ThemeManager'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Dimensions } from 'react-native'
import { SceneMap, TabView } from 'react-native-tab-view'
import { useSelector } from 'react-redux'
const Route = ({ route: { key: page } }: { route: any }) => {
const queryKey: QueryKeyTimeline = ['Timeline', { page }]
@@ -41,9 +39,8 @@ const Root: React.FC<NativeStackScreenProps<TabPublicStackParamList, 'Tab-Public
const { mode } = useTheme()
const { t } = useTranslation('screenTabs')
const dispatch = useAppDispatch()
const previousSegment = useSelector(getPreviousSegment, () => true)
const segments: ContextsLatest['previousSegment'][] = ['Local', 'LocalPublic', 'Trending']
const previousSegment = getGlobalStorage.string('app.prev_public_segment')
const segments: StorageGlobal['app.prev_public_segment'][] = ['Local', 'LocalPublic', 'Trending']
const [segment, setSegment] = useState<number>(
segments.findIndex(segment => segment === previousSegment)
)
@@ -62,7 +59,7 @@ const Root: React.FC<NativeStackScreenProps<TabPublicStackParamList, 'Tab-Public
selectedIndex={segment}
onChange={({ nativeEvent }) => {
setSegment(nativeEvent.selectedSegmentIndex)
dispatch(updatePreviousSegment(segments[nativeEvent.selectedSegmentIndex]))
setGlobalStorage('app.prev_public_segment', segments[nativeEvent.selectedSegmentIndex])
}}
style={{ flexBasis: '65%' }}
/>

View File

@@ -1,10 +1,9 @@
import GracefullyImage from '@components/GracefullyImage'
import navigationRef from '@helpers/navigationRef'
import { getInstanceActive } from '@utils/slices/instancesSlice'
import navigationRef from '@utils/navigation/navigationRef'
import { useGlobalStorage } from '@utils/storage/actions'
import React from 'react'
import { Dimensions, Image } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useSelector } from 'react-redux'
export interface Props {
account?: Mastodon.Account
@@ -13,7 +12,7 @@ export interface Props {
const AccountHeader: React.FC<Props> = ({ account }) => {
const topInset = useSafeAreaInsets().top
useSelector(getInstanceActive)
useGlobalStorage.string('account.active')
return (
<GracefullyImage

View File

@@ -3,7 +3,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { StyleSheet, View } from 'react-native'
import { Placeholder, Fade } from 'rn-placeholder'
import { Fade, Placeholder } from 'rn-placeholder'
import AccountInformationAccount from './Information/Account'
import AccountInformationActions from './Information/Actions'
import AccountInformationAvatar from './Information/Avatar'
@@ -37,7 +37,7 @@ const AccountInformation: React.FC<Props> = ({ account }) => {
<AccountInformationName account={account} />
<AccountInformationAccount account={account} />
<AccountInformationAccount account={account} myInfo={myInfo} />
<AccountInformationFields account={account} myInfo={myInfo} />

View File

@@ -1,35 +1,34 @@
import Icon from '@components/Icon'
import CustomText from '@components/Text'
import { useRelationshipQuery } from '@utils/queryHooks/relationship'
import { getInstanceAccount, getInstanceUri } from '@utils/slices/instancesSlice'
import { getAccountStorage, useAccountStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { View } from 'react-native'
import { useSelector } from 'react-redux'
import { PlaceholderLine } from 'rn-placeholder'
export interface Props {
account: Mastodon.Account | undefined
myInfo?: boolean
}
const AccountInformationAccount: React.FC<Props> = ({ account }) => {
const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => {
const { t } = useTranslation('screenTabs')
const { colors } = useTheme()
const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev?.acct === next?.acct)
const instanceUri = useSelector(getInstanceUri)
const [acct] = useAccountStorage.string('auth.account.acct')
const domain = getAccountStorage.string('auth.domain')
const { data: relationship } = useRelationshipQuery({
id: account?.id || '',
options: { enabled: account !== undefined }
})
const localInstance = account?.acct.includes('@')
? account?.acct.includes(`@${instanceUri}`)
: true
const localInstance = account?.acct.includes('@') ? account?.acct.includes(`@${domain}`) : true
if (account || (localInstance && instanceAccount)) {
if (account || localInstance) {
return (
<View
style={{
@@ -52,8 +51,8 @@ const AccountInformationAccount: React.FC<Props> = ({ account }) => {
}}
selectable
>
@{account?.acct}
{localInstance ? `@${instanceUri}` : null}
@{myInfo ? acct : account?.acct}
{localInstance ? `@${domain}` : null}
</CustomText>
{relationship?.followed_by ? t('shared.account.followed_by') : null}
</CustomText>

View File

@@ -3,12 +3,11 @@ import menuAt from '@components/contextMenu/at'
import { RelationshipOutgoing } from '@components/Relationship'
import { useNavigation } from '@react-navigation/native'
import { useRelationshipQuery } from '@utils/queryHooks/relationship'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { useAccountStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, View } from 'react-native'
import { useSelector } from 'react-redux'
import * as DropdownMenu from 'zeego/dropdown-menu'
export interface Props {
@@ -50,8 +49,8 @@ const AccountInformationActions: React.FC<Props> = ({ account, myInfo }) => {
)
}
const instanceAccount = useSelector(getInstanceAccount, () => true)
const ownAccount = account?.id === instanceAccount?.id && account?.acct === instanceAccount?.acct
const [accountId] = useAccountStorage.string('auth.account.id')
const ownAccount = account?.id === accountId
const query = useRelationshipQuery({ id: account.id })
const mAt = menuAt({ account })

View File

@@ -1,12 +1,11 @@
import GracefullyImage from '@components/GracefullyImage'
import navigationRef from '@helpers/navigationRef'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import navigationRef from '@utils/navigation/navigationRef'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { getInstanceActive } from '@utils/slices/instancesSlice'
import { useGlobalStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import React from 'react'
import { useSelector } from 'react-redux'
export interface Props {
account: Mastodon.Account | undefined
@@ -15,7 +14,9 @@ export interface Props {
const AccountInformationAvatar: React.FC<Props> = ({ account, myInfo }) => {
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
useSelector(getInstanceActive)
useGlobalStorage.string('account.active')
return (
<GracefullyImage
key={account?.avatar}

View File

@@ -1,9 +1,9 @@
import CustomText from '@components/Text'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { StyleConstants } from '@root/utils/styles/constants'
import { useTheme } from '@root/utils/styles/ThemeManager'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, View } from 'react-native'

View File

@@ -15,10 +15,10 @@ import { useTranslation } from 'react-i18next'
import { Text, View } from 'react-native'
import { useSharedValue } from 'react-native-reanimated'
import * as DropdownMenu from 'zeego/dropdown-menu'
import AccountAttachments from './Account/Attachments'
import AccountHeader from './Account/Header'
import AccountInformation from './Account/Information'
import AccountNav from './Account/Nav'
import AccountAttachments from './Attachments'
import AccountHeader from './Header'
import AccountInformation from './Information'
import AccountNav from './Nav'
const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>> = ({
navigation,

View File

@@ -4,13 +4,12 @@ import { displayMessage } from '@components/Message'
import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default'
import { useQueryClient } from '@tanstack/react-query'
import { featureCheck } from '@utils/helpers/featureCheck'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { QueryKeyFollowedTags, useTagsMutation, useTagsQuery } from '@utils/queryHooks/tags'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { checkInstanceFeature } from '@utils/slices/instancesSlice'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
const TabSharedHashtag: React.FC<TabSharedStackScreenProps<'Tab-Shared-Hashtag'>> = ({
navigation,
@@ -29,7 +28,7 @@ const TabSharedHashtag: React.FC<TabSharedStackScreenProps<'Tab-Shared-Hashtag'>
const { t } = useTranslation(['common', 'screenTabs'])
const canFollowTags = useSelector(checkInstanceFeature('follow_tags'))
const canFollowTags = featureCheck('follow_tags')
const { data, isFetching, refetch } = useTagsQuery({
tag: hashtag,
options: { enabled: canFollowTags }

View File

@@ -6,7 +6,7 @@ import CustomText from '@components/Text'
import TimelineAttachment from '@components/Timeline/Shared/Attachment'
import StatusContext from '@components/Timeline/Shared/Context'
import HeaderSharedCreated from '@components/Timeline/Shared/HeaderShared/Created'
import removeHTML from '@helpers/removeHTML'
import removeHTML from '@utils/helpers/removeHTML'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { useStatusHistory } from '@utils/queryHooks/statusesHistory'
import { StyleConstants } from '@utils/styles/constants'

View File

@@ -1,18 +1,17 @@
import apiInstance from '@api/instance'
import ComponentAccount from '@components/Account'
import { HeaderLeft, HeaderRight } from '@components/Header'
import Selections from '@components/Selections'
import CustomText from '@components/Text'
import apiInstance from '@utils/api/instance'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { useRulesQuery } from '@utils/queryHooks/reports'
import { getInstanceUri } from '@utils/slices/instancesSlice'
import { getAccountStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Platform, ScrollView, TextInput, View } from 'react-native'
import { Switch } from 'react-native-gesture-handler'
import { useSelector } from 'react-redux'
const TabSharedReport: React.FC<TabSharedStackScreenProps<'Tab-Shared-Report'>> = ({
navigation,
@@ -74,9 +73,8 @@ const TabSharedReport: React.FC<TabSharedStackScreenProps<'Tab-Shared-Report'>>
})
}, [isReporting, comment, forward, categories, rules])
const instanceUri = useSelector(getInstanceUri)
const localInstance = account?.acct.includes('@')
? account?.acct.includes(`@${instanceUri}`)
? account?.acct.includes(`@${getAccountStorage.string('auth.domain')}`)
: true
const rulesQuery = useRulesQuery()

View File

@@ -1,14 +1,14 @@
import { HeaderLeft } from '@components/Header'
import ComponentSeparator from '@components/Separator'
import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default'
import { InfiniteQueryObserver, useQueryClient } from '@tanstack/react-query'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FlatList, View } from 'react-native'
import { InfiniteQueryObserver, useQueryClient } from '@tanstack/react-query'
import ComponentSeparator from '@components/Separator'
import { StyleConstants } from '@utils/styles/constants'
const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
navigation,
@@ -132,7 +132,9 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
}
return (
<View style={{ marginLeft: Math.max(0, levels.current - 1) * StyleConstants.Spacing.S }}>
<View
style={{ marginLeft: Math.max(0, levels.current - 1) * StyleConstants.Spacing.S }}
>
<TimelineDefault
item={item}
queryKey={queryKey}

View File

@@ -1,9 +1,9 @@
import apiInstance from '@api/instance'
import ComponentAccount from '@components/Account'
import { HeaderLeft } from '@components/Header'
import Icon from '@components/Icon'
import ComponentSeparator from '@components/Separator'
import CustomText from '@components/Text'
import apiInstance from '@utils/api/instance'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { SearchResult } from '@utils/queryHooks/search'
import { QueryKeyUsers, useUsersQuery } from '@utils/queryHooks/users'

View File

@@ -3,27 +3,22 @@ import haptics from '@components/haptics'
import Icon from '@components/Icon'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import { RootStackScreenProps, ScreenTabsStackParamList } from '@utils/navigation/navigators'
import { getPreviousTab } from '@utils/slices/contextsSlice'
import { getInstanceAccount, getInstanceActive } from '@utils/slices/instancesSlice'
import { getGlobalStorage, useAccountStorage, useGlobalStorage } from '@utils/storage/actions'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useMemo } from 'react'
import { Platform } from 'react-native'
import { useSelector } from 'react-redux'
import TabLocal from './Tabs/Local'
import TabMe from './Tabs/Me'
import TabNotifications from './Tabs/Notifications'
import TabPublic from './Tabs/Public'
import TabLocal from './Local'
import TabMe from './Me'
import TabNotifications from './Notifications'
import TabPublic from './Public'
const Tab = createBottomTabNavigator<ScreenTabsStackParamList>()
const ScreenTabs = ({ navigation }: RootStackScreenProps<'Screen-Tabs'>) => {
const { colors } = useTheme()
const instanceActive = useSelector(getInstanceActive)
const instanceAccount = useSelector(
getInstanceAccount,
(prev, next) => prev?.avatarStatic === next?.avatarStatic
)
const [accountActive] = useGlobalStorage.string('account.active')
const [avatarStatic] = useAccountStorage.string('auth.account.avatar_static')
const composeListeners = useMemo(
() => ({
@@ -50,18 +45,16 @@ const ScreenTabs = ({ navigation }: RootStackScreenProps<'Screen-Tabs'>) => {
[]
)
const previousTab = useSelector(getPreviousTab, () => true)
return (
<Tab.Navigator
initialRouteName={instanceActive !== -1 ? previousTab : 'Tab-Me'}
initialRouteName={accountActive ? getGlobalStorage.string('app.prev_tab') : 'Tab-Me'}
screenOptions={({ route }) => ({
headerShown: false,
tabBarActiveTintColor: colors.primaryDefault,
tabBarInactiveTintColor: colors.secondary,
tabBarShowLabel: false,
...(Platform.OS === 'android' && { tabBarHideOnKeyboard: true }),
tabBarStyle: { display: instanceActive !== -1 ? 'flex' : 'none' },
tabBarStyle: { display: accountActive ? 'flex' : 'none' },
tabBarIcon: ({
focused,
color,
@@ -83,8 +76,7 @@ const ScreenTabs = ({ navigation }: RootStackScreenProps<'Screen-Tabs'>) => {
case 'Tab-Me':
return (
<GracefullyImage
key={instanceAccount?.avatarStatic}
uri={{ original: instanceAccount?.avatarStatic }}
uri={{ original: avatarStatic }}
dimension={{
width: size,
height: size

326
src/screens/index.tsx Normal file
View File

@@ -0,0 +1,326 @@
import { HeaderLeft } from '@components/Header'
import { displayMessage, Message } from '@components/Message'
import { NavigationContainer } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import ScreenAccountSelection from '@screens/AccountSelection'
import ScreenActions from '@screens/Actions'
import ScreenAnnouncements from '@screens/Announcements'
import ScreenCompose from '@screens/Compose'
import ScreenImagesViewer from '@screens/ImageViewer'
import ScreenTabs from '@screens/Tabs'
import navigationRef from '@utils/navigation/navigationRef'
import { RootStackParamList } from '@utils/navigation/navigators'
import pushUseConnect from '@utils/push/useConnect'
import pushUseReceive from '@utils/push/useReceive'
import pushUseRespond from '@utils/push/useRespond'
import { useEmojisQuery } from '@utils/queryHooks/emojis'
import { useFiltersQuery } from '@utils/queryHooks/filters'
import { useInstanceQuery } from '@utils/queryHooks/instance'
import { usePreferencesQuery } from '@utils/queryHooks/preferences'
import { useProfileQuery } from '@utils/queryHooks/profile'
import { setAccount, setGlobalStorage, useGlobalStorage } from '@utils/storage/actions'
import { useTheme } from '@utils/styles/ThemeManager'
import { themes } from '@utils/styles/themes'
import * as Linking from 'expo-linking'
import { addScreenshotListener } from 'expo-screen-capture'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { IntlProvider } from 'react-intl'
import { Alert, Platform, StatusBar } from 'react-native'
import ShareMenu from 'react-native-share-menu'
import { routingInstrumentation } from '../utils/startup/sentry'
const Stack = createNativeStackNavigator<RootStackParamList>()
export interface Props {
localCorrupt?: string
}
const Screens: React.FC<Props> = ({ localCorrupt }) => {
const { t, i18n } = useTranslation([
'common',
'screens',
'screenAnnouncements',
'screenAccountSelection'
])
const [accounts] = useGlobalStorage.object('accounts')
const [accountActive] = useGlobalStorage.string('account.active')
const { colors, theme } = useTheme()
// Push hooks
pushUseConnect()
pushUseReceive()
pushUseRespond()
// Prevent screenshot alert
useEffect(() => {
const screenshotListener = addScreenshotListener(() =>
Alert.alert(t('screens:screenshot.title'), t('screens:screenshot.message'), [
{ text: t('common:buttons.confirm'), style: 'destructive' }
])
)
Platform.select({ ios: screenshotListener })
return () => screenshotListener.remove()
}, [])
// On launch display login credentials corrupt information
useEffect(() => {
const showLocalCorrect = () => {
if (localCorrupt) {
displayMessage({
message: t('screens:localCorrupt.message'),
description: localCorrupt.length ? localCorrupt : undefined,
type: 'danger'
})
// @ts-ignore
navigationRef.navigate('Screen-Tabs', {
screen: 'Tab-Me'
})
}
}
return showLocalCorrect()
}, [localCorrupt])
// Lazily update users's preferences, for e.g. composing default visibility
useInstanceQuery({ options: { enabled: !!accountActive } })
useProfileQuery({ options: { enabled: !!accountActive } })
usePreferencesQuery({ options: { enabled: !!accountActive } })
useFiltersQuery({ options: { enabled: !!accountActive } })
useEmojisQuery({ options: { enabled: !!accountActive } })
// Callbacks
const navigationContainerOnStateChange = useCallback(() => {
const currentRoute = navigationRef.getCurrentRoute()
const matchTabName = currentRoute?.name?.match(/(Tab-.*)-Root/)
if (matchTabName?.[1]) {
// @ts-ignore
setGlobalStorage('app.prev_tab', matchTabName[1])
}
}, [])
// Deep linking for compose
const [deeplinked, setDeeplinked] = useState(false)
useEffect(() => {
const getUrlAsync = async () => {
setDeeplinked(true)
const initialUrl = await Linking.parseInitialURLAsync()
if (initialUrl.path) {
const paths = initialUrl.path.split('/')
if (paths.length) {
if (accountActive && !accounts?.includes(accountActive)) {
setAccount(accountActive)
}
}
}
if (initialUrl.hostname === 'compose') {
navigationRef.navigate('Screen-Compose')
}
}
if (!deeplinked) {
getUrlAsync()
}
}, [accounts, accountActive, deeplinked])
// Share Extension
const handleShare = useCallback(
(
item?:
| {
data: { mimeType: string; data: string }[]
mimeType: undefined
}
| { data: string | string[]; mimeType: string }
) => {
if (!accountActive) {
return
}
if (!item || !item.data) {
return
}
let text: string | undefined = undefined
let media: { uri: string; mime: string }[] = []
const typesImage = ['png', 'jpg', 'jpeg', 'gif']
const typesVideo = ['mp4', 'm4v', 'mov', 'webm', 'mpeg']
const filterMedia = ({ uri, mime }: { uri: string; mime: string }) => {
if (mime.startsWith('image/')) {
if (!typesImage.includes(mime.split('/')[1])) {
console.warn('Image type not supported:', mime.split('/')[1])
displayMessage({
message: t('screens:shareError.imageNotSupported', {
type: mime.split('/')[1]
}),
type: 'danger'
})
return
}
media.push({ uri, mime })
} else if (mime.startsWith('video/')) {
if (!typesVideo.includes(mime.split('/')[1])) {
console.warn('Video type not supported:', mime.split('/')[1])
displayMessage({
message: t('screens:shareError.videoNotSupported', {
type: mime.split('/')[1]
}),
type: 'danger'
})
return
}
media.push({ uri, mime })
} else {
if (typesImage.includes(uri.split('.').pop() || '')) {
media.push({ uri, mime: 'image/jpg' })
return
}
if (typesVideo.includes(uri.split('.').pop() || '')) {
media.push({ uri, mime: 'video/mp4' })
return
}
text = !text ? uri : text.concat(text, `\n${uri}`)
}
}
switch (Platform.OS) {
case 'ios':
if (!Array.isArray(item.data) || !item.data) {
return
}
for (const d of item.data) {
if (typeof d !== 'string') {
filterMedia({ uri: d.data, mime: d.mimeType })
}
}
break
case 'android':
if (!item.mimeType) {
return
}
if (Array.isArray(item.data)) {
for (const d of item.data) {
filterMedia({ uri: d, mime: item.mimeType })
}
} else {
filterMedia({ uri: item.data, mime: item.mimeType })
}
break
}
if (!text && !media.length) {
return
} else {
if (accounts?.length) {
navigationRef.navigate('Screen-AccountSelection', {
share: { text, media }
})
} else {
navigationRef.navigate('Screen-Compose', {
type: 'share',
text,
media
})
}
}
},
[]
)
useEffect(() => {
ShareMenu.getInitialShare(handleShare)
}, [])
useEffect(() => {
const listener = ShareMenu.addNewShareListener(handleShare)
return () => {
listener.remove()
}
}, [])
return (
<IntlProvider locale={i18n.language}>
<StatusBar
backgroundColor={colors.backgroundDefault}
{...(Platform.OS === 'android' && {
barStyle: theme === 'light' ? 'dark-content' : 'light-content'
})}
/>
<NavigationContainer
ref={navigationRef}
theme={themes[theme]}
onReady={() => routingInstrumentation.registerNavigationContainer(navigationRef)}
onStateChange={navigationContainerOnStateChange}
>
<Stack.Navigator initialRouteName='Screen-Tabs'>
<Stack.Screen
name='Screen-Tabs'
component={ScreenTabs}
options={{ headerShown: false }}
/>
<Stack.Screen
name='Screen-Actions'
component={ScreenActions}
options={{
presentation: 'transparentModal',
animation: 'fade',
headerShown: false
}}
/>
<Stack.Screen
name='Screen-Announcements'
component={ScreenAnnouncements}
options={({ navigation }) => ({
presentation: 'transparentModal',
animation: 'fade',
headerShown: true,
headerShadowVisible: false,
headerTransparent: true,
headerStyle: { backgroundColor: 'transparent' },
headerLeft: () => <HeaderLeft content='X' onPress={() => navigation.goBack()} />,
title: t('screenAnnouncements:heading')
})}
/>
<Stack.Screen
name='Screen-Compose'
component={ScreenCompose}
options={{
headerShown: false,
presentation: 'fullScreenModal'
}}
/>
<Stack.Screen
name='Screen-ImagesViewer'
component={ScreenImagesViewer}
options={{ headerShown: false, animation: 'fade' }}
/>
<Stack.Screen
name='Screen-AccountSelection'
component={ScreenAccountSelection}
options={({ navigation }) => ({
title: t('screenAccountSelection:heading'),
headerShadowVisible: false,
presentation: 'modal',
gestureEnabled: false,
headerLeft: () => (
<HeaderLeft
type='text'
content={t('common:buttons.cancel')}
onPress={() => navigation.goBack()}
/>
)
})}
/>
</Stack.Navigator>
<Message />
</NavigationContainer>
</IntlProvider>
)
}
export default Screens