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: {
versionCode: 4,
package: 'com.xmflsct.app.tooot',

View File

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

View File

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

View File

@ -343,6 +343,19 @@ declare namespace Mastodon {
'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 = {
id: string
following: boolean

View File

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

View File

@ -1,4 +1,4 @@
import client from '@api/client'
import apiInstance from '@api/instance'
import { toast, toastConfig } from '@components/toast'
import {
NavigationContainer,
@ -10,10 +10,8 @@ import ScreenCompose from '@screens/Compose'
import ScreenImagesViewer from '@screens/ImagesViewer'
import ScreenTabs from '@screens/Tabs'
import { updatePreviousTab } from '@utils/slices/contextsSlice'
import {
getLocalActiveIndex,
updateLocalAccountPreferences
} from '@utils/slices/instancesSlice'
import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences'
import { getInstanceActive } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import { themes } from '@utils/styles/themes'
import * as Analytics from 'expo-firebase-analytics'
@ -36,7 +34,7 @@ export const navigationRef = createRef<NavigationContainerRef>()
const Screens: React.FC<Props> = ({ localCorrupt }) => {
const { t } = useTranslation('common')
const dispatch = useDispatch()
const localActiveIndex = useSelector(getLocalActiveIndex)
const instanceActive = useSelector(getInstanceActive)
const { mode, theme } = useTheme()
enum barStyle {
light = 'dark-content',
@ -89,10 +87,9 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
// On launch check if there is any unread announcements
useEffect(() => {
localActiveIndex !== null &&
client<Mastodon.Announcement[]>({
instanceActive !== -1 &&
apiInstance<Mastodon.Announcement[]>({
method: 'get',
instance: 'local',
url: `announcements`
})
.then(res => {
@ -107,8 +104,8 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
// Lazily update users's preferences, for e.g. composing default visibility
useEffect(() => {
if (localActiveIndex !== null) {
dispatch(updateLocalAccountPreferences())
if (instanceActive !== -1) {
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 client = async <T = unknown>({
method,
instance,
localIndex,
instanceDomain,
version = 'v1',
url,
params,
headers,
body,
onUploadProgress
}: {
export type Params = {
method: 'get' | 'post' | 'put' | 'delete'
instance: 'local' | 'remote'
localIndex?: number
instanceDomain?: string
version?: 'v1' | 'v2'
url: string
params?: {
@ -29,30 +15,37 @@ const client = async <T = unknown>({
headers?: { [key: string]: string }
body?: FormData
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
let token = null
if (instance === 'remote') {
domain = instanceDomain || state.remote.url
const apiInstance = async <T = unknown>({
method,
version = 'v1',
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 {
if (theLocalIndex !== null && state.local.instances[theLocalIndex]) {
domain = state.local.instances[theLocalIndex].url
token = state.local.instances[theLocalIndex].token
} else {
console.error(
ctx.bgRed.white.bold(' API ') + ' ' + 'No instance domain is provided'
)
return Promise.reject()
}
console.error(
ctx.bgRed.white.bold(' API ') + ' ' + 'No instance domain is provided'
)
return Promise.reject()
}
console.log(
ctx.bgGreen.bold(' API ') +
ctx.bgGreen.bold(' API instance ') +
' ' +
domain +
' ' +
@ -97,7 +90,7 @@ const client = async <T = unknown>({
// 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 '),
ctx.bold(' API instance '),
ctx.bold('response'),
error.response.status,
error.response.data.error
@ -107,11 +100,11 @@ const client = async <T = unknown>({
// 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 '), ctx.bold('request'), error)
console.error(ctx.bold(' API instance '), ctx.bold('request'), error)
return Promise.reject()
} else {
console.error(
ctx.bold(' API '),
ctx.bold(' API instance '),
ctx.bold('internal'),
error.message,
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 {
getLocalInstance,
updateLocalNotification
getInstance,
updateInstanceNotification
} from '@utils/slices/instancesSlice'
import { useEffect, useRef } from 'react'
import { useQueryClient } from 'react-query'
@ -18,7 +18,7 @@ const useWebsocket = ({
const queryClient = useQueryClient()
const dispatch = useDispatch()
const localInstance = useSelector(
getLocalInstance,
getInstance,
(prev, next) =>
prev?.urls.streaming_api === next?.urls.streaming_api &&
prev?.token === next?.token
@ -39,7 +39,7 @@ const useWebsocket = ({
case 'notification':
const payload: Mastodon.Notification = JSON.parse(message.payload)
dispatch(
updateLocalNotification({ latestTime: payload.created_at })
updateInstanceNotification({ latestTime: payload.created_at })
)
const queryKey: QueryKeyTimeline = [
'Timeline',

View File

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

View File

@ -1,5 +1,6 @@
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 React, { useEffect } from 'react'
import { useQueryClient } from 'react-query'
@ -9,7 +10,7 @@ export interface Props {
instanceDomain: string
// Domain can be different than uri
instance: Mastodon.Instance
appData: InstanceLocal['appData']
appData: Instance['appData']
goBack?: boolean
}
@ -62,8 +63,8 @@ const InstanceAuth = React.memo(
)
queryClient.clear()
dispatch(
localAddInstance({
url: instanceDomain,
addInstance({
domain: instanceDomain,
token: accessToken,
instance,
max_toot_chars: instance.max_toot_chars,

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import TimelinePoll from '@components/Timeline/Shared/Poll'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
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 { useTheme } from '@utils/styles/ThemeManager'
import { uniqBy } from 'lodash'
@ -41,8 +41,8 @@ const TimelineDefault: React.FC<Props> = ({
pinned
}) => {
const { theme } = useTheme()
const localAccount = useSelector(
getLocalAccount,
const instanceAccount = useSelector(
getInstanceAccount,
(prev, next) => prev?.id === next?.id
)
const navigation = useNavigation<
@ -118,7 +118,7 @@ const TimelineDefault: React.FC<Props> = ({
statusId={actualStatus.id}
poll={actualStatus.poll}
reblog={item.reblog ? true : false}
sameAccount={actualStatus.account.id === localAccount?.id}
sameAccount={actualStatus.account.id === instanceAccount?.id}
/>
) : null}
{!disableDetails &&
@ -147,7 +147,7 @@ const TimelineDefault: React.FC<Props> = ({
([actualStatus.account] as Mastodon.Account[] &
Mastodon.Mention[])
.concat(actualStatus.mentions)
.filter(d => d.id !== localAccount?.id),
.filter(d => d.id !== instanceAccount?.id),
d => d.id
).map(d => d.acct)}
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 { StackNavigationProp } from '@react-navigation/stack'
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 { useTheme } from '@utils/styles/ThemeManager'
import { uniqBy } from 'lodash'
@ -30,8 +30,8 @@ const TimelineNotifications: React.FC<Props> = ({
highlighted = false
}) => {
const { theme } = useTheme()
const localAccount = useSelector(
getLocalAccount,
const instanceAccount = useSelector(
getInstanceAccount,
(prev, next) => prev?.id === next?.id
)
const navigation = useNavigation<
@ -103,7 +103,7 @@ const TimelineNotifications: React.FC<Props> = ({
statusId={notification.status.id}
poll={notification.status.poll}
reblog={false}
sameAccount={notification.account.id === localAccount?.id}
sameAccount={notification.account.id === instanceAccount?.id}
/>
)}
{notification.status.media_attachments.length > 0 && (
@ -131,7 +131,7 @@ const TimelineNotifications: React.FC<Props> = ({
([notification.status.account] as Mastodon.Account[] &
Mastodon.Mention[])
.concat(notification.status.mentions)
.filter(d => d.id !== localAccount?.id),
.filter(d => d.id !== instanceAccount?.id),
d => d.id
).map(d => d.acct)}
reblog={false}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,9 @@
export default {
heading: '设置',
content: {
notification: {
heading: '$t(meSettingsNotification:heading)'
},
language: {
heading: '切换语言',
options: {
@ -26,10 +29,6 @@ export default {
cancel: '$t(common:buttons.cancel)'
}
},
remote: {
heading: '$t(meSettingsUpdateRemote:heading)',
description: '外站只能浏览不能玩'
},
cache: {
heading: '清空缓存',
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 { updateStoreReview } from '@utils/slices/contextsSlice'
import {
getLocalAccount,
getLocalMaxTootChar,
removeLocalDraft,
updateLocalDraft
getInstanceAccount,
getInstanceMaxTootChar,
removeInstanceDraft,
updateInstanceDraft
} from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
@ -75,7 +75,7 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
setHasKeyboard(false)
}
const localAccount = useSelector(getLocalAccount, (prev, next) =>
const localAccount = useSelector(getInstanceAccount, (prev, next) =>
prev?.preferences && next?.preferences
? prev?.preferences['posting:default:visibility'] ===
next?.preferences['posting:default:visibility']
@ -102,7 +102,7 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
initialReducerState
)
const maxTootChars = useSelector(getLocalMaxTootChar)
const maxTootChars = useSelector(getInstanceMaxTootChar, () => true)
const totalTextCount =
(composeState.spoiler.active ? composeState.spoiler.count : 0) +
composeState.text.count
@ -158,7 +158,7 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
const saveDraft = () => {
dispatch(
updateLocalDraft({
updateInstanceDraft({
timestamp: composeState.timestamp,
spoiler: composeState.spoiler.raw,
text: composeState.text.raw,
@ -171,7 +171,7 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
)
}
const removeDraft = useCallback(() => {
dispatch(removeLocalDraft(composeState.timestamp))
dispatch(removeInstanceDraft(composeState.timestamp))
}, [composeState.timestamp])
useEffect(() => {
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 ComponentSeparator from '@components/Separator'
import HeaderSharedCreated from '@components/Timeline/Shared/HeaderShared/Created'
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 { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useContext, useState } from 'react'
@ -34,7 +37,7 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
const navigation = useNavigation()
const dispatch = useDispatch()
const { mode, theme } = useTheme()
const localDrafts = useSelector(getLocalDrafts)?.filter(
const instanceDrafts = useSelector(getInstanceDrafts)?.filter(
draft => draft.timestamp !== timestamp
)
@ -44,7 +47,7 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
const [checkingAttachments, setCheckingAttachments] = useState(false)
const removeDraft = useCallback(ts => {
dispatch(removeLocalDraft(ts))
dispatch(removeInstanceDraft(ts))
}, [])
const renderItem = useCallback(
@ -58,9 +61,8 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
let tempUploads: ExtendedAttachment[] = []
if (item.attachments && item.attachments.uploads.length) {
for (const attachment of item.attachments.uploads) {
await client<Mastodon.Attachment>({
await apiInstance<Mastodon.Attachment>({
method: 'get',
instance: 'local',
url: `media/${attachment.remote?.id}`
})
.then(res => {
@ -92,7 +94,7 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
type: 'loadDraft',
payload: tempDraft
})
dispatch(removeLocalDraft(item.timestamp))
dispatch(removeInstanceDraft(item.timestamp))
navigation.goBack()
}}
>
@ -156,14 +158,14 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
<>
<PanGestureHandler enabled={true}>
<SwipeListView
data={localDrafts}
data={instanceDrafts}
renderItem={renderItem}
renderHiddenItem={renderHiddenItem}
disableRightSwipe={true}
rightOpenValue={-actionWidth}
previewRowKey={
localDrafts?.length
? localDrafts[0].timestamp.toString()
instanceDrafts?.length
? instanceDrafts[0].timestamp.toString()
: undefined
}
// previewDuration={350}

View File

@ -1,4 +1,4 @@
import client from '@api/client'
import apiInstance from '@api/instance'
import analytics from '@components/analytics'
import haptics from '@components/haptics'
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}`)
}
client<Mastodon.Attachment>({
apiInstance<Mastodon.Attachment>({
method: 'put',
instance: 'local',
url: `media/${theAttachment.id}`,
body: formData
})

View File

@ -1,6 +1,6 @@
import Button from '@components/Button'
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 layoutAnimation from '@utils/styles/layoutAnimation'
import React, { useContext, useEffect } from 'react'
@ -13,7 +13,7 @@ const ComposeDrafts: React.FC = () => {
const { t } = useTranslation('sharedCompose')
const navigation = useNavigation()
const { composeState } = useContext(ComposeContext)
const localDrafts = useSelector(getLocalDrafts)?.filter(
const instanceDrafts = useSelector(getInstanceDrafts)?.filter(
draft => draft.timestamp !== composeState.timestamp
)
@ -21,7 +21,7 @@ const ComposeDrafts: React.FC = () => {
layoutAnimation()
}, [composeState.dirty])
if (!composeState.dirty && localDrafts?.length) {
if (!composeState.dirty && instanceDrafts?.length) {
return (
<View
style={styles.base}
@ -29,7 +29,7 @@ const ComposeDrafts: React.FC = () => {
<Button
type='text'
content={t('content.root.drafts', {
count: localDrafts.length
count: instanceDrafts.length
})}
onPress={() =>
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 Crypto from 'expo-crypto'
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 i18next from 'i18next'
import analytics from '@components/analytics'
import apiInstance from '@api/instance'
export interface Props {
composeDispatch: Dispatch<ComposeAction>
@ -106,9 +106,8 @@ const addAttachment = async ({
type: attachmentType
})
return client<Mastodon.Attachment>({
return apiInstance<Mastodon.Attachment>({
method: 'post',
instance: 'local',
url: 'media',
body: formData
})

View File

@ -1,7 +1,4 @@
import {
getLocalActiveIndex,
getLocalInstances
} from '@utils/slices/instancesSlice'
import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import React, { useContext } from 'react'
import { StyleSheet, View } from 'react-native'
@ -13,15 +10,15 @@ import ComposeTextInput from './Header/TextInput'
const ComposeRootHeader: React.FC = () => {
const { composeState } = useContext(ComposeContext)
const localActiveIndex = useSelector(getLocalActiveIndex)
const instanceActive = useSelector(getInstanceActive)
const localInstances = useSelector(
getLocalInstances,
getInstances,
(prev, next) => prev.length === next.length
)
return (
<>
{localActiveIndex !== null &&
{instanceActive !== -1 &&
localInstances.length &&
localInstances.length > 1 && (
<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 { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
@ -11,17 +14,17 @@ const ComposePostingAs = React.memo(
const { t } = useTranslation('sharedCompose')
const { theme } = useTheme()
const localAccount = useSelector(
getLocalAccount,
const instanceAccount = useSelector(
getInstanceAccount,
(prev, next) => prev?.acct === next?.acct
)
const localUri = useSelector(getLocalUri)
const instanceUri = useSelector(getInstanceUri)
return (
<Text style={[styles.text, { color: theme.secondary }]}>
{t('content.root.header.postingAs', {
acct: localAccount?.acct,
domain: localUri
acct: instanceAccount?.acct,
domain: instanceUri
})}
</Text>
)

View File

@ -1,5 +1,5 @@
import { store } from '@root/store'
import { getLocalAccount } from '@utils/slices/instancesSlice'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import composeInitialState from './initialState'
import { ComposeState } from './types'
@ -39,7 +39,7 @@ const composeParseState = (
}),
visibility:
params.incomingStatus.visibility ||
getLocalAccount(store.getState())?.preferences[
getInstanceAccount(store.getState()).preferences[
'posting:default:visibility'
] ||
'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 * as Crypto from 'expo-crypto'
@ -39,9 +39,8 @@ const composePost = async (
formData.append('visibility', composeState.visibility)
return client<Mastodon.Status>({
return apiInstance<Mastodon.Status>({
method: 'post',
instance: 'local',
url: 'statuses',
headers: {
'Idempotency-Key': await Crypto.digestStringAsync(

View File

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

View File

@ -3,7 +3,7 @@ import { HeaderCenter, HeaderRight } from '@components/Header'
import Timeline from '@components/Timeline'
import { BottomTabScreenProps } from '@react-navigation/bottom-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 { useTranslation } from 'react-i18next'
import { Platform } from 'react-native'
@ -21,7 +21,7 @@ const Stack = createNativeStackNavigator<Nav.TabLocalStackParamList>()
const TabLocal = React.memo(
({ navigation }: TabLocalProp) => {
const { t } = useTranslation('local')
const localActiveIndex = useSelector(getLocalActiveIndex)
const instanceActive = useSelector(getInstanceActive)
const screenOptions = useMemo(
() => ({
@ -49,7 +49,7 @@ const TabLocal = React.memo(
[]
)
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 { Platform } from 'react-native'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import ScreenMeSettingsNotification from './Me/Notification'
const Stack = createNativeStackNavigator<Nav.TabMeStackParamList>()
@ -114,6 +115,19 @@ const TabMe = React.memo(
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
name='Tab-Me-Switch'
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 accountInitialState from '@screens/Tabs/Shared/Account/utils/initialState'
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 Animated, {
useAnimatedScrollHandler,
@ -17,7 +17,7 @@ import Animated, {
import { useSelector } from 'react-redux'
const ScreenMeRoot: React.FC = () => {
const localActiveIndex = useSelector(getLocalActiveIndex)
const instanceActive = useSelector(getInstanceActive)
const scrollRef = useRef<Animated.ScrollView>(null)
useScrollToTop(scrollRef)
@ -36,7 +36,7 @@ const ScreenMeRoot: React.FC = () => {
return (
<AccountContext.Provider value={{ accountState, accountDispatch }}>
{localActiveIndex !== null && data ? (
{instanceActive !== -1 && data ? (
<AccountNav scrollY={scrollY} account={data} />
) : null}
<Animated.ScrollView
@ -45,14 +45,14 @@ const ScreenMeRoot: React.FC = () => {
onScroll={onScroll}
scrollEventThrottle={16}
>
{localActiveIndex !== null ? (
{instanceActive !== -1 ? (
<MyInfo setData={setData} />
) : (
<ComponentInstance />
)}
{localActiveIndex !== null ? <Collections /> : null}
{instanceActive !== -1 ? <Collections /> : null}
<Settings />
{localActiveIndex !== null ? <Logout /> : null}
{instanceActive !== -1 ? <Logout /> : null}
</Animated.ScrollView>
</AccountContext.Provider>
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { HeaderCenter } from '@components/Header'
import Timeline from '@components/Timeline'
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 { useTranslation } from 'react-i18next'
import { Platform, ViewToken } from 'react-native'
@ -46,7 +46,7 @@ const TabNotifications = React.memo(
viewableItems[0].index === 0
) {
dispatch(
updateLocalNotification({
updateInstanceNotification({
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 { useNavigation } from '@react-navigation/native'
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 React, { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -21,7 +21,7 @@ const TabPublic = React.memo(
const { t, i18n } = useTranslation()
const { mode } = useTheme()
const navigation = useNavigation()
const localActiveIndex = useSelector(getLocalActiveIndex)
const instanceActive = useSelector(getInstanceActive)
const [segment, setSegment] = useState(0)
const pages: { title: string; page: App.Pages }[] = [
@ -74,9 +74,9 @@ const TabPublic = React.memo(
key: App.Pages
}
}) => {
return localActiveIndex !== null && <Timeline page={route.key} />
return instanceActive !== -1 && <Timeline page={route.key} />
},
[localActiveIndex]
[instanceActive]
)
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 { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback } from 'react'
@ -23,7 +23,7 @@ export interface Props {
const AccountInformation: React.FC<Props> = ({ account, myInfo = false }) => {
const ownAccount =
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 animation = useCallback(

View File

@ -1,5 +1,8 @@
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 { useTheme } from '@utils/styles/ThemeManager'
import React, { useMemo } from 'react'
@ -14,11 +17,11 @@ export interface Props {
const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => {
const { theme } = useTheme()
const localAccount = useSelector(
getLocalAccount,
const instanceAccount = useSelector(
getInstanceAccount,
(prev, next) => prev?.acct === next?.acct
)
const localUri = useSelector(getLocalUri)
const instanceUri = useSelector(getInstanceUri)
const movedStyle = useMemo(
() =>
@ -45,7 +48,7 @@ const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => {
}
}, [account?.moved])
if (account || (myInfo && localAccount !== undefined)) {
if (account || (myInfo && instanceAccount)) {
return (
<View
style={[styles.base, { flexDirection: 'row', alignItems: 'center' }]}
@ -60,8 +63,8 @@ const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => {
]}
selectable
>
@{myInfo ? localAccount?.acct : account?.acct}
{myInfo ? `@${localUri}` : null}
@{myInfo ? instanceAccount.acct : account?.acct}
{myInfo ? `@${instanceUri}` : null}
</Text>
{movedContent}
{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 { store } from '@root/store'
import removeInstance from '@utils/slices/instances/remove'
import {
localRemoveInstance,
updateLocalAccount
getInstanceActive,
updateInstanceAccount
} from '@utils/slices/instancesSlice'
import log from './log'
@ -13,29 +14,28 @@ const netInfo = async (): Promise<{
}> => {
log('log', 'netInfo', 'initializing')
const netInfo = await NetInfo.fetch()
const activeIndex = store.getState().instances.local?.activeIndex
const activeIndex = getInstanceActive(store.getState())
if (netInfo.isConnected) {
log('log', 'netInfo', 'network connected')
if (activeIndex !== null) {
if (activeIndex !== -1) {
log('log', 'netInfo', 'checking locally stored credentials')
return client<Mastodon.Account>({
return apiInstance<Mastodon.Account>({
method: 'get',
instance: 'local',
url: `accounts/verify_credentials`
})
.then(res => {
log('log', 'netInfo', 'local credential check passed')
if (
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')
store.dispatch(localRemoveInstance(activeIndex))
store.dispatch(removeInstance(activeIndex))
return Promise.resolve({ connected: true, corruputed: '' })
} else {
store.dispatch(
updateLocalAccount({
updateInstanceAccount({
acct: res.body.acct,
avatarStatic: res.body.avatar_static
})
@ -50,7 +50,7 @@ const netInfo = async (): Promise<{
typeof error.status === 'number' &&
error.status === 401
) {
store.dispatch(localRemoveInstance(activeIndex))
store.dispatch(removeInstance(activeIndex))
}
return Promise.resolve({
connected: true,

View File

@ -6,9 +6,9 @@ import {
getDefaultMiddleware
} from '@reduxjs/toolkit'
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 { createMigrate, persistReducer, persistStore } from 'redux-persist'
import { persistReducer, persistStore } from 'redux-persist'
const secureStorage = createSecureStore()
@ -20,43 +20,10 @@ const contextsPersistConfig = {
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 = {
key: 'instances',
prefix,
version: 3,
storage: secureStorage,
migrate: createMigrate(instancesMigration, { debug: true })
storage: secureStorage
}
const settingsPersistConfig = {

View File

@ -1,4 +1,4 @@
import client from '@api/client'
import apiInstance from '@api/instance'
import { AxiosError } from 'axios'
import { useQuery, UseQueryOptions } from 'react-query'
@ -7,9 +7,8 @@ export type QueryKey = ['Account', { id: Mastodon.Account['id'] }]
const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
const { id } = queryKey[1]
return client<Mastodon.Account>({
return apiInstance<Mastodon.Account>({
method: 'get',
instance: 'local',
url: `accounts/${id}`
}).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 {
useMutation,
@ -12,9 +12,8 @@ type QueryKeyAnnouncement = ['Announcements', { showAll?: boolean }]
const queryFunction = ({ queryKey }: { queryKey: QueryKeyAnnouncement }) => {
const { showAll } = queryKey[1]
return client<Mastodon.Announcement[]>({
return apiInstance<Mastodon.Announcement[]>({
method: 'get',
instance: 'local',
url: `announcements`,
...(showAll && {
params: {
@ -52,15 +51,13 @@ const mutationFunction = async ({
}: MutationVarsAnnouncement) => {
switch (type) {
case 'reaction':
return client<{}>({
return apiInstance<{}>({
method: me ? 'delete' : 'put',
instance: 'local',
url: `announcements/${id}/reactions/${name}`
})
case 'dismiss':
return client<{}>({
return apiInstance<{}>({
method: 'post',
instance: 'local',
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 * as AuthSession from 'expo-auth-session'
import { useQuery, UseQueryOptions } from 'react-query'
export type QueryKey = ['Apps', { instanceDomain?: string }]
export type QueryKey = ['Apps', { domain?: string }]
const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
const redirectUri = AuthSession.makeRedirectUri({
@ -11,7 +11,7 @@ const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
useProxy: false
})
const { instanceDomain } = queryKey[1]
const { domain } = queryKey[1]
const formData = new FormData()
formData.append('client_name', 'tooot')
@ -19,11 +19,10 @@ const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
formData.append('redirect_uris', redirectUri)
formData.append('scopes', 'read write follow push')
return client<Mastodon.Apps>({
return apiGeneral<Mastodon.Apps>({
method: 'post',
instance: 'remote',
instanceDomain,
url: `apps`,
domain: domain || '',
url: `api/v1/apps`,
body: formData
}).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 { useQuery, UseQueryOptions } from 'react-query'
type QueryKey = ['Emojis']
const queryFunction = () => {
return client<Mastodon.Emoji[]>({
return apiInstance<Mastodon.Emoji[]>({
method: 'get',
instance: 'local',
url: 'custom_emojis'
}).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 { useQuery, UseQueryOptions } from 'react-query'
export type QueryKey = ['Instance', { instanceDomain?: string }]
export type QueryKey = ['Instance', { domain?: string }]
const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
const { instanceDomain } = queryKey[1]
const queryFunction = async ({ queryKey }: { queryKey: QueryKey }) => {
const { domain } = queryKey[1]
if (!domain) {
return Promise.reject()
}
return client<Mastodon.Instance>({
const res = await apiGeneral<Mastodon.Instance>({
method: 'get',
instance: 'remote',
instanceDomain,
url: `instance`
}).then(res => res.body)
domain: domain,
url: `api/v1/instance`
})
return res.body
}
const useInstanceQuery = <

View File

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

View File

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

View File

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

View File

@ -32,11 +32,11 @@ export const contextsInitialState = {
current: 0,
hidden: false
},
previousTab: 'Tab-Local'
previousTab: 'Tab-Me'
}
const contextsSlice = createSlice({
name: 'settings',
name: 'contexts',
initialState: contextsInitialState as ContextsState,
reducers: {
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 { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { RootState } from '@root/store'
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 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: {
clientId: string
clientSecret: string
@ -27,245 +29,105 @@ export type InstanceLocal = {
readTime?: Mastodon.Notification['created_at']
latestTime?: Mastodon.Notification['created_at']
}
push: {
loading: boolean
enabled: boolean
subscription?: Mastodon.PushSubscription
}
drafts: ComposeStateDraft[]
}
export type InstancesState = {
local: {
activeIndex: number | null
instances: InstanceLocal[]
}
remote: {
url: string
}
instances: Instance[]
}
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 = {
local: {
activeIndex: null,
instances: []
},
remote: {
url: Localization.locale.includes('zh') ? 'm.cmx.im' : 'mastodon.social'
}
instances: []
}
const findInstanceActive = (state: Instance[]) =>
state.findIndex(instance => instance.active)
const instancesSlice = createSlice({
name: 'instances',
initialState: instancesInitialState,
reducers: {
updateLocalActiveIndex: (state, action: PayloadAction<InstanceLocal>) => {
state.local.activeIndex = state.local.instances.findIndex(
instance =>
updateInstanceActive: ({ instances }, action: PayloadAction<Instance>) => {
instances = instances.map(instance => {
instance.active =
instance.url === action.payload.url &&
instance.token === action.payload.token &&
instance.account.id === action.payload.account.id
)
return instance
})
},
updateLocalAccount: (
state,
action: PayloadAction<
Pick<InstanceLocal['account'], 'acct' & 'avatarStatic'>
>
updateInstanceAccount: (
{ instances },
action: PayloadAction<Pick<Instance['account'], 'acct' & 'avatarStatic'>>
) => {
if (state.local.activeIndex !== null) {
state.local.instances[state.local.activeIndex].account = {
...state.local.instances[state.local.activeIndex].account,
...action.payload
}
const activeIndex = findInstanceActive(instances)
instances[activeIndex].account = {
...instances[activeIndex].account,
...action.payload
}
},
updateLocalNotification: (
state,
action: PayloadAction<Partial<InstanceLocal['notification']>>
updateInstanceNotification: (
{ instances },
action: PayloadAction<Partial<Instance['notification']>>
) => {
if (state.local.activeIndex !== null) {
state.local.instances[state.local.activeIndex].notification = {
...state.local.instances[state.local.activeIndex].notification,
...action.payload
}
const activeIndex = findInstanceActive(instances)
instances[activeIndex].notification = {
...instances[activeIndex].notification,
...action.payload
}
},
updateLocalDraft: (state, action: PayloadAction<ComposeStateDraft>) => {
if (state.local.activeIndex !== null) {
const draftIndex = findIndex(
state.local.instances[state.local.activeIndex].drafts,
['timestamp', action.payload.timestamp]
)
if (draftIndex === -1) {
state.local.instances[state.local.activeIndex].drafts.unshift(
action.payload
)
} else {
state.local.instances[state.local.activeIndex].drafts[draftIndex] =
action.payload
}
updateInstanceDraft: (
{ instances },
action: PayloadAction<ComposeStateDraft>
) => {
const activeIndex = findInstanceActive(instances)
const draftIndex = findIndex(instances[activeIndex].drafts, [
'timestamp',
action.payload.timestamp
])
if (draftIndex === -1) {
instances[activeIndex].drafts.unshift(action.payload)
} else {
instances[activeIndex].drafts[draftIndex] = action.payload
}
},
removeLocalDraft: (
state,
removeInstanceDraft: (
{ instances },
action: PayloadAction<ComposeStateDraft['timestamp']>
) => {
if (state.local.activeIndex !== null) {
state.local.instances[
state.local.activeIndex
].drafts = state.local.instances[
state.local.activeIndex
].drafts?.filter(draft => draft.timestamp !== action.payload)
}
const activeIndex = findInstanceActive(instances)
instances[activeIndex].drafts = instances[activeIndex].drafts?.filter(
draft => draft.timestamp !== action.payload
)
}
},
extraReducers: builder => {
builder
.addCase(localAddInstance.fulfilled, (state, action) => {
.addCase(addInstance.fulfilled, (state, action) => {
switch (action.payload.type) {
case 'add':
state.local.instances.push(action.payload.data)
state.local.activeIndex = state.local.instances.length - 1
state.instances.length &&
(state.instances = state.instances.map(instance => {
instance.active = false
return instance
}))
state.instances.push(action.payload.data)
break
case 'overwrite':
state.local.instances = state.local.instances.map(instance => {
console.log('overwriting')
state.instances = state.instances.map(instance => {
if (
instance.url === action.payload.data.url &&
instance.account.id === action.payload.data.account.id
) {
return action.payload.data
} else {
instance.active = false
return instance
}
})
@ -273,76 +135,99 @@ const instancesSlice = createSlice({
analytics('login')
})
.addCase(localAddInstance.rejected, (state, action) => {
console.error(state.local)
.addCase(addInstance.rejected, (state, action) => {
console.error(state.instances)
console.error(action.error)
})
.addCase(localRemoveInstance.fulfilled, (state, action) => {
state.local.instances.splice(action.payload, 1)
state.local.activeIndex = state.local.instances.length
? state.local.instances.length - 1
: null
.addCase(removeInstance.fulfilled, (state, action) => {
state.instances.splice(action.payload, 1)
state.instances.length &&
(state.instances[state.instances.length - 1].active = true)
analytics('logout')
})
.addCase(localRemoveInstance.rejected, (state, action) => {
console.error(state.local)
.addCase(removeInstance.rejected, (state, action) => {
console.error(state)
console.error(action.error)
})
.addCase(updateLocalAccountPreferences.fulfilled, (state, action) => {
state.local.instances[state.local.activeIndex!].account.preferences =
action.payload
.addCase(updateAccountPreferences.fulfilled, (state, action) => {
const activeIndex = findInstanceActive(state.instances)
state.instances[activeIndex].account.preferences = action.payload
})
.addCase(updateLocalAccountPreferences.rejected, (_, action) => {
.addCase(updateAccountPreferences.rejected, (_, action) => {
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) =>
local.activeIndex
export const getLocalInstances = ({ instances: { local } }: RootState) =>
local.instances
export const getLocalInstance = ({ instances: { local } }: RootState) =>
local.activeIndex !== null ? local.instances[local.activeIndex] : undefined
export const getLocalUrl = ({ instances: { local } }: RootState) =>
local.activeIndex !== null
? local.instances[local.activeIndex].url
: undefined
export const getLocalUri = ({ instances: { local } }: RootState) =>
local.activeIndex !== null
? local.instances[local.activeIndex].uri
: undefined
export const getLocalUrls = ({ instances: { local } }: RootState) =>
local.activeIndex !== null
? local.instances[local.activeIndex].urls
: undefined
export const getLocalMaxTootChar = ({ instances: { local } }: RootState) =>
local.activeIndex !== null
? local.instances[local.activeIndex].max_toot_chars
: 500
export const getLocalAccount = ({ instances: { local } }: RootState) =>
local.activeIndex !== null
? local.instances[local.activeIndex].account
: undefined
export const getLocalNotification = ({ instances: { local } }: RootState) =>
local.activeIndex !== null
? local.instances[local.activeIndex].notification
: undefined
export const getLocalDrafts = ({ instances: { local } }: RootState) =>
local.activeIndex !== null
? local.instances[local.activeIndex].drafts
: undefined
export const getRemoteUrl = ({ instances: { remote } }: RootState) => remote.url
export const getInstanceActive = ({ instances: { instances } }: RootState) =>
findInstanceActive(instances)
export const getInstances = ({ instances: { instances } }: RootState) =>
instances
export const getInstance = ({ instances: { instances } }: RootState) => {
const instanceActive = findInstanceActive(instances)
return instanceActive !== -1 ? instances[instanceActive] : null
}
export const getInstanceUrl = ({ instances: { instances } }: RootState) => {
const instanceActive = findInstanceActive(instances)
return instanceActive !== -1 ? instances[instanceActive].url : null
}
export const getInstanceUri = ({ instances: { instances } }: RootState) => {
const instanceActive = findInstanceActive(instances)
return instanceActive !== -1 ? instances[instanceActive].uri : null
}
export const getInstanceUrls = ({ instances: { instances } }: RootState) => {
const instanceActive = findInstanceActive(instances)
return instanceActive !== -1 ? instances[instanceActive].urls : null
}
export const getInstanceMaxTootChar = ({
instances: { instances }
}: RootState) => {
const instanceActive = findInstanceActive(instances)
return instanceActive !== -1 ? instances[instanceActive].max_toot_chars : null
}
export const getInstanceAccount = ({ instances: { instances } }: RootState) => {
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 {
updateLocalActiveIndex,
updateLocalAccount,
updateLocalNotification,
updateLocalDraft,
removeLocalDraft
updateInstanceActive,
updateInstanceAccount,
updateInstanceNotification,
updateInstanceDraft,
removeInstanceDraft
} = instancesSlice.actions
export default instancesSlice.reducer

View File

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

View File

@ -1224,6 +1224,11 @@
dependencies:
"@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":
version "1.1.0"
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"
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:
version "1.0.0"
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-preset-current-node-syntax "^1.0.0"
badgin@^1.1.2:
badgin@^1.1.2, badgin@^1.1.5:
version "1.2.2"
resolved "https://registry.yarnpkg.com/badgin/-/badgin-1.2.2.tgz#cbb0b71b047230c681a68911eb24136f0632adc6"
integrity sha512-XtoSjNhy2D09qGiLhFWBJmBwBlmleQuwyYyjddWNCJ3gqGRBOBR25VGcd8CAOSghpEUmghB3LD4NpHrUG89zCg==
@ -4236,6 +4251,11 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1"
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:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@ -4455,6 +4475,14 @@ expo-constants@*:
"@expo/config" "^3.3.18"
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:
version "9.3.5"
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"
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:
version "10.0.0"
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"
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:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
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:
version "2.0.1"
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"
integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==
object-is@^1.1.4:
object-is@^1.0.1, object-is@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.4.tgz#63d6c83c00a43f4cbc9434eb9757c8a5b8565068"
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"
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:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"