Restructure removing remote

This commit is contained in:
Zhiyuan Zheng 2021-02-20 19:12:44 +01:00
parent 9fdf3ab640
commit 45681fc1f5
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
71 changed files with 998 additions and 756 deletions

View File

@ -30,6 +30,9 @@ export default (): ExpoConfig => ({
} }
] ]
}, },
ios: {
bundleIdentifier: 'com.xmflsct.app.tooot'
},
android: { android: {
versionCode: 4, versionCode: 4,
package: 'com.xmflsct.app.tooot', package: 'com.xmflsct.app.tooot',

View File

@ -49,6 +49,9 @@ PODS:
- UMCore - UMCore
- UMPermissionsInterface - UMPermissionsInterface
- UMTaskManagerInterface - UMTaskManagerInterface
- EXNotifications (0.8.2):
- UMCore
- UMPermissionsInterface
- EXPermissions (10.0.0): - EXPermissions (10.0.0):
- UMCore - UMCore
- UMPermissionsInterface - UMPermissionsInterface
@ -499,6 +502,7 @@ DEPENDENCIES:
- EXLinearGradient (from `../node_modules/expo-linear-gradient/ios`) - EXLinearGradient (from `../node_modules/expo-linear-gradient/ios`)
- EXLocalization (from `../node_modules/expo-localization/ios`) - EXLocalization (from `../node_modules/expo-localization/ios`)
- EXLocation (from `../node_modules/expo-location/ios`) - EXLocation (from `../node_modules/expo-location/ios`)
- EXNotifications (from `../node_modules/expo-notifications/ios`)
- EXPermissions (from `../node_modules/expo-permissions/ios`) - EXPermissions (from `../node_modules/expo-permissions/ios`)
- EXRandom (from `../node_modules/expo-random/ios`) - EXRandom (from `../node_modules/expo-random/ios`)
- EXScreenCapture (from `../node_modules/expo-screen-capture/ios`) - EXScreenCapture (from `../node_modules/expo-screen-capture/ios`)
@ -622,6 +626,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-localization/ios" :path: "../node_modules/expo-localization/ios"
EXLocation: EXLocation:
:path: "../node_modules/expo-location/ios" :path: "../node_modules/expo-location/ios"
EXNotifications:
:path: "../node_modules/expo-notifications/ios"
EXPermissions: EXPermissions:
:path: "../node_modules/expo-permissions/ios" :path: "../node_modules/expo-permissions/ios"
EXRandom: EXRandom:
@ -769,6 +775,7 @@ SPEC CHECKSUMS:
EXLinearGradient: c803fbd1aa974be038177b1e45524bc35759fe9c EXLinearGradient: c803fbd1aa974be038177b1e45524bc35759fe9c
EXLocalization: 8b9463c81843da214476b541a27811dd885c9a76 EXLocalization: 8b9463c81843da214476b541a27811dd885c9a76
EXLocation: d55e2a37f61bcfb4eba9c813b3f4621d896c4c00 EXLocation: d55e2a37f61bcfb4eba9c813b3f4621d896c4c00
EXNotifications: fba3319b1555961b99ddd185021b4c7ff978d5dd
EXPermissions: 17d4846ad1880f6891c74ae58ca1acb43e47ed47 EXPermissions: 17d4846ad1880f6891c74ae58ca1acb43e47ed47
EXRandom: d7e0f3dd64810aabd27d59f8ecffee359177e2c3 EXRandom: d7e0f3dd64810aabd27d59f8ecffee359177e2c3
EXScreenCapture: 5b8447139e56e2b922e93ccdc7c773c103fb44fd EXScreenCapture: 5b8447139e56e2b922e93ccdc7c773c103fb44fd

View File

@ -34,6 +34,7 @@
"expo-linear-gradient": "~8.4.0", "expo-linear-gradient": "~8.4.0",
"expo-linking": "~2.0.1", "expo-linking": "~2.0.1",
"expo-localization": "~9.1.0", "expo-localization": "~9.1.0",
"expo-notifications": "~0.8.2",
"expo-random": "~10.0.0", "expo-random": "~10.0.0",
"expo-screen-capture": "^3.0.0", "expo-screen-capture": "^3.0.0",
"expo-secure-store": "~9.3.0", "expo-secure-store": "~9.3.0",

View File

@ -343,6 +343,19 @@ declare namespace Mastodon {
'reading:expand:spoilers'?: boolean 'reading:expand:spoilers'?: boolean
} }
type PushSubscription = {
id: string
endpoint: string
alerts: {
follow: boolean
favourite: boolean
reblog: boolean
mention: boolean
poll: boolean
}
server_key: string
}
type Relationship = { type Relationship = {
id: string id: string
following: boolean following: boolean

View File

@ -117,7 +117,7 @@ declare namespace Nav {
title: Mastodon.List['title'] title: Mastodon.List['title']
} }
'Tab-Me-Settings': undefined 'Tab-Me-Settings': undefined
'Tab-Me-Settings-UpdateRemote': undefined 'Tab-Me-Settings-Notification': undefined
'Tab-Me-Switch': undefined 'Tab-Me-Switch': undefined
} & TabSharedStackParamList } & TabSharedStackParamList
} }

View File

@ -1,4 +1,4 @@
import client from '@api/client' import apiInstance from '@api/instance'
import { toast, toastConfig } from '@components/toast' import { toast, toastConfig } from '@components/toast'
import { import {
NavigationContainer, NavigationContainer,
@ -10,10 +10,8 @@ import ScreenCompose from '@screens/Compose'
import ScreenImagesViewer from '@screens/ImagesViewer' import ScreenImagesViewer from '@screens/ImagesViewer'
import ScreenTabs from '@screens/Tabs' import ScreenTabs from '@screens/Tabs'
import { updatePreviousTab } from '@utils/slices/contextsSlice' import { updatePreviousTab } from '@utils/slices/contextsSlice'
import { import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences'
getLocalActiveIndex, import { getInstanceActive } from '@utils/slices/instancesSlice'
updateLocalAccountPreferences
} from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { themes } from '@utils/styles/themes' import { themes } from '@utils/styles/themes'
import * as Analytics from 'expo-firebase-analytics' import * as Analytics from 'expo-firebase-analytics'
@ -36,7 +34,7 @@ export const navigationRef = createRef<NavigationContainerRef>()
const Screens: React.FC<Props> = ({ localCorrupt }) => { const Screens: React.FC<Props> = ({ localCorrupt }) => {
const { t } = useTranslation('common') const { t } = useTranslation('common')
const dispatch = useDispatch() const dispatch = useDispatch()
const localActiveIndex = useSelector(getLocalActiveIndex) const instanceActive = useSelector(getInstanceActive)
const { mode, theme } = useTheme() const { mode, theme } = useTheme()
enum barStyle { enum barStyle {
light = 'dark-content', light = 'dark-content',
@ -89,10 +87,9 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
// On launch check if there is any unread announcements // On launch check if there is any unread announcements
useEffect(() => { useEffect(() => {
localActiveIndex !== null && instanceActive !== -1 &&
client<Mastodon.Announcement[]>({ apiInstance<Mastodon.Announcement[]>({
method: 'get', method: 'get',
instance: 'local',
url: `announcements` url: `announcements`
}) })
.then(res => { .then(res => {
@ -107,8 +104,8 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
// Lazily update users's preferences, for e.g. composing default visibility // Lazily update users's preferences, for e.g. composing default visibility
useEffect(() => { useEffect(() => {
if (localActiveIndex !== null) { if (instanceActive !== -1) {
dispatch(updateLocalAccountPreferences()) dispatch(updateAccountPreferences())
} }
}, []) }, [])

87
src/api/general.ts Normal file
View File

@ -0,0 +1,87 @@
import axios from 'axios'
import chalk from 'chalk'
const ctx = new chalk.Instance({ level: 3 })
export type Params = {
method: 'get' | 'post' | 'put' | 'delete'
domain?: string
url: string
params?: {
[key: string]: string | number | boolean
}
headers?: { [key: string]: string }
body?: FormData | Object
}
const apiGeneral = async <T = unknown>({
method,
domain,
url,
params,
headers,
body
}: Params): Promise<{ body: T }> => {
if (!domain) {
return Promise.reject()
}
console.log(
ctx.bgGreen.bold(' API general ') +
' ' +
domain +
' ' +
method +
ctx.green(' -> ') +
`/${url}` +
(params ? ctx.green(' -> ') : ''),
params ? params : ''
)
return axios({
timeout: method === 'post' ? 1000 * 60 : 1000 * 15,
method,
baseURL: `https://${domain}/`,
url,
params,
headers: {
'Content-Type': 'application/json',
...headers
},
...(body && { data: body })
})
.then(response => {
return Promise.resolve({
body: response.data
})
})
.catch(error => {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.error(
ctx.bold(' API general '),
ctx.bold('response'),
error.response.status,
error.response.data.error
)
return Promise.reject(error.response)
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
console.error(ctx.bold(' API general '), ctx.bold('request'), error)
return Promise.reject()
} else {
console.error(
ctx.bold(' API general '),
ctx.bold('internal'),
error.message,
url
)
return Promise.reject()
}
})
}
export default apiGeneral

View File

@ -5,22 +5,8 @@ import li from 'li'
const ctx = new chalk.Instance({ level: 3 }) const ctx = new chalk.Instance({ level: 3 })
const client = async <T = unknown>({ export type Params = {
method,
instance,
localIndex,
instanceDomain,
version = 'v1',
url,
params,
headers,
body,
onUploadProgress
}: {
method: 'get' | 'post' | 'put' | 'delete' method: 'get' | 'post' | 'put' | 'delete'
instance: 'local' | 'remote'
localIndex?: number
instanceDomain?: string
version?: 'v1' | 'v2' version?: 'v1' | 'v2'
url: string url: string
params?: { params?: {
@ -29,30 +15,37 @@ const client = async <T = unknown>({
headers?: { [key: string]: string } headers?: { [key: string]: string }
body?: FormData body?: FormData
onUploadProgress?: (progressEvent: any) => void onUploadProgress?: (progressEvent: any) => void
}): Promise<{ body: T; links: { prev?: string; next?: string } }> => { }
const { store } = require('@root/store')
const state = (store.getState() as RootState).instances
const theLocalIndex =
localIndex !== undefined ? localIndex : state.local.activeIndex
let domain = null const apiInstance = async <T = unknown>({
let token = null method,
if (instance === 'remote') { version = 'v1',
domain = instanceDomain || state.remote.url url,
params,
headers,
body,
onUploadProgress
}: Params): Promise<{ body: T; links: { prev?: string; next?: string } }> => {
const { store } = require('@root/store')
const state = store.getState() as RootState
const instanceActive = state.instances.instances.findIndex(
instance => instance.active
)
let domain
let token
if (instanceActive !== -1 && state.instances.instances[instanceActive]) {
domain = state.instances.instances[instanceActive].url
token = state.instances.instances[instanceActive].token
} else { } else {
if (theLocalIndex !== null && state.local.instances[theLocalIndex]) { console.error(
domain = state.local.instances[theLocalIndex].url ctx.bgRed.white.bold(' API ') + ' ' + 'No instance domain is provided'
token = state.local.instances[theLocalIndex].token )
} else { return Promise.reject()
console.error(
ctx.bgRed.white.bold(' API ') + ' ' + 'No instance domain is provided'
)
return Promise.reject()
}
} }
console.log( console.log(
ctx.bgGreen.bold(' API ') + ctx.bgGreen.bold(' API instance ') +
' ' + ' ' +
domain + domain +
' ' + ' ' +
@ -97,7 +90,7 @@ const client = async <T = unknown>({
// The request was made and the server responded with a status code // The request was made and the server responded with a status code
// that falls out of the range of 2xx // that falls out of the range of 2xx
console.error( console.error(
ctx.bold(' API '), ctx.bold(' API instance '),
ctx.bold('response'), ctx.bold('response'),
error.response.status, error.response.status,
error.response.data.error error.response.data.error
@ -107,11 +100,11 @@ const client = async <T = unknown>({
// The request was made but no response was received // The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js // http.ClientRequest in node.js
console.error(ctx.bold(' API '), ctx.bold('request'), error) console.error(ctx.bold(' API instance '), ctx.bold('request'), error)
return Promise.reject() return Promise.reject()
} else { } else {
console.error( console.error(
ctx.bold(' API '), ctx.bold(' API instance '),
ctx.bold('internal'), ctx.bold('internal'),
error.message, error.message,
url url
@ -121,4 +114,4 @@ const client = async <T = unknown>({
}) })
} }
export default client export default apiInstance

View File

@ -1,7 +1,7 @@
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { import {
getLocalInstance, getInstance,
updateLocalNotification updateInstanceNotification
} from '@utils/slices/instancesSlice' } from '@utils/slices/instancesSlice'
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import { useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
@ -18,7 +18,7 @@ const useWebsocket = ({
const queryClient = useQueryClient() const queryClient = useQueryClient()
const dispatch = useDispatch() const dispatch = useDispatch()
const localInstance = useSelector( const localInstance = useSelector(
getLocalInstance, getInstance,
(prev, next) => (prev, next) =>
prev?.urls.streaming_api === next?.urls.streaming_api && prev?.urls.streaming_api === next?.urls.streaming_api &&
prev?.token === next?.token prev?.token === next?.token
@ -39,7 +39,7 @@ const useWebsocket = ({
case 'notification': case 'notification':
const payload: Mastodon.Notification = JSON.parse(message.payload) const payload: Mastodon.Notification = JSON.parse(message.payload)
dispatch( dispatch(
updateLocalNotification({ latestTime: payload.created_at }) updateInstanceNotification({ latestTime: payload.created_at })
) )
const queryKey: QueryKeyTimeline = [ const queryKey: QueryKeyTimeline = [
'Timeline', 'Timeline',

View File

@ -2,7 +2,7 @@ import Button from '@components/Button'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { useAppsQuery } from '@utils/queryHooks/apps' import { useAppsQuery } from '@utils/queryHooks/apps'
import { useInstanceQuery } from '@utils/queryHooks/instance' import { useInstanceQuery } from '@utils/queryHooks/instance'
import { getLocalInstances } from '@utils/slices/instancesSlice' import { getInstances } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import * as WebBrowser from 'expo-web-browser' import * as WebBrowser from 'expo-web-browser'
@ -14,7 +14,6 @@ import { useSelector } from 'react-redux'
import { Placeholder, Fade } from 'rn-placeholder' import { Placeholder, Fade } from 'rn-placeholder'
import analytics from './analytics' import analytics from './analytics'
import InstanceAuth from './Instance/Auth' import InstanceAuth from './Instance/Auth'
import EULA from './Instance/EULA'
import InstanceInfo from './Instance/Info' import InstanceInfo from './Instance/Info'
export interface Props { export interface Props {
@ -29,26 +28,23 @@ const ComponentInstance: React.FC<Props> = ({
const { t } = useTranslation('componentInstance') const { t } = useTranslation('componentInstance')
const { theme } = useTheme() const { theme } = useTheme()
const localInstances = useSelector(getLocalInstances, () => true) const instances = useSelector(getInstances, () => true)
const [instanceDomain, setInstanceDomain] = useState<string>() const [domain, setDomain] = useState<string>()
const instanceQuery = useInstanceQuery({ const instanceQuery = useInstanceQuery({
instanceDomain, domain,
options: { enabled: false, retry: false } options: { enabled: !!domain, retry: false }
}) })
const appsQuery = useAppsQuery({ const appsQuery = useAppsQuery({
instanceDomain, domain,
options: { enabled: false, retry: false } options: { enabled: false, retry: false }
}) })
const onChangeText = useCallback( const onChangeText = useCallback(
debounce( debounce(
text => { text => {
setInstanceDomain(text.replace(/^http(s)?\:\/\//i, '')) setDomain(text.replace(/^http(s)?\:\/\//i, ''))
appsQuery.remove() appsQuery.remove()
if (text) {
instanceQuery.refetch()
}
}, },
1000, 1000,
{ trailing: true } { trailing: true }
@ -57,40 +53,35 @@ const ComponentInstance: React.FC<Props> = ({
) )
const processUpdate = useCallback(() => { const processUpdate = useCallback(() => {
if (instanceDomain) { if (domain) {
analytics('instance_local_login') analytics('instance_login')
if ( if (
localInstances && instances &&
localInstances.filter(instance => instance.url === instanceDomain) instances.filter(instance => instance.url === domain).length
.length
) { ) {
Alert.alert( Alert.alert(t('update.alert.title'), t('update.alert.message'), [
t('update.local.alert.title'), {
t('update.local.alert.message'), text: t('update.alert.buttons.cancel'),
[ style: 'cancel'
{ },
text: t('update.local.alert.buttons.cancel'), {
style: 'cancel' text: t('update.alert.buttons.continue'),
}, onPress: () => {
{ appsQuery.refetch()
text: t('update.local.alert.buttons.continue'),
onPress: () => {
appsQuery.refetch()
}
} }
] }
) ])
} else { } else {
appsQuery.refetch() appsQuery.refetch()
} }
} }
}, [instanceDomain]) }, [domain])
const onSubmitEditing = useCallback( const onSubmitEditing = useCallback(
({ nativeEvent: { text } }) => { ({ nativeEvent: { text } }) => {
analytics('instance_textinput_submit', { match: text === instanceDomain }) analytics('instance_textinput_submit', { match: text === domain })
if ( if (
text === instanceDomain && text === domain &&
instanceQuery.isSuccess && instanceQuery.isSuccess &&
instanceQuery.data && instanceQuery.data &&
instanceQuery.data.uri instanceQuery.data.uri
@ -98,12 +89,12 @@ const ComponentInstance: React.FC<Props> = ({
processUpdate() processUpdate()
} }
}, },
[instanceDomain, instanceQuery.isSuccess, instanceQuery.data] [domain, instanceQuery.isSuccess, instanceQuery.data]
) )
const requestAuth = useMemo(() => { const requestAuth = useMemo(() => {
if ( if (
instanceDomain && domain &&
instanceQuery.data?.uri && instanceQuery.data?.uri &&
appsQuery.data?.client_id && appsQuery.data?.client_id &&
appsQuery.data.client_secret appsQuery.data.client_secret
@ -111,7 +102,7 @@ const ComponentInstance: React.FC<Props> = ({
return ( return (
<InstanceAuth <InstanceAuth
key={Math.random()} key={Math.random()}
instanceDomain={instanceDomain} instanceDomain={domain}
instance={instanceQuery.data} instance={instanceQuery.data}
appData={{ appData={{
clientId: appsQuery.data.client_id, clientId: appsQuery.data.client_id,
@ -121,9 +112,7 @@ const ComponentInstance: React.FC<Props> = ({
/> />
) )
} }
}, [instanceDomain, instanceQuery.data, appsQuery.data]) }, [domain, instanceQuery.data, appsQuery.data])
const [agreed, setAgreed] = useState(false)
return ( return (
<> <>
@ -160,15 +149,13 @@ const ComponentInstance: React.FC<Props> = ({
/> />
<Button <Button
type='text' type='text'
content={t('server.button.local')} content={t('server.button')}
onPress={processUpdate} onPress={processUpdate}
disabled={!instanceQuery.data?.uri} disabled={!instanceQuery.data?.uri}
loading={instanceQuery.isFetching || appsQuery.isFetching} loading={instanceQuery.isFetching || appsQuery.isFetching}
/> />
</View> </View>
{/* <EULA agreed={agreed} setAgreed={setAgreed} /> */}
<View> <View>
<Placeholder <Placeholder
{...(instanceQuery.isFetching && { {...(instanceQuery.isFetching && {

View File

@ -1,5 +1,6 @@
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { InstanceLocal, localAddInstance } from '@utils/slices/instancesSlice' import addInstance from '@utils/slices/instances/add'
import { Instance } from '@utils/slices/instancesSlice'
import * as AuthSession from 'expo-auth-session' import * as AuthSession from 'expo-auth-session'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
@ -9,7 +10,7 @@ export interface Props {
instanceDomain: string instanceDomain: string
// Domain can be different than uri // Domain can be different than uri
instance: Mastodon.Instance instance: Mastodon.Instance
appData: InstanceLocal['appData'] appData: Instance['appData']
goBack?: boolean goBack?: boolean
} }
@ -62,8 +63,8 @@ const InstanceAuth = React.memo(
) )
queryClient.clear() queryClient.clear()
dispatch( dispatch(
localAddInstance({ addInstance({
url: instanceDomain, domain: instanceDomain,
token: accessToken, token: accessToken,
instance, instance,
max_toot_chars: instance.max_toot_chars, max_toot_chars: instance.max_toot_chars,

View File

@ -1,7 +1,7 @@
import ComponentSeparator from '@components/Separator' import ComponentSeparator from '@components/Separator'
import { useNavigation, useScrollToTop } from '@react-navigation/native' import { useNavigation, useScrollToTop } from '@react-navigation/native'
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
import { getLocalActiveIndex } from '@utils/slices/instancesSlice' import { getInstanceActive } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { findIndex } from 'lodash' import { findIndex } from 'lodash'
@ -54,7 +54,7 @@ const Timeline: React.FC<Props> = ({
const { theme } = useTheme() const { theme } = useTheme()
// Update timeline when account switched // Update timeline when account switched
useSelector(getLocalActiveIndex) useSelector(getInstanceActive)
const queryKeyParams = { const queryKeyParams = {
page, page,

View File

@ -1,10 +1,10 @@
import client from '@api/client' import apiInstance from '@api/instance'
import analytics from '@components/analytics' import analytics from '@components/analytics'
import GracefullyImage from '@components/GracefullyImage' import GracefullyImage from '@components/GracefullyImage'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack' import { StackNavigationProp } from '@react-navigation/stack'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getLocalAccount } from '@utils/slices/instancesSlice' import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
@ -58,17 +58,16 @@ const TimelineConversation: React.FC<Props> = ({
queryKey, queryKey,
highlighted = false highlighted = false
}) => { }) => {
const localAccount = useSelector( const instanceAccount = useSelector(
getLocalAccount, getInstanceAccount,
(prev, next) => prev?.id === next?.id (prev, next) => prev?.id === next?.id
) )
const { theme } = useTheme() const { theme } = useTheme()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const fireMutation = useCallback(() => { const fireMutation = useCallback(() => {
return client<Mastodon.Conversation>({ return apiInstance<Mastodon.Conversation>({
method: 'post', method: 'post',
instance: 'local',
url: `conversations/${conversation.id}/read` url: `conversations/${conversation.id}/read`
}) })
}, []) }, [])
@ -135,7 +134,9 @@ const TimelineConversation: React.FC<Props> = ({
statusId={conversation.last_status.id} statusId={conversation.last_status.id}
poll={conversation.last_status.poll} poll={conversation.last_status.poll}
reblog={false} reblog={false}
sameAccount={conversation.last_status.id === localAccount?.id} sameAccount={
conversation.last_status.id === instanceAccount?.id
}
/> />
)} )}
</View> </View>

View File

@ -10,7 +10,7 @@ import TimelinePoll from '@components/Timeline/Shared/Poll'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack' import { StackNavigationProp } from '@react-navigation/stack'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getLocalAccount } from '@utils/slices/instancesSlice' import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { uniqBy } from 'lodash' import { uniqBy } from 'lodash'
@ -41,8 +41,8 @@ const TimelineDefault: React.FC<Props> = ({
pinned pinned
}) => { }) => {
const { theme } = useTheme() const { theme } = useTheme()
const localAccount = useSelector( const instanceAccount = useSelector(
getLocalAccount, getInstanceAccount,
(prev, next) => prev?.id === next?.id (prev, next) => prev?.id === next?.id
) )
const navigation = useNavigation< const navigation = useNavigation<
@ -118,7 +118,7 @@ const TimelineDefault: React.FC<Props> = ({
statusId={actualStatus.id} statusId={actualStatus.id}
poll={actualStatus.poll} poll={actualStatus.poll}
reblog={item.reblog ? true : false} reblog={item.reblog ? true : false}
sameAccount={actualStatus.account.id === localAccount?.id} sameAccount={actualStatus.account.id === instanceAccount?.id}
/> />
) : null} ) : null}
{!disableDetails && {!disableDetails &&
@ -147,7 +147,7 @@ const TimelineDefault: React.FC<Props> = ({
([actualStatus.account] as Mastodon.Account[] & ([actualStatus.account] as Mastodon.Account[] &
Mastodon.Mention[]) Mastodon.Mention[])
.concat(actualStatus.mentions) .concat(actualStatus.mentions)
.filter(d => d.id !== localAccount?.id), .filter(d => d.id !== instanceAccount?.id),
d => d.id d => d.id
).map(d => d.acct)} ).map(d => d.acct)}
reblog={item.reblog ? true : false} reblog={item.reblog ? true : false}

View File

@ -10,7 +10,7 @@ import TimelinePoll from '@components/Timeline/Shared/Poll'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack' import { StackNavigationProp } from '@react-navigation/stack'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getLocalAccount } from '@utils/slices/instancesSlice' import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { uniqBy } from 'lodash' import { uniqBy } from 'lodash'
@ -30,8 +30,8 @@ const TimelineNotifications: React.FC<Props> = ({
highlighted = false highlighted = false
}) => { }) => {
const { theme } = useTheme() const { theme } = useTheme()
const localAccount = useSelector( const instanceAccount = useSelector(
getLocalAccount, getInstanceAccount,
(prev, next) => prev?.id === next?.id (prev, next) => prev?.id === next?.id
) )
const navigation = useNavigation< const navigation = useNavigation<
@ -103,7 +103,7 @@ const TimelineNotifications: React.FC<Props> = ({
statusId={notification.status.id} statusId={notification.status.id}
poll={notification.status.poll} poll={notification.status.poll}
reblog={false} reblog={false}
sameAccount={notification.account.id === localAccount?.id} sameAccount={notification.account.id === instanceAccount?.id}
/> />
)} )}
{notification.status.media_attachments.length > 0 && ( {notification.status.media_attachments.length > 0 && (
@ -131,7 +131,7 @@ const TimelineNotifications: React.FC<Props> = ({
([notification.status.account] as Mastodon.Account[] & ([notification.status.account] as Mastodon.Account[] &
Mastodon.Mention[]) Mastodon.Mention[])
.concat(notification.status.mentions) .concat(notification.status.mentions)
.filter(d => d.id !== localAccount?.id), .filter(d => d.id !== instanceAccount?.id),
d => d.id d => d.id
).map(d => d.acct)} ).map(d => d.acct)}
reblog={false} reblog={false}

View File

@ -3,10 +3,7 @@ export default {
textInput: { placeholder: "Instance' domain" }, textInput: { placeholder: "Instance' domain" },
privateInstance: 'Private instance, peeping not allowed', privateInstance: 'Private instance, peeping not allowed',
EULA: { base: 'I have read and agreed to ', EULA: 'EULA' }, EULA: { base: 'I have read and agreed to ', EULA: 'EULA' },
button: { button: 'Login',
local: 'Login',
remote: 'Peep'
},
information: { information: {
name: 'Name', name: 'Name',
description: { heading: 'Description', expandHint: 'description' }, description: { heading: 'Description', expandHint: 'description' },
@ -21,19 +18,14 @@ export default {
} }
}, },
update: { update: {
local: { alert: {
alert: { title: 'Logged in to this instance',
title: 'Logged in to this instance', message:
message: 'You can login to another account, keeping existing logged in account',
'You can login to another account, keeping existing logged in account', buttons: {
buttons: { cancel: '$t(common:buttons.cancel)',
cancel: '$t(common:buttons.cancel)', continue: 'Continue'
continue: 'Continue'
}
} }
},
remote: {
succeed: 'Register peeping succeed'
} }
} }
} }

View File

@ -26,10 +26,6 @@ export default {
cancel: '$t(common:buttons.cancel)' cancel: '$t(common:buttons.cancel)'
} }
}, },
remote: {
heading: '$t(meSettingsUpdateRemote:heading)',
description: 'External instance can only be read'
},
cache: { cache: {
heading: 'Clear cache', heading: 'Clear cache',
empty: 'Cache empty' empty: 'Cache empty'

View File

@ -23,8 +23,7 @@ i18n.use(initReactI18next).init({
}, },
react: { react: {
useSuspense: false useSuspense: false
}, }
debug: true
}) })
export default i18n export default i18n

View File

@ -14,6 +14,7 @@ export default {
meLists: require('./screens/meLists').default, meLists: require('./screens/meLists').default,
meListsList: require('./screens/meListsList').default, meListsList: require('./screens/meListsList').default,
meSettings: require('./screens/meSettings').default, meSettings: require('./screens/meSettings').default,
meSettingsNotification: require('./screens/meSettingsNotification').default,
meSwitch: require('./screens/meSwitch').default, meSwitch: require('./screens/meSwitch').default,
sharedAccount: require('./screens/sharedAccount').default, sharedAccount: require('./screens/sharedAccount').default,

View File

@ -3,10 +3,7 @@ export default {
textInput: { placeholder: '输入社区服务器地址' }, textInput: { placeholder: '输入社区服务器地址' },
privateInstance: '非公开社区, 不能围观', privateInstance: '非公开社区, 不能围观',
EULA: { base: '我阅读并同意 ', EULA: '最终用户条款' }, EULA: { base: '我阅读并同意 ', EULA: '最终用户条款' },
button: { button: '登录',
local: '登录',
remote: '围观'
},
information: { information: {
name: '社区名称', name: '社区名称',
description: { heading: '社区简介', expandHint: '简介' }, description: { heading: '社区简介', expandHint: '简介' },
@ -21,18 +18,13 @@ export default {
} }
}, },
update: { update: {
local: { alert: {
alert: { title: '此社区已登录',
title: '此社区已登录', message: '你可以登录同个社区的另一个账号,不影响已登录的账号',
message: '你可以登录同个社区的另一个账号,不影响已登录的账号', buttons: {
buttons: { cancel: '$t(common:buttons.cancel)',
cancel: '$t(common:buttons.cancel)', continue: '继续'
continue: '继续'
}
} }
},
remote: {
succeed: '围观登记成功'
} }
} }
} }

View File

@ -1,6 +1,9 @@
export default { export default {
heading: '设置', heading: '设置',
content: { content: {
notification: {
heading: '$t(meSettingsNotification:heading)'
},
language: { language: {
heading: '切换语言', heading: '切换语言',
options: { options: {
@ -26,10 +29,6 @@ export default {
cancel: '$t(common:buttons.cancel)' cancel: '$t(common:buttons.cancel)'
} }
}, },
remote: {
heading: '$t(meSettingsUpdateRemote:heading)',
description: '外站只能浏览不能玩'
},
cache: { cache: {
heading: '清空缓存', heading: '清空缓存',
empty: '暂无缓存' empty: '暂无缓存'

View File

@ -0,0 +1,24 @@
export default {
heading: '通知',
content: {
global: {
heading: '启用通知',
description: 'blahblahblah'
},
follow: {
heading: '新关注者'
},
favourite: {
heading: '嘟文被喜欢'
},
reblog: {
heading: '嘟文被转嘟'
},
mention: {
heading: '提及你'
},
poll: {
heading: '投票'
}
}
}

View File

@ -7,10 +7,10 @@ import ComposeRoot from '@screens/Compose/Root'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { updateStoreReview } from '@utils/slices/contextsSlice' import { updateStoreReview } from '@utils/slices/contextsSlice'
import { import {
getLocalAccount, getInstanceAccount,
getLocalMaxTootChar, getInstanceMaxTootChar,
removeLocalDraft, removeInstanceDraft,
updateLocalDraft updateInstanceDraft
} from '@utils/slices/instancesSlice' } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
@ -75,7 +75,7 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
setHasKeyboard(false) setHasKeyboard(false)
} }
const localAccount = useSelector(getLocalAccount, (prev, next) => const localAccount = useSelector(getInstanceAccount, (prev, next) =>
prev?.preferences && next?.preferences prev?.preferences && next?.preferences
? prev?.preferences['posting:default:visibility'] === ? prev?.preferences['posting:default:visibility'] ===
next?.preferences['posting:default:visibility'] next?.preferences['posting:default:visibility']
@ -102,7 +102,7 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
initialReducerState initialReducerState
) )
const maxTootChars = useSelector(getLocalMaxTootChar) const maxTootChars = useSelector(getInstanceMaxTootChar, () => true)
const totalTextCount = const totalTextCount =
(composeState.spoiler.active ? composeState.spoiler.count : 0) + (composeState.spoiler.active ? composeState.spoiler.count : 0) +
composeState.text.count composeState.text.count
@ -158,7 +158,7 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
const saveDraft = () => { const saveDraft = () => {
dispatch( dispatch(
updateLocalDraft({ updateInstanceDraft({
timestamp: composeState.timestamp, timestamp: composeState.timestamp,
spoiler: composeState.spoiler.raw, spoiler: composeState.spoiler.raw,
text: composeState.text.raw, text: composeState.text.raw,
@ -171,7 +171,7 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
) )
} }
const removeDraft = useCallback(() => { const removeDraft = useCallback(() => {
dispatch(removeLocalDraft(composeState.timestamp)) dispatch(removeInstanceDraft(composeState.timestamp))
}, [composeState.timestamp]) }, [composeState.timestamp])
useEffect(() => { useEffect(() => {
const autoSave = composeState.dirty const autoSave = composeState.dirty

View File

@ -1,9 +1,12 @@
import client from '@api/client' import apiInstance from '@api/instance'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import ComponentSeparator from '@components/Separator' import ComponentSeparator from '@components/Separator'
import HeaderSharedCreated from '@components/Timeline/Shared/HeaderShared/Created' import HeaderSharedCreated from '@components/Timeline/Shared/HeaderShared/Created'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { getLocalDrafts, removeLocalDraft } from '@utils/slices/instancesSlice' import {
getInstanceDrafts,
removeInstanceDraft
} from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useContext, useState } from 'react' import React, { useCallback, useContext, useState } from 'react'
@ -34,7 +37,7 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
const navigation = useNavigation() const navigation = useNavigation()
const dispatch = useDispatch() const dispatch = useDispatch()
const { mode, theme } = useTheme() const { mode, theme } = useTheme()
const localDrafts = useSelector(getLocalDrafts)?.filter( const instanceDrafts = useSelector(getInstanceDrafts)?.filter(
draft => draft.timestamp !== timestamp draft => draft.timestamp !== timestamp
) )
@ -44,7 +47,7 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
const [checkingAttachments, setCheckingAttachments] = useState(false) const [checkingAttachments, setCheckingAttachments] = useState(false)
const removeDraft = useCallback(ts => { const removeDraft = useCallback(ts => {
dispatch(removeLocalDraft(ts)) dispatch(removeInstanceDraft(ts))
}, []) }, [])
const renderItem = useCallback( const renderItem = useCallback(
@ -58,9 +61,8 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
let tempUploads: ExtendedAttachment[] = [] let tempUploads: ExtendedAttachment[] = []
if (item.attachments && item.attachments.uploads.length) { if (item.attachments && item.attachments.uploads.length) {
for (const attachment of item.attachments.uploads) { for (const attachment of item.attachments.uploads) {
await client<Mastodon.Attachment>({ await apiInstance<Mastodon.Attachment>({
method: 'get', method: 'get',
instance: 'local',
url: `media/${attachment.remote?.id}` url: `media/${attachment.remote?.id}`
}) })
.then(res => { .then(res => {
@ -92,7 +94,7 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
type: 'loadDraft', type: 'loadDraft',
payload: tempDraft payload: tempDraft
}) })
dispatch(removeLocalDraft(item.timestamp)) dispatch(removeInstanceDraft(item.timestamp))
navigation.goBack() navigation.goBack()
}} }}
> >
@ -156,14 +158,14 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
<> <>
<PanGestureHandler enabled={true}> <PanGestureHandler enabled={true}>
<SwipeListView <SwipeListView
data={localDrafts} data={instanceDrafts}
renderItem={renderItem} renderItem={renderItem}
renderHiddenItem={renderHiddenItem} renderHiddenItem={renderHiddenItem}
disableRightSwipe={true} disableRightSwipe={true}
rightOpenValue={-actionWidth} rightOpenValue={-actionWidth}
previewRowKey={ previewRowKey={
localDrafts?.length instanceDrafts?.length
? localDrafts[0].timestamp.toString() ? instanceDrafts[0].timestamp.toString()
: undefined : undefined
} }
// previewDuration={350} // previewDuration={350}

View File

@ -1,4 +1,4 @@
import client from '@api/client' import apiInstance from '@api/instance'
import analytics from '@components/analytics' import analytics from '@components/analytics'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header' import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header'
@ -91,9 +91,8 @@ const ComposeEditAttachment: React.FC<ScreenComposeEditAttachmentProp> = ({
formData.append('focus', `${focus.value.x},${focus.value.y}`) formData.append('focus', `${focus.value.x},${focus.value.y}`)
} }
client<Mastodon.Attachment>({ apiInstance<Mastodon.Attachment>({
method: 'put', method: 'put',
instance: 'local',
url: `media/${theAttachment.id}`, url: `media/${theAttachment.id}`,
body: formData body: formData
}) })

View File

@ -1,6 +1,6 @@
import Button from '@components/Button' import Button from '@components/Button'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { getLocalDrafts } from '@utils/slices/instancesSlice' import { getInstanceDrafts } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation' import layoutAnimation from '@utils/styles/layoutAnimation'
import React, { useContext, useEffect } from 'react' import React, { useContext, useEffect } from 'react'
@ -13,7 +13,7 @@ const ComposeDrafts: React.FC = () => {
const { t } = useTranslation('sharedCompose') const { t } = useTranslation('sharedCompose')
const navigation = useNavigation() const navigation = useNavigation()
const { composeState } = useContext(ComposeContext) const { composeState } = useContext(ComposeContext)
const localDrafts = useSelector(getLocalDrafts)?.filter( const instanceDrafts = useSelector(getInstanceDrafts)?.filter(
draft => draft.timestamp !== composeState.timestamp draft => draft.timestamp !== composeState.timestamp
) )
@ -21,7 +21,7 @@ const ComposeDrafts: React.FC = () => {
layoutAnimation() layoutAnimation()
}, [composeState.dirty]) }, [composeState.dirty])
if (!composeState.dirty && localDrafts?.length) { if (!composeState.dirty && instanceDrafts?.length) {
return ( return (
<View <View
style={styles.base} style={styles.base}
@ -29,7 +29,7 @@ const ComposeDrafts: React.FC = () => {
<Button <Button
type='text' type='text'
content={t('content.root.drafts', { content={t('content.root.drafts', {
count: localDrafts.length count: instanceDrafts.length
})} })}
onPress={() => onPress={() =>
navigation.navigate('Screen-Compose-DraftsList', { navigation.navigate('Screen-Compose-DraftsList', {

View File

@ -1,4 +1,3 @@
import client from '@api/client'
import * as ImagePicker from 'expo-image-picker' import * as ImagePicker from 'expo-image-picker'
import * as Crypto from 'expo-crypto' import * as Crypto from 'expo-crypto'
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types' import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types'
@ -9,6 +8,7 @@ import { ComposeAction } from '../../utils/types'
import { ActionSheetOptions } from '@expo/react-native-action-sheet' import { ActionSheetOptions } from '@expo/react-native-action-sheet'
import i18next from 'i18next' import i18next from 'i18next'
import analytics from '@components/analytics' import analytics from '@components/analytics'
import apiInstance from '@api/instance'
export interface Props { export interface Props {
composeDispatch: Dispatch<ComposeAction> composeDispatch: Dispatch<ComposeAction>
@ -106,9 +106,8 @@ const addAttachment = async ({
type: attachmentType type: attachmentType
}) })
return client<Mastodon.Attachment>({ return apiInstance<Mastodon.Attachment>({
method: 'post', method: 'post',
instance: 'local',
url: 'media', url: 'media',
body: formData body: formData
}) })

View File

@ -1,7 +1,4 @@
import { import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
getLocalActiveIndex,
getLocalInstances
} from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
@ -13,15 +10,15 @@ import ComposeTextInput from './Header/TextInput'
const ComposeRootHeader: React.FC = () => { const ComposeRootHeader: React.FC = () => {
const { composeState } = useContext(ComposeContext) const { composeState } = useContext(ComposeContext)
const localActiveIndex = useSelector(getLocalActiveIndex) const instanceActive = useSelector(getInstanceActive)
const localInstances = useSelector( const localInstances = useSelector(
getLocalInstances, getInstances,
(prev, next) => prev.length === next.length (prev, next) => prev.length === next.length
) )
return ( return (
<> <>
{localActiveIndex !== null && {instanceActive !== -1 &&
localInstances.length && localInstances.length &&
localInstances.length > 1 && ( localInstances.length > 1 && (
<View style={styles.postingAs}> <View style={styles.postingAs}>

View File

@ -1,4 +1,7 @@
import { getLocalAccount, getLocalUri } from '@utils/slices/instancesSlice' import {
getInstanceAccount,
getInstanceUri
} from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React from 'react'
@ -11,17 +14,17 @@ const ComposePostingAs = React.memo(
const { t } = useTranslation('sharedCompose') const { t } = useTranslation('sharedCompose')
const { theme } = useTheme() const { theme } = useTheme()
const localAccount = useSelector( const instanceAccount = useSelector(
getLocalAccount, getInstanceAccount,
(prev, next) => prev?.acct === next?.acct (prev, next) => prev?.acct === next?.acct
) )
const localUri = useSelector(getLocalUri) const instanceUri = useSelector(getInstanceUri)
return ( return (
<Text style={[styles.text, { color: theme.secondary }]}> <Text style={[styles.text, { color: theme.secondary }]}>
{t('content.root.header.postingAs', { {t('content.root.header.postingAs', {
acct: localAccount?.acct, acct: instanceAccount?.acct,
domain: localUri domain: instanceUri
})} })}
</Text> </Text>
) )

View File

@ -1,5 +1,5 @@
import { store } from '@root/store' import { store } from '@root/store'
import { getLocalAccount } from '@utils/slices/instancesSlice' import { getInstanceAccount } from '@utils/slices/instancesSlice'
import composeInitialState from './initialState' import composeInitialState from './initialState'
import { ComposeState } from './types' import { ComposeState } from './types'
@ -39,7 +39,7 @@ const composeParseState = (
}), }),
visibility: visibility:
params.incomingStatus.visibility || params.incomingStatus.visibility ||
getLocalAccount(store.getState())?.preferences[ getInstanceAccount(store.getState()).preferences[
'posting:default:visibility' 'posting:default:visibility'
] || ] ||
'public', 'public',

View File

@ -1,4 +1,4 @@
import client from '@root/api/client' import apiInstance from '@api/instance'
import { ComposeState } from '@screens/Compose/utils/types' import { ComposeState } from '@screens/Compose/utils/types'
import * as Crypto from 'expo-crypto' import * as Crypto from 'expo-crypto'
@ -39,9 +39,8 @@ const composePost = async (
formData.append('visibility', composeState.visibility) formData.append('visibility', composeState.visibility)
return client<Mastodon.Status>({ return apiInstance<Mastodon.Status>({
method: 'post', method: 'post',
instance: 'local',
url: 'statuses', url: 'statuses',
headers: { headers: {
'Idempotency-Key': await Crypto.digestStringAsync( 'Idempotency-Key': await Crypto.digestStringAsync(

View File

@ -10,10 +10,10 @@ import { StackScreenProps } from '@react-navigation/stack'
import { useTimelineQuery } from '@utils/queryHooks/timeline' import { useTimelineQuery } from '@utils/queryHooks/timeline'
import { getPreviousTab } from '@utils/slices/contextsSlice' import { getPreviousTab } from '@utils/slices/contextsSlice'
import { import {
getLocalAccount, getInstanceAccount,
getLocalActiveIndex, getInstanceActive,
getLocalNotification, getInstanceNotification,
updateLocalNotification updateInstanceNotification
} from '@utils/slices/instancesSlice' } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useMemo } from 'react' import React, { useCallback, useMemo } from 'react'
@ -44,14 +44,15 @@ const ScreenTabs = React.memo(
({ navigation }: ScreenTabsProp) => { ({ navigation }: ScreenTabsProp) => {
const { mode, theme } = useTheme() const { mode, theme } = useTheme()
const dispatch = useDispatch() const dispatch = useDispatch()
const localActiveIndex = useSelector(getLocalActiveIndex) const instanceActive = useSelector(getInstanceActive)
const localAccount = useSelector( const localAccount = useSelector(
getLocalAccount, getInstanceAccount,
(prev, next) => prev?.avatarStatic === next?.avatarStatic (prev, next) => prev?.avatarStatic === next?.avatarStatic
) )
const screenOptions = useCallback( const screenOptions = useCallback(
({ route }): BottomTabNavigationOptions => ({ ({ route }): BottomTabNavigationOptions => ({
tabBarVisible: instanceActive !== -1,
tabBarIcon: ({ tabBarIcon: ({
focused, focused,
color, color,
@ -71,7 +72,7 @@ const ScreenTabs = React.memo(
case 'Tab-Notifications': case 'Tab-Notifications':
return <Icon name='Bell' size={size} color={color} /> return <Icon name='Bell' size={size} color={color} />
case 'Tab-Me': case 'Tab-Me':
return localActiveIndex !== null ? ( return instanceActive !== -1 ? (
<FastImage <FastImage
source={{ uri: localAccount?.avatarStatic }} source={{ uri: localAccount?.avatarStatic }}
style={{ style={{
@ -94,61 +95,39 @@ const ScreenTabs = React.memo(
} }
} }
}), }),
[localActiveIndex, localAccount?.avatarStatic] [instanceActive, localAccount?.avatarStatic]
) )
const tabBarOptions = useMemo( const tabBarOptions = useMemo(
() => ({ () => ({
activeTintColor: theme.primary, activeTintColor: theme.primary,
inactiveTintColor: inactiveTintColor: theme.secondary,
localActiveIndex !== null ? theme.secondary : theme.disabled,
showLabel: false, showLabel: false,
...(Platform.OS === 'android' && { keyboardHidesTabBar: true }) ...(Platform.OS === 'android' && { keyboardHidesTabBar: true })
}), }),
[mode, localActiveIndex] [mode]
)
const localListeners = useCallback(
() => ({
tabPress: (e: any) => {
if (!(localActiveIndex !== null)) {
e.preventDefault()
}
}
}),
[localActiveIndex]
) )
const composeListeners = useMemo( const composeListeners = useMemo(
() => ({ () => ({
tabPress: (e: any) => { tabPress: (e: any) => {
e.preventDefault() e.preventDefault()
if (localActiveIndex !== null) { haptics('Light')
haptics('Light') navigation.navigate('Screen-Compose')
navigation.navigate('Screen-Compose')
}
} }
}), }),
[localActiveIndex] []
) )
const composeComponent = useCallback(() => null, []) const composeComponent = useCallback(() => null, [])
const notificationsListeners = useCallback(
() => ({
tabPress: (e: any) => {
if (!(localActiveIndex !== null)) {
e.preventDefault()
}
}
}),
[localActiveIndex]
)
// On launch check if there is any unread noficiations // On launch check if there is any unread noficiations
useTimelineQuery({ useTimelineQuery({
page: 'Notifications', page: 'Notifications',
options: { options: {
enabled: instanceActive !== -1,
notifyOnChangeProps: [], notifyOnChangeProps: [],
select: data => { select: data => {
if (data.pages[0].body.length) { if (data.pages[0].body.length) {
dispatch( dispatch(
updateLocalNotification({ updateInstanceNotification({
// @ts-ignore // @ts-ignore
latestTime: data.pages[0].body[0].created_at latestTime: data.pages[0].body[0].created_at
}) })
@ -160,27 +139,21 @@ const ScreenTabs = React.memo(
}) })
useWebsocket({ stream: 'user', event: 'notification' }) useWebsocket({ stream: 'user', event: 'notification' })
const localNotification = useSelector( const localNotification = useSelector(
getLocalNotification, getInstanceNotification,
(prev, next) => (prev, next) =>
prev?.readTime === next?.readTime && prev?.readTime === next?.readTime &&
prev?.latestTime === next?.latestTime prev?.latestTime === next?.latestTime
) )
const previousTab = useSelector(getPreviousTab, () => true)
return ( return (
<Tab.Navigator <Tab.Navigator
initialRouteName={ initialRouteName={instanceActive !== -1 ? previousTab : 'Tab-Me'}
localActiveIndex !== null
? useSelector(getPreviousTab, () => true)
: 'Tab-Me'
}
screenOptions={screenOptions} screenOptions={screenOptions}
tabBarOptions={tabBarOptions} tabBarOptions={tabBarOptions}
> >
<Tab.Screen <Tab.Screen name='Tab-Local' component={TabLocal} />
name='Tab-Local'
component={TabLocal}
listeners={localListeners}
/>
<Tab.Screen name='Tab-Public' component={TabPublic} /> <Tab.Screen name='Tab-Public' component={TabPublic} />
<Tab.Screen <Tab.Screen
name='Tab-Compose' name='Tab-Compose'
@ -190,7 +163,6 @@ const ScreenTabs = React.memo(
<Tab.Screen <Tab.Screen
name='Tab-Notifications' name='Tab-Notifications'
component={TabNotifications} component={TabNotifications}
listeners={notificationsListeners}
options={{ options={{
tabBarBadge: localNotification?.latestTime tabBarBadge: localNotification?.latestTime
? !localNotification.readTime || ? !localNotification.readTime ||

View File

@ -3,7 +3,7 @@ import { HeaderCenter, HeaderRight } from '@components/Header'
import Timeline from '@components/Timeline' import Timeline from '@components/Timeline'
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs' import { BottomTabScreenProps } from '@react-navigation/bottom-tabs'
import { ScreenTabsParamList } from '@screens/Tabs' import { ScreenTabsParamList } from '@screens/Tabs'
import { getLocalActiveIndex } from '@utils/slices/instancesSlice' import { getInstanceActive } from '@utils/slices/instancesSlice'
import React, { useCallback, useMemo } from 'react' import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native' import { Platform } from 'react-native'
@ -21,7 +21,7 @@ const Stack = createNativeStackNavigator<Nav.TabLocalStackParamList>()
const TabLocal = React.memo( const TabLocal = React.memo(
({ navigation }: TabLocalProp) => { ({ navigation }: TabLocalProp) => {
const { t } = useTranslation('local') const { t } = useTranslation('local')
const localActiveIndex = useSelector(getLocalActiveIndex) const instanceActive = useSelector(getInstanceActive)
const screenOptions = useMemo( const screenOptions = useMemo(
() => ({ () => ({
@ -49,7 +49,7 @@ const TabLocal = React.memo(
[] []
) )
const children = useCallback( const children = useCallback(
() => (localActiveIndex !== null ? <Timeline page='Following' /> : null), () => (instanceActive !== -1 ? <Timeline page='Following' /> : null),
[] []
) )

View File

@ -12,6 +12,7 @@ import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native' import { Platform } from 'react-native'
import { createNativeStackNavigator } from 'react-native-screens/native-stack' import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import ScreenMeSettingsNotification from './Me/Notification'
const Stack = createNativeStackNavigator<Nav.TabMeStackParamList>() const Stack = createNativeStackNavigator<Nav.TabMeStackParamList>()
@ -114,6 +115,19 @@ const TabMe = React.memo(
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})} })}
/> />
<Stack.Screen
name='Tab-Me-Settings-Notification'
component={ScreenMeSettingsNotification}
options={({ navigation }: any) => ({
headerTitle: t('meSettingsNotification:heading'),
...(Platform.OS === 'android' && {
headerCenter: () => (
<HeaderCenter content={t('meSettingsNotification:heading')} />
)
}),
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})}
/>
<Stack.Screen <Stack.Screen
name='Tab-Me-Switch' name='Tab-Me-Switch'
component={ScreenMeSwitch} component={ScreenMeSwitch}

View File

@ -0,0 +1,37 @@
import { MenuContainer, MenuRow } from '@components/Menu'
import { usePushQuery } from '@utils/queryHooks/push'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { ScrollView } from 'react-native-gesture-handler'
import { useDispatch } from 'react-redux'
const ScreenMeSettingsNotification: React.FC = () => {
const { t } = useTranslation('meSettingsNotification')
const dispatch = useDispatch()
const { data, isLoading } = usePushQuery({})
return (
<ScrollView>
<MenuContainer>
<MenuRow
title={t('content.global.heading')}
description={t('content.global.description')}
// switchValue={notification.enabled}
// switchOnValueChange={() => dispatch(updateNotification(true))}
/>
</MenuContainer>
<MenuContainer>
<MenuRow
title={t('content.follow.heading')}
loading={isLoading}
// switchDisabled={!notification.enabled}
// switchValue={notification.enabled ? data?.alerts.follow : false}
// switchOnValueChange={() => dispatch(updateNotification(true))}
/>
</MenuContainer>
</ScrollView>
)
}
export default ScreenMeSettingsNotification

View File

@ -8,7 +8,7 @@ import AccountNav from '@screens/Tabs/Shared/Account/Nav'
import AccountContext from '@screens/Tabs/Shared/Account/utils/createContext' import AccountContext from '@screens/Tabs/Shared/Account/utils/createContext'
import accountInitialState from '@screens/Tabs/Shared/Account/utils/initialState' import accountInitialState from '@screens/Tabs/Shared/Account/utils/initialState'
import accountReducer from '@screens/Tabs/Shared/Account/utils/reducer' import accountReducer from '@screens/Tabs/Shared/Account/utils/reducer'
import { getLocalActiveIndex } from '@utils/slices/instancesSlice' import { getInstanceActive } from '@utils/slices/instancesSlice'
import React, { useReducer, useRef, useState } from 'react' import React, { useReducer, useRef, useState } from 'react'
import Animated, { import Animated, {
useAnimatedScrollHandler, useAnimatedScrollHandler,
@ -17,7 +17,7 @@ import Animated, {
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
const ScreenMeRoot: React.FC = () => { const ScreenMeRoot: React.FC = () => {
const localActiveIndex = useSelector(getLocalActiveIndex) const instanceActive = useSelector(getInstanceActive)
const scrollRef = useRef<Animated.ScrollView>(null) const scrollRef = useRef<Animated.ScrollView>(null)
useScrollToTop(scrollRef) useScrollToTop(scrollRef)
@ -36,7 +36,7 @@ const ScreenMeRoot: React.FC = () => {
return ( return (
<AccountContext.Provider value={{ accountState, accountDispatch }}> <AccountContext.Provider value={{ accountState, accountDispatch }}>
{localActiveIndex !== null && data ? ( {instanceActive !== -1 && data ? (
<AccountNav scrollY={scrollY} account={data} /> <AccountNav scrollY={scrollY} account={data} />
) : null} ) : null}
<Animated.ScrollView <Animated.ScrollView
@ -45,14 +45,14 @@ const ScreenMeRoot: React.FC = () => {
onScroll={onScroll} onScroll={onScroll}
scrollEventThrottle={16} scrollEventThrottle={16}
> >
{localActiveIndex !== null ? ( {instanceActive !== -1 ? (
<MyInfo setData={setData} /> <MyInfo setData={setData} />
) : ( ) : (
<ComponentInstance /> <ComponentInstance />
)} )}
{localActiveIndex !== null ? <Collections /> : null} {instanceActive !== -1 ? <Collections /> : null}
<Settings /> <Settings />
{localActiveIndex !== null ? <Logout /> : null} {instanceActive !== -1 ? <Logout /> : null}
</Animated.ScrollView> </Animated.ScrollView>
</AccountContext.Provider> </AccountContext.Provider>
) )

View File

@ -1,17 +1,19 @@
import Button from '@components/Button' import Button from '@components/Button'
import haptics from '@root/components/haptics' import haptics from '@root/components/haptics'
import { localRemoveInstance } from '@utils/slices/instancesSlice' import removeInstance from '@utils/slices/instances/remove'
import { getInstanceActive } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native' import { Alert } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
import { useDispatch } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
const Logout: React.FC = () => { const Logout: React.FC = () => {
const { t } = useTranslation('meRoot') const { t } = useTranslation('meRoot')
const dispatch = useDispatch() const dispatch = useDispatch()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const instanceActive = useSelector(getInstanceActive)
return ( return (
<Button <Button
@ -33,7 +35,7 @@ const Logout: React.FC = () => {
onPress: () => { onPress: () => {
haptics('Success') haptics('Success')
queryClient.clear() queryClient.clear()
dispatch(localRemoveInstance()) dispatch(removeInstance(instanceActive))
} }
}, },
{ {

View File

@ -1,7 +1,7 @@
import AccountHeader from '@screens/Tabs/Shared/Account/Header' import AccountHeader from '@screens/Tabs/Shared/Account/Header'
import AccountInformation from '@screens/Tabs/Shared/Account/Information' import AccountInformation from '@screens/Tabs/Shared/Account/Information'
import { useAccountQuery } from '@utils/queryHooks/account' import { useAccountQuery } from '@utils/queryHooks/account'
import { getLocalAccount } from '@utils/slices/instancesSlice' import { getInstanceAccount } from '@utils/slices/instancesSlice'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
@ -10,11 +10,11 @@ export interface Props {
} }
const MyInfo: React.FC<Props> = ({ setData }) => { const MyInfo: React.FC<Props> = ({ setData }) => {
const localAccount = useSelector( const instanceAccount = useSelector(
getLocalAccount, getInstanceAccount,
(prev, next) => prev?.id === next?.id (prev, next) => prev?.id === next?.id
) )
const { data } = useAccountQuery({ id: localAccount!.id }) const { data } = useAccountQuery({ id: instanceAccount!.id })
useEffect(() => { useEffect(() => {
if (data) { if (data) {

View File

@ -1,17 +1,12 @@
import { MenuContainer, MenuRow } from '@components/Menu' import { MenuContainer, MenuRow } from '@components/Menu'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { getLocalUrl } from '@utils/slices/instancesSlice'
import * as WebBrowser from 'expo-web-browser'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
const Settings: React.FC = () => { const Settings: React.FC = () => {
const { t } = useTranslation('meRoot') const { t } = useTranslation('meRoot')
const navigation = useNavigation() const navigation = useNavigation()
const localUrl = useSelector(getLocalUrl)
return ( return (
<MenuContainer> <MenuContainer>
{/* <MenuRow {/* <MenuRow

View File

@ -2,7 +2,9 @@ import analytics from '@components/analytics'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { MenuContainer, MenuRow } from '@components/Menu' import { MenuContainer, MenuRow } from '@components/Menu'
import { useActionSheet } from '@expo/react-native-action-sheet' import { useActionSheet } from '@expo/react-native-action-sheet'
import { useNavigation } from '@react-navigation/native'
import i18n from '@root/i18n/i18n' import i18n from '@root/i18n/i18n'
import { getInstanceActive } from '@utils/slices/instancesSlice'
import { import {
changeBrowser, changeBrowser,
changeLanguage, changeLanguage,
@ -17,17 +19,28 @@ import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
const SettingsApp: React.FC = () => { const SettingsApp: React.FC = () => {
const navigation = useNavigation()
const dispatch = useDispatch() const dispatch = useDispatch()
const { showActionSheetWithOptions } = useActionSheet() const { showActionSheetWithOptions } = useActionSheet()
const { setTheme } = useTheme() const { setTheme } = useTheme()
const { t } = useTranslation('meSettings') const { t } = useTranslation('meSettings')
const instanceActive = useSelector(getInstanceActive)
const settingsLanguage = useSelector(getSettingsLanguage) const settingsLanguage = useSelector(getSettingsLanguage)
const settingsTheme = useSelector(getSettingsTheme) const settingsTheme = useSelector(getSettingsTheme)
const settingsBrowser = useSelector(getSettingsBrowser) const settingsBrowser = useSelector(getSettingsBrowser)
return ( return (
<MenuContainer> <MenuContainer>
{instanceActive !== -1 ? (
<MenuRow
title={t('content.notification.heading')}
iconBack='ChevronRight'
onPress={() => {
navigation.navigate('Tab-Me-Settings-Notification')
}}
/>
) : null}
<MenuRow <MenuRow
title={t('content.language.heading')} title={t('content.language.heading')}
content={t(`content.language.options.${settingsLanguage}`)} content={t(`content.language.options.${settingsLanguage}`)}

View File

@ -2,39 +2,36 @@ import Button from '@components/Button'
import { MenuContainer, MenuRow } from '@components/Menu' import { MenuContainer, MenuRow } from '@components/Menu'
import { useActionSheet } from '@expo/react-native-action-sheet' import { useActionSheet } from '@expo/react-native-action-sheet'
import { persistor } from '@root/store' import { persistor } from '@root/store'
import { import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
getLocalActiveIndex,
getLocalInstances
} from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import React from 'react' import React from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
const SettingsDev: React.FC = () => { const SettingsDev: React.FC = () => {
const { showActionSheetWithOptions } = useActionSheet() const { showActionSheetWithOptions } = useActionSheet()
const localActiveIndex = useSelector(getLocalActiveIndex) const instanceActive = useSelector(getInstanceActive)
const localInstances = useSelector(getLocalInstances) const instances = useSelector(getInstances)
return ( return (
<MenuContainer> <MenuContainer>
<MenuRow <MenuRow
title={'Local active index'} title={'Local active index'}
content={typeof localActiveIndex + ' - ' + localActiveIndex} content={typeof instanceActive + ' - ' + instanceActive}
onPress={() => {}} onPress={() => {}}
/> />
<MenuRow <MenuRow
title={'Saved local instances'} title={'Saved local instances'}
content={localInstances.length.toString()} content={instances.length.toString()}
iconBack='ChevronRight' iconBack='ChevronRight'
onPress={() => onPress={() =>
showActionSheetWithOptions( showActionSheetWithOptions(
{ {
options: localInstances options: instances
.map(instance => { .map(instance => {
return instance.url + ': ' + instance.account.id return instance.url + ': ' + instance.account.id
}) })
.concat(['Cancel']), .concat(['Cancel']),
cancelButtonIndex: localInstances.length cancelButtonIndex: instances.length
}, },
buttonIndex => {} buttonIndex => {}
) )

View File

@ -3,7 +3,6 @@ import Icon from '@components/Icon'
import { MenuContainer, MenuRow } from '@components/Menu' import { MenuContainer, MenuRow } from '@components/Menu'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { useSearchQuery } from '@utils/queryHooks/search' import { useSearchQuery } from '@utils/queryHooks/search'
import { getLocalActiveIndex } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import * as Updates from 'expo-updates' import * as Updates from 'expo-updates'
@ -13,16 +12,17 @@ import * as WebBrowser from 'expo-web-browser'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { getInstanceActive } from '@utils/slices/instancesSlice'
const SettingsTooot: React.FC = () => { const SettingsTooot: React.FC = () => {
const localActiveIndex = useSelector(getLocalActiveIndex) const instanceActive = useSelector(getInstanceActive)
const navigation = useNavigation() const navigation = useNavigation()
const { theme } = useTheme() const { theme } = useTheme()
const { t } = useTranslation('meSettings') const { t } = useTranslation('meSettings')
const { isLoading, data } = useSearchQuery({ const { isLoading, data } = useSearchQuery({
term: '@tooot@xmflsct.com', term: '@tooot@xmflsct.com',
options: { enabled: localActiveIndex !== null } options: { enabled: instanceActive !== -1 }
}) })
return ( return (

View File

@ -4,10 +4,10 @@ import haptics from '@components/haptics'
import ComponentInstance from '@components/Instance' import ComponentInstance from '@components/Instance'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { import {
getLocalActiveIndex, getInstanceActive,
getLocalInstances, getInstances,
InstanceLocal, Instance,
updateLocalActiveIndex updateInstanceActive
} from '@utils/slices/instancesSlice' } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
@ -25,7 +25,7 @@ import { useQueryClient } from 'react-query'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
interface Props { interface Props {
instance: InstanceLocal instance: Instance
disabled?: boolean disabled?: boolean
} }
@ -45,7 +45,7 @@ const AccountButton: React.FC<Props> = ({ instance, disabled = false }) => {
onPress={() => { onPress={() => {
haptics('Light') haptics('Light')
analytics('switch_existing_press') analytics('switch_existing_press')
dispatch(updateLocalActiveIndex(instance)) dispatch(updateInstanceActive(instance))
queryClient.clear() queryClient.clear()
navigation.goBack() navigation.goBack()
}} }}
@ -56,8 +56,8 @@ const AccountButton: React.FC<Props> = ({ instance, disabled = false }) => {
const ScreenMeSwitchRoot: React.FC = () => { const ScreenMeSwitchRoot: React.FC = () => {
const { t } = useTranslation('meSwitch') const { t } = useTranslation('meSwitch')
const { theme } = useTheme() const { theme } = useTheme()
const localInstances = useSelector(getLocalInstances) const instances = useSelector(getInstances)
const localActiveIndex = useSelector(getLocalActiveIndex) const instanceActive = useSelector(getInstanceActive)
return ( return (
<KeyboardAvoidingView <KeyboardAvoidingView
@ -72,8 +72,8 @@ const ScreenMeSwitchRoot: React.FC = () => {
{t('content.existing')} {t('content.existing')}
</Text> </Text>
<View style={styles.accountButtons}> <View style={styles.accountButtons}>
{localInstances.length {instances.length
? localInstances ? instances
.slice() .slice()
.sort((a, b) => .sort((a, b) =>
`${a.uri}${a.account.acct}`.localeCompare( `${a.uri}${a.account.acct}`.localeCompare(
@ -81,7 +81,7 @@ const ScreenMeSwitchRoot: React.FC = () => {
) )
) )
.map((instance, index) => { .map((instance, index) => {
const localAccount = localInstances[localActiveIndex!] const localAccount = instances[instanceActive!]
return ( return (
<AccountButton <AccountButton
key={index} key={index}

View File

@ -1,7 +1,7 @@
import { HeaderCenter } from '@components/Header' import { HeaderCenter } from '@components/Header'
import Timeline from '@components/Timeline' import Timeline from '@components/Timeline'
import sharedScreens from '@screens/Tabs/Shared/sharedScreens' import sharedScreens from '@screens/Tabs/Shared/sharedScreens'
import { updateLocalNotification } from '@utils/slices/instancesSlice' import { updateInstanceNotification } from '@utils/slices/instancesSlice'
import React, { useCallback, useMemo } from 'react' import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Platform, ViewToken } from 'react-native' import { Platform, ViewToken } from 'react-native'
@ -46,7 +46,7 @@ const TabNotifications = React.memo(
viewableItems[0].index === 0 viewableItems[0].index === 0
) { ) {
dispatch( dispatch(
updateLocalNotification({ updateInstanceNotification({
readTime: viewableItems[0].item.created_at readTime: viewableItems[0].item.created_at
}) })
) )

View File

@ -4,7 +4,7 @@ import Timeline from '@components/Timeline'
import SegmentedControl from '@react-native-community/segmented-control' import SegmentedControl from '@react-native-community/segmented-control'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import sharedScreens from '@screens/Tabs/Shared/sharedScreens' import sharedScreens from '@screens/Tabs/Shared/sharedScreens'
import { getLocalActiveIndex } from '@utils/slices/instancesSlice' import { getInstanceActive } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useMemo, useState } from 'react' import React, { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -21,7 +21,7 @@ const TabPublic = React.memo(
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
const { mode } = useTheme() const { mode } = useTheme()
const navigation = useNavigation() const navigation = useNavigation()
const localActiveIndex = useSelector(getLocalActiveIndex) const instanceActive = useSelector(getInstanceActive)
const [segment, setSegment] = useState(0) const [segment, setSegment] = useState(0)
const pages: { title: string; page: App.Pages }[] = [ const pages: { title: string; page: App.Pages }[] = [
@ -74,9 +74,9 @@ const TabPublic = React.memo(
key: App.Pages key: App.Pages
} }
}) => { }) => {
return localActiveIndex !== null && <Timeline page={route.key} /> return instanceActive !== -1 && <Timeline page={route.key} />
}, },
[localActiveIndex] [instanceActive]
) )
const children = useCallback( const children = useCallback(
() => ( () => (

View File

@ -1,4 +1,4 @@
import { getLocalAccount } from '@utils/slices/instancesSlice' import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
@ -23,7 +23,7 @@ export interface Props {
const AccountInformation: React.FC<Props> = ({ account, myInfo = false }) => { const AccountInformation: React.FC<Props> = ({ account, myInfo = false }) => {
const ownAccount = const ownAccount =
account?.id === account?.id ===
useSelector(getLocalAccount, (prev, next) => prev?.id === next?.id)?.id useSelector(getInstanceAccount, (prev, next) => prev?.id === next?.id)?.id
const { mode, theme } = useTheme() const { mode, theme } = useTheme()
const animation = useCallback( const animation = useCallback(

View File

@ -1,5 +1,8 @@
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { getLocalAccount, getLocalUri } from '@utils/slices/instancesSlice' import {
getInstanceAccount,
getInstanceUri
} from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
@ -14,11 +17,11 @@ export interface Props {
const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => { const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => {
const { theme } = useTheme() const { theme } = useTheme()
const localAccount = useSelector( const instanceAccount = useSelector(
getLocalAccount, getInstanceAccount,
(prev, next) => prev?.acct === next?.acct (prev, next) => prev?.acct === next?.acct
) )
const localUri = useSelector(getLocalUri) const instanceUri = useSelector(getInstanceUri)
const movedStyle = useMemo( const movedStyle = useMemo(
() => () =>
@ -45,7 +48,7 @@ const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => {
} }
}, [account?.moved]) }, [account?.moved])
if (account || (myInfo && localAccount !== undefined)) { if (account || (myInfo && instanceAccount)) {
return ( return (
<View <View
style={[styles.base, { flexDirection: 'row', alignItems: 'center' }]} style={[styles.base, { flexDirection: 'row', alignItems: 'center' }]}
@ -60,8 +63,8 @@ const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => {
]} ]}
selectable selectable
> >
@{myInfo ? localAccount?.acct : account?.acct} @{myInfo ? instanceAccount.acct : account?.acct}
{myInfo ? `@${localUri}` : null} {myInfo ? `@${instanceUri}` : null}
</Text> </Text>
{movedContent} {movedContent}
{account?.locked ? ( {account?.locked ? (

View File

@ -1,9 +1,10 @@
import client from '@api/client' import apiInstance from '@api/instance'
import NetInfo from '@react-native-community/netinfo' import NetInfo from '@react-native-community/netinfo'
import { store } from '@root/store' import { store } from '@root/store'
import removeInstance from '@utils/slices/instances/remove'
import { import {
localRemoveInstance, getInstanceActive,
updateLocalAccount updateInstanceAccount
} from '@utils/slices/instancesSlice' } from '@utils/slices/instancesSlice'
import log from './log' import log from './log'
@ -13,29 +14,28 @@ const netInfo = async (): Promise<{
}> => { }> => {
log('log', 'netInfo', 'initializing') log('log', 'netInfo', 'initializing')
const netInfo = await NetInfo.fetch() const netInfo = await NetInfo.fetch()
const activeIndex = store.getState().instances.local?.activeIndex const activeIndex = getInstanceActive(store.getState())
if (netInfo.isConnected) { if (netInfo.isConnected) {
log('log', 'netInfo', 'network connected') log('log', 'netInfo', 'network connected')
if (activeIndex !== null) { if (activeIndex !== -1) {
log('log', 'netInfo', 'checking locally stored credentials') log('log', 'netInfo', 'checking locally stored credentials')
return client<Mastodon.Account>({ return apiInstance<Mastodon.Account>({
method: 'get', method: 'get',
instance: 'local',
url: `accounts/verify_credentials` url: `accounts/verify_credentials`
}) })
.then(res => { .then(res => {
log('log', 'netInfo', 'local credential check passed') log('log', 'netInfo', 'local credential check passed')
if ( if (
res.body.id !== res.body.id !==
store.getState().instances.local?.instances[activeIndex].account.id store.getState().instances.instances[activeIndex].account.id
) { ) {
log('error', 'netInfo', 'local id does not match remote id') log('error', 'netInfo', 'local id does not match remote id')
store.dispatch(localRemoveInstance(activeIndex)) store.dispatch(removeInstance(activeIndex))
return Promise.resolve({ connected: true, corruputed: '' }) return Promise.resolve({ connected: true, corruputed: '' })
} else { } else {
store.dispatch( store.dispatch(
updateLocalAccount({ updateInstanceAccount({
acct: res.body.acct, acct: res.body.acct,
avatarStatic: res.body.avatar_static avatarStatic: res.body.avatar_static
}) })
@ -50,7 +50,7 @@ const netInfo = async (): Promise<{
typeof error.status === 'number' && typeof error.status === 'number' &&
error.status === 401 error.status === 401
) { ) {
store.dispatch(localRemoveInstance(activeIndex)) store.dispatch(removeInstance(activeIndex))
} }
return Promise.resolve({ return Promise.resolve({
connected: true, connected: true,

View File

@ -6,9 +6,9 @@ import {
getDefaultMiddleware getDefaultMiddleware
} from '@reduxjs/toolkit' } from '@reduxjs/toolkit'
import contextsSlice from '@utils/slices/contextsSlice' import contextsSlice from '@utils/slices/contextsSlice'
import instancesSlice, { InstancesState } from '@utils/slices/instancesSlice' import instancesSlice from '@utils/slices/instancesSlice'
import settingsSlice from '@utils/slices/settingsSlice' import settingsSlice from '@utils/slices/settingsSlice'
import { createMigrate, persistReducer, persistStore } from 'redux-persist' import { persistReducer, persistStore } from 'redux-persist'
const secureStorage = createSecureStore() const secureStorage = createSecureStore()
@ -20,43 +20,10 @@ const contextsPersistConfig = {
storage: AsyncStorage storage: AsyncStorage
} }
const instancesMigration = {
2: (state: InstancesState) => {
return {
...state,
local: {
...state.local,
instances: state.local.instances.map(instance => {
instance.max_toot_chars = 500
instance.drafts = []
return instance
})
}
}
},
3: (state: InstancesState) => {
return {
...state,
local: {
...state.local,
instances: state.local.instances.map(instance => {
if (!instance.urls) {
instance.urls = {
streaming_api: `wss://${instance.url}`
}
}
return instance
})
}
}
}
}
const instancesPersistConfig = { const instancesPersistConfig = {
key: 'instances', key: 'instances',
prefix, prefix,
version: 3, storage: secureStorage
storage: secureStorage,
migrate: createMigrate(instancesMigration, { debug: true })
} }
const settingsPersistConfig = { const settingsPersistConfig = {

View File

@ -1,4 +1,4 @@
import client from '@api/client' import apiInstance from '@api/instance'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { useQuery, UseQueryOptions } from 'react-query' import { useQuery, UseQueryOptions } from 'react-query'
@ -7,9 +7,8 @@ export type QueryKey = ['Account', { id: Mastodon.Account['id'] }]
const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => { const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
const { id } = queryKey[1] const { id } = queryKey[1]
return client<Mastodon.Account>({ return apiInstance<Mastodon.Account>({
method: 'get', method: 'get',
instance: 'local',
url: `accounts/${id}` url: `accounts/${id}`
}).then(res => res.body) }).then(res => res.body)
} }

View File

@ -1,35 +0,0 @@
import client from '@api/client'
import { InstancesState } from '@utils/slices/instancesSlice'
import { AxiosError } from 'axios'
import { useQuery, UseQueryOptions } from 'react-query'
export type QueryKey = [
'AccountCheck',
{
id: Mastodon.Account['id']
index: NonNullable<InstancesState['local']['activeIndex']>
}
]
const queryFunction = async ({ queryKey }: { queryKey: QueryKey }) => {
const { id, index } = queryKey[1]
return client<Mastodon.Account>({
method: 'get',
instance: 'local',
localIndex: index,
url: `accounts/${id}`
}).then(res => res.body)
}
const useAccountCheckQuery = <TData = Mastodon.Account>({
options,
...queryKeyParams
}: QueryKey[1] & {
options?: UseQueryOptions<Mastodon.Account, AxiosError, TData>
}) => {
const queryKey: QueryKey = ['AccountCheck', { ...queryKeyParams }]
return useQuery(queryKey, queryFunction, options)
}
export { useAccountCheckQuery }

View File

@ -1,4 +1,4 @@
import client from '@api/client' import apiInstance from '@api/instance'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { import {
useMutation, useMutation,
@ -12,9 +12,8 @@ type QueryKeyAnnouncement = ['Announcements', { showAll?: boolean }]
const queryFunction = ({ queryKey }: { queryKey: QueryKeyAnnouncement }) => { const queryFunction = ({ queryKey }: { queryKey: QueryKeyAnnouncement }) => {
const { showAll } = queryKey[1] const { showAll } = queryKey[1]
return client<Mastodon.Announcement[]>({ return apiInstance<Mastodon.Announcement[]>({
method: 'get', method: 'get',
instance: 'local',
url: `announcements`, url: `announcements`,
...(showAll && { ...(showAll && {
params: { params: {
@ -52,15 +51,13 @@ const mutationFunction = async ({
}: MutationVarsAnnouncement) => { }: MutationVarsAnnouncement) => {
switch (type) { switch (type) {
case 'reaction': case 'reaction':
return client<{}>({ return apiInstance<{}>({
method: me ? 'delete' : 'put', method: me ? 'delete' : 'put',
instance: 'local',
url: `announcements/${id}/reactions/${name}` url: `announcements/${id}/reactions/${name}`
}) })
case 'dismiss': case 'dismiss':
return client<{}>({ return apiInstance<{}>({
method: 'post', method: 'post',
instance: 'local',
url: `announcements/${id}/dismiss` url: `announcements/${id}/dismiss`
}) })
} }

View File

@ -1,9 +1,9 @@
import client from '@api/client' import apiGeneral from '@api/general'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import * as AuthSession from 'expo-auth-session' import * as AuthSession from 'expo-auth-session'
import { useQuery, UseQueryOptions } from 'react-query' import { useQuery, UseQueryOptions } from 'react-query'
export type QueryKey = ['Apps', { instanceDomain?: string }] export type QueryKey = ['Apps', { domain?: string }]
const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => { const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
const redirectUri = AuthSession.makeRedirectUri({ const redirectUri = AuthSession.makeRedirectUri({
@ -11,7 +11,7 @@ const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
useProxy: false useProxy: false
}) })
const { instanceDomain } = queryKey[1] const { domain } = queryKey[1]
const formData = new FormData() const formData = new FormData()
formData.append('client_name', 'tooot') formData.append('client_name', 'tooot')
@ -19,11 +19,10 @@ const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
formData.append('redirect_uris', redirectUri) formData.append('redirect_uris', redirectUri)
formData.append('scopes', 'read write follow push') formData.append('scopes', 'read write follow push')
return client<Mastodon.Apps>({ return apiGeneral<Mastodon.Apps>({
method: 'post', method: 'post',
instance: 'remote', domain: domain || '',
instanceDomain, url: `api/v1/apps`,
url: `apps`,
body: formData body: formData
}).then(res => res.body) }).then(res => res.body)
} }

View File

@ -1,13 +1,12 @@
import client from '@api/client' import apiInstance from '@api/instance'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { useQuery, UseQueryOptions } from 'react-query' import { useQuery, UseQueryOptions } from 'react-query'
type QueryKey = ['Emojis'] type QueryKey = ['Emojis']
const queryFunction = () => { const queryFunction = () => {
return client<Mastodon.Emoji[]>({ return apiInstance<Mastodon.Emoji[]>({
method: 'get', method: 'get',
instance: 'local',
url: 'custom_emojis' url: 'custom_emojis'
}).then(res => res.body) }).then(res => res.body)
} }

View File

@ -1,18 +1,21 @@
import client from '@api/client' import apiGeneral from '@api/general'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { useQuery, UseQueryOptions } from 'react-query' import { useQuery, UseQueryOptions } from 'react-query'
export type QueryKey = ['Instance', { instanceDomain?: string }] export type QueryKey = ['Instance', { domain?: string }]
const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => { const queryFunction = async ({ queryKey }: { queryKey: QueryKey }) => {
const { instanceDomain } = queryKey[1] const { domain } = queryKey[1]
if (!domain) {
return Promise.reject()
}
return client<Mastodon.Instance>({ const res = await apiGeneral<Mastodon.Instance>({
method: 'get', method: 'get',
instance: 'remote', domain: domain,
instanceDomain, url: `api/v1/instance`
url: `instance` })
}).then(res => res.body) return res.body
} }
const useInstanceQuery = < const useInstanceQuery = <

View File

@ -1,13 +1,12 @@
import client from '@api/client' import apiInstance from '@api/instance'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { useQuery, UseQueryOptions } from 'react-query' import { useQuery, UseQueryOptions } from 'react-query'
export type QueryKey = ['Lists'] export type QueryKey = ['Lists']
const queryFunction = () => { const queryFunction = () => {
return client<Mastodon.List[]>({ return apiInstance<Mastodon.List[]>({
method: 'get', method: 'get',
instance: 'local',
url: 'lists' url: 'lists'
}).then(res => res.body) }).then(res => res.body)
} }

View File

@ -0,0 +1,24 @@
import apiInstance from '@api/instance'
import { AxiosError } from 'axios'
import { useQuery, UseQueryOptions } from 'react-query'
export type QueryKey = ['Push']
const queryFunction = async () => {
const res = await apiInstance<Mastodon.PushSubscription>({
method: 'get',
url: 'push/subscription'
})
return res.body
}
const usePushQuery = <TData = Mastodon.PushSubscription>({
options
}: {
options?: UseQueryOptions<Mastodon.PushSubscription, AxiosError, TData>
}) => {
const queryKey: QueryKey = ['Push']
return useQuery(queryKey, queryFunction, options)
}
export { usePushQuery }

View File

@ -1,4 +1,4 @@
import client from '@api/client' import apiInstance from '@api/instance'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { import {
useMutation, useMutation,
@ -15,9 +15,8 @@ export type QueryKeyRelationship = [
const queryFunction = ({ queryKey }: { queryKey: QueryKeyRelationship }) => { const queryFunction = ({ queryKey }: { queryKey: QueryKeyRelationship }) => {
const { id } = queryKey[1] const { id } = queryKey[1]
return client<Mastodon.Relationship[]>({ return apiInstance<Mastodon.Relationship[]>({
method: 'get', method: 'get',
instance: 'local',
url: `accounts/relationships`, url: `accounts/relationships`,
params: { params: {
'id[]': id 'id[]': id
@ -57,15 +56,13 @@ type MutationVarsRelationship =
const mutationFunction = async (params: MutationVarsRelationship) => { const mutationFunction = async (params: MutationVarsRelationship) => {
switch (params.type) { switch (params.type) {
case 'incoming': case 'incoming':
return client<Mastodon.Relationship>({ return apiInstance<Mastodon.Relationship>({
method: 'post', method: 'post',
instance: 'local',
url: `follow_requests/${params.id}/${params.payload.action}` url: `follow_requests/${params.id}/${params.payload.action}`
}).then(res => res.body) }).then(res => res.body)
case 'outgoing': case 'outgoing':
return client<Mastodon.Relationship>({ return apiInstance<Mastodon.Relationship>({
method: 'post', method: 'post',
instance: 'local',
url: `accounts/${params.id}/${params.payload.state ? 'un' : ''}${ url: `accounts/${params.id}/${params.payload.state ? 'un' : ''}${
params.payload.action params.payload.action
}` }`

View File

@ -1,4 +1,4 @@
import client from '@api/client' import apiInstance from '@api/instance'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { useInfiniteQuery, UseInfiniteQueryOptions } from 'react-query' import { useInfiniteQuery, UseInfiniteQueryOptions } from 'react-query'
@ -17,9 +17,8 @@ const queryFunction = ({
const { type, id } = queryKey[1] const { type, id } = queryKey[1]
let params: { [key: string]: string } = { ...pageParam } let params: { [key: string]: string } = { ...pageParam }
return client<Mastodon.Account[]>({ return apiInstance<Mastodon.Account[]>({
method: 'get', method: 'get',
instance: 'local',
url: `accounts/${id}/${type}`, url: `accounts/${id}/${type}`,
params params
}) })

View File

@ -1,4 +1,4 @@
import client from '@api/client' import apiInstance from '@api/instance'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { useQuery, UseQueryOptions } from 'react-query' import { useQuery, UseQueryOptions } from 'react-query'
@ -19,10 +19,9 @@ type SearchResult = {
const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => { const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
const { type, term, limit = 20 } = queryKey[1] const { type, term, limit = 20 } = queryKey[1]
return client<SearchResult>({ return apiInstance<SearchResult>({
version: 'v2', version: 'v2',
method: 'get', method: 'get',
instance: 'local',
url: 'search', url: 'search',
params: { ...(type && { type }), ...(term && { q: term }), limit } params: { ...(type && { type }), ...(term && { q: term }), limit }
}).then(res => res.body) }).then(res => res.body)

View File

@ -1,4 +1,4 @@
import client from '@api/client' import apiInstance from '@api/instance'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { uniqBy } from 'lodash' import { uniqBy } from 'lodash'
@ -35,17 +35,15 @@ const queryFunction = ({
switch (page) { switch (page) {
case 'Following': case 'Following':
return client<Mastodon.Status[]>({ return apiInstance<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local',
url: 'timelines/home', url: 'timelines/home',
params params
}) })
case 'Local': case 'Local':
return client<Mastodon.Status[]>({ return apiInstance<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local',
url: 'timelines/public', url: 'timelines/public',
params: { params: {
...params, ...params,
@ -54,26 +52,23 @@ const queryFunction = ({
}) })
case 'LocalPublic': case 'LocalPublic':
return client<Mastodon.Status[]>({ return apiInstance<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local',
url: 'timelines/public', url: 'timelines/public',
params params
}) })
case 'Notifications': case 'Notifications':
return client<Mastodon.Notification[]>({ return apiInstance<Mastodon.Notification[]>({
method: 'get', method: 'get',
instance: 'local',
url: 'notifications', url: 'notifications',
params params
}) })
case 'Account_Default': case 'Account_Default':
if (pageParam && pageParam.hasOwnProperty('max_id')) { if (pageParam && pageParam.hasOwnProperty('max_id')) {
return client<Mastodon.Status[]>({ return apiInstance<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local',
url: `accounts/${account}/statuses`, url: `accounts/${account}/statuses`,
params: { params: {
exclude_replies: 'true', exclude_replies: 'true',
@ -81,9 +76,8 @@ const queryFunction = ({
} }
}) })
} else { } else {
return client<(Mastodon.Status & { isPinned: boolean })[]>({ return apiInstance<(Mastodon.Status & { isPinned: boolean })[]>({
method: 'get', method: 'get',
instance: 'local',
url: `accounts/${account}/statuses`, url: `accounts/${account}/statuses`,
params: { params: {
pinned: 'true' pinned: 'true'
@ -91,9 +85,8 @@ const queryFunction = ({
}).then(async res1 => { }).then(async res1 => {
let pinned: Mastodon.Status['id'][] = [] let pinned: Mastodon.Status['id'][] = []
res1.body.forEach(status => pinned.push(status.id)) res1.body.forEach(status => pinned.push(status.id))
const res2 = await client<Mastodon.Status[]>({ const res2 = await apiInstance<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local',
url: `accounts/${account}/statuses`, url: `accounts/${account}/statuses`,
params: { params: {
exclude_replies: 'true' exclude_replies: 'true'
@ -108,17 +101,15 @@ const queryFunction = ({
} }
case 'Account_All': case 'Account_All':
return client<Mastodon.Status[]>({ return apiInstance<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local',
url: `accounts/${account}/statuses`, url: `accounts/${account}/statuses`,
params params
}) })
case 'Account_Attachments': case 'Account_Attachments':
return client<Mastodon.Status[]>({ return apiInstance<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local',
url: `accounts/${account}/statuses`, url: `accounts/${account}/statuses`,
params: { params: {
only_media: 'true', only_media: 'true',
@ -127,57 +118,50 @@ const queryFunction = ({
}) })
case 'Hashtag': case 'Hashtag':
return client<Mastodon.Status[]>({ return apiInstance<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local',
url: `timelines/tag/${hashtag}`, url: `timelines/tag/${hashtag}`,
params params
}) })
case 'Conversations': case 'Conversations':
return client<Mastodon.Conversation[]>({ return apiInstance<Mastodon.Conversation[]>({
method: 'get', method: 'get',
instance: 'local',
url: `conversations`, url: `conversations`,
params params
}) })
case 'Bookmarks': case 'Bookmarks':
return client<Mastodon.Status[]>({ return apiInstance<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local',
url: `bookmarks`, url: `bookmarks`,
params params
}) })
case 'Favourites': case 'Favourites':
return client<Mastodon.Status[]>({ return apiInstance<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local',
url: `favourites`, url: `favourites`,
params params
}) })
case 'List': case 'List':
return client<Mastodon.Status[]>({ return apiInstance<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local',
url: `timelines/list/${list}`, url: `timelines/list/${list}`,
params params
}) })
case 'Toot': case 'Toot':
return client<Mastodon.Status>({ return apiInstance<Mastodon.Status>({
method: 'get', method: 'get',
instance: 'local',
url: `statuses/${toot}` url: `statuses/${toot}`
}).then(async res1 => { }).then(async res1 => {
const res2 = await client<{ const res2 = await apiInstance<{
ancestors: Mastodon.Status[] ancestors: Mastodon.Status[]
descendants: Mastodon.Status[] descendants: Mastodon.Status[]
}>({ }>({
method: 'get', method: 'get',
instance: 'local',
url: `statuses/${toot}/context` url: `statuses/${toot}/context`
}) })
return { return {
@ -296,9 +280,8 @@ const mutationFunction = async (params: MutationVarsTimeline) => {
} }
}) })
return client<Mastodon.Poll>({ return apiInstance<Mastodon.Poll>({
method: params.payload.type === 'vote' ? 'post' : 'get', method: params.payload.type === 'vote' ? 'post' : 'get',
instance: 'local',
url: url:
params.payload.type === 'vote' params.payload.type === 'vote'
? `polls/${params.payload.id}/votes` ? `polls/${params.payload.id}/votes`
@ -306,9 +289,8 @@ const mutationFunction = async (params: MutationVarsTimeline) => {
...(params.payload.type === 'vote' && { body: formData }) ...(params.payload.type === 'vote' && { body: formData })
}) })
default: default:
return client<Mastodon.Status>({ return apiInstance<Mastodon.Status>({
method: 'post', method: 'post',
instance: 'local',
url: `statuses/${params.id}/${ url: `statuses/${params.id}/${
params.payload.currentValue ? 'un' : '' params.payload.currentValue ? 'un' : ''
}${MapPropertyToUrl[params.payload.property]}` }${MapPropertyToUrl[params.payload.property]}`
@ -318,15 +300,13 @@ const mutationFunction = async (params: MutationVarsTimeline) => {
switch (params.payload.property) { switch (params.payload.property) {
case 'block': case 'block':
case 'mute': case 'mute':
return client<Mastodon.Account>({ return apiInstance<Mastodon.Account>({
method: 'post', method: 'post',
instance: 'local',
url: `accounts/${params.id}/${params.payload.property}` url: `accounts/${params.id}/${params.payload.property}`
}) })
case 'reports': case 'reports':
return client<Mastodon.Account>({ return apiInstance<Mastodon.Account>({
method: 'post', method: 'post',
instance: 'local',
url: `reports`, url: `reports`,
params: { params: {
account_id: params.id account_id: params.id
@ -334,15 +314,13 @@ const mutationFunction = async (params: MutationVarsTimeline) => {
}) })
} }
case 'deleteItem': case 'deleteItem':
return client<Mastodon.Conversation>({ return apiInstance<Mastodon.Conversation>({
method: 'delete', method: 'delete',
instance: 'local',
url: `${params.source}/${params.id}` url: `${params.source}/${params.id}`
}) })
case 'domainBlock': case 'domainBlock':
return client<any>({ return apiInstance<any>({
method: 'post', method: 'post',
instance: 'local',
url: `domain_blocks`, url: `domain_blocks`,
params: { params: {
domain: params.domain domain: params.domain

View File

@ -32,11 +32,11 @@ export const contextsInitialState = {
current: 0, current: 0,
hidden: false hidden: false
}, },
previousTab: 'Tab-Local' previousTab: 'Tab-Me'
} }
const contextsSlice = createSlice({ const contextsSlice = createSlice({
name: 'settings', name: 'contexts',
initialState: contextsInitialState as ContextsState, initialState: contextsInitialState as ContextsState,
reducers: { reducers: {
updateStoreReview: (state, action: PayloadAction<1>) => { updateStoreReview: (state, action: PayloadAction<1>) => {

View File

@ -0,0 +1,87 @@
import apiGeneral from '@api/general'
import { createAsyncThunk } from '@reduxjs/toolkit'
import { RootState } from '@root/store'
import { Instance } from '../instancesSlice'
const addInstance = createAsyncThunk(
'instances/add',
async ({
domain,
token,
instance,
max_toot_chars = 500,
appData
}: {
domain: Instance['url']
token: Instance['token']
instance: Mastodon.Instance
max_toot_chars?: number
appData: Instance['appData']
}): Promise<{ type: 'add' | 'overwrite'; data: Instance }> => {
const { store } = require('@root/store')
const instances = (store.getState() as RootState).instances.instances
const {
body: { id, acct, avatar_static }
} = await apiGeneral<Mastodon.Account>({
method: 'get',
domain,
url: `api/v1/accounts/verify_credentials`,
headers: { Authorization: `Bearer ${token}` }
})
let type: 'add' | 'overwrite'
type = 'add'
if (
instances.length &&
instances.filter(instance => {
if (instance.url === domain && instance.account.id === id) {
return true
} else {
return false
}
}).length
) {
type = 'overwrite'
} else {
type = 'add'
}
const { body: preferences } = await apiGeneral<Mastodon.Preferences>({
method: 'get',
domain,
url: `api/v1/preferences`,
headers: { Authorization: `Bearer ${token}` }
})
return Promise.resolve({
type,
data: {
active: true,
appData,
url: domain,
token,
uri: instance.uri,
urls: instance.urls,
max_toot_chars,
account: {
id,
acct,
avatarStatic: avatar_static,
preferences
},
notification: {
readTime: undefined,
latestTime: undefined
},
push: {
loading: false,
enabled: false
},
drafts: []
}
})
}
)
export default addInstance

View File

@ -0,0 +1,21 @@
import apiGeneral from '@api/general'
import * as Notifications from 'expo-notifications'
const serverUnregister = async () => {
const deviceToken = (await Notifications.getDevicePushTokenAsync()).data
return apiGeneral<{ endpoint: string; publicKey: string; auth: string }>({
method: 'post',
domain: 'testpush.home.xmflsct.com',
url: 'unregister',
body: { deviceToken }
})
}
const pushDisable = async () => {
await serverUnregister()
return false
}
export default pushDisable

View File

@ -0,0 +1,61 @@
import apiGeneral from '@api/general'
import apiInstance from '@api/instance'
import * as Notifications from 'expo-notifications'
import { Platform } from 'react-native'
const serverRegister = async () => {
const deviceToken = (await Notifications.getDevicePushTokenAsync()).data
const expoToken = (
await Notifications.getExpoPushTokenAsync({
experienceId: '@xmflsct/tooot'
})
).data
return apiGeneral<{ endpoint: string; publicKey: string; auth: string }>({
method: 'post',
domain: 'testpush.home.xmflsct.com',
url: 'register',
body: { deviceToken, expoToken }
})
}
const pushEnable = async (): Promise<Mastodon.PushSubscription> => {
const { status: existingStatus } = await Notifications.getPermissionsAsync()
let finalStatus = existingStatus
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync()
finalStatus = status
}
if (finalStatus !== 'granted') {
alert('Failed to get push token for push notification!')
return Promise.reject()
}
const serverRes = (await serverRegister()).body
const formData = new FormData()
formData.append(
'subscription[endpoint]',
'https://testpush.home.xmflsct.com/test1'
)
formData.append('subscription[keys][p256dh]', serverRes.publicKey)
formData.append('subscription[keys][auth]', serverRes.auth)
const res = await apiInstance<Mastodon.PushSubscription>({
method: 'post',
url: 'push/subscription',
body: formData
})
return res.body
// if (Platform.OS === 'android') {
// Notifications.setNotificationChannelAsync('default', {
// name: 'default',
// importance: Notifications.AndroidImportance.MAX,
// vibrationPattern: [0, 250, 250, 250],
// lightColor: '#FF231F7C'
// })
// }
}
export default pushEnable

View File

@ -0,0 +1,40 @@
import { createAsyncThunk } from '@reduxjs/toolkit'
import { RootState } from '@root/store'
import * as AuthSession from 'expo-auth-session'
const removeInstance = createAsyncThunk(
'instances/remove',
async (index: number): Promise<number> => {
const { store } = require('@root/store')
const instances = (store.getState() as RootState).instances.instances
if (index !== -1) {
const currentInstance = instances[index]
let revoked = undefined
try {
revoked = await AuthSession.revokeAsync(
{
clientId: currentInstance.appData.clientId,
clientSecret: currentInstance.appData.clientSecret,
token: currentInstance.token,
scopes: ['read', 'write', 'follow', 'push']
},
{
revocationEndpoint: `https://${currentInstance.url}/oauth/revoke`
}
)
} catch {
console.warn('Revoking error')
}
if (!revoked) {
console.warn('Revoking error')
}
}
return Promise.resolve(index)
}
)
export default removeInstance

View File

@ -0,0 +1,12 @@
import apiInstance from '@api/instance'
import { createAsyncThunk } from '@reduxjs/toolkit'
export const updateAccountPreferences = createAsyncThunk(
'instances/updateAccountPreferences',
async (): Promise<Mastodon.Preferences> => {
return apiInstance<Mastodon.Preferences>({
method: 'get',
url: `preferences`
}).then(res => res.body)
}
)

View File

@ -0,0 +1,17 @@
import { createAsyncThunk } from '@reduxjs/toolkit'
import { Instance } from '../instancesSlice'
import pushDisable from './push/disable'
import pushEnable from './push/enable'
export const updatePush = createAsyncThunk(
'instances/updatePush',
async (
enable: boolean
): Promise<Instance['push']['subscription'] | boolean> => {
if (enable) {
return pushEnable()
} else {
return pushDisable()
}
}
)

View File

@ -1,13 +1,15 @@
import client from '@api/client'
import analytics from '@components/analytics' import analytics from '@components/analytics'
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit' import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { RootState } from '@root/store' import { RootState } from '@root/store'
import { ComposeStateDraft } from '@screens/Compose/utils/types' import { ComposeStateDraft } from '@screens/Compose/utils/types'
import * as AuthSession from 'expo-auth-session'
import * as Localization from 'expo-localization'
import { findIndex } from 'lodash' import { findIndex } from 'lodash'
import addInstance from './instances/add'
import removeInstance from './instances/remove'
import { updateAccountPreferences } from './instances/updateAccountPreferences'
import { updatePush } from './instances/updatePush'
export type InstanceLocal = { export type Instance = {
active: boolean
appData: { appData: {
clientId: string clientId: string
clientSecret: string clientSecret: string
@ -27,245 +29,105 @@ export type InstanceLocal = {
readTime?: Mastodon.Notification['created_at'] readTime?: Mastodon.Notification['created_at']
latestTime?: Mastodon.Notification['created_at'] latestTime?: Mastodon.Notification['created_at']
} }
push: {
loading: boolean
enabled: boolean
subscription?: Mastodon.PushSubscription
}
drafts: ComposeStateDraft[] drafts: ComposeStateDraft[]
} }
export type InstancesState = { export type InstancesState = {
local: { instances: Instance[]
activeIndex: number | null
instances: InstanceLocal[]
}
remote: {
url: string
}
} }
export const updateLocalAccountPreferences = createAsyncThunk(
'instances/updateLocalAccountPreferences',
async (): Promise<Mastodon.Preferences> => {
const res = await client<Mastodon.Preferences>({
method: 'get',
instance: 'local',
url: `preferences`
})
return Promise.resolve(res.body)
}
)
export const localAddInstance = createAsyncThunk(
'instances/localAddInstance',
async ({
url,
token,
instance,
max_toot_chars = 500,
appData
}: {
url: InstanceLocal['url']
token: InstanceLocal['token']
instance: Mastodon.Instance
max_toot_chars?: number
appData: InstanceLocal['appData']
}): Promise<{ type: 'add' | 'overwrite'; data: InstanceLocal }> => {
const { store } = require('@root/store')
const instanceLocal: InstancesState['local'] = store.getState().instances
.local
const {
body: { id, acct, avatar_static }
} = await client<Mastodon.Account>({
method: 'get',
instance: 'remote',
instanceDomain: url,
url: `accounts/verify_credentials`,
headers: { Authorization: `Bearer ${token}` }
})
let type: 'add' | 'overwrite'
if (
instanceLocal.instances.filter(instance => {
if (instance) {
if (instance.url === url && instance.account.id === id) {
return true
} else {
return false
}
} else {
return false
}
}).length
) {
type = 'overwrite'
} else {
type = 'add'
}
const { body: preferences } = await client<Mastodon.Preferences>({
method: 'get',
instance: 'remote',
instanceDomain: url,
url: `preferences`,
headers: { Authorization: `Bearer ${token}` }
})
return Promise.resolve({
type,
data: {
appData,
url,
token,
uri: instance.uri,
urls: instance.urls,
max_toot_chars,
account: {
id,
acct,
avatarStatic: avatar_static,
preferences
},
notification: {
readTime: undefined,
latestTime: undefined
},
drafts: []
}
})
}
)
export const localRemoveInstance = createAsyncThunk(
'instances/localRemoveInstance',
async (index?: InstancesState['local']['activeIndex']): Promise<number> => {
const { store } = require('@root/store')
const instanceLocal: InstancesState['local'] = store.getState().instances
.local
if (index) {
return Promise.resolve(index)
} else {
if (instanceLocal.activeIndex !== null) {
const currentInstance =
instanceLocal.instances[instanceLocal.activeIndex]
let revoked = undefined
try {
revoked = await AuthSession.revokeAsync(
{
clientId: currentInstance.appData.clientId,
clientSecret: currentInstance.appData.clientSecret,
token: currentInstance.token,
scopes: ['read', 'write', 'follow', 'push']
},
{
revocationEndpoint: `https://${currentInstance.url}/oauth/revoke`
}
)
} catch {}
if (!revoked) {
console.warn('Revoking error')
}
return Promise.resolve(instanceLocal.activeIndex)
} else {
throw new Error('Active index invalid, cannot remove instance')
}
}
}
)
export const instancesInitialState: InstancesState = { export const instancesInitialState: InstancesState = {
local: { instances: []
activeIndex: null,
instances: []
},
remote: {
url: Localization.locale.includes('zh') ? 'm.cmx.im' : 'mastodon.social'
}
} }
const findInstanceActive = (state: Instance[]) =>
state.findIndex(instance => instance.active)
const instancesSlice = createSlice({ const instancesSlice = createSlice({
name: 'instances', name: 'instances',
initialState: instancesInitialState, initialState: instancesInitialState,
reducers: { reducers: {
updateLocalActiveIndex: (state, action: PayloadAction<InstanceLocal>) => { updateInstanceActive: ({ instances }, action: PayloadAction<Instance>) => {
state.local.activeIndex = state.local.instances.findIndex( instances = instances.map(instance => {
instance => instance.active =
instance.url === action.payload.url && instance.url === action.payload.url &&
instance.token === action.payload.token && instance.token === action.payload.token &&
instance.account.id === action.payload.account.id instance.account.id === action.payload.account.id
) return instance
})
}, },
updateLocalAccount: ( updateInstanceAccount: (
state, { instances },
action: PayloadAction< action: PayloadAction<Pick<Instance['account'], 'acct' & 'avatarStatic'>>
Pick<InstanceLocal['account'], 'acct' & 'avatarStatic'>
>
) => { ) => {
if (state.local.activeIndex !== null) { const activeIndex = findInstanceActive(instances)
state.local.instances[state.local.activeIndex].account = { instances[activeIndex].account = {
...state.local.instances[state.local.activeIndex].account, ...instances[activeIndex].account,
...action.payload ...action.payload
}
} }
}, },
updateLocalNotification: ( updateInstanceNotification: (
state, { instances },
action: PayloadAction<Partial<InstanceLocal['notification']>> action: PayloadAction<Partial<Instance['notification']>>
) => { ) => {
if (state.local.activeIndex !== null) { const activeIndex = findInstanceActive(instances)
state.local.instances[state.local.activeIndex].notification = { instances[activeIndex].notification = {
...state.local.instances[state.local.activeIndex].notification, ...instances[activeIndex].notification,
...action.payload ...action.payload
}
} }
}, },
updateLocalDraft: (state, action: PayloadAction<ComposeStateDraft>) => { updateInstanceDraft: (
if (state.local.activeIndex !== null) { { instances },
const draftIndex = findIndex( action: PayloadAction<ComposeStateDraft>
state.local.instances[state.local.activeIndex].drafts, ) => {
['timestamp', action.payload.timestamp] const activeIndex = findInstanceActive(instances)
) const draftIndex = findIndex(instances[activeIndex].drafts, [
if (draftIndex === -1) { 'timestamp',
state.local.instances[state.local.activeIndex].drafts.unshift( action.payload.timestamp
action.payload ])
) if (draftIndex === -1) {
} else { instances[activeIndex].drafts.unshift(action.payload)
state.local.instances[state.local.activeIndex].drafts[draftIndex] = } else {
action.payload instances[activeIndex].drafts[draftIndex] = action.payload
}
} }
}, },
removeLocalDraft: ( removeInstanceDraft: (
state, { instances },
action: PayloadAction<ComposeStateDraft['timestamp']> action: PayloadAction<ComposeStateDraft['timestamp']>
) => { ) => {
if (state.local.activeIndex !== null) { const activeIndex = findInstanceActive(instances)
state.local.instances[ instances[activeIndex].drafts = instances[activeIndex].drafts?.filter(
state.local.activeIndex draft => draft.timestamp !== action.payload
].drafts = state.local.instances[ )
state.local.activeIndex
].drafts?.filter(draft => draft.timestamp !== action.payload)
}
} }
}, },
extraReducers: builder => { extraReducers: builder => {
builder builder
.addCase(localAddInstance.fulfilled, (state, action) => { .addCase(addInstance.fulfilled, (state, action) => {
switch (action.payload.type) { switch (action.payload.type) {
case 'add': case 'add':
state.local.instances.push(action.payload.data) state.instances.length &&
state.local.activeIndex = state.local.instances.length - 1 (state.instances = state.instances.map(instance => {
instance.active = false
return instance
}))
state.instances.push(action.payload.data)
break break
case 'overwrite': case 'overwrite':
state.local.instances = state.local.instances.map(instance => { console.log('overwriting')
state.instances = state.instances.map(instance => {
if ( if (
instance.url === action.payload.data.url && instance.url === action.payload.data.url &&
instance.account.id === action.payload.data.account.id instance.account.id === action.payload.data.account.id
) { ) {
return action.payload.data return action.payload.data
} else { } else {
instance.active = false
return instance return instance
} }
}) })
@ -273,76 +135,99 @@ const instancesSlice = createSlice({
analytics('login') analytics('login')
}) })
.addCase(localAddInstance.rejected, (state, action) => { .addCase(addInstance.rejected, (state, action) => {
console.error(state.local) console.error(state.instances)
console.error(action.error) console.error(action.error)
}) })
.addCase(localRemoveInstance.fulfilled, (state, action) => { .addCase(removeInstance.fulfilled, (state, action) => {
state.local.instances.splice(action.payload, 1) state.instances.splice(action.payload, 1)
state.local.activeIndex = state.local.instances.length state.instances.length &&
? state.local.instances.length - 1 (state.instances[state.instances.length - 1].active = true)
: null
analytics('logout') analytics('logout')
}) })
.addCase(localRemoveInstance.rejected, (state, action) => { .addCase(removeInstance.rejected, (state, action) => {
console.error(state.local) console.error(state)
console.error(action.error) console.error(action.error)
}) })
.addCase(updateLocalAccountPreferences.fulfilled, (state, action) => { .addCase(updateAccountPreferences.fulfilled, (state, action) => {
state.local.instances[state.local.activeIndex!].account.preferences = const activeIndex = findInstanceActive(state.instances)
action.payload state.instances[activeIndex].account.preferences = action.payload
}) })
.addCase(updateLocalAccountPreferences.rejected, (_, action) => { .addCase(updateAccountPreferences.rejected, (_, action) => {
console.error(action.error) console.error(action.error)
}) })
.addCase(updatePush.fulfilled, (state, action) => {
const activeIndex = findInstanceActive(state.instances)
if (typeof action.payload === 'boolean') {
state.instances[activeIndex].push.enabled = action.payload
} else {
state.instances[activeIndex].push.enabled = true
state.instances[activeIndex].push.subscription = action.payload
}
})
} }
}) })
export const getLocalActiveIndex = ({ instances: { local } }: RootState) => export const getInstanceActive = ({ instances: { instances } }: RootState) =>
local.activeIndex findInstanceActive(instances)
export const getLocalInstances = ({ instances: { local } }: RootState) =>
local.instances export const getInstances = ({ instances: { instances } }: RootState) =>
export const getLocalInstance = ({ instances: { local } }: RootState) => instances
local.activeIndex !== null ? local.instances[local.activeIndex] : undefined
export const getLocalUrl = ({ instances: { local } }: RootState) => export const getInstance = ({ instances: { instances } }: RootState) => {
local.activeIndex !== null const instanceActive = findInstanceActive(instances)
? local.instances[local.activeIndex].url return instanceActive !== -1 ? instances[instanceActive] : null
: undefined }
export const getLocalUri = ({ instances: { local } }: RootState) =>
local.activeIndex !== null export const getInstanceUrl = ({ instances: { instances } }: RootState) => {
? local.instances[local.activeIndex].uri const instanceActive = findInstanceActive(instances)
: undefined return instanceActive !== -1 ? instances[instanceActive].url : null
export const getLocalUrls = ({ instances: { local } }: RootState) => }
local.activeIndex !== null
? local.instances[local.activeIndex].urls export const getInstanceUri = ({ instances: { instances } }: RootState) => {
: undefined const instanceActive = findInstanceActive(instances)
export const getLocalMaxTootChar = ({ instances: { local } }: RootState) => return instanceActive !== -1 ? instances[instanceActive].uri : null
local.activeIndex !== null }
? local.instances[local.activeIndex].max_toot_chars
: 500 export const getInstanceUrls = ({ instances: { instances } }: RootState) => {
export const getLocalAccount = ({ instances: { local } }: RootState) => const instanceActive = findInstanceActive(instances)
local.activeIndex !== null return instanceActive !== -1 ? instances[instanceActive].urls : null
? local.instances[local.activeIndex].account }
: undefined
export const getLocalNotification = ({ instances: { local } }: RootState) => export const getInstanceMaxTootChar = ({
local.activeIndex !== null instances: { instances }
? local.instances[local.activeIndex].notification }: RootState) => {
: undefined const instanceActive = findInstanceActive(instances)
export const getLocalDrafts = ({ instances: { local } }: RootState) => return instanceActive !== -1 ? instances[instanceActive].max_toot_chars : null
local.activeIndex !== null }
? local.instances[local.activeIndex].drafts
: undefined export const getInstanceAccount = ({ instances: { instances } }: RootState) => {
export const getRemoteUrl = ({ instances: { remote } }: RootState) => remote.url const instanceActive = findInstanceActive(instances)
return instanceActive !== -1 ? instances[instanceActive].account : null
}
export const getInstanceNotification = ({
instances: { instances }
}: RootState) => {
const instanceActive = findInstanceActive(instances)
return instanceActive !== -1 ? instances[instanceActive].notification : null
}
export const getInstanceDrafts = ({ instances: { instances } }: RootState) => {
const instanceActive = findInstanceActive(instances)
return instanceActive !== -1 ? instances[instanceActive].drafts : null
}
export const { export const {
updateLocalActiveIndex, updateInstanceActive,
updateLocalAccount, updateInstanceAccount,
updateLocalNotification, updateInstanceNotification,
updateLocalDraft, updateInstanceDraft,
removeLocalDraft removeInstanceDraft
} = instancesSlice.actions } = instancesSlice.actions
export default instancesSlice.reducer export default instancesSlice.reducer

View File

@ -9,6 +9,14 @@ enum availableLanguages {
'en' 'en'
} }
export const changeAnalytics = createAsyncThunk(
'settings/changeAnalytics',
async (newValue: SettingsState['analytics']) => {
await Analytics.setAnalyticsCollectionEnabled(newValue)
return newValue
}
)
export type SettingsState = { export type SettingsState = {
language: keyof availableLanguages language: keyof availableLanguages
theme: 'light' | 'dark' | 'auto' theme: 'light' | 'dark' | 'auto'
@ -17,6 +25,9 @@ export type SettingsState = {
} }
export const settingsInitialState = { export const settingsInitialState = {
notification: {
enabled: false
},
language: Object.keys( language: Object.keys(
pickBy(availableLanguages, (_, key) => Localization.locale.includes(key)) pickBy(availableLanguages, (_, key) => Localization.locale.includes(key))
) )
@ -31,14 +42,6 @@ export const settingsInitialState = {
analytics: true analytics: true
} }
export const changeAnalytics = createAsyncThunk(
'settings/changeAnalytics',
async (newValue: SettingsState['analytics']) => {
await Analytics.setAnalyticsCollectionEnabled(newValue)
return newValue
}
)
const settingsSlice = createSlice({ const settingsSlice = createSlice({
name: 'settings', name: 'settings',
initialState: settingsInitialState as SettingsState, initialState: settingsInitialState as SettingsState,

View File

@ -1224,6 +1224,11 @@
dependencies: dependencies:
"@hapi/hoek" "^8.3.0" "@hapi/hoek" "^8.3.0"
"@ide/backoff@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@ide/backoff/-/backoff-1.0.0.tgz#466842c25bd4a4833e0642fab41ccff064010176"
integrity sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==
"@istanbuljs/load-nyc-config@^1.0.0": "@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@ -2936,6 +2941,16 @@ assert-plus@1.0.0, assert-plus@^1.0.0:
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
assert@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/assert/-/assert-2.0.0.tgz#95fc1c616d48713510680f2eaf2d10dd22e02d32"
integrity sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==
dependencies:
es6-object-assign "^1.1.0"
is-nan "^1.2.1"
object-is "^1.0.1"
util "^0.12.0"
assign-symbols@^1.0.0: assign-symbols@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
@ -3197,7 +3212,7 @@ babel-preset-jest@^26.6.2:
babel-plugin-jest-hoist "^26.6.2" babel-plugin-jest-hoist "^26.6.2"
babel-preset-current-node-syntax "^1.0.0" babel-preset-current-node-syntax "^1.0.0"
badgin@^1.1.2: badgin@^1.1.2, badgin@^1.1.5:
version "1.2.2" version "1.2.2"
resolved "https://registry.yarnpkg.com/badgin/-/badgin-1.2.2.tgz#cbb0b71b047230c681a68911eb24136f0632adc6" resolved "https://registry.yarnpkg.com/badgin/-/badgin-1.2.2.tgz#cbb0b71b047230c681a68911eb24136f0632adc6"
integrity sha512-XtoSjNhy2D09qGiLhFWBJmBwBlmleQuwyYyjddWNCJ3gqGRBOBR25VGcd8CAOSghpEUmghB3LD4NpHrUG89zCg== integrity sha512-XtoSjNhy2D09qGiLhFWBJmBwBlmleQuwyYyjddWNCJ3gqGRBOBR25VGcd8CAOSghpEUmghB3LD4NpHrUG89zCg==
@ -4236,6 +4251,11 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1" is-date-object "^1.0.1"
is-symbol "^1.0.2" is-symbol "^1.0.2"
es6-object-assign@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c"
integrity sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw=
escalade@^3.1.1: escalade@^3.1.1:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@ -4455,6 +4475,14 @@ expo-constants@*:
"@expo/config" "^3.3.18" "@expo/config" "^3.3.18"
uuid "^3.3.2" uuid "^3.3.2"
expo-constants@9.3.1:
version "9.3.1"
resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-9.3.1.tgz#1cab4896ea5e626fc7f19f49893526c94b481443"
integrity sha512-58ENdEeVxZ29INv87IqZf5ZD4+NjvxzrHOCFha8iW3TbTL8GXY/6QjlIZ/yGHu4TwBoatozeJQ+9WCCz/hXM0A==
dependencies:
fbjs "1.0.0"
uuid "^3.3.2"
expo-constants@^9.3.3, expo-constants@~9.3.0, expo-constants@~9.3.3: expo-constants@^9.3.3, expo-constants@~9.3.0, expo-constants@~9.3.3:
version "9.3.5" version "9.3.5"
resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-9.3.5.tgz#78085763e8ed100a5f2df7c682fd99631aa03d5e" resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-9.3.5.tgz#78085763e8ed100a5f2df7c682fd99631aa03d5e"
@ -4560,6 +4588,19 @@ expo-location@~10.0.0:
resolved "https://registry.yarnpkg.com/expo-location/-/expo-location-10.0.0.tgz#2923411649434f2f079343b163b13c5c9eee8b2d" resolved "https://registry.yarnpkg.com/expo-location/-/expo-location-10.0.0.tgz#2923411649434f2f079343b163b13c5c9eee8b2d"
integrity sha512-QLEb0iaBv4/blLxxfKRj2/HPisY+1t+g6MgegqZ1j1U/0qih4dvzUQrxie9ZOZyQB9gnXFCRnZv3QzHEb52dcA== integrity sha512-QLEb0iaBv4/blLxxfKRj2/HPisY+1t+g6MgegqZ1j1U/0qih4dvzUQrxie9ZOZyQB9gnXFCRnZv3QzHEb52dcA==
expo-notifications@~0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/expo-notifications/-/expo-notifications-0.8.2.tgz#69e04a4e48ec6bafaeb354d284fbc23c26f2d62d"
integrity sha512-eX/HB96FqXzSMAwtA/fhxB1tGYELQywPm7oBTfnALHH3MFHy1bW7NZYGcU82sDF+DF09uLZ4Fn4p5ValMWA5TA==
dependencies:
"@ide/backoff" "^1.0.0"
abort-controller "^3.0.0"
assert "^2.0.0"
badgin "^1.1.5"
expo-application "~2.4.1"
expo-constants "9.3.1"
uuid "^3.4.0"
expo-permissions@~10.0.0: expo-permissions@~10.0.0:
version "10.0.0" version "10.0.0"
resolved "https://registry.yarnpkg.com/expo-permissions/-/expo-permissions-10.0.0.tgz#5b31c54d561d00c7e46cd02321bc3704c51c584b" resolved "https://registry.yarnpkg.com/expo-permissions/-/expo-permissions-10.0.0.tgz#5b31c54d561d00c7e46cd02321bc3704c51c584b"
@ -5612,11 +5653,24 @@ is-generator-fn@^2.0.0:
resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118"
integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==
is-generator-function@^1.0.7:
version "1.0.8"
resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.8.tgz#dfb5c2b120e02b0a8d9d2c6806cd5621aa922f7b"
integrity sha512-2Omr/twNtufVZFr1GhxjOMFPAj2sjc/dKaIqBhvo4qciXfJmITGH6ZGd8eZYNHza8t1y0e01AuqRhJwfWp26WQ==
is-map@^2.0.1, is-map@^2.0.2: is-map@^2.0.1, is-map@^2.0.2:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==
is-nan@^1.2.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d"
integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==
dependencies:
call-bind "^1.0.0"
define-properties "^1.1.3"
is-negative-zero@^2.0.1: is-negative-zero@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
@ -7899,7 +7953,7 @@ object-inspect@^1.9.0:
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a"
integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==
object-is@^1.1.4: object-is@^1.0.1, object-is@^1.1.4:
version "1.1.4" version "1.1.4"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.4.tgz#63d6c83c00a43f4cbc9434eb9757c8a5b8565068" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.4.tgz#63d6c83c00a43f4cbc9434eb9757c8a5b8565068"
integrity sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg== integrity sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg==
@ -10218,6 +10272,18 @@ util-deprecate@~1.0.1:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
util@^0.12.0:
version "0.12.3"
resolved "https://registry.yarnpkg.com/util/-/util-0.12.3.tgz#971bb0292d2cc0c892dab7c6a5d37c2bec707888"
integrity sha512-I8XkoQwE+fPQEhy9v012V+TSdH2kp9ts29i20TaaDUXsg7x/onePbhFJUExBfv/2ay1ZOp/Vsm3nDlmnFGSAog==
dependencies:
inherits "^2.0.3"
is-arguments "^1.0.4"
is-generator-function "^1.0.7"
is-typed-array "^1.1.3"
safe-buffer "^5.1.2"
which-typed-array "^1.1.2"
utils-merge@1.0.1: utils-merge@1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"