mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Lots of updates
This commit is contained in:
163
App.tsx
163
App.tsx
@ -1,88 +1,38 @@
|
|||||||
import NetInfo from '@react-native-community/netinfo'
|
|
||||||
import client from '@root/api/client'
|
|
||||||
import Index from '@root/Index'
|
import Index from '@root/Index'
|
||||||
import { persistor, store } from '@root/store'
|
import { persistor, store } from '@root/store'
|
||||||
import { resetLocal } from '@root/utils/slices/instancesSlice'
|
|
||||||
import ThemeManager from '@utils/styles/ThemeManager'
|
import ThemeManager from '@utils/styles/ThemeManager'
|
||||||
import chalk from 'chalk'
|
|
||||||
import * as Analytics from 'expo-firebase-analytics'
|
|
||||||
import { Audio } from 'expo-av'
|
|
||||||
import * as SplashScreen from 'expo-splash-screen'
|
import * as SplashScreen from 'expo-splash-screen'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { enableScreens } from 'react-native-screens'
|
import { enableScreens } from 'react-native-screens'
|
||||||
import { onlineManager, QueryClient, QueryClientProvider } from 'react-query'
|
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import { PersistGate } from 'redux-persist/integration/react'
|
import { PersistGate } from 'redux-persist/integration/react'
|
||||||
import * as Sentry from 'sentry-expo'
|
import checkSecureStorageVersion from '@root/startup/checkSecureStorageVersion'
|
||||||
|
import dev from '@root/startup/dev'
|
||||||
|
import sentry from '@root/startup/sentry'
|
||||||
|
import log from '@root/startup/log'
|
||||||
|
import audio from '@root/startup/audio'
|
||||||
|
import onlineStatus from '@root/startup/onlineStatus'
|
||||||
|
import netInfo from '@root/startup/netInfo'
|
||||||
|
|
||||||
const ctx = new chalk.Instance({ level: 3 })
|
dev()
|
||||||
const startingLog = (type: 'log' | 'warn' | 'error', message: string) => {
|
sentry()
|
||||||
switch (type) {
|
audio()
|
||||||
case 'log':
|
onlineStatus()
|
||||||
console.log(ctx.bgBlue.bold(' Start up ') + ' ' + message)
|
|
||||||
break
|
|
||||||
case 'warn':
|
|
||||||
console.log(ctx.bgBlue.bold(' Start up ') + ' ' + message)
|
|
||||||
break
|
|
||||||
case 'error':
|
|
||||||
console.log(ctx.bgBlue.bold(' Start up ') + ' ' + message)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (__DEV__) {
|
log('log', 'react-query', 'initializing')
|
||||||
Analytics.setDebugModeEnabled(true)
|
|
||||||
|
|
||||||
startingLog('log', 'initializing wdyr')
|
|
||||||
const whyDidYouRender = require('@welldone-software/why-did-you-render')
|
|
||||||
whyDidYouRender(React, {
|
|
||||||
trackHooks: true,
|
|
||||||
hotReloadBufferMs: 1000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
startingLog('log', 'initializing Sentry')
|
|
||||||
Sentry.init({
|
|
||||||
dsn:
|
|
||||||
'https://c9e29aa05f774aca8f36def98244ce04@o389581.ingest.sentry.io/5571975',
|
|
||||||
enableInExpoDevelopment: false,
|
|
||||||
debug: __DEV__
|
|
||||||
})
|
|
||||||
|
|
||||||
startingLog('log', 'initializing react-query')
|
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
startingLog('log', 'setting audio playback default options')
|
log('log', 'react-native-screens', 'initializing')
|
||||||
Audio.setAudioModeAsync({
|
|
||||||
playsInSilentModeIOS: true,
|
|
||||||
interruptionModeIOS: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
startingLog('log', 'initializing native screen')
|
|
||||||
enableScreens()
|
enableScreens()
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
startingLog('log', 'rendering App')
|
log('log', 'App', 'rendering App')
|
||||||
const [appLoaded, setAppLoaded] = useState(false)
|
|
||||||
const [localCorrupt, setLocalCorrupt] = useState<string>()
|
const [localCorrupt, setLocalCorrupt] = useState<string>()
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onlineState = onlineManager.setEventListener(setOnline => {
|
|
||||||
startingLog('log', 'added onlineManager listener')
|
|
||||||
return NetInfo.addEventListener(state => {
|
|
||||||
startingLog('warn', `setting online state ${state.isConnected}`)
|
|
||||||
// @ts-ignore
|
|
||||||
setOnline(state.isConnected)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return () => {
|
|
||||||
onlineState
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const delaySplash = async () => {
|
const delaySplash = async () => {
|
||||||
startingLog('log', 'delay splash')
|
log('log', 'App', 'delay splash')
|
||||||
try {
|
try {
|
||||||
await SplashScreen.preventAutoHideAsync()
|
await SplashScreen.preventAutoHideAsync()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -91,72 +41,29 @@ const App: React.FC = () => {
|
|||||||
}
|
}
|
||||||
delaySplash()
|
delaySplash()
|
||||||
}, [])
|
}, [])
|
||||||
useEffect(() => {
|
|
||||||
const hideSplash = async () => {
|
|
||||||
startingLog('log', 'hide splash')
|
|
||||||
try {
|
|
||||||
await SplashScreen.hideAsync()
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (appLoaded) {
|
|
||||||
hideSplash()
|
|
||||||
}
|
|
||||||
}, [appLoaded])
|
|
||||||
|
|
||||||
const onBeforeLift = useCallback(() => {
|
const onBeforeLift = useCallback(async () => {
|
||||||
NetInfo.fetch().then(netInfo => {
|
const netInfoRes = await netInfo()
|
||||||
startingLog('log', 'on before lift')
|
|
||||||
const localUrl = store.getState().instances.local.url
|
if (netInfoRes.corrupted && netInfoRes.corrupted.length) {
|
||||||
const localToken = store.getState().instances.local.token
|
setLocalCorrupt(netInfoRes.corrupted)
|
||||||
if (netInfo.isConnected) {
|
}
|
||||||
startingLog('log', 'network connected')
|
|
||||||
if (localUrl && localToken) {
|
log('log', 'App', 'hide splash')
|
||||||
startingLog('log', 'checking locally stored credentials')
|
try {
|
||||||
client({
|
await SplashScreen.hideAsync()
|
||||||
method: 'get',
|
return Promise.resolve()
|
||||||
instance: 'remote',
|
} catch (e) {
|
||||||
instanceDomain: localUrl,
|
console.warn(e)
|
||||||
url: `accounts/verify_credentials`,
|
return Promise.reject()
|
||||||
headers: { Authorization: `Bearer ${localToken}` }
|
}
|
||||||
})
|
|
||||||
.then(res => {
|
|
||||||
startingLog('log', 'local credential check passed')
|
|
||||||
if (res.body.id !== store.getState().instances.local.account.id) {
|
|
||||||
store.dispatch(resetLocal())
|
|
||||||
setLocalCorrupt('')
|
|
||||||
}
|
|
||||||
setAppLoaded(true)
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
startingLog('error', 'local credential check failed')
|
|
||||||
if (
|
|
||||||
error.status &&
|
|
||||||
typeof error.status === 'number' &&
|
|
||||||
error.status === 401
|
|
||||||
) {
|
|
||||||
store.dispatch(resetLocal())
|
|
||||||
}
|
|
||||||
setLocalCorrupt(error.data.error)
|
|
||||||
setAppLoaded(true)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
startingLog('log', 'no local credential found')
|
|
||||||
setAppLoaded(true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
startingLog('warn', 'network not connected')
|
|
||||||
setAppLoaded(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const main = useCallback(
|
const main = useCallback(
|
||||||
bootstrapped => {
|
bootstrapped => {
|
||||||
startingLog('log', 'bootstrapped')
|
log('log', 'App', 'bootstrapped')
|
||||||
if (bootstrapped && appLoaded) {
|
if (bootstrapped) {
|
||||||
startingLog('log', 'loading actual app :)')
|
log('log', 'App', 'loading actual app :)')
|
||||||
require('@root/i18n/i18n')
|
require('@root/i18n/i18n')
|
||||||
return (
|
return (
|
||||||
<ThemeManager>
|
<ThemeManager>
|
||||||
@ -167,7 +74,7 @@ const App: React.FC = () => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[appLoaded]
|
[localCorrupt]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
BIN
configs/Microsoft_AutoUpdate_4.30.20121301_Updater.pkg
Normal file
BIN
configs/Microsoft_AutoUpdate_4.30.20121301_Updater.pkg
Normal file
Binary file not shown.
@ -9,6 +9,7 @@
|
|||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-native-async-storage/async-storage": "^1.13.2",
|
||||||
"@react-native-community/masked-view": "0.1.10",
|
"@react-native-community/masked-view": "0.1.10",
|
||||||
"@react-native-community/netinfo": "^5.9.7",
|
"@react-native-community/netinfo": "^5.9.7",
|
||||||
"@react-native-community/segmented-control": "2.2.1",
|
"@react-native-community/segmented-control": "2.2.1",
|
||||||
@ -116,4 +117,4 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
3
publish.sh
Executable file
3
publish.sh
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
SENTRY_ORG=xmflsct SENTRY_PROJECT=mastodon-app SENTRY_AUTH_TOKEN=771a5746bad54f5987aa5d5d8b1c10abc751b3bf28554c908d452e0054eb6003 expo publish --release-channel testing
|
37
src/@types/app.d.ts
vendored
37
src/@types/app.d.ts
vendored
@ -15,40 +15,3 @@ declare namespace App {
|
|||||||
| 'Bookmarks'
|
| 'Bookmarks'
|
||||||
| 'Favourites'
|
| 'Favourites'
|
||||||
}
|
}
|
||||||
|
|
||||||
declare namespace QueryKey {
|
|
||||||
type Account = ['Account', { id: Mastodon.Account['id'] }]
|
|
||||||
|
|
||||||
type Announcements = ['Announcements', { showAll?: boolean }]
|
|
||||||
|
|
||||||
type Application = ['Application', { instanceDomain: string }]
|
|
||||||
|
|
||||||
type Instance = ['Instance', { instanceDomain: string }]
|
|
||||||
|
|
||||||
type Relationship = ['Relationship', { id: Mastodon.Account['id'] }]
|
|
||||||
|
|
||||||
type Relationships = [
|
|
||||||
'Relationships',
|
|
||||||
'following' | 'followers',
|
|
||||||
{ id: Mastodon.Account['id'] }
|
|
||||||
]
|
|
||||||
|
|
||||||
type Search = [
|
|
||||||
'Search',
|
|
||||||
{
|
|
||||||
type?: 'accounts' | 'hashtags' | 'statuses'
|
|
||||||
term: string
|
|
||||||
limit?: number
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
type Timeline = [
|
|
||||||
Pages,
|
|
||||||
{
|
|
||||||
hashtag?: Mastodon.Tag['name']
|
|
||||||
list?: Mastodon.List['id']
|
|
||||||
toot?: Mastodon.Status['id']
|
|
||||||
account?: Mastodon.Account['id']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
2
src/@types/mastodon.d.ts
vendored
2
src/@types/mastodon.d.ts
vendored
@ -67,7 +67,7 @@ declare namespace Mastodon {
|
|||||||
vapid_key?: string
|
vapid_key?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppOauth = {
|
type Apps = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
website?: string
|
website?: string
|
||||||
|
@ -11,39 +11,38 @@ import ScreenLocal from '@screens/Local'
|
|||||||
import ScreenMe from '@screens/Me'
|
import ScreenMe from '@screens/Me'
|
||||||
import ScreenNotifications from '@screens/Notifications'
|
import ScreenNotifications from '@screens/Notifications'
|
||||||
import ScreenPublic from '@screens/Public'
|
import ScreenPublic from '@screens/Public'
|
||||||
import { timelineFetch } from '@utils/fetches/timelineFetch'
|
import hookTimeline from '@utils/queryHooks/timeline'
|
||||||
import {
|
import {
|
||||||
|
getLocalActiveIndex,
|
||||||
getLocalNotification,
|
getLocalNotification,
|
||||||
getLocalUrl,
|
localUpdateAccountPreferences,
|
||||||
updateLocalAccountPreferences,
|
localUpdateNotification
|
||||||
updateNotification
|
|
||||||
} from '@utils/slices/instancesSlice'
|
} from '@utils/slices/instancesSlice'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { themes } from '@utils/styles/themes'
|
import { themes } from '@utils/styles/themes'
|
||||||
import * as Analytics from 'expo-firebase-analytics'
|
import * as Analytics from 'expo-firebase-analytics'
|
||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
import React, {
|
||||||
|
createRef,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef
|
||||||
|
} from 'react'
|
||||||
import { StatusBar } from 'react-native'
|
import { StatusBar } from 'react-native'
|
||||||
import Toast from 'react-native-toast-message'
|
import Toast from 'react-native-toast-message'
|
||||||
import { useInfiniteQuery } from 'react-query'
|
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
const Tab = createBottomTabNavigator<RootStackParamList>()
|
const Tab = createBottomTabNavigator()
|
||||||
|
|
||||||
export type RootStackParamList = {
|
|
||||||
'Screen-Local': undefined
|
|
||||||
'Screen-Public': { publicTab: boolean }
|
|
||||||
'Screen-Post': undefined
|
|
||||||
'Screen-Notifications': undefined
|
|
||||||
'Screen-Me': undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
localCorrupt?: string
|
localCorrupt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const navigationRef = createRef<NavigationContainerRef>()
|
||||||
|
|
||||||
const Index: React.FC<Props> = ({ localCorrupt }) => {
|
const Index: React.FC<Props> = ({ localCorrupt }) => {
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const localInstance = useSelector(getLocalUrl)
|
const localActiveIndex = useSelector(getLocalActiveIndex)
|
||||||
const { mode, theme } = useTheme()
|
const { mode, theme } = useTheme()
|
||||||
enum barStyle {
|
enum barStyle {
|
||||||
light = 'dark-content',
|
light = 'dark-content',
|
||||||
@ -51,7 +50,7 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const routeNameRef = useRef<string | undefined>()
|
const routeNameRef = useRef<string | undefined>()
|
||||||
const navigationRef = useRef<NavigationContainerRef>(null)
|
// const navigationRef = useRef<NavigationContainerRef>(null)
|
||||||
|
|
||||||
// const isConnected = useNetInfo().isConnected
|
// const isConnected = useNetInfo().isConnected
|
||||||
// const [firstRender, setFirstRender] = useState(false)
|
// const [firstRender, setFirstRender] = useState(false)
|
||||||
@ -82,14 +81,14 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
// On launch check if there is any unread announcements
|
// On launch check if there is any unread announcements
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('Checking announcements')
|
console.log('Checking announcements')
|
||||||
localInstance &&
|
localActiveIndex !== null &&
|
||||||
client({
|
client<Mastodon.Announcement[]>({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: `announcements`
|
url: `announcements`
|
||||||
})
|
})
|
||||||
.then(({ body }: { body?: Mastodon.Announcement[] }) => {
|
.then(res => {
|
||||||
if (body?.filter(announcement => !announcement.read).length) {
|
if (res?.filter(announcement => !announcement.read).length) {
|
||||||
navigationRef.current?.navigate('Screen-Shared-Announcements', {
|
navigationRef.current?.navigate('Screen-Shared-Announcements', {
|
||||||
showAll: false
|
showAll: false
|
||||||
})
|
})
|
||||||
@ -99,15 +98,15 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// On launch check if there is any unread noficiations
|
// On launch check if there is any unread noficiations
|
||||||
const queryNotification = useInfiniteQuery(
|
const queryNotification = hookTimeline({
|
||||||
['Notifications', {}],
|
page: 'Notifications',
|
||||||
timelineFetch,
|
options: {
|
||||||
{
|
enabled: localActiveIndex !== null ? true : false,
|
||||||
enabled: localInstance ? true : false,
|
|
||||||
refetchInterval: 1000 * 60,
|
refetchInterval: 1000 * 60,
|
||||||
refetchIntervalInBackground: true
|
refetchIntervalInBackground: true
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
|
|
||||||
const prevNotification = useSelector(getLocalNotification)
|
const prevNotification = useSelector(getLocalNotification)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (queryNotification.data?.pages) {
|
if (queryNotification.data?.pages) {
|
||||||
@ -120,7 +119,7 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
|
|
||||||
if (!prevNotification || !prevNotification.latestTime) {
|
if (!prevNotification || !prevNotification.latestTime) {
|
||||||
dispatch(
|
dispatch(
|
||||||
updateNotification({
|
localUpdateNotification({
|
||||||
unread: true
|
unread: true
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@ -129,7 +128,7 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
new Date(prevNotification.latestTime) < new Date(latestNotificationTime)
|
new Date(prevNotification.latestTime) < new Date(latestNotificationTime)
|
||||||
) {
|
) {
|
||||||
dispatch(
|
dispatch(
|
||||||
updateNotification({
|
localUpdateNotification({
|
||||||
unread: true,
|
unread: true,
|
||||||
latestTime: latestNotificationTime
|
latestTime: latestNotificationTime
|
||||||
})
|
})
|
||||||
@ -140,8 +139,8 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
|
|
||||||
// Lazily update users's preferences, for e.g. composing default visibility
|
// Lazily update users's preferences, for e.g. composing default visibility
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (localInstance) {
|
if (localActiveIndex !== null) {
|
||||||
dispatch(updateLocalAccountPreferences())
|
dispatch(localUpdateAccountPreferences())
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -204,51 +203,48 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
const tabNavigatorTabBarOptions = useMemo(
|
const tabNavigatorTabBarOptions = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
activeTintColor: theme.primary,
|
activeTintColor: theme.primary,
|
||||||
inactiveTintColor: localInstance ? theme.secondary : theme.disabled,
|
inactiveTintColor:
|
||||||
|
localActiveIndex !== null ? theme.secondary : theme.disabled,
|
||||||
showLabel: false
|
showLabel: false
|
||||||
}),
|
}),
|
||||||
[theme, localInstance]
|
[theme, localActiveIndex]
|
||||||
)
|
)
|
||||||
const tabScreenLocalListeners = useCallback(
|
const tabScreenLocalListeners = useCallback(
|
||||||
() => ({
|
() => ({
|
||||||
tabPress: (e: any) => {
|
tabPress: (e: any) => {
|
||||||
if (!localInstance) {
|
if (!(localActiveIndex !== null)) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
[localInstance]
|
[localActiveIndex]
|
||||||
)
|
)
|
||||||
const tabScreenComposeListeners = useMemo(
|
const tabScreenComposeListeners = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
tabPress: (e: any) => {
|
tabPress: (e: any) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (localInstance) {
|
if (localActiveIndex !== null) {
|
||||||
haptics('Medium')
|
haptics('Medium')
|
||||||
navigationRef.current?.navigate('Screen-Shared-Compose')
|
navigationRef.current?.navigate('Screen-Shared-Compose')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
[localInstance]
|
[localActiveIndex]
|
||||||
)
|
)
|
||||||
const tabScreenComposeComponent = useCallback(() => null, [])
|
const tabScreenComposeComponent = useCallback(() => null, [])
|
||||||
const tabScreenNotificationsListeners = useCallback(
|
const tabScreenNotificationsListeners = useCallback(
|
||||||
() => ({
|
() => ({
|
||||||
tabPress: (e: any) => {
|
tabPress: (e: any) => {
|
||||||
if (!localInstance) {
|
if (!(localActiveIndex !== null)) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
[localInstance]
|
[localActiveIndex]
|
||||||
)
|
)
|
||||||
const tabScreenNotificationsOptions = useMemo(
|
const tabScreenNotificationsOptions = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
tabBarBadge: prevNotification && prevNotification.unread ? '' : undefined,
|
tabBarBadge: prevNotification && prevNotification.unread ? '' : undefined
|
||||||
tabBarBadgeStyle: {
|
|
||||||
transform: [{ scale: 0.5 }],
|
|
||||||
backgroundColor: theme.red
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
[theme, prevNotification]
|
[theme, prevNotification]
|
||||||
)
|
)
|
||||||
@ -263,7 +259,9 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
onStateChange={navigationContainerOnStateChange}
|
onStateChange={navigationContainerOnStateChange}
|
||||||
>
|
>
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
initialRouteName={localInstance ? 'Screen-Local' : 'Screen-Public'}
|
initialRouteName={
|
||||||
|
localActiveIndex !== null ? 'Screen-Local' : 'Screen-Public'
|
||||||
|
}
|
||||||
screenOptions={tabNavigatorScreenOptions}
|
screenOptions={tabNavigatorScreenOptions}
|
||||||
tabBarOptions={tabNavigatorTabBarOptions}
|
tabBarOptions={tabNavigatorTabBarOptions}
|
||||||
>
|
>
|
||||||
@ -282,7 +280,13 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
name='Screen-Notifications'
|
name='Screen-Notifications'
|
||||||
component={ScreenNotifications}
|
component={ScreenNotifications}
|
||||||
listeners={tabScreenNotificationsListeners}
|
listeners={tabScreenNotificationsListeners}
|
||||||
options={tabScreenNotificationsOptions}
|
options={{
|
||||||
|
tabBarBadgeStyle: {
|
||||||
|
transform: [{ scale: 0.5 }],
|
||||||
|
backgroundColor: theme.red
|
||||||
|
},
|
||||||
|
...tabScreenNotificationsOptions
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Tab.Screen name='Screen-Me' component={ScreenMe} />
|
<Tab.Screen name='Screen-Me' component={ScreenMe} />
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
|
import { RootState } from '@root/store'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
|
|
||||||
const ctx = new chalk.Instance({ level: 3 })
|
const ctx = new chalk.Instance({ level: 3 })
|
||||||
|
|
||||||
const client = async ({
|
const client = async <T = unknown>({
|
||||||
method,
|
method,
|
||||||
instance,
|
instance,
|
||||||
|
localIndex,
|
||||||
instanceDomain,
|
instanceDomain,
|
||||||
version = 'v1',
|
version = 'v1',
|
||||||
url,
|
url,
|
||||||
@ -16,6 +18,7 @@ const client = async ({
|
|||||||
}: {
|
}: {
|
||||||
method: 'get' | 'post' | 'put' | 'delete'
|
method: 'get' | 'post' | 'put' | 'delete'
|
||||||
instance: 'local' | 'remote'
|
instance: 'local' | 'remote'
|
||||||
|
localIndex?: number
|
||||||
instanceDomain?: string
|
instanceDomain?: string
|
||||||
version?: 'v1' | 'v2'
|
version?: 'v1' | 'v2'
|
||||||
url: string
|
url: string
|
||||||
@ -25,9 +28,30 @@ const client = async ({
|
|||||||
headers?: { [key: string]: string }
|
headers?: { [key: string]: string }
|
||||||
body?: FormData
|
body?: FormData
|
||||||
onUploadProgress?: (progressEvent: any) => void
|
onUploadProgress?: (progressEvent: any) => void
|
||||||
}): Promise<any> => {
|
}): Promise<T> => {
|
||||||
|
const { store } = require('@root/store')
|
||||||
|
const state = (store.getState() as RootState).instances
|
||||||
|
const theLocalIndex =
|
||||||
|
localIndex !== undefined ? localIndex : state.local.activeIndex
|
||||||
|
|
||||||
|
let domain = null
|
||||||
|
if (instance === 'remote') {
|
||||||
|
domain = instanceDomain || state.remote.url
|
||||||
|
} else {
|
||||||
|
if (theLocalIndex !== null && state.local.instances[theLocalIndex]) {
|
||||||
|
domain = state.local.instances[theLocalIndex].url
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
ctx.bgRed.white.bold(' API ') + ' ' + 'No instance domain is provided'
|
||||||
|
)
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
ctx.bgGreen.bold(' API ') +
|
ctx.bgGreen.bold(' API ') +
|
||||||
|
' ' +
|
||||||
|
domain +
|
||||||
' ' +
|
' ' +
|
||||||
method +
|
method +
|
||||||
ctx.green(' -> ') +
|
ctx.green(' -> ') +
|
||||||
@ -35,10 +59,6 @@ const client = async ({
|
|||||||
(params ? ctx.green(' -> ') : ''),
|
(params ? ctx.green(' -> ') : ''),
|
||||||
params ? params : ''
|
params ? params : ''
|
||||||
)
|
)
|
||||||
const { store } = require('@root/store')
|
|
||||||
const state = store.getState().instances
|
|
||||||
const domain =
|
|
||||||
instance === 'remote' ? instanceDomain || state.remote.url : state.local.url
|
|
||||||
|
|
||||||
return axios({
|
return axios({
|
||||||
timeout: method === 'post' ? 1000 * 60 : 1000 * 15,
|
timeout: method === 'post' ? 1000 * 60 : 1000 * 15,
|
||||||
@ -50,18 +70,13 @@ const client = async ({
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...headers,
|
...headers,
|
||||||
...(instance === 'local' && {
|
...(instance === 'local' && {
|
||||||
Authorization: `Bearer ${state.local.token}`
|
Authorization: `Bearer ${state.local!.instances[theLocalIndex!].token}`
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
...(body && { data: body }),
|
...(body && { data: body }),
|
||||||
...(onUploadProgress && { onUploadProgress: onUploadProgress })
|
...(onUploadProgress && { onUploadProgress: onUploadProgress })
|
||||||
})
|
})
|
||||||
.then(response =>
|
.then(response => Promise.resolve(response.data))
|
||||||
Promise.resolve({
|
|
||||||
headers: response.headers,
|
|
||||||
body: response.data
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
// The request was made and the server responded with a status code
|
// The request was made and the server responded with a status code
|
||||||
@ -70,7 +85,8 @@ const client = async ({
|
|||||||
ctx.bold(' API '),
|
ctx.bold(' API '),
|
||||||
ctx.bold('response'),
|
ctx.bold('response'),
|
||||||
error.response.status,
|
error.response.status,
|
||||||
error.response.data.error
|
error.response.data.error,
|
||||||
|
error.request
|
||||||
)
|
)
|
||||||
return Promise.reject(error.response)
|
return Promise.reject(error.response)
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
|
@ -23,6 +23,7 @@ export interface Props {
|
|||||||
loading?: boolean
|
loading?: boolean
|
||||||
destructive?: boolean
|
destructive?: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
active?: boolean
|
||||||
|
|
||||||
strokeWidth?: number
|
strokeWidth?: number
|
||||||
size?: 'S' | 'M' | 'L'
|
size?: 'S' | 'M' | 'L'
|
||||||
@ -40,6 +41,7 @@ const Button: React.FC<Props> = ({
|
|||||||
loading = false,
|
loading = false,
|
||||||
destructive = false,
|
destructive = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
active = false,
|
||||||
strokeWidth,
|
strokeWidth,
|
||||||
size = 'M',
|
size = 'M',
|
||||||
spacing = 'S',
|
spacing = 'S',
|
||||||
@ -68,10 +70,29 @@ const Button: React.FC<Props> = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const colorContent = useMemo(() => {
|
const colorContent = useMemo(() => {
|
||||||
if (overlay) {
|
if (active) {
|
||||||
return theme.primaryOverlay
|
return theme.blue
|
||||||
} else {
|
} else {
|
||||||
if (disabled) {
|
if (overlay) {
|
||||||
|
return theme.primaryOverlay
|
||||||
|
} else {
|
||||||
|
if (disabled) {
|
||||||
|
return theme.secondary
|
||||||
|
} else {
|
||||||
|
if (destructive) {
|
||||||
|
return theme.red
|
||||||
|
} else {
|
||||||
|
return theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [theme, disabled])
|
||||||
|
const colorBorder = useMemo(() => {
|
||||||
|
if (active) {
|
||||||
|
return theme.blue
|
||||||
|
} else {
|
||||||
|
if (disabled || loading) {
|
||||||
return theme.secondary
|
return theme.secondary
|
||||||
} else {
|
} else {
|
||||||
if (destructive) {
|
if (destructive) {
|
||||||
@ -81,7 +102,14 @@ const Button: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [theme, disabled])
|
}, [theme, loading, disabled])
|
||||||
|
const colorBackground = useMemo(() => {
|
||||||
|
if (overlay) {
|
||||||
|
return theme.backgroundOverlay
|
||||||
|
} else {
|
||||||
|
return theme.background
|
||||||
|
}
|
||||||
|
}, [theme])
|
||||||
|
|
||||||
const children = useMemo(() => {
|
const children = useMemo(() => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -118,26 +146,7 @@ const Button: React.FC<Props> = ({
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [theme, content, loading, disabled])
|
}, [theme, content, loading, disabled, active])
|
||||||
|
|
||||||
const colorBorder = useMemo(() => {
|
|
||||||
if (disabled || loading) {
|
|
||||||
return theme.secondary
|
|
||||||
} else {
|
|
||||||
if (destructive) {
|
|
||||||
return theme.red
|
|
||||||
} else {
|
|
||||||
return theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [theme, loading, disabled])
|
|
||||||
const colorBackground = useMemo(() => {
|
|
||||||
if (overlay) {
|
|
||||||
return theme.backgroundOverlay
|
|
||||||
} else {
|
|
||||||
return theme.background
|
|
||||||
}
|
|
||||||
}, [theme])
|
|
||||||
|
|
||||||
enum spacingMapping {
|
enum spacingMapping {
|
||||||
XS = 'S',
|
XS = 'S',
|
||||||
@ -164,7 +173,7 @@ const Button: React.FC<Props> = ({
|
|||||||
testID='base'
|
testID='base'
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
children={children}
|
children={children}
|
||||||
disabled={disabled || loading}
|
disabled={disabled || active || loading}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { StyleConstants } from '@root/utils/styles/constants'
|
|
||||||
import React, { createElement } from 'react'
|
import React, { createElement } from 'react'
|
||||||
import { StyleProp, View, ViewStyle } from 'react-native'
|
import { StyleProp, View, ViewStyle } from 'react-native'
|
||||||
import * as FeatherIcon from 'react-native-feather'
|
import * as FeatherIcon from 'react-native-feather'
|
||||||
|
258
src/components/Instance.tsx
Normal file
258
src/components/Instance.tsx
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
import Button from '@components/Button'
|
||||||
|
import haptics from '@components/haptics'
|
||||||
|
import Icon from '@components/Icon'
|
||||||
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
import hookApps from '@utils/queryHooks/apps'
|
||||||
|
import hookInstance from '@utils/queryHooks/instance'
|
||||||
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
|
import { InstanceLocal, remoteUpdate } from '@utils/slices/instancesSlice'
|
||||||
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
|
import { debounce } from 'lodash'
|
||||||
|
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Image, StyleSheet, Text, TextInput, View } from 'react-native'
|
||||||
|
import { useQueryClient } from 'react-query'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
|
import InstanceAuth from './Instance/Auth'
|
||||||
|
import InstanceInfo from './Instance/Info'
|
||||||
|
import { toast } from './toast'
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
type: 'local' | 'remote'
|
||||||
|
disableHeaderImage?: boolean
|
||||||
|
goBack?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const ComponentInstance: React.FC<Props> = ({
|
||||||
|
type,
|
||||||
|
disableHeaderImage,
|
||||||
|
goBack = false
|
||||||
|
}) => {
|
||||||
|
const navigation = useNavigation()
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const { t } = useTranslation('meRoot')
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const [instanceDomain, setInstanceDomain] = useState<string | undefined>()
|
||||||
|
const [appData, setApplicationData] = useState<InstanceLocal['appData']>()
|
||||||
|
|
||||||
|
const instanceQuery = hookInstance({
|
||||||
|
instanceDomain,
|
||||||
|
options: { enabled: false, retry: false }
|
||||||
|
})
|
||||||
|
const applicationQuery = hookApps({
|
||||||
|
instanceDomain,
|
||||||
|
options: { enabled: false, retry: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
applicationQuery.data?.client_id.length &&
|
||||||
|
applicationQuery.data?.client_secret.length
|
||||||
|
) {
|
||||||
|
setApplicationData({
|
||||||
|
clientId: applicationQuery.data.client_id,
|
||||||
|
clientSecret: applicationQuery.data.client_secret
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [applicationQuery.data?.client_id])
|
||||||
|
|
||||||
|
const onChangeText = useCallback(
|
||||||
|
debounce(
|
||||||
|
text => {
|
||||||
|
setInstanceDomain(text.replace(/^http(s)?\:\/\//i, ''))
|
||||||
|
setApplicationData(undefined)
|
||||||
|
},
|
||||||
|
1000,
|
||||||
|
{
|
||||||
|
trailing: true
|
||||||
|
}
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
useEffect(() => {
|
||||||
|
if (instanceDomain) {
|
||||||
|
instanceQuery.refetch()
|
||||||
|
}
|
||||||
|
}, [instanceDomain])
|
||||||
|
|
||||||
|
const processUpdate = useCallback(() => {
|
||||||
|
if (instanceDomain) {
|
||||||
|
haptics('Success')
|
||||||
|
switch (type) {
|
||||||
|
case 'local':
|
||||||
|
applicationQuery.refetch()
|
||||||
|
return
|
||||||
|
case 'remote':
|
||||||
|
const queryKey: QueryKeyTimeline = [
|
||||||
|
'Timeline',
|
||||||
|
{ page: 'RemotePublic' }
|
||||||
|
]
|
||||||
|
dispatch(remoteUpdate(instanceDomain))
|
||||||
|
queryClient.resetQueries(queryKey)
|
||||||
|
toast({ type: 'success', message: '重置成功' })
|
||||||
|
navigation.navigate('Screen-Remote-Root')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [instanceDomain])
|
||||||
|
|
||||||
|
const onSubmitEditing = useCallback(
|
||||||
|
({ nativeEvent: { text } }) => {
|
||||||
|
if (
|
||||||
|
text === instanceDomain &&
|
||||||
|
instanceQuery.isSuccess &&
|
||||||
|
instanceQuery.data &&
|
||||||
|
instanceQuery.data.uri
|
||||||
|
) {
|
||||||
|
processUpdate()
|
||||||
|
} else {
|
||||||
|
setInstanceDomain(text)
|
||||||
|
setApplicationData(undefined)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[instanceDomain, instanceQuery.isSuccess, instanceQuery.data]
|
||||||
|
)
|
||||||
|
|
||||||
|
const buttonContent = useMemo(() => {
|
||||||
|
switch (type) {
|
||||||
|
case 'local':
|
||||||
|
return t('content.login.button')
|
||||||
|
case 'remote':
|
||||||
|
return '登记'
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!disableHeaderImage ? (
|
||||||
|
<View style={styles.imageContainer}>
|
||||||
|
<Image
|
||||||
|
source={require('assets/screens/meRoot/welcome.png')}
|
||||||
|
style={styles.image}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
<View style={styles.base}>
|
||||||
|
<View style={styles.inputRow}>
|
||||||
|
<TextInput
|
||||||
|
style={[
|
||||||
|
styles.textInput,
|
||||||
|
{
|
||||||
|
color: theme.primary,
|
||||||
|
borderBottomColor: theme.border
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
onChangeText={onChangeText}
|
||||||
|
autoCapitalize='none'
|
||||||
|
autoCorrect={false}
|
||||||
|
clearButtonMode='never'
|
||||||
|
keyboardType='url'
|
||||||
|
textContentType='URL'
|
||||||
|
onSubmitEditing={onSubmitEditing}
|
||||||
|
placeholder={t('content.login.server.placeholder')}
|
||||||
|
placeholderTextColor={theme.secondary}
|
||||||
|
returnKeyType='go'
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type='text'
|
||||||
|
content={buttonContent}
|
||||||
|
onPress={processUpdate}
|
||||||
|
disabled={!instanceQuery.data?.uri}
|
||||||
|
loading={instanceQuery.isFetching || applicationQuery.isFetching}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<InstanceInfo
|
||||||
|
visible={instanceQuery.data?.title !== undefined}
|
||||||
|
header='实例名称'
|
||||||
|
content={instanceQuery.data?.title || undefined}
|
||||||
|
potentialWidth={10}
|
||||||
|
/>
|
||||||
|
<InstanceInfo
|
||||||
|
visible={instanceQuery.data?.short_description !== undefined}
|
||||||
|
header='实例介绍'
|
||||||
|
content={instanceQuery.data?.short_description || undefined}
|
||||||
|
potentialLines={5}
|
||||||
|
/>
|
||||||
|
<View style={styles.instanceStats}>
|
||||||
|
<InstanceInfo
|
||||||
|
style={{ alignItems: 'flex-start' }}
|
||||||
|
visible={instanceQuery.data?.stats?.user_count !== null}
|
||||||
|
header='用户总数'
|
||||||
|
content={
|
||||||
|
instanceQuery.data?.stats?.user_count?.toString() || undefined
|
||||||
|
}
|
||||||
|
potentialWidth={4}
|
||||||
|
/>
|
||||||
|
<InstanceInfo
|
||||||
|
style={{ alignItems: 'center' }}
|
||||||
|
visible={instanceQuery.data?.stats?.status_count !== null}
|
||||||
|
header='嘟嘟总数'
|
||||||
|
content={
|
||||||
|
instanceQuery.data?.stats?.status_count?.toString() || undefined
|
||||||
|
}
|
||||||
|
potentialWidth={4}
|
||||||
|
/>
|
||||||
|
<InstanceInfo
|
||||||
|
style={{ alignItems: 'flex-end' }}
|
||||||
|
visible={instanceQuery.data?.stats?.domain_count !== null}
|
||||||
|
header='嘟嘟总数'
|
||||||
|
content={
|
||||||
|
instanceQuery.data?.stats?.domain_count?.toString() || undefined
|
||||||
|
}
|
||||||
|
potentialWidth={4}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text style={[styles.disclaimer, { color: theme.secondary }]}>
|
||||||
|
<Icon
|
||||||
|
name='Lock'
|
||||||
|
size={StyleConstants.Font.Size.M}
|
||||||
|
color={theme.secondary}
|
||||||
|
/>{' '}
|
||||||
|
本站不留存任何信息
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{type === 'local' && appData ? (
|
||||||
|
<InstanceAuth
|
||||||
|
instanceDomain={instanceDomain!}
|
||||||
|
appData={appData}
|
||||||
|
goBack={goBack}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
imageContainer: { flexDirection: 'row' },
|
||||||
|
image: { resizeMode: 'contain', flex: 1, aspectRatio: 16 / 9 },
|
||||||
|
base: {
|
||||||
|
marginVertical: StyleConstants.Spacing.L,
|
||||||
|
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
|
||||||
|
},
|
||||||
|
inputRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
|
||||||
|
},
|
||||||
|
textInput: {
|
||||||
|
flex: 1,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
...StyleConstants.FontStyle.M,
|
||||||
|
marginRight: StyleConstants.Spacing.M
|
||||||
|
},
|
||||||
|
instanceStats: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row'
|
||||||
|
},
|
||||||
|
disclaimer: {
|
||||||
|
...StyleConstants.FontStyle.S,
|
||||||
|
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
||||||
|
marginVertical: StyleConstants.Spacing.M
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default ComponentInstance
|
74
src/components/Instance/Auth.tsx
Normal file
74
src/components/Instance/Auth.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
import { InstanceLocal, localAddInstance } from '@utils/slices/instancesSlice'
|
||||||
|
import * as AuthSession from 'expo-auth-session'
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import { useQueryClient } from 'react-query'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
instanceDomain: string
|
||||||
|
appData: InstanceLocal['appData']
|
||||||
|
goBack?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const InstanceAuth = React.memo(
|
||||||
|
({ instanceDomain, appData, goBack }: Props) => {
|
||||||
|
const navigation = useNavigation()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
const [request, response, promptAsync] = AuthSession.useAuthRequest(
|
||||||
|
{
|
||||||
|
clientId: appData.clientId,
|
||||||
|
clientSecret: appData.clientSecret,
|
||||||
|
scopes: ['read', 'write', 'follow', 'push'],
|
||||||
|
redirectUri: 'exp://127.0.0.1:19000'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authorizationEndpoint: `https://${instanceDomain}/oauth/authorize`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
useEffect(() => {
|
||||||
|
;(async () => {
|
||||||
|
if (request?.clientId) {
|
||||||
|
await promptAsync()
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}, [request])
|
||||||
|
useEffect(() => {
|
||||||
|
;(async () => {
|
||||||
|
if (response?.type === 'success') {
|
||||||
|
const { accessToken } = await AuthSession.exchangeCodeAsync(
|
||||||
|
{
|
||||||
|
clientId: appData.clientId,
|
||||||
|
clientSecret: appData.clientSecret,
|
||||||
|
scopes: ['read', 'write', 'follow', 'push'],
|
||||||
|
redirectUri: 'exp://127.0.0.1:19000',
|
||||||
|
code: response.params.code,
|
||||||
|
extraParams: {
|
||||||
|
grant_type: 'authorization_code'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tokenEndpoint: `https://${instanceDomain}/oauth/token`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
queryClient.clear()
|
||||||
|
dispatch(
|
||||||
|
localAddInstance({
|
||||||
|
url: instanceDomain,
|
||||||
|
token: accessToken,
|
||||||
|
appData
|
||||||
|
})
|
||||||
|
)
|
||||||
|
goBack && navigation.goBack()
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}, [response])
|
||||||
|
|
||||||
|
return <></>
|
||||||
|
},
|
||||||
|
() => true
|
||||||
|
)
|
||||||
|
|
||||||
|
export default InstanceAuth
|
73
src/components/Instance/Info.tsx
Normal file
73
src/components/Instance/Info.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { ParseHTML } from '@components/Parse'
|
||||||
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient'
|
||||||
|
import React from 'react'
|
||||||
|
import { Dimensions, StyleSheet, Text, View, ViewStyle } from 'react-native'
|
||||||
|
import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder'
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
style?: ViewStyle
|
||||||
|
visible: boolean
|
||||||
|
header: string
|
||||||
|
content?: string
|
||||||
|
potentialWidth?: number
|
||||||
|
potentialLines?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const InstanceInfo = React.memo(
|
||||||
|
({
|
||||||
|
style,
|
||||||
|
visible,
|
||||||
|
header,
|
||||||
|
content,
|
||||||
|
potentialWidth,
|
||||||
|
potentialLines = 1
|
||||||
|
}: Props) => {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const ShimmerPlaceholder = createShimmerPlaceholder(LinearGradient)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.base, style]}>
|
||||||
|
<Text style={[styles.header, { color: theme.primary }]}>{header}</Text>
|
||||||
|
<ShimmerPlaceholder
|
||||||
|
visible={visible}
|
||||||
|
stopAutoRun
|
||||||
|
width={
|
||||||
|
potentialWidth
|
||||||
|
? potentialWidth * StyleConstants.Font.Size.M
|
||||||
|
: Dimensions.get('screen').width -
|
||||||
|
StyleConstants.Spacing.Global.PagePadding * 4
|
||||||
|
}
|
||||||
|
height={StyleConstants.Font.LineHeight.M * potentialLines}
|
||||||
|
shimmerColors={[theme.shimmerDefault, theme.shimmerHighlight, theme.shimmerDefault]}
|
||||||
|
>
|
||||||
|
{content ? (
|
||||||
|
<ParseHTML
|
||||||
|
content={content}
|
||||||
|
size={'M'}
|
||||||
|
numberOfLines={5}
|
||||||
|
expandHint='介绍'
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</ShimmerPlaceholder>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
base: {
|
||||||
|
flex: 1,
|
||||||
|
marginTop: StyleConstants.Spacing.M,
|
||||||
|
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
|
||||||
|
paddingRight: StyleConstants.Spacing.Global.PagePadding
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
...StyleConstants.FontStyle.S,
|
||||||
|
fontWeight: StyleConstants.Font.Weight.Bold,
|
||||||
|
marginBottom: StyleConstants.Spacing.XS
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default InstanceInfo
|
@ -134,7 +134,7 @@ const MenuRow: React.FC<Props> = ({
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
base: {
|
base: {
|
||||||
height: 50
|
minHeight: 50
|
||||||
},
|
},
|
||||||
core: {
|
core: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
@ -5,16 +5,12 @@ import { useNavigation } from '@react-navigation/native'
|
|||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { LinearGradient } from 'expo-linear-gradient'
|
import { LinearGradient } from 'expo-linear-gradient'
|
||||||
import React, { useCallback, useMemo, useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { Pressable, Text, View } from 'react-native'
|
import { Image, Pressable, Text, View } from 'react-native'
|
||||||
import HTMLView from 'react-native-htmlview'
|
import HTMLView from 'react-native-htmlview'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
measure,
|
|
||||||
useAnimatedRef,
|
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useDerivedValue,
|
useDerivedValue,
|
||||||
useSharedValue,
|
|
||||||
withSpring,
|
|
||||||
withTiming
|
withTiming
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
|
|
||||||
@ -40,11 +36,62 @@ const renderNode = ({
|
|||||||
showFullLink: boolean
|
showFullLink: boolean
|
||||||
disableDetails: boolean
|
disableDetails: boolean
|
||||||
}) => {
|
}) => {
|
||||||
if (node.name == 'a') {
|
switch (node.name) {
|
||||||
const classes = node.attribs.class
|
case 'a':
|
||||||
const href = node.attribs.href
|
const classes = node.attribs.class
|
||||||
if (classes) {
|
const href = node.attribs.href
|
||||||
if (classes.includes('hashtag')) {
|
if (classes) {
|
||||||
|
if (classes.includes('hashtag')) {
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
color: theme.blue,
|
||||||
|
...StyleConstants.FontStyle[size]
|
||||||
|
}}
|
||||||
|
onPress={() => {
|
||||||
|
const tag = href.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/))
|
||||||
|
!disableDetails &&
|
||||||
|
navigation.push('Screen-Shared-Hashtag', {
|
||||||
|
hashtag: tag[1] || tag[2]
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{node.children[0].data}
|
||||||
|
{node.children[1]?.children[0].data}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
} else if (classes.includes('mention') && mentions) {
|
||||||
|
const accountIndex = mentions.findIndex(
|
||||||
|
mention => mention.url === href
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
color: accountIndex !== -1 ? theme.blue : undefined,
|
||||||
|
...StyleConstants.FontStyle[size]
|
||||||
|
}}
|
||||||
|
onPress={() => {
|
||||||
|
accountIndex !== -1 &&
|
||||||
|
!disableDetails &&
|
||||||
|
navigation.push('Screen-Shared-Account', {
|
||||||
|
account: mentions[accountIndex]
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{node.children[0].data}
|
||||||
|
{node.children[1]?.children[0].data}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const domain = href.split(new RegExp(/:\/\/(.[^\/]+)/))
|
||||||
|
// Need example here
|
||||||
|
const content =
|
||||||
|
node.children && node.children[0] && node.children[0].data
|
||||||
|
const shouldBeTag =
|
||||||
|
tags && tags.filter(tag => `#${tag.name}` === content).length > 0
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
key={index}
|
key={index}
|
||||||
@ -52,78 +99,31 @@ const renderNode = ({
|
|||||||
color: theme.blue,
|
color: theme.blue,
|
||||||
...StyleConstants.FontStyle[size]
|
...StyleConstants.FontStyle[size]
|
||||||
}}
|
}}
|
||||||
onPress={() => {
|
onPress={async () =>
|
||||||
const tag = href.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/))
|
!disableDetails && !shouldBeTag
|
||||||
!disableDetails &&
|
? await openLink(href)
|
||||||
navigation.push('Screen-Shared-Hashtag', {
|
: navigation.push('Screen-Shared-Hashtag', {
|
||||||
hashtag: tag[1] || tag[2]
|
hashtag: content.substring(1)
|
||||||
})
|
})
|
||||||
}}
|
}
|
||||||
>
|
>
|
||||||
{node.children[0].data}
|
{!shouldBeTag ? (
|
||||||
{node.children[1]?.children[0].data}
|
<Icon
|
||||||
</Text>
|
color={theme.blue}
|
||||||
)
|
name='ExternalLink'
|
||||||
} else if (classes.includes('mention') && mentions) {
|
size={StyleConstants.Font.Size[size]}
|
||||||
const accountIndex = mentions.findIndex(mention => mention.url === href)
|
/>
|
||||||
return (
|
) : null}
|
||||||
<Text
|
{content || (showFullLink ? href : domain[1])}
|
||||||
key={index}
|
|
||||||
style={{
|
|
||||||
color: accountIndex !== -1 ? theme.blue : undefined,
|
|
||||||
...StyleConstants.FontStyle[size]
|
|
||||||
}}
|
|
||||||
onPress={() => {
|
|
||||||
accountIndex !== -1 &&
|
|
||||||
!disableDetails &&
|
|
||||||
navigation.push('Screen-Shared-Account', {
|
|
||||||
account: mentions[accountIndex]
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{node.children[0].data}
|
|
||||||
{node.children[1]?.children[0].data}
|
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
break
|
||||||
const domain = href.split(new RegExp(/:\/\/(.[^\/]+)/))
|
case 'p':
|
||||||
// Need example here
|
|
||||||
const content = node.children && node.children[0] && node.children[0].data
|
|
||||||
const shouldBeTag =
|
|
||||||
tags && tags.filter(tag => `#${tag.name}` === content).length > 0
|
|
||||||
return (
|
|
||||||
<Text
|
|
||||||
key={index}
|
|
||||||
style={{
|
|
||||||
color: theme.blue,
|
|
||||||
...StyleConstants.FontStyle[size]
|
|
||||||
}}
|
|
||||||
onPress={async () =>
|
|
||||||
!disableDetails && !shouldBeTag
|
|
||||||
? await openLink(href)
|
|
||||||
: navigation.push('Screen-Shared-Hashtag', {
|
|
||||||
hashtag: content.substring(1)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{!shouldBeTag ? (
|
|
||||||
<Icon
|
|
||||||
color={theme.blue}
|
|
||||||
name='ExternalLink'
|
|
||||||
size={StyleConstants.Font.Size[size]}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{content || (showFullLink ? href : domain[1])}
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (node.name === 'p') {
|
|
||||||
if (!node.children.length) {
|
if (!node.children.length) {
|
||||||
return <View key={index} /> // bug when the tag is empty
|
return <View key={index} /> // bug when the tag is empty
|
||||||
}
|
}
|
||||||
}
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,12 +15,12 @@ export interface Props {
|
|||||||
const RelationshipIncoming: React.FC<Props> = ({ id }) => {
|
const RelationshipIncoming: React.FC<Props> = ({ id }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const relationshipQueryKey: QueryKey.Relationship = ['Relationship', { id }]
|
const relationshipQueryKey = ['Relationship', { id }]
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const fireMutation = useCallback(
|
const fireMutation = useCallback(
|
||||||
({ type }: { type: 'authorize' | 'reject' }) => {
|
({ type }: { type: 'authorize' | 'reject' }) => {
|
||||||
return client({
|
return client<Mastodon.Relationship>({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: `follow_requests/${id}/${type}`
|
url: `follow_requests/${id}/${type}`
|
||||||
@ -29,9 +29,9 @@ const RelationshipIncoming: React.FC<Props> = ({ id }) => {
|
|||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
const mutation = useMutation(fireMutation, {
|
const mutation = useMutation(fireMutation, {
|
||||||
onSuccess: ({ body }) => {
|
onSuccess: res => {
|
||||||
haptics('Success')
|
haptics('Success')
|
||||||
queryClient.setQueryData(relationshipQueryKey, body)
|
queryClient.setQueryData(relationshipQueryKey, res)
|
||||||
queryClient.refetchQueries(['Notifications'])
|
queryClient.refetchQueries(['Notifications'])
|
||||||
},
|
},
|
||||||
onError: (err: any, { type }) => {
|
onError: (err: any, { type }) => {
|
||||||
|
@ -2,10 +2,10 @@ import client from '@api/client'
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import haptics from '@components/haptics'
|
import haptics from '@components/haptics'
|
||||||
import { toast } from '@components/toast'
|
import { toast } from '@components/toast'
|
||||||
import { relationshipFetch } from '@utils/fetches/relationshipFetch'
|
import hookRelationship from '@utils/queryHooks/relationship'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useMutation, useQuery, useQueryClient } from 'react-query'
|
import { useMutation, useQueryClient } from 'react-query'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
id: Mastodon.Account['id']
|
id: Mastodon.Account['id']
|
||||||
@ -14,13 +14,13 @@ export interface Props {
|
|||||||
const RelationshipOutgoing: React.FC<Props> = ({ id }) => {
|
const RelationshipOutgoing: React.FC<Props> = ({ id }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const relationshipQueryKey: QueryKey.Relationship = ['Relationship', { id }]
|
const relationshipQueryKey = ['Relationship', { id }]
|
||||||
const query = useQuery(relationshipQueryKey, relationshipFetch)
|
const query = hookRelationship({ id })
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const fireMutation = useCallback(
|
const fireMutation = useCallback(
|
||||||
({ type, state }: { type: 'follow' | 'block'; state: boolean }) => {
|
({ type, state }: { type: 'follow' | 'block'; state: boolean }) => {
|
||||||
return client({
|
return client<Mastodon.Relationship>({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: `accounts/${id}/${state ? 'un' : ''}${type}`
|
url: `accounts/${id}/${state ? 'un' : ''}${type}`
|
||||||
@ -29,9 +29,9 @@ const RelationshipOutgoing: React.FC<Props> = ({ id }) => {
|
|||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
const mutation = useMutation(fireMutation, {
|
const mutation = useMutation(fireMutation, {
|
||||||
onSuccess: ({ body }) => {
|
onSuccess: res => {
|
||||||
haptics('Success')
|
haptics('Success')
|
||||||
queryClient.setQueryData(relationshipQueryKey, body)
|
queryClient.setQueryData(relationshipQueryKey, res)
|
||||||
},
|
},
|
||||||
onError: (err: any, { type }) => {
|
onError: (err: any, { type }) => {
|
||||||
haptics('Error')
|
haptics('Error')
|
||||||
|
@ -3,7 +3,7 @@ import Timeline from '@components/Timelines/Timeline'
|
|||||||
import SegmentedControl from '@react-native-community/segmented-control'
|
import SegmentedControl from '@react-native-community/segmented-control'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import sharedScreens from '@screens/Shared/sharedScreens'
|
import sharedScreens from '@screens/Shared/sharedScreens'
|
||||||
import { getLocalUrl, getRemoteUrl } from '@utils/slices/instancesSlice'
|
import { getLocalActiveIndex, getRemoteUrl } from '@utils/slices/instancesSlice'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { Dimensions, StyleSheet, View } from 'react-native'
|
import { Dimensions, StyleSheet, View } from 'react-native'
|
||||||
@ -21,7 +21,7 @@ export interface Props {
|
|||||||
const Timelines: React.FC<Props> = ({ name, content }) => {
|
const Timelines: React.FC<Props> = ({ name, content }) => {
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const { mode } = useTheme()
|
const { mode } = useTheme()
|
||||||
const localRegistered = useSelector(getLocalUrl)
|
const localActiveIndex = useSelector(getLocalActiveIndex)
|
||||||
const publicDomain = useSelector(getRemoteUrl)
|
const publicDomain = useSelector(getRemoteUrl)
|
||||||
const [segment, setSegment] = useState(0)
|
const [segment, setSegment] = useState(0)
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const routes = content
|
const routes = content
|
||||||
.filter(p => (localRegistered ? true : p.page === 'RemotePublic'))
|
.filter(p => (localActiveIndex !== null ? true : p.page === 'RemotePublic'))
|
||||||
.map(p => ({ key: p.page }))
|
.map(p => ({ key: p.page }))
|
||||||
|
|
||||||
const renderScene = useCallback(
|
const renderScene = useCallback(
|
||||||
@ -42,12 +42,12 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
|||||||
}
|
}
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
(localRegistered || route.key === 'RemotePublic') && (
|
(localActiveIndex !== null || route.key === 'RemotePublic') && (
|
||||||
<Timeline page={route.key} />
|
<Timeline page={route.key} />
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[localRegistered]
|
[localActiveIndex]
|
||||||
)
|
)
|
||||||
|
|
||||||
const screenComponent = useCallback(
|
const screenComponent = useCallback(
|
||||||
@ -62,7 +62,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
|||||||
initialLayout={{ width: Dimensions.get('window').width }}
|
initialLayout={{ width: Dimensions.get('window').width }}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[segment, localRegistered]
|
[segment, localActiveIndex]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -71,7 +71,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
|||||||
name={`Screen-${name}-Root`}
|
name={`Screen-${name}-Root`}
|
||||||
options={{
|
options={{
|
||||||
headerTitle: name === 'Public' ? publicDomain : '',
|
headerTitle: name === 'Public' ? publicDomain : '',
|
||||||
...(localRegistered && {
|
...(localActiveIndex !== null && {
|
||||||
headerCenter: () => (
|
headerCenter: () => (
|
||||||
<View style={styles.segmentsContainer}>
|
<View style={styles.segmentsContainer}>
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
|
@ -3,16 +3,17 @@ import TimelineConversation from '@components/Timelines/Timeline/Conversation'
|
|||||||
import TimelineDefault from '@components/Timelines/Timeline/Default'
|
import TimelineDefault from '@components/Timelines/Timeline/Default'
|
||||||
import TimelineEmpty from '@components/Timelines/Timeline/Empty'
|
import TimelineEmpty from '@components/Timelines/Timeline/Empty'
|
||||||
import TimelineEnd from '@root/components/Timelines/Timeline/End'
|
import TimelineEnd from '@root/components/Timelines/Timeline/End'
|
||||||
|
import TimelineHeader from '@components/Timelines/Timeline/Header'
|
||||||
import TimelineNotifications from '@components/Timelines/Timeline/Notifications'
|
import TimelineNotifications from '@components/Timelines/Timeline/Notifications'
|
||||||
import { useScrollToTop } from '@react-navigation/native'
|
import { useScrollToTop } from '@react-navigation/native'
|
||||||
import { timelineFetch } from '@utils/fetches/timelineFetch'
|
import { localUpdateNotification } from '@utils/slices/instancesSlice'
|
||||||
import { updateNotification } from '@utils/slices/instancesSlice'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
import { RefreshControl, StyleSheet } from 'react-native'
|
import { RefreshControl, StyleSheet } from 'react-native'
|
||||||
import { FlatList } from 'react-native-gesture-handler'
|
import { FlatList } from 'react-native-gesture-handler'
|
||||||
import { InfiniteData, useInfiniteQuery } from 'react-query'
|
import { InfiniteData } from 'react-query'
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
|
import hookTimeline, { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
|
|
||||||
export type TimelineData =
|
export type TimelineData =
|
||||||
| InfiniteData<{
|
| InfiniteData<{
|
||||||
@ -41,15 +42,14 @@ const Timeline: React.FC<Props> = ({
|
|||||||
disableRefresh = false,
|
disableRefresh = false,
|
||||||
disableInfinity = false
|
disableInfinity = false
|
||||||
}) => {
|
}) => {
|
||||||
const queryKey: QueryKey.Timeline = [
|
const queryKeyParams = {
|
||||||
page,
|
page,
|
||||||
{
|
...(hashtag && { hashtag }),
|
||||||
...(hashtag && { hashtag }),
|
...(list && { list }),
|
||||||
...(list && { list }),
|
...(toot && { toot }),
|
||||||
...(toot && { toot }),
|
...(account && { account })
|
||||||
...(account && { account })
|
}
|
||||||
}
|
const queryKey: QueryKeyTimeline = ['Timeline', queryKeyParams]
|
||||||
]
|
|
||||||
const {
|
const {
|
||||||
status,
|
status,
|
||||||
data,
|
data,
|
||||||
@ -61,24 +61,28 @@ const Timeline: React.FC<Props> = ({
|
|||||||
hasNextPage,
|
hasNextPage,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
isFetchingNextPage
|
isFetchingNextPage
|
||||||
} = useInfiniteQuery(queryKey, timelineFetch, {
|
} = hookTimeline({
|
||||||
getPreviousPageParam: firstPage => {
|
...queryKeyParams,
|
||||||
return firstPage.toots.length
|
options: {
|
||||||
? {
|
getPreviousPageParam: firstPage => {
|
||||||
direction: 'prev',
|
return firstPage.toots.length
|
||||||
id: firstPage.toots[0].id
|
? {
|
||||||
}
|
direction: 'prev',
|
||||||
: undefined
|
id: firstPage.toots[0].id
|
||||||
},
|
}
|
||||||
getNextPageParam: lastPage => {
|
: undefined
|
||||||
return lastPage.toots.length
|
},
|
||||||
? {
|
getNextPageParam: lastPage => {
|
||||||
direction: 'next',
|
return lastPage.toots.length
|
||||||
id: lastPage.toots[lastPage.toots.length - 1].id
|
? {
|
||||||
}
|
direction: 'next',
|
||||||
: undefined
|
id: lastPage.toots[lastPage.toots.length - 1].id
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const flattenData = data?.pages ? data.pages.flatMap(d => [...d?.toots]) : []
|
const flattenData = data?.pages ? data.pages.flatMap(d => [...d?.toots]) : []
|
||||||
const flattenPointer = data?.pages
|
const flattenPointer = data?.pages
|
||||||
? data.pages.flatMap(d => [d?.pointer])
|
? data.pages.flatMap(d => [d?.pointer])
|
||||||
@ -92,9 +96,9 @@ const Timeline: React.FC<Props> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (page === 'Notifications' && flattenData.length) {
|
if (page === 'Notifications' && flattenData.length) {
|
||||||
dispatch(
|
dispatch(
|
||||||
updateNotification({
|
localUpdateNotification({
|
||||||
unread: false,
|
unread: false,
|
||||||
latestTime: flattenData[0].created_at
|
latestTime: (flattenData[0] as Mastodon.Notification).created_at
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -130,7 +134,7 @@ const Timeline: React.FC<Props> = ({
|
|||||||
item={item}
|
item={item}
|
||||||
queryKey={queryKey}
|
queryKey={queryKey}
|
||||||
index={index}
|
index={index}
|
||||||
{...(queryKey[0] === 'RemotePublic' && {
|
{...(queryKey[1].page === 'RemotePublic' && {
|
||||||
disableDetails: true,
|
disableDetails: true,
|
||||||
disableOnPress: true
|
disableOnPress: true
|
||||||
})}
|
})}
|
||||||
@ -166,6 +170,7 @@ const Timeline: React.FC<Props> = ({
|
|||||||
() => !disableInfinity && !isFetchingNextPage && fetchNextPage(),
|
() => !disableInfinity && !isFetchingNextPage && fetchNextPage(),
|
||||||
[isFetchingNextPage]
|
[isFetchingNextPage]
|
||||||
)
|
)
|
||||||
|
const ListHeaderComponent = useCallback(() => <TimelineHeader />, [])
|
||||||
const ListFooterComponent = useCallback(
|
const ListFooterComponent = useCallback(
|
||||||
() => <TimelineEnd hasNextPage={!disableInfinity ? hasNextPage : false} />,
|
() => <TimelineEnd hasNextPage={!disableInfinity ? hasNextPage : false} />,
|
||||||
[hasNextPage]
|
[hasNextPage]
|
||||||
@ -207,6 +212,8 @@ const Timeline: React.FC<Props> = ({
|
|||||||
ListEmptyComponent={flItemEmptyComponent}
|
ListEmptyComponent={flItemEmptyComponent}
|
||||||
{...(!disableRefresh && { refreshControl })}
|
{...(!disableRefresh && { refreshControl })}
|
||||||
ItemSeparatorComponent={ItemSeparatorComponent}
|
ItemSeparatorComponent={ItemSeparatorComponent}
|
||||||
|
{...(queryKey &&
|
||||||
|
queryKey[1].page === 'RemotePublic' && { ListHeaderComponent })}
|
||||||
{...(toot && isSuccess && { onScrollToIndexFailed })}
|
{...(toot && isSuccess && { onScrollToIndexFailed })}
|
||||||
maintainVisibleContentPosition={{
|
maintainVisibleContentPosition={{
|
||||||
minIndexForVisible: 0,
|
minIndexForVisible: 0,
|
||||||
|
@ -10,27 +10,14 @@ import TimelineActions from '@components/Timelines/Timeline/Shared/Actions'
|
|||||||
import client from '@root/api/client'
|
import client from '@root/api/client'
|
||||||
import { useMutation, useQueryClient } from 'react-query'
|
import { useMutation, useQueryClient } from 'react-query'
|
||||||
import { useTheme } from '@root/utils/styles/ThemeManager'
|
import { useTheme } from '@root/utils/styles/ThemeManager'
|
||||||
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
conversation: Mastodon.Conversation
|
conversation: Mastodon.Conversation
|
||||||
queryKey: QueryKey.Timeline
|
queryKey: QueryKeyTimeline
|
||||||
highlighted?: boolean
|
highlighted?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const fireMutation = async ({ id }: { id: Mastodon.Conversation['id'] }) => {
|
|
||||||
const res = await client({
|
|
||||||
method: 'post',
|
|
||||||
instance: 'local',
|
|
||||||
url: `conversations/${id}/read`
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res.body.id === id) {
|
|
||||||
return Promise.resolve()
|
|
||||||
} else {
|
|
||||||
return Promise.reject()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const TimelineConversation: React.FC<Props> = ({
|
const TimelineConversation: React.FC<Props> = ({
|
||||||
conversation,
|
conversation,
|
||||||
queryKey,
|
queryKey,
|
||||||
@ -39,6 +26,13 @@ const TimelineConversation: React.FC<Props> = ({
|
|||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const fireMutation = useCallback(() => {
|
||||||
|
return client<Mastodon.Conversation>({
|
||||||
|
method: 'post',
|
||||||
|
instance: 'local',
|
||||||
|
url: `conversations/${conversation.id}/read`
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
const { mutate } = useMutation(fireMutation, {
|
const { mutate } = useMutation(fireMutation, {
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries(queryKey)
|
queryClient.invalidateQueries(queryKey)
|
||||||
@ -49,7 +43,7 @@ const TimelineConversation: React.FC<Props> = ({
|
|||||||
|
|
||||||
const onPress = useCallback(() => {
|
const onPress = useCallback(() => {
|
||||||
if (conversation.last_status) {
|
if (conversation.last_status) {
|
||||||
conversation.unread && mutate({ id: conversation.id })
|
conversation.unread && mutate()
|
||||||
navigation.push('Screen-Shared-Toot', {
|
navigation.push('Screen-Shared-Toot', {
|
||||||
toot: conversation.last_status
|
toot: conversation.last_status
|
||||||
})
|
})
|
||||||
|
@ -7,7 +7,8 @@ import TimelineContent from '@components/Timelines/Timeline/Shared/Content'
|
|||||||
import TimelineHeaderDefault from '@components/Timelines/Timeline/Shared/HeaderDefault'
|
import TimelineHeaderDefault from '@components/Timelines/Timeline/Shared/HeaderDefault'
|
||||||
import TimelinePoll from '@components/Timelines/Timeline/Shared/Poll'
|
import TimelinePoll from '@components/Timelines/Timeline/Shared/Poll'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { getLocalAccountId } from '@utils/slices/instancesSlice'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
|
import { getLocalAccount } from '@utils/slices/instancesSlice'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { Pressable, StyleSheet, View } from 'react-native'
|
import { Pressable, StyleSheet, View } from 'react-native'
|
||||||
@ -15,7 +16,7 @@ import { useSelector } from 'react-redux'
|
|||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
item: Mastodon.Status
|
item: Mastodon.Status
|
||||||
queryKey?: QueryKey.Timeline
|
queryKey?: QueryKeyTimeline
|
||||||
index: number
|
index: number
|
||||||
pinnedLength?: number
|
pinnedLength?: number
|
||||||
highlighted?: boolean
|
highlighted?: boolean
|
||||||
@ -33,7 +34,7 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
disableDetails = false,
|
disableDetails = false,
|
||||||
disableOnPress = false
|
disableOnPress = false
|
||||||
}) => {
|
}) => {
|
||||||
const localAccountId = useSelector(getLocalAccountId)
|
const localAccount = useSelector(getLocalAccount)
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
|
|
||||||
let actualStatus = item.reblog ? item.reblog : item
|
let actualStatus = item.reblog ? item.reblog : item
|
||||||
@ -64,7 +65,7 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
<TimelineHeaderDefault
|
<TimelineHeaderDefault
|
||||||
queryKey={disableOnPress ? undefined : queryKey}
|
queryKey={disableOnPress ? undefined : queryKey}
|
||||||
status={actualStatus}
|
status={actualStatus}
|
||||||
sameAccount={actualStatus.account.id === localAccountId}
|
sameAccount={actualStatus.account.id === localAccount?.id}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@ -88,7 +89,7 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
queryKey={queryKey}
|
queryKey={queryKey}
|
||||||
poll={actualStatus.poll}
|
poll={actualStatus.poll}
|
||||||
reblog={item.reblog ? true : false}
|
reblog={item.reblog ? true : false}
|
||||||
sameAccount={actualStatus.account.id === localAccountId}
|
sameAccount={actualStatus.account.id === localAccount?.id}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!disableDetails && actualStatus.media_attachments.length > 0 && (
|
{!disableDetails && actualStatus.media_attachments.length > 0 && (
|
||||||
|
52
src/components/Timelines/Timeline/Header.tsx
Normal file
52
src/components/Timelines/Timeline/Header.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
import Icon from '@root/components/Icon'
|
||||||
|
import { StyleConstants } from '@root/utils/styles/constants'
|
||||||
|
import { useTheme } from '@root/utils/styles/ThemeManager'
|
||||||
|
import React from 'react'
|
||||||
|
import { StyleSheet, Text, View } from 'react-native'
|
||||||
|
|
||||||
|
const TimelineHeader = React.memo(
|
||||||
|
() => {
|
||||||
|
const navigation = useNavigation()
|
||||||
|
const { theme } = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.base, { borderColor: theme.border }]}>
|
||||||
|
<Text style={[styles.text, { color: theme.primary }]}>
|
||||||
|
一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字{' '}
|
||||||
|
<Text
|
||||||
|
style={{ color: theme.blue }}
|
||||||
|
onPress={() =>
|
||||||
|
navigation.navigate('Screen-Me', {
|
||||||
|
screen: 'Screen-Me-Settings-UpdateRemote'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
前往设置{' '}
|
||||||
|
<Icon
|
||||||
|
name='ArrowRight'
|
||||||
|
size={StyleConstants.Font.Size.S}
|
||||||
|
color={theme.blue}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
() => true
|
||||||
|
)
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
base: {
|
||||||
|
margin: StyleConstants.Spacing.Global.PagePadding,
|
||||||
|
paddingHorizontal: StyleConstants.Spacing.M,
|
||||||
|
paddingVertical: StyleConstants.Spacing.S,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 6
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
...StyleConstants.FontStyle.S
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default TimelineHeader
|
@ -7,7 +7,8 @@ import TimelineContent from '@components/Timelines/Timeline/Shared/Content'
|
|||||||
import TimelineHeaderNotification from '@components/Timelines/Timeline/Shared/HeaderNotification'
|
import TimelineHeaderNotification from '@components/Timelines/Timeline/Shared/HeaderNotification'
|
||||||
import TimelinePoll from '@components/Timelines/Timeline/Shared/Poll'
|
import TimelinePoll from '@components/Timelines/Timeline/Shared/Poll'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { getLocalAccountId } from '@utils/slices/instancesSlice'
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
|
import { getLocalAccount } from '@utils/slices/instancesSlice'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { Pressable, StyleSheet, View } from 'react-native'
|
import { Pressable, StyleSheet, View } from 'react-native'
|
||||||
@ -15,7 +16,7 @@ import { useSelector } from 'react-redux'
|
|||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
notification: Mastodon.Notification
|
notification: Mastodon.Notification
|
||||||
queryKey: QueryKey.Timeline
|
queryKey: QueryKeyTimeline
|
||||||
highlighted?: boolean
|
highlighted?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,7 +25,7 @@ const TimelineNotifications: React.FC<Props> = ({
|
|||||||
queryKey,
|
queryKey,
|
||||||
highlighted = false
|
highlighted = false
|
||||||
}) => {
|
}) => {
|
||||||
const localAccountId = useSelector(getLocalAccountId)
|
const localAccount = useSelector(getLocalAccount)
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const actualAccount = notification.status
|
const actualAccount = notification.status
|
||||||
? notification.status.account
|
? notification.status.account
|
||||||
@ -83,7 +84,7 @@ const TimelineNotifications: React.FC<Props> = ({
|
|||||||
queryKey={queryKey}
|
queryKey={queryKey}
|
||||||
poll={notification.status.poll}
|
poll={notification.status.poll}
|
||||||
reblog={false}
|
reblog={false}
|
||||||
sameAccount={notification.account.id === localAccountId}
|
sameAccount={notification.account.id === localAccount?.id}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{notification.status.media_attachments.length > 0 && (
|
{notification.status.media_attachments.length > 0 && (
|
||||||
|
@ -4,6 +4,7 @@ import Icon from '@components/Icon'
|
|||||||
import { TimelineData } from '@components/Timelines/Timeline'
|
import { TimelineData } from '@components/Timelines/Timeline'
|
||||||
import { toast } from '@components/toast'
|
import { toast } from '@components/toast'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { findIndex } from 'lodash'
|
import { findIndex } from 'lodash'
|
||||||
@ -13,7 +14,7 @@ import { ActionSheetIOS, Pressable, StyleSheet, Text, View } from 'react-native'
|
|||||||
import { useMutation, useQueryClient } from 'react-query'
|
import { useMutation, useQueryClient } from 'react-query'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
queryKey: QueryKey.Timeline
|
queryKey: QueryKeyTimeline
|
||||||
status: Mastodon.Status
|
status: Mastodon.Status
|
||||||
reblog: boolean
|
reblog: boolean
|
||||||
}
|
}
|
||||||
@ -36,7 +37,7 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
|||||||
stateKey: 'favourited' | 'reblogged' | 'bookmarked'
|
stateKey: 'favourited' | 'reblogged' | 'bookmarked'
|
||||||
state?: boolean
|
state?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
return client({
|
return client<Mastodon.Status>({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: `statuses/${status.id}/${state ? 'un' : ''}${type}`
|
url: `statuses/${status.id}/${state ? 'un' : ''}${type}`
|
||||||
@ -58,7 +59,7 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
|||||||
let tootIndex = -1
|
let tootIndex = -1
|
||||||
const pageIndex = findIndex(old?.pages, page => {
|
const pageIndex = findIndex(old?.pages, page => {
|
||||||
const tempIndex = findIndex(page.toots, [
|
const tempIndex = findIndex(page.toots, [
|
||||||
queryKey[0] === 'Notifications'
|
queryKey[1].page === 'Notifications'
|
||||||
? 'status.id'
|
? 'status.id'
|
||||||
: reblog
|
: reblog
|
||||||
? 'reblog.id'
|
? 'reblog.id'
|
||||||
@ -75,12 +76,12 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
|||||||
|
|
||||||
if (pageIndex >= 0 && tootIndex >= 0) {
|
if (pageIndex >= 0 && tootIndex >= 0) {
|
||||||
if (
|
if (
|
||||||
(type === 'favourite' && queryKey[0] === 'Favourites') ||
|
(type === 'favourite' && queryKey[1].page === 'Favourites') ||
|
||||||
(type === 'bookmark' && queryKey[0] === 'Bookmarks')
|
(type === 'bookmark' && queryKey[1].page === 'Bookmarks')
|
||||||
) {
|
) {
|
||||||
old!.pages[pageIndex].toots.splice(tootIndex, 1)
|
old!.pages[pageIndex].toots.splice(tootIndex, 1)
|
||||||
} else {
|
} else {
|
||||||
if (queryKey[0] === 'Notifications') {
|
if (queryKey[1].page === 'Notifications') {
|
||||||
old!.pages[pageIndex].toots[tootIndex].status[stateKey] =
|
old!.pages[pageIndex].toots[tootIndex].status[stateKey] =
|
||||||
typeof state === 'boolean' ? !state : true
|
typeof state === 'boolean' ? !state : true
|
||||||
} else {
|
} else {
|
||||||
|
@ -3,9 +3,10 @@ import { Pressable, StyleSheet } from 'react-native'
|
|||||||
import { Image } from 'react-native-expo-image-cache'
|
import { Image } from 'react-native-expo-image-cache'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
queryKey?: QueryKey.Timeline
|
queryKey?: QueryKeyTimeline
|
||||||
account: Mastodon.Account
|
account: Mastodon.Account
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import haptics from '@components/haptics'
|
|||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import { TimelineData } from '@components/Timelines/Timeline'
|
import { TimelineData } from '@components/Timelines/Timeline'
|
||||||
import { toast } from '@components/toast'
|
import { toast } from '@components/toast'
|
||||||
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { findIndex } from 'lodash'
|
import { findIndex } from 'lodash'
|
||||||
@ -14,7 +15,7 @@ import HeaderSharedAccount from './HeaderShared/Account'
|
|||||||
import HeaderSharedCreated from './HeaderShared/Created'
|
import HeaderSharedCreated from './HeaderShared/Created'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
queryKey: QueryKey.Timeline
|
queryKey: QueryKeyTimeline
|
||||||
conversation: Mastodon.Conversation
|
conversation: Mastodon.Conversation
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,7 +24,7 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
|
|||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const fireMutation = useCallback(() => {
|
const fireMutation = useCallback(() => {
|
||||||
return client({
|
return client<Mastodon.Conversation>({
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: `conversations/${conversation.id}`
|
url: `conversations/${conversation.id}`
|
||||||
|
@ -13,9 +13,10 @@ import HeaderSharedAccount from './HeaderShared/Account'
|
|||||||
import HeaderSharedApplication from './HeaderShared/Application'
|
import HeaderSharedApplication from './HeaderShared/Application'
|
||||||
import HeaderSharedCreated from './HeaderShared/Created'
|
import HeaderSharedCreated from './HeaderShared/Created'
|
||||||
import HeaderSharedVisibility from './HeaderShared/Visibility'
|
import HeaderSharedVisibility from './HeaderShared/Visibility'
|
||||||
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
queryKey?: QueryKey.Timeline
|
queryKey?: QueryKeyTimeline
|
||||||
status: Mastodon.Status
|
status: Mastodon.Status
|
||||||
sameAccount: boolean
|
sameAccount: boolean
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,13 @@ import client from '@api/client'
|
|||||||
import haptics from '@components/haptics'
|
import haptics from '@components/haptics'
|
||||||
import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu'
|
import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu'
|
||||||
import { toast } from '@components/toast'
|
import { toast } from '@components/toast'
|
||||||
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useMutation, useQueryClient } from 'react-query'
|
import { useMutation, useQueryClient } from 'react-query'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
queryKey?: QueryKey.Timeline
|
queryKey?: QueryKeyTimeline
|
||||||
account: Pick<Mastodon.Account, 'id' | 'acct'>
|
account: Pick<Mastodon.Account, 'id' | 'acct'>
|
||||||
setBottomSheetVisible: React.Dispatch<React.SetStateAction<boolean>>
|
setBottomSheetVisible: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
}
|
}
|
||||||
@ -25,14 +26,14 @@ const HeaderDefaultActionsAccount: React.FC<Props> = ({
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case 'mute':
|
case 'mute':
|
||||||
case 'block':
|
case 'block':
|
||||||
return client({
|
return client<Mastodon.Account>({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: `accounts/${account.id}/${type}`
|
url: `accounts/${account.id}/${type}`
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'reports':
|
case 'reports':
|
||||||
return client({
|
return client<Mastodon.Account>({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: `reports`,
|
url: `reports`,
|
||||||
|
@ -3,12 +3,13 @@ import MenuContainer from '@components/Menu/Container'
|
|||||||
import MenuHeader from '@components/Menu/Header'
|
import MenuHeader from '@components/Menu/Header'
|
||||||
import MenuRow from '@components/Menu/Row'
|
import MenuRow from '@components/Menu/Row'
|
||||||
import { toast } from '@components/toast'
|
import { toast } from '@components/toast'
|
||||||
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useMutation, useQueryClient } from 'react-query'
|
import { useMutation, useQueryClient } from 'react-query'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
queryKey: QueryKey.Timeline
|
queryKey: QueryKeyTimeline
|
||||||
domain: string
|
domain: string
|
||||||
setBottomSheetVisible: React.Dispatch<React.SetStateAction<boolean>>
|
setBottomSheetVisible: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
}
|
}
|
||||||
@ -21,7 +22,7 @@ const HeaderDefaultActionsDomain: React.FC<Props> = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const fireMutation = useCallback(() => {
|
const fireMutation = useCallback(() => {
|
||||||
return client({
|
return client<{}>({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: `domain_blocks`,
|
url: `domain_blocks`,
|
||||||
|
@ -9,9 +9,10 @@ import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu'
|
|||||||
import { TimelineData } from '@components/Timelines/Timeline'
|
import { TimelineData } from '@components/Timelines/Timeline'
|
||||||
import { toast } from '@components/toast'
|
import { toast } from '@components/toast'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
queryKey: QueryKey.Timeline
|
queryKey: QueryKeyTimeline
|
||||||
status: Mastodon.Status
|
status: Mastodon.Status
|
||||||
setBottomSheetVisible: React.Dispatch<React.SetStateAction<boolean>>
|
setBottomSheetVisible: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
}
|
}
|
||||||
@ -30,14 +31,14 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case 'mute':
|
case 'mute':
|
||||||
case 'pin':
|
case 'pin':
|
||||||
return client({
|
return client<Mastodon.Status>({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: `statuses/${status.id}/${state ? 'un' : ''}${type}`
|
url: `statuses/${status.id}/${state ? 'un' : ''}${type}`
|
||||||
}) // bug in response from Mastodon, but onMutate ignore the error in response
|
}) // bug in response from Mastodon, but onMutate ignore the error in response
|
||||||
break
|
break
|
||||||
case 'delete':
|
case 'delete':
|
||||||
return client({
|
return client<Mastodon.Status>({
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: `statuses/${status.id}`
|
url: `statuses/${status.id}`
|
||||||
@ -153,7 +154,7 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
|
|||||||
),
|
),
|
||||||
style: 'destructive',
|
style: 'destructive',
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
await client({
|
await client<Mastodon.Status>({
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: `statuses/${status.id}`
|
url: `statuses/${status.id}`
|
||||||
@ -163,7 +164,7 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
|
|||||||
setBottomSheetVisible(false)
|
setBottomSheetVisible(false)
|
||||||
navigation.navigate('Screen-Shared-Compose', {
|
navigation.navigate('Screen-Shared-Compose', {
|
||||||
type: 'edit',
|
type: 'edit',
|
||||||
incomingStatus: res.body
|
incomingStatus: res
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
@ -6,6 +6,7 @@ import relativeTime from '@components/relativeTime'
|
|||||||
import { TimelineData } from '@components/Timelines/Timeline'
|
import { TimelineData } from '@components/Timelines/Timeline'
|
||||||
import { ParseEmojis } from '@root/components/Parse'
|
import { ParseEmojis } from '@root/components/Parse'
|
||||||
import { toast } from '@root/components/toast'
|
import { toast } from '@root/components/toast'
|
||||||
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { findIndex } from 'lodash'
|
import { findIndex } from 'lodash'
|
||||||
@ -15,7 +16,7 @@ import { Pressable, StyleSheet, Text, View } from 'react-native'
|
|||||||
import { useMutation, useQueryClient } from 'react-query'
|
import { useMutation, useQueryClient } from 'react-query'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
queryKey: QueryKey.Timeline
|
queryKey: QueryKeyTimeline
|
||||||
poll: NonNullable<Mastodon.Status['poll']>
|
poll: NonNullable<Mastodon.Status['poll']>
|
||||||
reblog: boolean
|
reblog: boolean
|
||||||
sameAccount: boolean
|
sameAccount: boolean
|
||||||
@ -45,7 +46,7 @@ const TimelinePoll: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return client({
|
return client<Mastodon.Poll>({
|
||||||
method: type === 'vote' ? 'post' : 'get',
|
method: type === 'vote' ? 'post' : 'get',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: type === 'vote' ? `polls/${poll.id}/votes` : `polls/${poll.id}`,
|
url: type === 'vote' ? `polls/${poll.id}/votes` : `polls/${poll.id}`,
|
||||||
@ -55,7 +56,7 @@ const TimelinePoll: React.FC<Props> = ({
|
|||||||
[allOptions]
|
[allOptions]
|
||||||
)
|
)
|
||||||
const mutation = useMutation(fireMutation, {
|
const mutation = useMutation(fireMutation, {
|
||||||
onSuccess: ({ body }) => {
|
onSuccess: (res) => {
|
||||||
queryClient.cancelQueries(queryKey)
|
queryClient.cancelQueries(queryKey)
|
||||||
|
|
||||||
queryClient.setQueryData<TimelineData>(queryKey, old => {
|
queryClient.setQueryData<TimelineData>(queryKey, old => {
|
||||||
@ -75,9 +76,9 @@ const TimelinePoll: React.FC<Props> = ({
|
|||||||
|
|
||||||
if (pageIndex >= 0 && tootIndex >= 0) {
|
if (pageIndex >= 0 && tootIndex >= 0) {
|
||||||
if (reblog) {
|
if (reblog) {
|
||||||
old!.pages[pageIndex].toots[tootIndex].reblog!.poll = body
|
old!.pages[pageIndex].toots[tootIndex].reblog!.poll = res
|
||||||
} else {
|
} else {
|
||||||
old!.pages[pageIndex].toots[tootIndex].poll = body
|
old!.pages[pageIndex].toots[tootIndex].poll = res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return old
|
return old
|
||||||
|
@ -41,9 +41,7 @@ i18next.use(initReactI18next).init({
|
|||||||
// react options
|
// react options
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false
|
escapeValue: false
|
||||||
},
|
}
|
||||||
|
|
||||||
debug: true
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export default i18next
|
export default i18next
|
||||||
|
@ -14,6 +14,7 @@ export default {
|
|||||||
meLists: require('./screens/meLists').default,
|
meLists: require('./screens/meLists').default,
|
||||||
meListsList: require('./screens/meListsList').default,
|
meListsList: require('./screens/meListsList').default,
|
||||||
meSettings: require('./screens/meSettings').default,
|
meSettings: require('./screens/meSettings').default,
|
||||||
|
meSettingsUpdateRemote: require('./screens/meSettingsUpdateRemote').default,
|
||||||
|
|
||||||
sharedAccount: require('./screens/sharedAccount').default,
|
sharedAccount: require('./screens/sharedAccount').default,
|
||||||
sharedToot: require('./screens/sharedToot').default,
|
sharedToot: require('./screens/sharedToot').default,
|
||||||
|
@ -26,8 +26,13 @@ export default {
|
|||||||
cancel: '$t(common:buttons.cancel)'
|
cancel: '$t(common:buttons.cancel)'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
remote: {
|
||||||
|
heading: '$t(meSettingsUpdateRemote:heading)',
|
||||||
|
description: '外站只能看不能玩'
|
||||||
|
},
|
||||||
cache: {
|
cache: {
|
||||||
heading: '清空缓存'
|
heading: '清空缓存',
|
||||||
|
empty: '暂无缓存'
|
||||||
},
|
},
|
||||||
analytics: {
|
analytics: {
|
||||||
heading: '帮助我们改进',
|
heading: '帮助我们改进',
|
||||||
|
45
src/i18n/zh/screens/meSettingsUpdateRemote.ts
Normal file
45
src/i18n/zh/screens/meSettingsUpdateRemote.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
export default {
|
||||||
|
heading: '外站链接',
|
||||||
|
content: {
|
||||||
|
language: {
|
||||||
|
heading: '切换语言',
|
||||||
|
options: {
|
||||||
|
zh: '简体中文',
|
||||||
|
en: 'English',
|
||||||
|
cancel: '$t(common:buttons.cancel)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
heading: '应用外观',
|
||||||
|
options: {
|
||||||
|
auto: '跟随系统',
|
||||||
|
light: '浅色模式',
|
||||||
|
dark: '深色模式',
|
||||||
|
cancel: '$t(common:buttons.cancel)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
browser: {
|
||||||
|
heading: '打开链接',
|
||||||
|
options: {
|
||||||
|
internal: '应用内',
|
||||||
|
external: '系统浏览器',
|
||||||
|
cancel: '$t(common:buttons.cancel)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
remote: {
|
||||||
|
heading: '外站链接',
|
||||||
|
description: '外站只能看不能玩'
|
||||||
|
},
|
||||||
|
cache: {
|
||||||
|
heading: '清空缓存'
|
||||||
|
},
|
||||||
|
analytics: {
|
||||||
|
heading: '帮助我们改进',
|
||||||
|
description: '允许我们收集不与用户相关联的使用信息'
|
||||||
|
},
|
||||||
|
copyrights: {
|
||||||
|
heading: '版权信息'
|
||||||
|
},
|
||||||
|
version: '版本 v{{version}}'
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,9 @@ import ScreenMeListsList from '@screens/Me/Root/Lists/List'
|
|||||||
import ScreenMeSettings from '@screens/Me/Settings'
|
import ScreenMeSettings from '@screens/Me/Settings'
|
||||||
|
|
||||||
import { HeaderLeft } from '@root/components/Header'
|
import { HeaderLeft } from '@root/components/Header'
|
||||||
|
import UpdateRemote from './Me/Settings/UpdateRemote'
|
||||||
|
import { CommonActions } from '@react-navigation/native'
|
||||||
|
import ScreenMeSwitch from './Me/Switch'
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator()
|
const Stack = createNativeStackNavigator()
|
||||||
|
|
||||||
@ -34,7 +37,7 @@ const ScreenMe: React.FC = () => {
|
|||||||
component={ScreenMeConversations}
|
component={ScreenMeConversations}
|
||||||
options={({ navigation }: any) => ({
|
options={({ navigation }: any) => ({
|
||||||
headerTitle: t('meConversations:heading'),
|
headerTitle: t('meConversations:heading'),
|
||||||
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
|
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@ -42,7 +45,7 @@ const ScreenMe: React.FC = () => {
|
|||||||
component={ScreenMeBookmarks}
|
component={ScreenMeBookmarks}
|
||||||
options={({ navigation }: any) => ({
|
options={({ navigation }: any) => ({
|
||||||
headerTitle: t('meBookmarks:heading'),
|
headerTitle: t('meBookmarks:heading'),
|
||||||
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
|
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@ -50,7 +53,7 @@ const ScreenMe: React.FC = () => {
|
|||||||
component={ScreenMeFavourites}
|
component={ScreenMeFavourites}
|
||||||
options={({ navigation }: any) => ({
|
options={({ navigation }: any) => ({
|
||||||
headerTitle: t('meFavourites:heading'),
|
headerTitle: t('meFavourites:heading'),
|
||||||
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
|
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@ -58,7 +61,7 @@ const ScreenMe: React.FC = () => {
|
|||||||
component={ScreenMeLists}
|
component={ScreenMeLists}
|
||||||
options={({ navigation }: any) => ({
|
options={({ navigation }: any) => ({
|
||||||
headerTitle: t('meLists:heading'),
|
headerTitle: t('meLists:heading'),
|
||||||
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
|
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@ -66,7 +69,7 @@ const ScreenMe: React.FC = () => {
|
|||||||
component={ScreenMeListsList}
|
component={ScreenMeListsList}
|
||||||
options={({ route, navigation }: any) => ({
|
options={({ route, navigation }: any) => ({
|
||||||
headerTitle: t('meListsList:heading', { list: route.params.title }),
|
headerTitle: t('meListsList:heading', { list: route.params.title }),
|
||||||
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
|
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@ -74,7 +77,24 @@ const ScreenMe: React.FC = () => {
|
|||||||
component={ScreenMeSettings}
|
component={ScreenMeSettings}
|
||||||
options={({ navigation }: any) => ({
|
options={({ navigation }: any) => ({
|
||||||
headerTitle: t('meSettings:heading'),
|
headerTitle: t('meSettings:heading'),
|
||||||
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
|
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Screen-Me-Settings-UpdateRemote'
|
||||||
|
component={UpdateRemote}
|
||||||
|
options={({ navigation }: any) => ({
|
||||||
|
headerTitle: t('meSettingsUpdateRemote:heading'),
|
||||||
|
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Screen-Me-Switch'
|
||||||
|
component={ScreenMeSwitch}
|
||||||
|
options={({ navigation }: any) => ({
|
||||||
|
stackPresentation: 'fullScreenModal',
|
||||||
|
headerTitle: t('meSettings:heading'),
|
||||||
|
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import { MenuRow } from '@components/Menu'
|
import { MenuRow } from '@components/Menu'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import TimelineEmpty from '@root/components/Timelines/Timeline/Empty'
|
import TimelineEmpty from '@root/components/Timelines/Timeline/Empty'
|
||||||
import { listsFetch } from '@utils/fetches/listsFetch'
|
import hookLists from '@utils/queryHooks/lists'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { StyleSheet } from 'react-native'
|
import { StyleSheet } from 'react-native'
|
||||||
import { useQuery } from 'react-query'
|
|
||||||
|
|
||||||
const ScreenMeLists: React.FC = () => {
|
const ScreenMeLists: React.FC = () => {
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const { status, data, refetch } = useQuery(['Lists'], listsFetch)
|
const { status, data, refetch } = hookLists({})
|
||||||
|
|
||||||
const children = useMemo(() => {
|
const children = useMemo(() => {
|
||||||
if (status === 'success') {
|
if (status === 'success') {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { useScrollToTop } from '@react-navigation/native'
|
import { useScrollToTop } from '@react-navigation/native'
|
||||||
import Collections from '@screens/Me/Root/Collections'
|
import Collections from '@screens/Me/Root/Collections'
|
||||||
import Login from '@screens/Me/Root/Login'
|
|
||||||
import MyInfo from '@screens/Me/Root/MyInfo'
|
import MyInfo from '@screens/Me/Root/MyInfo'
|
||||||
import Settings from '@screens/Me/Root/Settings'
|
import Settings from '@screens/Me/Root/Settings'
|
||||||
import Logout from '@screens/Me/Root/Logout'
|
import Logout from '@screens/Me/Root/Logout'
|
||||||
@ -8,16 +7,17 @@ import AccountNav from '@screens/Shared/Account/Nav'
|
|||||||
import accountReducer from '@screens/Shared/Account/utils/reducer'
|
import accountReducer from '@screens/Shared/Account/utils/reducer'
|
||||||
import accountInitialState from '@screens/Shared/Account/utils/initialState'
|
import accountInitialState from '@screens/Shared/Account/utils/initialState'
|
||||||
import AccountContext from '@screens/Shared/Account/utils/createContext'
|
import AccountContext from '@screens/Shared/Account/utils/createContext'
|
||||||
import { getLocalUrl } from '@utils/slices/instancesSlice'
|
import { getLocalActiveIndex } from '@utils/slices/instancesSlice'
|
||||||
import React, { useReducer, useRef, useState } from 'react'
|
import React, { useReducer, useRef, useState } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
useAnimatedScrollHandler,
|
useAnimatedScrollHandler,
|
||||||
useSharedValue
|
useSharedValue
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
|
import ComponentInstance from '@components/Instance'
|
||||||
|
|
||||||
const ScreenMeRoot: React.FC = () => {
|
const ScreenMeRoot: React.FC = () => {
|
||||||
const localRegistered = useSelector(getLocalUrl)
|
const localActiveIndex = useSelector(getLocalActiveIndex)
|
||||||
|
|
||||||
const scrollRef = useRef<Animated.ScrollView>(null)
|
const scrollRef = useRef<Animated.ScrollView>(null)
|
||||||
useScrollToTop(scrollRef)
|
useScrollToTop(scrollRef)
|
||||||
@ -36,7 +36,7 @@ const ScreenMeRoot: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AccountContext.Provider value={{ accountState, accountDispatch }}>
|
<AccountContext.Provider value={{ accountState, accountDispatch }}>
|
||||||
{localRegistered && data ? (
|
{localActiveIndex !== null && data ? (
|
||||||
<AccountNav scrollY={scrollY} account={data} />
|
<AccountNav scrollY={scrollY} account={data} />
|
||||||
) : null}
|
) : null}
|
||||||
<Animated.ScrollView
|
<Animated.ScrollView
|
||||||
@ -45,10 +45,14 @@ const ScreenMeRoot: React.FC = () => {
|
|||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
>
|
>
|
||||||
{localRegistered ? <MyInfo setData={setData} /> : <Login />}
|
{localActiveIndex !== null ? (
|
||||||
{localRegistered && <Collections />}
|
<MyInfo setData={setData} />
|
||||||
|
) : (
|
||||||
|
<ComponentInstance type='local' />
|
||||||
|
)}
|
||||||
|
{localActiveIndex !== null ? <Collections /> : null}
|
||||||
<Settings />
|
<Settings />
|
||||||
{localRegistered && <Logout />}
|
{localActiveIndex !== null ? <Logout /> : null}
|
||||||
</Animated.ScrollView>
|
</Animated.ScrollView>
|
||||||
</AccountContext.Provider>
|
</AccountContext.Provider>
|
||||||
)
|
)
|
||||||
|
@ -3,15 +3,13 @@ import React, { useMemo } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { MenuContainer, MenuRow } from '@components/Menu'
|
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||||
import { useQuery } from 'react-query'
|
import hookAnnouncement from '@utils/queryHooks/announcement'
|
||||||
import { announcementFetch } from '@root/utils/fetches/announcementsFetch'
|
|
||||||
|
|
||||||
const Collections: React.FC = () => {
|
const Collections: React.FC = () => {
|
||||||
const { t } = useTranslation('meRoot')
|
const { t } = useTranslation('meRoot')
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
|
|
||||||
const queryKey = ['Announcements', { showAll: true }]
|
const { data, isFetching } = hookAnnouncement({ showAll: true })
|
||||||
const { data, isFetching } = useQuery(queryKey, announcementFetch)
|
|
||||||
|
|
||||||
const announcementContent = useMemo(() => {
|
const announcementContent = useMemo(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
|
@ -1,345 +0,0 @@
|
|||||||
import analytics from '@components/analytics'
|
|
||||||
import Button from '@components/Button'
|
|
||||||
import haptics from '@components/haptics'
|
|
||||||
import Icon from '@components/Icon'
|
|
||||||
import { ParseHTML } from '@components/Parse'
|
|
||||||
import { useNavigation } from '@react-navigation/native'
|
|
||||||
import { applicationFetch } from '@utils/fetches/applicationFetch'
|
|
||||||
import { instanceFetch } from '@utils/fetches/instanceFetch'
|
|
||||||
import { loginLocal } from '@utils/slices/instancesSlice'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
|
||||||
import * as AuthSession from 'expo-auth-session'
|
|
||||||
import { LinearGradient } from 'expo-linear-gradient'
|
|
||||||
import { debounce } from 'lodash'
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import {
|
|
||||||
Dimensions,
|
|
||||||
Image,
|
|
||||||
StyleSheet,
|
|
||||||
Text,
|
|
||||||
TextInput,
|
|
||||||
View
|
|
||||||
} from 'react-native'
|
|
||||||
import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder'
|
|
||||||
import { useQuery } from 'react-query'
|
|
||||||
import { useDispatch } from 'react-redux'
|
|
||||||
|
|
||||||
const Login: React.FC = () => {
|
|
||||||
const { t } = useTranslation('meRoot')
|
|
||||||
const { theme } = useTheme()
|
|
||||||
const navigation = useNavigation()
|
|
||||||
const dispatch = useDispatch()
|
|
||||||
const [instanceDomain, setInstanceDomain] = useState<string | undefined>()
|
|
||||||
const [applicationData, setApplicationData] = useState<{
|
|
||||||
clientId: string
|
|
||||||
clientSecret: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const ShimmerPlaceholder = createShimmerPlaceholder(LinearGradient)
|
|
||||||
|
|
||||||
const instanceQuery = useQuery(
|
|
||||||
['Instance', { instanceDomain }],
|
|
||||||
instanceFetch,
|
|
||||||
{ enabled: false, retry: false }
|
|
||||||
)
|
|
||||||
const applicationQuery = useQuery(
|
|
||||||
['Application', { instanceDomain }],
|
|
||||||
applicationFetch,
|
|
||||||
{ enabled: false, retry: false }
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
applicationQuery.data?.client_id.length &&
|
|
||||||
applicationQuery.data?.client_secret.length
|
|
||||||
) {
|
|
||||||
setApplicationData({
|
|
||||||
clientId: applicationQuery.data.client_id,
|
|
||||||
clientSecret: applicationQuery.data.client_secret
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [applicationQuery.data?.client_id])
|
|
||||||
|
|
||||||
const [request, response, promptAsync] = AuthSession.useAuthRequest(
|
|
||||||
{
|
|
||||||
clientId: applicationData?.clientId!,
|
|
||||||
clientSecret: applicationData?.clientSecret,
|
|
||||||
scopes: ['read', 'write', 'follow', 'push'],
|
|
||||||
redirectUri: 'exp://127.0.0.1:19000'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
authorizationEndpoint: `https://${instanceDomain}/oauth/authorize`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
useEffect(() => {
|
|
||||||
;(async () => {
|
|
||||||
if (request?.clientId) {
|
|
||||||
await promptAsync()
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
}, [request])
|
|
||||||
useEffect(() => {
|
|
||||||
;(async () => {
|
|
||||||
if (response?.type === 'success') {
|
|
||||||
const { accessToken } = await AuthSession.exchangeCodeAsync(
|
|
||||||
{
|
|
||||||
clientId: applicationData?.clientId!,
|
|
||||||
clientSecret: applicationData?.clientSecret,
|
|
||||||
scopes: ['read', 'write', 'follow', 'push'],
|
|
||||||
redirectUri: 'exp://127.0.0.1:19000',
|
|
||||||
code: response.params.code,
|
|
||||||
extraParams: {
|
|
||||||
grant_type: 'authorization_code'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tokenEndpoint: `https://${instanceDomain}/oauth/token`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
dispatch(loginLocal({ url: instanceDomain, token: accessToken }))
|
|
||||||
analytics('login', {
|
|
||||||
instance: instanceDomain!,
|
|
||||||
method: 'OAuth2'
|
|
||||||
})
|
|
||||||
navigation.navigate('Screen-Local')
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
}, [response])
|
|
||||||
|
|
||||||
const onChangeText = useCallback(
|
|
||||||
debounce(
|
|
||||||
text => {
|
|
||||||
setInstanceDomain(text.replace(/^http(s)?\:\/\//i, ''))
|
|
||||||
setApplicationData(undefined)
|
|
||||||
},
|
|
||||||
1000,
|
|
||||||
{
|
|
||||||
trailing: true
|
|
||||||
}
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
useEffect(() => {
|
|
||||||
if (instanceDomain) {
|
|
||||||
instanceQuery.refetch()
|
|
||||||
}
|
|
||||||
}, [instanceDomain])
|
|
||||||
|
|
||||||
const instanceInfo = useCallback(
|
|
||||||
({ header, content }: { header: string; content?: string }) => {
|
|
||||||
return (
|
|
||||||
<View style={styles.instanceInfo}>
|
|
||||||
<Text style={[styles.instanceInfoHeader, { color: theme.primary }]}>
|
|
||||||
{header}
|
|
||||||
</Text>
|
|
||||||
<ShimmerPlaceholder
|
|
||||||
visible={instanceQuery.data?.uri !== undefined}
|
|
||||||
stopAutoRun
|
|
||||||
width={
|
|
||||||
Dimensions.get('screen').width -
|
|
||||||
StyleConstants.Spacing.Global.PagePadding * 4
|
|
||||||
}
|
|
||||||
height={StyleConstants.Font.Size.M}
|
|
||||||
shimmerColors={theme.shimmer}
|
|
||||||
>
|
|
||||||
<ParseHTML
|
|
||||||
content={content!}
|
|
||||||
size={'M'}
|
|
||||||
numberOfLines={5}
|
|
||||||
expandHint='介绍'
|
|
||||||
/>
|
|
||||||
</ShimmerPlaceholder>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
[instanceQuery.data?.uri]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<View style={{ flexDirection: 'row' }}>
|
|
||||||
<Image
|
|
||||||
source={require('assets/screens/meRoot/welcome.png')}
|
|
||||||
style={{ resizeMode: 'contain', flex: 1, aspectRatio: 16 / 9 }}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={styles.base}>
|
|
||||||
<View style={styles.inputRow}>
|
|
||||||
<TextInput
|
|
||||||
style={[
|
|
||||||
styles.textInput,
|
|
||||||
{
|
|
||||||
color: theme.primary,
|
|
||||||
borderBottomColor: theme.secondary
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
onChangeText={onChangeText}
|
|
||||||
autoCapitalize='none'
|
|
||||||
autoCorrect={false}
|
|
||||||
clearButtonMode='never'
|
|
||||||
keyboardType='url'
|
|
||||||
textContentType='URL'
|
|
||||||
onSubmitEditing={({ nativeEvent: { text } }) => {
|
|
||||||
if (
|
|
||||||
text === instanceDomain &&
|
|
||||||
instanceQuery.isSuccess &&
|
|
||||||
instanceQuery.data &&
|
|
||||||
instanceQuery.data.uri
|
|
||||||
) {
|
|
||||||
haptics('Success')
|
|
||||||
applicationQuery.refetch()
|
|
||||||
} else {
|
|
||||||
setInstanceDomain(text)
|
|
||||||
setApplicationData(undefined)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={t('content.login.server.placeholder')}
|
|
||||||
placeholderTextColor={theme.secondary}
|
|
||||||
returnKeyType='go'
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type='text'
|
|
||||||
content={t('content.login.button')}
|
|
||||||
onPress={() => {
|
|
||||||
haptics('Success')
|
|
||||||
applicationQuery.refetch()
|
|
||||||
}}
|
|
||||||
disabled={!instanceQuery.data?.uri}
|
|
||||||
loading={instanceQuery.isFetching || applicationQuery.isFetching}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View>
|
|
||||||
{instanceInfo({
|
|
||||||
header: '实例名称',
|
|
||||||
content: instanceQuery.data?.title
|
|
||||||
})}
|
|
||||||
{instanceInfo({
|
|
||||||
header: '实例介绍',
|
|
||||||
content: instanceQuery.data?.short_description
|
|
||||||
})}
|
|
||||||
<View style={styles.instanceStats}>
|
|
||||||
<View style={styles.instanceStat}>
|
|
||||||
<Text
|
|
||||||
style={[styles.instanceInfoHeader, { color: theme.primary }]}
|
|
||||||
>
|
|
||||||
用户总数
|
|
||||||
</Text>
|
|
||||||
<ShimmerPlaceholder
|
|
||||||
visible={instanceQuery.data?.stats?.user_count !== undefined}
|
|
||||||
stopAutoRun
|
|
||||||
width={StyleConstants.Font.Size.M * 4}
|
|
||||||
height={StyleConstants.Font.Size.M}
|
|
||||||
shimmerColors={theme.shimmer}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[styles.instanceInfoContent, { color: theme.primary }]}
|
|
||||||
>
|
|
||||||
{instanceQuery.data?.stats?.user_count}
|
|
||||||
</Text>
|
|
||||||
</ShimmerPlaceholder>
|
|
||||||
</View>
|
|
||||||
<View style={[styles.instanceStat, { alignItems: 'center' }]}>
|
|
||||||
<Text
|
|
||||||
style={[styles.instanceInfoHeader, { color: theme.primary }]}
|
|
||||||
>
|
|
||||||
嘟嘟总数
|
|
||||||
</Text>
|
|
||||||
<ShimmerPlaceholder
|
|
||||||
visible={instanceQuery.data?.stats?.user_count !== undefined}
|
|
||||||
stopAutoRun
|
|
||||||
width={StyleConstants.Font.Size.M * 4}
|
|
||||||
height={StyleConstants.Font.Size.M}
|
|
||||||
shimmerColors={theme.shimmer}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[styles.instanceInfoContent, { color: theme.primary }]}
|
|
||||||
>
|
|
||||||
{instanceQuery.data?.stats?.status_count}
|
|
||||||
</Text>
|
|
||||||
</ShimmerPlaceholder>
|
|
||||||
</View>
|
|
||||||
<View style={[styles.instanceStat, { alignItems: 'flex-end' }]}>
|
|
||||||
<Text
|
|
||||||
style={[styles.instanceInfoHeader, { color: theme.primary }]}
|
|
||||||
>
|
|
||||||
连结总数
|
|
||||||
</Text>
|
|
||||||
<ShimmerPlaceholder
|
|
||||||
visible={instanceQuery.data?.stats?.user_count !== undefined}
|
|
||||||
stopAutoRun
|
|
||||||
width={StyleConstants.Font.Size.M * 4}
|
|
||||||
height={StyleConstants.Font.Size.M}
|
|
||||||
shimmerColors={theme.shimmer}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[styles.instanceInfoContent, { color: theme.primary }]}
|
|
||||||
>
|
|
||||||
{instanceQuery.data?.stats?.domain_count}
|
|
||||||
</Text>
|
|
||||||
</ShimmerPlaceholder>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<Text style={[styles.disclaimer, { color: theme.secondary }]}>
|
|
||||||
<Icon
|
|
||||||
name='Lock'
|
|
||||||
size={StyleConstants.Font.Size.M}
|
|
||||||
color={theme.secondary}
|
|
||||||
/>{' '}
|
|
||||||
本站不留存任何信息
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
base: {
|
|
||||||
padding: StyleConstants.Spacing.Global.PagePadding
|
|
||||||
},
|
|
||||||
inputRow: {
|
|
||||||
flexDirection: 'row'
|
|
||||||
},
|
|
||||||
textInput: {
|
|
||||||
flex: 1,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
paddingRight: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
...StyleConstants.FontStyle.M,
|
|
||||||
marginRight: StyleConstants.Spacing.S
|
|
||||||
},
|
|
||||||
instanceInfo: {
|
|
||||||
marginTop: StyleConstants.Spacing.M,
|
|
||||||
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
paddingRight: StyleConstants.Spacing.Global.PagePadding
|
|
||||||
},
|
|
||||||
instanceInfoHeader: {
|
|
||||||
...StyleConstants.FontStyle.S,
|
|
||||||
fontWeight: StyleConstants.Font.Weight.Bold,
|
|
||||||
marginBottom: StyleConstants.Spacing.XS
|
|
||||||
},
|
|
||||||
instanceInfoContent: { ...StyleConstants.FontStyle.M },
|
|
||||||
instanceStats: {
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
marginTop: StyleConstants.Spacing.M,
|
|
||||||
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
paddingRight: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
marginBottom: StyleConstants.Spacing.M
|
|
||||||
},
|
|
||||||
instanceStat: {
|
|
||||||
flex: 1
|
|
||||||
},
|
|
||||||
disclaimer: {
|
|
||||||
...StyleConstants.FontStyle.S,
|
|
||||||
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
paddingRight: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
marginBottom: StyleConstants.Spacing.M
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Login.whyDidYouRender = true
|
|
||||||
|
|
||||||
export default Login
|
|
@ -1,8 +1,6 @@
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
|
||||||
import analytics from '@root/components/analytics'
|
|
||||||
import haptics from '@root/components/haptics'
|
import haptics from '@root/components/haptics'
|
||||||
import { resetLocal } from '@utils/slices/instancesSlice'
|
import { localRemoveInstance } from '@utils/slices/instancesSlice'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -13,7 +11,6 @@ import { useDispatch } from 'react-redux'
|
|||||||
const Logout: React.FC = () => {
|
const Logout: React.FC = () => {
|
||||||
const { t } = useTranslation('meRoot')
|
const { t } = useTranslation('meRoot')
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const navigation = useNavigation()
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -36,12 +33,7 @@ const Logout: React.FC = () => {
|
|||||||
onPress: () => {
|
onPress: () => {
|
||||||
haptics('Success')
|
haptics('Success')
|
||||||
queryClient.clear()
|
queryClient.clear()
|
||||||
dispatch(resetLocal())
|
dispatch(localRemoveInstance())
|
||||||
analytics('logout')
|
|
||||||
navigation.navigate('Screen-Public', {
|
|
||||||
screen: 'Screen-Public-Root',
|
|
||||||
params: { publicTab: true }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import AccountHeader from '@screens/Shared/Account/Header'
|
import AccountHeader from '@screens/Shared/Account/Header'
|
||||||
import AccountInformation from '@screens/Shared/Account/Information'
|
import AccountInformation from '@screens/Shared/Account/Information'
|
||||||
import { accountFetch } from '@utils/fetches/accountFetch'
|
import hookAccount from '@utils/queryHooks/account'
|
||||||
import { getLocalAccountId } from '@utils/slices/instancesSlice'
|
import { getLocalAccount } from '@utils/slices/instancesSlice'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { useQuery } from 'react-query'
|
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@ -11,8 +10,9 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MyInfo: React.FC<Props> = ({ setData }) => {
|
const MyInfo: React.FC<Props> = ({ setData }) => {
|
||||||
const localAccountId = useSelector(getLocalAccountId)
|
const localAccount = useSelector(getLocalAccount)
|
||||||
const { data } = useQuery(['Account', { id: localAccountId }], accountFetch)
|
const { data } = hookAccount({ id: localAccount!.id })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
setData(data)
|
setData(data)
|
||||||
@ -22,7 +22,7 @@ const MyInfo: React.FC<Props> = ({ setData }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AccountHeader account={data} limitHeight />
|
<AccountHeader account={data} limitHeight />
|
||||||
<AccountInformation account={data} disableActions />
|
<AccountInformation account={data} ownAccount />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,13 @@
|
|||||||
|
import Button from '@components/Button'
|
||||||
import { MenuContainer, MenuRow } from '@components/Menu'
|
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||||
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import haptics from '@root/components/haptics'
|
import haptics from '@root/components/haptics'
|
||||||
|
import { persistor } from '@root/store'
|
||||||
|
import {
|
||||||
|
getLocalActiveIndex,
|
||||||
|
getLocalInstances,
|
||||||
|
getRemoteUrl
|
||||||
|
} from '@root/utils/slices/instancesSlice'
|
||||||
import {
|
import {
|
||||||
changeAnalytics,
|
changeAnalytics,
|
||||||
changeBrowser,
|
changeBrowser,
|
||||||
@ -13,18 +21,63 @@ import {
|
|||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import prettyBytes from 'pretty-bytes'
|
import prettyBytes from 'pretty-bytes'
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ActionSheetIOS, Button, StyleSheet, Text, View } from 'react-native'
|
import { ActionSheetIOS, StyleSheet, Text } from 'react-native'
|
||||||
import { CacheManager } from 'react-native-expo-image-cache'
|
import { CacheManager } from 'react-native-expo-image-cache'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
const DevDebug: React.FC = () => {
|
||||||
|
const localActiveIndex = useSelector(getLocalActiveIndex)
|
||||||
|
const localInstances = useSelector(getLocalInstances)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuContainer>
|
||||||
|
<MenuRow
|
||||||
|
title={'Local active index'}
|
||||||
|
content={typeof localActiveIndex + ' - ' + localActiveIndex}
|
||||||
|
onPress={() => {}}
|
||||||
|
/>
|
||||||
|
<MenuRow
|
||||||
|
title={'Saved local instances'}
|
||||||
|
content={localInstances.length.toString()}
|
||||||
|
iconBack='ChevronRight'
|
||||||
|
onPress={() =>
|
||||||
|
ActionSheetIOS.showActionSheetWithOptions(
|
||||||
|
{
|
||||||
|
options: localInstances
|
||||||
|
.map(instance => {
|
||||||
|
return instance.url + ': ' + instance.account.id
|
||||||
|
})
|
||||||
|
.concat(['Cancel']),
|
||||||
|
cancelButtonIndex: localInstances.length
|
||||||
|
},
|
||||||
|
buttonIndex => {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type='text'
|
||||||
|
content={'Purge secure storage'}
|
||||||
|
style={{
|
||||||
|
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2,
|
||||||
|
marginBottom: StyleConstants.Spacing.Global.PagePadding * 2
|
||||||
|
}}
|
||||||
|
destructive
|
||||||
|
onPress={() => persistor.purge()}
|
||||||
|
/>
|
||||||
|
</MenuContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const ScreenMeSettings: React.FC = () => {
|
const ScreenMeSettings: React.FC = () => {
|
||||||
|
const navigation = useNavigation()
|
||||||
const { t, i18n } = useTranslation('meSettings')
|
const { t, i18n } = useTranslation('meSettings')
|
||||||
const { setTheme, theme } = useTheme()
|
const { setTheme, theme } = useTheme()
|
||||||
const settingsLanguage = useSelector(getSettingsLanguage)
|
const settingsLanguage = useSelector(getSettingsLanguage)
|
||||||
const settingsTheme = useSelector(getSettingsTheme)
|
const settingsTheme = useSelector(getSettingsTheme)
|
||||||
const settingsBrowser = useSelector(getSettingsBrowser)
|
const settingsBrowser = useSelector(getSettingsBrowser)
|
||||||
|
const settingsRemote = useSelector(getRemoteUrl)
|
||||||
const settingsAnalytics = useSelector(getSettingsAnalytics)
|
const settingsAnalytics = useSelector(getSettingsAnalytics)
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
@ -134,9 +187,18 @@ const ScreenMeSettings: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</MenuContainer>
|
</MenuContainer>
|
||||||
<MenuContainer>
|
<MenuContainer>
|
||||||
|
<MenuRow
|
||||||
|
title={t('content.remote.heading')}
|
||||||
|
description={t('content.remote.description')}
|
||||||
|
content={settingsRemote}
|
||||||
|
iconBack='ChevronRight'
|
||||||
|
onPress={() => navigation.navigate('Screen-Me-Settings-UpdateRemote')}
|
||||||
|
/>
|
||||||
<MenuRow
|
<MenuRow
|
||||||
title={t('content.cache.heading')}
|
title={t('content.cache.heading')}
|
||||||
content={cacheSize ? prettyBytes(cacheSize) : '暂无缓存'}
|
content={
|
||||||
|
cacheSize ? prettyBytes(cacheSize) : t('content.cache.empty')
|
||||||
|
}
|
||||||
iconBack='ChevronRight'
|
iconBack='ChevronRight'
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
await CacheManager.clearCache()
|
await CacheManager.clearCache()
|
||||||
@ -144,6 +206,8 @@ const ScreenMeSettings: React.FC = () => {
|
|||||||
setCacheSize(0)
|
setCacheSize(0)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</MenuContainer>
|
||||||
|
<MenuContainer>
|
||||||
<MenuRow
|
<MenuRow
|
||||||
title={t('content.analytics.heading')}
|
title={t('content.analytics.heading')}
|
||||||
description={t('content.analytics.description')}
|
description={t('content.analytics.description')}
|
||||||
@ -160,6 +224,8 @@ const ScreenMeSettings: React.FC = () => {
|
|||||||
{t('content.version', { version: '1.0.0' })}
|
{t('content.version', { version: '1.0.0' })}
|
||||||
</Text>
|
</Text>
|
||||||
</MenuContainer>
|
</MenuContainer>
|
||||||
|
|
||||||
|
{__DEV__ ? <DevDebug /> : null}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
16
src/screens/Me/Settings/UpdateRemote.tsx
Normal file
16
src/screens/Me/Settings/UpdateRemote.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import ComponentInstance from '@components/Instance'
|
||||||
|
import React from 'react'
|
||||||
|
import { KeyboardAvoidingView } from 'react-native'
|
||||||
|
import { ScrollView } from 'react-native-gesture-handler'
|
||||||
|
|
||||||
|
const UpdateRemote: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
|
||||||
|
<ScrollView keyboardShouldPersistTaps='handled'>
|
||||||
|
<ComponentInstance type='remote' disableHeaderImage />
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpdateRemote
|
28
src/screens/Me/Switch.tsx
Normal file
28
src/screens/Me/Switch.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||||
|
import React from 'react'
|
||||||
|
import { StyleSheet } from 'react-native'
|
||||||
|
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||||
|
import ScreenMeSwitchRoot from './Switch/Root'
|
||||||
|
|
||||||
|
const Stack = createNativeStackNavigator()
|
||||||
|
|
||||||
|
const ScreenMeSwitch: React.FC = ({ navigation }) => {
|
||||||
|
return (
|
||||||
|
<Stack.Navigator screenOptions={{ headerHideShadow: true }}>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Screen-Me-Switch-Root'
|
||||||
|
component={ScreenMeSwitchRoot}
|
||||||
|
options={{
|
||||||
|
headerTitle: '切换账号',
|
||||||
|
headerLeft: () => (
|
||||||
|
<HeaderLeft content='X' onPress={() => navigation.goBack()} />
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack.Navigator>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({})
|
||||||
|
|
||||||
|
export default ScreenMeSwitch
|
117
src/screens/Me/Switch/Root.tsx
Normal file
117
src/screens/Me/Switch/Root.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import Button from '@components/Button'
|
||||||
|
import ComponentInstance from '@components/Instance'
|
||||||
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
import hookAccountCheck from '@utils/queryHooks/accountCheck'
|
||||||
|
import {
|
||||||
|
getLocalActiveIndex,
|
||||||
|
getLocalInstances,
|
||||||
|
InstanceLocal,
|
||||||
|
InstancesState,
|
||||||
|
localUpdateActiveIndex
|
||||||
|
} from '@utils/slices/instancesSlice'
|
||||||
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
|
import React from 'react'
|
||||||
|
import { KeyboardAvoidingView, StyleSheet, Text, View } from 'react-native'
|
||||||
|
import { ScrollView } from 'react-native-gesture-handler'
|
||||||
|
import { useQueryClient } from 'react-query'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
index: NonNullable<InstancesState['local']['activeIndex']>
|
||||||
|
instance: InstanceLocal
|
||||||
|
active?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const AccountButton: React.FC<Props> = ({
|
||||||
|
index,
|
||||||
|
instance,
|
||||||
|
active = false
|
||||||
|
}) => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const navigation = useNavigation()
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const { isLoading, data } = hookAccountCheck({
|
||||||
|
id: instance.account.id,
|
||||||
|
index,
|
||||||
|
options: { retry: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type='text'
|
||||||
|
active={active}
|
||||||
|
loading={isLoading}
|
||||||
|
style={styles.button}
|
||||||
|
content={`@${data?.acct || '...'}@${instance.url}`}
|
||||||
|
onPress={() => {
|
||||||
|
dispatch(localUpdateActiveIndex(index))
|
||||||
|
queryClient.clear()
|
||||||
|
navigation.goBack()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScreenMeSwitchRoot = () => {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const localInstances = useSelector(getLocalInstances)
|
||||||
|
const localActiveIndex = useSelector(getLocalActiveIndex)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
|
||||||
|
<ScrollView keyboardShouldPersistTaps='handled'>
|
||||||
|
<View style={styles.firstSection}>
|
||||||
|
<Text style={[styles.header, { color: theme.primary }]}>
|
||||||
|
登录新的服务器
|
||||||
|
</Text>
|
||||||
|
<ComponentInstance type='local' disableHeaderImage goBack />
|
||||||
|
</View>
|
||||||
|
<View style={[styles.secondSection, { borderTopColor: theme.border }]}>
|
||||||
|
<Text style={[styles.header, { color: theme.primary }]}>
|
||||||
|
选择已有账号
|
||||||
|
</Text>
|
||||||
|
<View style={styles.accountButtons}>
|
||||||
|
{localInstances.length
|
||||||
|
? localInstances.map((instance, index) => (
|
||||||
|
<AccountButton
|
||||||
|
key={index}
|
||||||
|
index={index}
|
||||||
|
instance={instance}
|
||||||
|
active={localActiveIndex === index}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
header: {
|
||||||
|
...StyleConstants.FontStyle.M,
|
||||||
|
textAlign: 'center',
|
||||||
|
paddingVertical: StyleConstants.Spacing.S
|
||||||
|
},
|
||||||
|
firstSection: {
|
||||||
|
marginTop: StyleConstants.Spacing.S
|
||||||
|
},
|
||||||
|
secondSection: {
|
||||||
|
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
||||||
|
paddingTop: StyleConstants.Spacing.M,
|
||||||
|
borderTopWidth: StyleSheet.hairlineWidth
|
||||||
|
},
|
||||||
|
accountButtons: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
marginTop: StyleConstants.Spacing.M
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
marginBottom: StyleConstants.Spacing.M
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default ScreenMeSwitchRoot
|
@ -1,19 +1,16 @@
|
|||||||
import React from 'react'
|
|
||||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
|
||||||
|
|
||||||
import Timeline from '@components/Timelines/Timeline'
|
import Timeline from '@components/Timelines/Timeline'
|
||||||
import sharedScreens from '@screens/Shared/sharedScreens'
|
import sharedScreens from '@screens/Shared/sharedScreens'
|
||||||
import { useSelector } from 'react-redux'
|
import { getLocalActiveIndex } from '@utils/slices/instancesSlice'
|
||||||
import { RootState } from '@root/store'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator()
|
const Stack = createNativeStackNavigator()
|
||||||
|
|
||||||
const ScreenNotifications: React.FC = () => {
|
const ScreenNotifications: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const localRegistered = useSelector(
|
const localActiveIndex = useSelector(getLocalActiveIndex)
|
||||||
(state: RootState) => state.instances.local.url
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack.Navigator
|
<Stack.Navigator
|
||||||
@ -23,7 +20,9 @@ const ScreenNotifications: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack.Screen name='Screen-Notifications-Root'>
|
<Stack.Screen name='Screen-Notifications-Root'>
|
||||||
{() => (localRegistered ? <Timeline page='Notifications' /> : null)}
|
{() =>
|
||||||
|
localActiveIndex !== null ? <Timeline page='Notifications' /> : null
|
||||||
|
}
|
||||||
</Stack.Screen>
|
</Stack.Screen>
|
||||||
|
|
||||||
{sharedScreens(Stack)}
|
{sharedScreens(Stack)}
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import BottomSheet from '@components/BottomSheet'
|
import BottomSheet from '@components/BottomSheet'
|
||||||
import { HeaderRight } from '@components/Header'
|
import { HeaderRight } from '@components/Header'
|
||||||
import HeaderDefaultActionsAccount from '@components/Timelines/Timeline/Shared/HeaderDefault/ActionsAccount'
|
import HeaderDefaultActionsAccount from '@components/Timelines/Timeline/Shared/HeaderDefault/ActionsAccount'
|
||||||
import { accountFetch } from '@utils/fetches/accountFetch'
|
import hookAccount from '@utils/queryHooks/account'
|
||||||
import { getLocalAccountId } from '@utils/slices/instancesSlice'
|
import { getLocalAccount } from '@utils/slices/instancesSlice'
|
||||||
import React, { useEffect, useReducer, useRef, useState } from 'react'
|
import React, { useEffect, useReducer, useRef, useState } from 'react'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
useAnimatedScrollHandler,
|
useAnimatedScrollHandler,
|
||||||
useSharedValue
|
useSharedValue
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import { useQuery } from 'react-query'
|
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import AccountHeader from './Account/Header'
|
import AccountHeader from './Account/Header'
|
||||||
import AccountInformation from './Account/Information'
|
import AccountInformation from './Account/Information'
|
||||||
@ -36,8 +35,8 @@ const ScreenSharedAccount: React.FC<Props> = ({
|
|||||||
},
|
},
|
||||||
navigation
|
navigation
|
||||||
}) => {
|
}) => {
|
||||||
const localAccountId = useSelector(getLocalAccountId)
|
const localAccount = useSelector(getLocalAccount)
|
||||||
const { data } = useQuery(['Account', { id: account.id }], accountFetch)
|
const { data } = hookAccount({ id: account.id })
|
||||||
|
|
||||||
const scrollY = useSharedValue(0)
|
const scrollY = useSharedValue(0)
|
||||||
const [accountState, accountDispatch] = useReducer(
|
const [accountState, accountDispatch] = useReducer(
|
||||||
@ -85,7 +84,7 @@ const ScreenSharedAccount: React.FC<Props> = ({
|
|||||||
handleDismiss={() => setBottomSheetVisible(false)}
|
handleDismiss={() => setBottomSheetVisible(false)}
|
||||||
>
|
>
|
||||||
{/* 添加到列表 */}
|
{/* 添加到列表 */}
|
||||||
{localAccountId !== account.id && (
|
{localAccount?.id !== account.id && (
|
||||||
<HeaderDefaultActionsAccount
|
<HeaderDefaultActionsAccount
|
||||||
account={account}
|
account={account}
|
||||||
setBottomSheetVisible={setBottomSheetVisible}
|
setBottomSheetVisible={setBottomSheetVisible}
|
||||||
|
@ -1,24 +1,25 @@
|
|||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React, { createRef, useCallback, useContext, useEffect } from 'react'
|
import React, { createRef, useCallback, useContext, useEffect } from 'react'
|
||||||
import { Animated, StyleSheet, View } from 'react-native'
|
import { Animated, StyleSheet, View } from 'react-native'
|
||||||
import AccountInformationAvatar from './Information/Avatar'
|
|
||||||
import AccountInformationName from './Information/Name'
|
|
||||||
import AccountInformationAccount from './Information/Account'
|
import AccountInformationAccount from './Information/Account'
|
||||||
import AccountInformationCreated from './Information/Created'
|
|
||||||
import AccountInformationStats from './Information/Stats'
|
|
||||||
import AccountInformationActions from './Information/Actions'
|
import AccountInformationActions from './Information/Actions'
|
||||||
|
import AccountInformationAvatar from './Information/Avatar'
|
||||||
|
import AccountInformationCreated from './Information/Created'
|
||||||
import AccountInformationFields from './Information/Fields'
|
import AccountInformationFields from './Information/Fields'
|
||||||
|
import AccountInformationName from './Information/Name'
|
||||||
import AccountInformationNotes from './Information/Notes'
|
import AccountInformationNotes from './Information/Notes'
|
||||||
|
import AccountInformationStats from './Information/Stats'
|
||||||
|
import AccountInformationSwitch from './Information/Switch'
|
||||||
import AccountContext from './utils/createContext'
|
import AccountContext from './utils/createContext'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
account: Mastodon.Account | undefined
|
account: Mastodon.Account | undefined
|
||||||
disableActions?: boolean
|
ownAccount?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccountInformation: React.FC<Props> = ({
|
const AccountInformation: React.FC<Props> = ({
|
||||||
account,
|
account,
|
||||||
disableActions = false
|
ownAccount = false
|
||||||
}) => {
|
}) => {
|
||||||
const { accountDispatch } = useContext(AccountContext)
|
const { accountDispatch } = useContext(AccountContext)
|
||||||
const shimmerAvatarRef = createRef<any>()
|
const shimmerAvatarRef = createRef<any>()
|
||||||
@ -59,9 +60,13 @@ const AccountInformation: React.FC<Props> = ({
|
|||||||
{/* <Text>Moved or not: {account.moved}</Text> */}
|
{/* <Text>Moved or not: {account.moved}</Text> */}
|
||||||
<View style={styles.avatarAndActions}>
|
<View style={styles.avatarAndActions}>
|
||||||
<AccountInformationAvatar ref={shimmerAvatarRef} account={account} />
|
<AccountInformationAvatar ref={shimmerAvatarRef} account={account} />
|
||||||
{!disableActions ? (
|
<View style={styles.actions}>
|
||||||
<AccountInformationActions account={account} />
|
{ownAccount ? (
|
||||||
) : null}
|
<AccountInformationSwitch />
|
||||||
|
) : (
|
||||||
|
<AccountInformationActions account={account} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<AccountInformationName ref={shimmerNameRef} account={account} />
|
<AccountInformationName ref={shimmerNameRef} account={account} />
|
||||||
@ -94,6 +99,10 @@ const styles = StyleSheet.create({
|
|||||||
avatarAndActions: {
|
avatarAndActions: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between'
|
justifyContent: 'space-between'
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
alignSelf: 'flex-end',
|
||||||
|
flexDirection: 'row'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ const AccountInformationAccount = forwardRef<ShimmerPlaceholder, Props>(
|
|||||||
width={StyleConstants.Font.Size.M * 8}
|
width={StyleConstants.Font.Size.M * 8}
|
||||||
height={StyleConstants.Font.Size.M}
|
height={StyleConstants.Font.Size.M}
|
||||||
style={{ marginBottom: StyleConstants.Spacing.L }}
|
style={{ marginBottom: StyleConstants.Spacing.L }}
|
||||||
shimmerColors={theme.shimmer}
|
shimmerColors={[theme.shimmerDefault, theme.shimmerHighlight, theme.shimmerDefault]}
|
||||||
>
|
>
|
||||||
<View style={styles.account}>
|
<View style={styles.account}>
|
||||||
<Text
|
<Text
|
||||||
|
@ -1,47 +1,45 @@
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import { RelationshipOutgoing } from '@components/Relationship'
|
import { RelationshipOutgoing } from '@components/Relationship'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { relationshipFetch } from '@utils/fetches/relationshipFetch'
|
import hookRelationship from '@utils/queryHooks/relationship'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { StyleSheet, View } from 'react-native'
|
import { StyleSheet } from 'react-native'
|
||||||
import { useQuery } from 'react-query'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
account: Mastodon.Account | undefined
|
account: Mastodon.Account | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccountInformationActions: React.FC<Props> = ({ account }) => {
|
const Conversation = ({ account }: { account: Mastodon.Account }) => {
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const relationshipQueryKey = ['Relationship', { id: account?.id }]
|
const query = hookRelationship({ id: account.id })
|
||||||
const query = useQuery(relationshipQueryKey, relationshipFetch)
|
|
||||||
|
|
||||||
return (
|
return query.data && !query.data.blocked_by ? (
|
||||||
<View style={styles.actions}>
|
<Button
|
||||||
{query.data && !query.data.blocked_by ? (
|
round
|
||||||
<Button
|
type='icon'
|
||||||
round
|
content='Mail'
|
||||||
type='icon'
|
style={styles.actionConversation}
|
||||||
content='Mail'
|
onPress={() =>
|
||||||
style={styles.actionConversation}
|
navigation.navigate('Screen-Shared-Compose', {
|
||||||
onPress={() =>
|
type: 'conversation',
|
||||||
navigation.navigate('Screen-Shared-Compose', {
|
incomingStatus: { account }
|
||||||
type: 'conversation',
|
})
|
||||||
incomingStatus: { account }
|
}
|
||||||
})
|
/>
|
||||||
}
|
) : null
|
||||||
/>
|
}
|
||||||
) : null}
|
|
||||||
{account && account.id && <RelationshipOutgoing id={account.id} />}
|
const AccountInformationActions: React.FC<Props> = ({ account }) => {
|
||||||
</View>
|
return account && account.id ? (
|
||||||
)
|
<>
|
||||||
|
<Conversation account={account} />
|
||||||
|
<RelationshipOutgoing id={account.id} />
|
||||||
|
</>
|
||||||
|
) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
actions: {
|
|
||||||
alignSelf: 'flex-end',
|
|
||||||
flexDirection: 'row'
|
|
||||||
},
|
|
||||||
actionConversation: { marginRight: StyleConstants.Spacing.S }
|
actionConversation: { marginRight: StyleConstants.Spacing.S }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ const AccountInformationAvatar = forwardRef<ShimmerPlaceholder, Props>(
|
|||||||
visible={avatarLoaded}
|
visible={avatarLoaded}
|
||||||
width={StyleConstants.Avatar.L}
|
width={StyleConstants.Avatar.L}
|
||||||
height={StyleConstants.Avatar.L}
|
height={StyleConstants.Avatar.L}
|
||||||
shimmerColors={theme.shimmer}
|
shimmerColors={[theme.shimmerDefault, theme.shimmerHighlight, theme.shimmerDefault]}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: account?.avatar }}
|
source={{ uri: account?.avatar }}
|
||||||
|
@ -26,7 +26,7 @@ const AccountInformationCreated = forwardRef<ShimmerPlaceholder, Props>(
|
|||||||
width={StyleConstants.Font.Size.S * 8}
|
width={StyleConstants.Font.Size.S * 8}
|
||||||
height={StyleConstants.Font.Size.S}
|
height={StyleConstants.Font.Size.S}
|
||||||
style={{ marginBottom: StyleConstants.Spacing.M }}
|
style={{ marginBottom: StyleConstants.Spacing.M }}
|
||||||
shimmerColors={theme.shimmer}
|
shimmerColors={[theme.shimmerDefault, theme.shimmerHighlight, theme.shimmerDefault]}
|
||||||
>
|
>
|
||||||
<View style={styles.created}>
|
<View style={styles.created}>
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -25,6 +25,7 @@ const AccountInformationFields: React.FC<Props> = ({ account }) => {
|
|||||||
size={'M'}
|
size={'M'}
|
||||||
emojis={account.emojis}
|
emojis={account.emojis}
|
||||||
showFullLink
|
showFullLink
|
||||||
|
numberOfLines={3}
|
||||||
/>
|
/>
|
||||||
{field.verified_at ? (
|
{field.verified_at ? (
|
||||||
<Icon
|
<Icon
|
||||||
@ -41,6 +42,7 @@ const AccountInformationFields: React.FC<Props> = ({ account }) => {
|
|||||||
size={'M'}
|
size={'M'}
|
||||||
emojis={account.emojis}
|
emojis={account.emojis}
|
||||||
showFullLink
|
showFullLink
|
||||||
|
numberOfLines={3}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
@ -26,7 +26,7 @@ const AccountInformationName = forwardRef<ShimmerPlaceholder, Props>(
|
|||||||
width={StyleConstants.Font.Size.L * 8}
|
width={StyleConstants.Font.Size.L * 8}
|
||||||
height={StyleConstants.Font.Size.L}
|
height={StyleConstants.Font.Size.L}
|
||||||
style={styles.name}
|
style={styles.name}
|
||||||
shimmerColors={theme.shimmer}
|
shimmerColors={[theme.shimmerDefault, theme.shimmerHighlight, theme.shimmerDefault]}
|
||||||
>
|
>
|
||||||
{account ? (
|
{account ? (
|
||||||
<ParseEmojis
|
<ParseEmojis
|
||||||
|
@ -41,7 +41,7 @@ const AccountInformationStats = forwardRef<any, Props>(({ account }, ref) => {
|
|||||||
visible={account !== undefined}
|
visible={account !== undefined}
|
||||||
width={StyleConstants.Font.Size.S * 5}
|
width={StyleConstants.Font.Size.S * 5}
|
||||||
height={StyleConstants.Font.Size.S}
|
height={StyleConstants.Font.Size.S}
|
||||||
shimmerColors={theme.shimmer}
|
shimmerColors={[theme.shimmerDefault, theme.shimmerHighlight, theme.shimmerDefault]}
|
||||||
>
|
>
|
||||||
<Text style={[styles.stat, { color: theme.primary }]}>
|
<Text style={[styles.stat, { color: theme.primary }]}>
|
||||||
{t('content.summary.statuses_count', {
|
{t('content.summary.statuses_count', {
|
||||||
@ -54,7 +54,7 @@ const AccountInformationStats = forwardRef<any, Props>(({ account }, ref) => {
|
|||||||
visible={account !== undefined}
|
visible={account !== undefined}
|
||||||
width={StyleConstants.Font.Size.S * 5}
|
width={StyleConstants.Font.Size.S * 5}
|
||||||
height={StyleConstants.Font.Size.S}
|
height={StyleConstants.Font.Size.S}
|
||||||
shimmerColors={theme.shimmer}
|
shimmerColors={[theme.shimmerDefault, theme.shimmerHighlight, theme.shimmerDefault]}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[styles.stat, { color: theme.primary, textAlign: 'right' }]}
|
style={[styles.stat, { color: theme.primary, textAlign: 'right' }]}
|
||||||
@ -76,7 +76,7 @@ const AccountInformationStats = forwardRef<any, Props>(({ account }, ref) => {
|
|||||||
visible={account !== undefined}
|
visible={account !== undefined}
|
||||||
width={StyleConstants.Font.Size.S * 5}
|
width={StyleConstants.Font.Size.S * 5}
|
||||||
height={StyleConstants.Font.Size.S}
|
height={StyleConstants.Font.Size.S}
|
||||||
shimmerColors={theme.shimmer}
|
shimmerColors={[theme.shimmerDefault, theme.shimmerHighlight, theme.shimmerDefault]}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[styles.stat, { color: theme.primary, textAlign: 'center' }]}
|
style={[styles.stat, { color: theme.primary, textAlign: 'center' }]}
|
||||||
|
17
src/screens/Shared/Account/Information/Switch.tsx
Normal file
17
src/screens/Shared/Account/Information/Switch.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import Button from '@components/Button'
|
||||||
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const AccountInformationSwitch: React.FC = () => {
|
||||||
|
const navigation = useNavigation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type='text'
|
||||||
|
content='切换账号'
|
||||||
|
onPress={() => navigation.navigate('Screen-Me-Switch')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AccountInformationSwitch
|
@ -4,7 +4,7 @@ import haptics from '@components/haptics'
|
|||||||
import { ParseHTML } from '@components/Parse'
|
import { ParseHTML } from '@components/Parse'
|
||||||
import relativeTime from '@components/relativeTime'
|
import relativeTime from '@components/relativeTime'
|
||||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'
|
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'
|
||||||
import { announcementFetch } from '@utils/fetches/announcementsFetch'
|
import hookAnnouncement from '@utils/queryHooks/announcement'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
@ -19,7 +19,7 @@ import {
|
|||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import { FlatList, ScrollView } from 'react-native-gesture-handler'
|
import { FlatList, ScrollView } from 'react-native-gesture-handler'
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||||
import { useMutation, useQuery } from 'react-query'
|
import { useMutation } from 'react-query'
|
||||||
|
|
||||||
const fireMutation = async ({
|
const fireMutation = async ({
|
||||||
announcementId,
|
announcementId,
|
||||||
@ -34,13 +34,13 @@ const fireMutation = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'reaction':
|
case 'reaction':
|
||||||
return client({
|
return client<{}>({
|
||||||
method: me ? 'delete' : 'put',
|
method: me ? 'delete' : 'put',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: `announcements/${announcementId}/reactions/${name}`
|
url: `announcements/${announcementId}/reactions/${name}`
|
||||||
})
|
})
|
||||||
case 'dismiss':
|
case 'dismiss':
|
||||||
return client({
|
return client<{}>({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: `announcements/${announcementId}/dismiss`
|
url: `announcements/${announcementId}/dismiss`
|
||||||
@ -59,12 +59,14 @@ const ScreenSharedAnnouncements: React.FC = ({
|
|||||||
const [index, setIndex] = useState(0)
|
const [index, setIndex] = useState(0)
|
||||||
const { t, i18n } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
|
|
||||||
const queryKey = ['Announcements', { showAll }]
|
const { data, refetch } = hookAnnouncement({
|
||||||
const { data, refetch } = useQuery(queryKey, announcementFetch, {
|
showAll,
|
||||||
select: announcements =>
|
options: {
|
||||||
announcements.filter(announcement =>
|
select: announcements =>
|
||||||
showAll ? announcement : !announcement.read
|
announcements.filter(announcement =>
|
||||||
)
|
showAll ? announcement : !announcement.read
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
const queryMutation = useMutation(fireMutation, {
|
const queryMutation = useMutation(fireMutation, {
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
|
@ -4,7 +4,7 @@ import { store } from '@root/store'
|
|||||||
import layoutAnimation from '@root/utils/styles/layoutAnimation'
|
import layoutAnimation from '@root/utils/styles/layoutAnimation'
|
||||||
import formatText from '@screens/Shared/Compose/formatText'
|
import formatText from '@screens/Shared/Compose/formatText'
|
||||||
import ComposeRoot from '@screens/Shared/Compose/Root'
|
import ComposeRoot from '@screens/Shared/Compose/Root'
|
||||||
import { getLocalAccountPreferences } from '@utils/slices/instancesSlice'
|
import { getLocalAccount } from '@utils/slices/instancesSlice'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useEffect, useReducer, useState } from 'react'
|
import React, { useCallback, useEffect, useReducer, useState } from 'react'
|
||||||
@ -24,7 +24,6 @@ import composeInitialState from './Compose/utils/initialState'
|
|||||||
import composeParseState from './Compose/utils/parseState'
|
import composeParseState from './Compose/utils/parseState'
|
||||||
import composePost from './Compose/utils/post'
|
import composePost from './Compose/utils/post'
|
||||||
import composeReducer from './Compose/utils/reducer'
|
import composeReducer from './Compose/utils/reducer'
|
||||||
import { ComposeState } from './Compose/utils/types'
|
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator()
|
const Stack = createNativeStackNavigator()
|
||||||
|
|
||||||
@ -34,7 +33,6 @@ export interface Props {
|
|||||||
| {
|
| {
|
||||||
type?: 'reply' | 'conversation' | 'edit'
|
type?: 'reply' | 'conversation' | 'edit'
|
||||||
incomingStatus: Mastodon.Status
|
incomingStatus: Mastodon.Status
|
||||||
visibilityLock?: boolean
|
|
||||||
}
|
}
|
||||||
| undefined
|
| undefined
|
||||||
}
|
}
|
||||||
@ -63,19 +61,21 @@ const Compose: React.FC<Props> = ({ route: { params }, navigation }) => {
|
|||||||
setHasKeyboard(false)
|
setHasKeyboard(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const localAccount = getLocalAccount(store.getState())
|
||||||
const [composeState, composeDispatch] = useReducer(
|
const [composeState, composeDispatch] = useReducer(
|
||||||
composeReducer,
|
composeReducer,
|
||||||
params?.type && params?.incomingStatus
|
params?.type && params?.incomingStatus
|
||||||
? composeParseState({
|
? composeParseState({
|
||||||
type: params.type,
|
type: params.type,
|
||||||
incomingStatus: params.incomingStatus,
|
incomingStatus: params.incomingStatus
|
||||||
visibilityLock: params.visibilityLock
|
|
||||||
})
|
})
|
||||||
: {
|
: {
|
||||||
...composeInitialState,
|
...composeInitialState,
|
||||||
visibility: getLocalAccountPreferences(store.getState())[
|
visibility:
|
||||||
'posting:default:visibility'
|
localAccount?.preferences &&
|
||||||
] as ComposeState['visibility']
|
localAccount.preferences['posting:default:visibility']
|
||||||
|
? localAccount.preferences['posting:default:visibility']
|
||||||
|
: 'public'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
@ -97,7 +97,7 @@ const ComposeEditAttachment: React.FC<Props> = ({
|
|||||||
formData.append('focus', `${focus.current.x},${focus.current.y}`)
|
formData.append('focus', `${focus.current.x},${focus.current.y}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
client({
|
client<Mastodon.Attachment>({
|
||||||
method: 'put',
|
method: 'put',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: `media/${theAttachment.id}`,
|
url: `media/${theAttachment.id}`,
|
||||||
|
@ -1,131 +1,43 @@
|
|||||||
import haptics from '@components/haptics'
|
import hookEmojis from '@utils/queryHooks/emojis'
|
||||||
import { ParseEmojis } from '@components/Parse'
|
import hookSearch from '@utils/queryHooks/search'
|
||||||
import { emojisFetch } from '@utils/fetches/emojisFetch'
|
|
||||||
import { searchFetch } from '@utils/fetches/searchFetch'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { forEach, groupBy, sortBy } from 'lodash'
|
import { forEach, groupBy, sortBy } from 'lodash'
|
||||||
import React, {
|
import React, { useCallback, useContext, useEffect, useMemo } from 'react'
|
||||||
Dispatch,
|
import { View, FlatList, StyleSheet } from 'react-native'
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo
|
|
||||||
} from 'react'
|
|
||||||
import {
|
|
||||||
View,
|
|
||||||
FlatList,
|
|
||||||
Pressable,
|
|
||||||
StyleSheet,
|
|
||||||
Text,
|
|
||||||
Image
|
|
||||||
} from 'react-native'
|
|
||||||
import { Chase } from 'react-native-animated-spinkit'
|
import { Chase } from 'react-native-animated-spinkit'
|
||||||
import { useQuery } from 'react-query'
|
|
||||||
import ComposeActions from './Actions'
|
import ComposeActions from './Actions'
|
||||||
import ComposeRootFooter from './Root/Footer'
|
import ComposeRootFooter from './Root/Footer'
|
||||||
import ComposeRootHeader from './Root/Header'
|
import ComposeRootHeader from './Root/Header'
|
||||||
import updateText from './updateText'
|
import ComposeRootSuggestion from './Root/Suggestion'
|
||||||
import ComposeContext from './utils/createContext'
|
import ComposeContext from './utils/createContext'
|
||||||
import { ComposeAction, ComposeState } from './utils/types'
|
|
||||||
|
|
||||||
const ListItem = React.memo(
|
|
||||||
({
|
|
||||||
item,
|
|
||||||
composeState,
|
|
||||||
composeDispatch
|
|
||||||
}: {
|
|
||||||
item: Mastodon.Account & Mastodon.Tag
|
|
||||||
composeState: ComposeState
|
|
||||||
composeDispatch: Dispatch<ComposeAction>
|
|
||||||
}) => {
|
|
||||||
const { theme } = useTheme()
|
|
||||||
const onPress = useCallback(() => {
|
|
||||||
const focusedInput = composeState.textInputFocus.current
|
|
||||||
updateText({
|
|
||||||
composeState: {
|
|
||||||
...composeState,
|
|
||||||
[focusedInput]: {
|
|
||||||
...composeState[focusedInput],
|
|
||||||
selection: {
|
|
||||||
start: composeState.tag!.offset,
|
|
||||||
end: composeState.tag!.offset + composeState.tag!.text.length + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
composeDispatch,
|
|
||||||
newText: item.acct ? `@${item.acct}` : `#${item.name}`,
|
|
||||||
type: 'suggestion'
|
|
||||||
})
|
|
||||||
haptics('Success')
|
|
||||||
}, [])
|
|
||||||
const children = useMemo(
|
|
||||||
() =>
|
|
||||||
item.acct ? (
|
|
||||||
<View style={[styles.account, { borderBottomColor: theme.border }]}>
|
|
||||||
<Image source={{ uri: item.avatar }} style={styles.accountAvatar} />
|
|
||||||
<View>
|
|
||||||
<Text
|
|
||||||
style={[styles.accountName, { color: theme.primary }]}
|
|
||||||
numberOfLines={1}
|
|
||||||
>
|
|
||||||
<ParseEmojis
|
|
||||||
content={item.display_name || item.username}
|
|
||||||
emojis={item.emojis}
|
|
||||||
size='S'
|
|
||||||
/>
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
style={[styles.accountAccount, { color: theme.primary }]}
|
|
||||||
numberOfLines={1}
|
|
||||||
>
|
|
||||||
@{item.acct}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<View style={[styles.hashtag, { borderBottomColor: theme.border }]}>
|
|
||||||
<Text style={[styles.hashtagText, { color: theme.primary }]}>
|
|
||||||
#{item.name}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<Pressable
|
|
||||||
onPress={onPress}
|
|
||||||
style={styles.suggestion}
|
|
||||||
children={children}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
() => true
|
|
||||||
)
|
|
||||||
|
|
||||||
const ComposeRoot: React.FC = () => {
|
const ComposeRoot: React.FC = () => {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
const { composeState, composeDispatch } = useContext(ComposeContext)
|
const { composeState, composeDispatch } = useContext(ComposeContext)
|
||||||
|
|
||||||
const { isFetching, data, refetch } = useQuery(
|
const { isFetching, data, refetch } = hookSearch({
|
||||||
[
|
type:
|
||||||
'Search',
|
composeState.tag?.type === 'accounts' ||
|
||||||
{
|
composeState.tag?.type === 'hashtags'
|
||||||
type: composeState.tag?.type,
|
? composeState.tag.type
|
||||||
term: composeState.tag?.text.substring(1)
|
: undefined,
|
||||||
}
|
term: composeState.tag?.text.substring(1),
|
||||||
],
|
options: { enabled: false }
|
||||||
searchFetch,
|
})
|
||||||
{ enabled: false }
|
|
||||||
)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (composeState.tag?.text) {
|
if (
|
||||||
|
(composeState.tag?.type === 'accounts' ||
|
||||||
|
composeState.tag?.type === 'hashtags') &&
|
||||||
|
composeState.tag?.text
|
||||||
|
) {
|
||||||
refetch()
|
refetch()
|
||||||
}
|
}
|
||||||
}, [composeState.tag?.text])
|
}, [composeState.tag])
|
||||||
|
|
||||||
const { data: emojisData } = useQuery(['Emojis'], emojisFetch)
|
const { data: emojisData } = hookEmojis({})
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (emojisData && emojisData.length) {
|
if (emojisData && emojisData.length) {
|
||||||
let sortedEmojis: { title: string; data: Mastodon.Emoji[] }[] = []
|
let sortedEmojis: { title: string; data: Mastodon.Emoji[] }[] = []
|
||||||
@ -155,7 +67,7 @@ const ComposeRoot: React.FC = () => {
|
|||||||
|
|
||||||
const listItem = useCallback(
|
const listItem = useCallback(
|
||||||
({ item }) => (
|
({ item }) => (
|
||||||
<ListItem
|
<ComposeRootSuggestion
|
||||||
item={item}
|
item={item}
|
||||||
composeState={composeState}
|
composeState={composeState}
|
||||||
composeDispatch={composeDispatch}
|
composeDispatch={composeDispatch}
|
||||||
@ -167,13 +79,14 @@ const ComposeRoot: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<View style={styles.base}>
|
<View style={styles.base}>
|
||||||
<FlatList
|
<FlatList
|
||||||
|
renderItem={listItem}
|
||||||
|
ListEmptyComponent={listEmpty}
|
||||||
keyboardShouldPersistTaps='handled'
|
keyboardShouldPersistTaps='handled'
|
||||||
ListHeaderComponent={ComposeRootHeader}
|
ListHeaderComponent={ComposeRootHeader}
|
||||||
ListFooterComponent={ComposeRootFooter}
|
ListFooterComponent={ComposeRootFooter}
|
||||||
ListEmptyComponent={listEmpty}
|
// @ts-ignore
|
||||||
data={data as Mastodon.Account[] & Mastodon.Tag[]}
|
data={data ? data[composeState.tag?.type] : undefined}
|
||||||
renderItem={listItem}
|
keyExtractor={({ item }) => item.acct || item.name}
|
||||||
keyExtractor={(item: any) => item.acct || item.name}
|
|
||||||
/>
|
/>
|
||||||
<ComposeActions />
|
<ComposeActions />
|
||||||
</View>
|
</View>
|
||||||
@ -185,46 +98,6 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1
|
flex: 1
|
||||||
},
|
},
|
||||||
contentView: { flex: 1 },
|
contentView: { flex: 1 },
|
||||||
suggestion: {
|
|
||||||
flex: 1
|
|
||||||
},
|
|
||||||
account: {
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingTop: StyleConstants.Spacing.S,
|
|
||||||
paddingBottom: StyleConstants.Spacing.S,
|
|
||||||
marginLeft: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
marginRight: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth
|
|
||||||
},
|
|
||||||
accountAvatar: {
|
|
||||||
width: StyleConstants.Font.LineHeight.M * 2,
|
|
||||||
height: StyleConstants.Font.LineHeight.M * 2,
|
|
||||||
marginRight: StyleConstants.Spacing.S,
|
|
||||||
borderRadius: StyleConstants.Avatar.M
|
|
||||||
},
|
|
||||||
accountName: {
|
|
||||||
...StyleConstants.FontStyle.S,
|
|
||||||
fontWeight: StyleConstants.Font.Weight.Bold,
|
|
||||||
marginBottom: StyleConstants.Spacing.XS
|
|
||||||
},
|
|
||||||
accountAccount: {
|
|
||||||
...StyleConstants.FontStyle.S
|
|
||||||
},
|
|
||||||
hashtag: {
|
|
||||||
flex: 1,
|
|
||||||
paddingTop: StyleConstants.Spacing.S,
|
|
||||||
paddingBottom: StyleConstants.Spacing.S,
|
|
||||||
marginLeft: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
marginRight: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth
|
|
||||||
},
|
|
||||||
hashtagText: {
|
|
||||||
...StyleConstants.FontStyle.S,
|
|
||||||
fontWeight: StyleConstants.Font.Weight.Bold,
|
|
||||||
marginBottom: StyleConstants.Spacing.XS
|
|
||||||
},
|
|
||||||
loading: {
|
loading: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
alignItems: 'center'
|
alignItems: 'center'
|
||||||
|
127
src/screens/Shared/Compose/Root/Suggestion.tsx
Normal file
127
src/screens/Shared/Compose/Root/Suggestion.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import haptics from '@components/haptics'
|
||||||
|
import { ParseEmojis } from '@components/Parse'
|
||||||
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
|
import React, { Dispatch, useCallback, useMemo } from 'react'
|
||||||
|
import { Image, Pressable, StyleSheet, Text, View } from 'react-native'
|
||||||
|
import updateText from '../updateText'
|
||||||
|
import { ComposeAction, ComposeState } from '../utils/types'
|
||||||
|
|
||||||
|
const ComposeRootSuggestion = React.memo(
|
||||||
|
({
|
||||||
|
item,
|
||||||
|
composeState,
|
||||||
|
composeDispatch
|
||||||
|
}: {
|
||||||
|
item: Mastodon.Account & Mastodon.Tag
|
||||||
|
composeState: ComposeState
|
||||||
|
composeDispatch: Dispatch<ComposeAction>
|
||||||
|
}) => {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const onPress = useCallback(() => {
|
||||||
|
const focusedInput = composeState.textInputFocus.current
|
||||||
|
updateText({
|
||||||
|
composeState: {
|
||||||
|
...composeState,
|
||||||
|
[focusedInput]: {
|
||||||
|
...composeState[focusedInput],
|
||||||
|
selection: {
|
||||||
|
start: composeState.tag!.offset,
|
||||||
|
end: composeState.tag!.offset + composeState.tag!.text.length + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
composeDispatch,
|
||||||
|
newText: item.acct ? `@${item.acct}` : `#${item.name}`,
|
||||||
|
type: 'suggestion'
|
||||||
|
})
|
||||||
|
haptics('Light')
|
||||||
|
}, [])
|
||||||
|
const children = useMemo(
|
||||||
|
() =>
|
||||||
|
item.acct ? (
|
||||||
|
<View style={[styles.account, { borderBottomColor: theme.border }]}>
|
||||||
|
<Image source={{ uri: item.avatar }} style={styles.accountAvatar} />
|
||||||
|
<View>
|
||||||
|
<Text
|
||||||
|
style={[styles.accountName, { color: theme.primary }]}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
<ParseEmojis
|
||||||
|
content={item.display_name || item.username}
|
||||||
|
emojis={item.emojis}
|
||||||
|
size='S'
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[styles.accountAccount, { color: theme.primary }]}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
@{item.acct}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={[styles.hashtag, { borderBottomColor: theme.border }]}>
|
||||||
|
<Text style={[styles.hashtagText, { color: theme.primary }]}>
|
||||||
|
#{item.name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
onPress={onPress}
|
||||||
|
style={styles.suggestion}
|
||||||
|
children={children}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
() => true
|
||||||
|
)
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
suggestion: {
|
||||||
|
flex: 1
|
||||||
|
},
|
||||||
|
account: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingTop: StyleConstants.Spacing.S,
|
||||||
|
paddingBottom: StyleConstants.Spacing.S,
|
||||||
|
marginLeft: StyleConstants.Spacing.Global.PagePadding,
|
||||||
|
marginRight: StyleConstants.Spacing.Global.PagePadding,
|
||||||
|
borderBottomWidth: StyleSheet.hairlineWidth
|
||||||
|
},
|
||||||
|
accountAvatar: {
|
||||||
|
width: StyleConstants.Font.LineHeight.M * 2,
|
||||||
|
height: StyleConstants.Font.LineHeight.M * 2,
|
||||||
|
marginRight: StyleConstants.Spacing.S,
|
||||||
|
borderRadius: StyleConstants.Avatar.M
|
||||||
|
},
|
||||||
|
accountName: {
|
||||||
|
...StyleConstants.FontStyle.S,
|
||||||
|
fontWeight: StyleConstants.Font.Weight.Bold,
|
||||||
|
marginBottom: StyleConstants.Spacing.XS
|
||||||
|
},
|
||||||
|
accountAccount: {
|
||||||
|
...StyleConstants.FontStyle.S
|
||||||
|
},
|
||||||
|
hashtag: {
|
||||||
|
flex: 1,
|
||||||
|
paddingTop: StyleConstants.Spacing.S,
|
||||||
|
paddingBottom: StyleConstants.Spacing.S,
|
||||||
|
marginLeft: StyleConstants.Spacing.Global.PagePadding,
|
||||||
|
marginRight: StyleConstants.Spacing.Global.PagePadding,
|
||||||
|
borderBottomWidth: StyleSheet.hairlineWidth
|
||||||
|
},
|
||||||
|
hashtagText: {
|
||||||
|
...StyleConstants.FontStyle.S,
|
||||||
|
fontWeight: StyleConstants.Font.Weight.Bold,
|
||||||
|
marginBottom: StyleConstants.Spacing.XS
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default ComposeRootSuggestion
|
@ -67,17 +67,17 @@ const addAttachment = async ({ composeDispatch }: Props): Promise<any> => {
|
|||||||
type: 'image/jpeg/jpg'
|
type: 'image/jpeg/jpg'
|
||||||
})
|
})
|
||||||
|
|
||||||
return client({
|
return client<Mastodon.Attachment>({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: 'media',
|
url: 'media',
|
||||||
body: formData
|
body: formData
|
||||||
})
|
})
|
||||||
.then(({ body }: { body: Mastodon.Attachment }) => {
|
.then(res => {
|
||||||
if (body.id) {
|
if (res.id) {
|
||||||
composeDispatch({
|
composeDispatch({
|
||||||
type: 'attachment/upload/end',
|
type: 'attachment/upload/end',
|
||||||
payload: { remote: body, local: result }
|
payload: { remote: res, local: result }
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
composeDispatch({
|
composeDispatch({
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { store } from '@root/store'
|
import { store } from '@root/store'
|
||||||
import { getLocalAccountPreferences } from '@utils/slices/instancesSlice'
|
import { getLocalAccount } from '@utils/slices/instancesSlice'
|
||||||
import composeInitialState from './initialState'
|
import composeInitialState from './initialState'
|
||||||
import { ComposeState } from './types'
|
import { ComposeState } from './types'
|
||||||
|
|
||||||
@ -40,9 +40,10 @@ const composeParseState = ({ type, incomingStatus }: Props): ComposeState => {
|
|||||||
}),
|
}),
|
||||||
visibility:
|
visibility:
|
||||||
incomingStatus.visibility ||
|
incomingStatus.visibility ||
|
||||||
getLocalAccountPreferences(store.getState())[
|
getLocalAccount(store.getState())?.preferences[
|
||||||
'posting:default:visibility'
|
'posting:default:visibility'
|
||||||
],
|
] ||
|
||||||
|
'public',
|
||||||
...(incomingStatus.visibility === 'direct' && { visibilityLock: true })
|
...(incomingStatus.visibility === 'direct' && { visibilityLock: true })
|
||||||
}
|
}
|
||||||
case 'reply':
|
case 'reply':
|
||||||
|
@ -40,7 +40,7 @@ const composePost = async (
|
|||||||
|
|
||||||
formData.append('visibility', composeState.visibility)
|
formData.append('visibility', composeState.visibility)
|
||||||
|
|
||||||
return client({
|
return client<Mastodon.Status>({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: 'statuses',
|
url: 'statuses',
|
||||||
|
@ -20,7 +20,6 @@ const ScreenSharedRelationships: React.FC<Props> = ({
|
|||||||
params: { account, initialType }
|
params: { account, initialType }
|
||||||
}
|
}
|
||||||
}) => {
|
}) => {
|
||||||
console.log(account.id)
|
|
||||||
const { mode } = useTheme()
|
const { mode } = useTheme()
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
|
|
||||||
|
@ -3,11 +3,10 @@ import ComponentSeparator from '@components/Separator'
|
|||||||
import TimelineEmpty from '@components/Timelines/Timeline/Empty'
|
import TimelineEmpty from '@components/Timelines/Timeline/Empty'
|
||||||
import TimelineEnd from '@root/components/Timelines/Timeline/End'
|
import TimelineEnd from '@root/components/Timelines/Timeline/End'
|
||||||
import { useScrollToTop } from '@react-navigation/native'
|
import { useScrollToTop } from '@react-navigation/native'
|
||||||
import { relationshipsFetch } from '@utils/fetches/relationshipsFetch'
|
|
||||||
import React, { useCallback, useMemo, useRef } from 'react'
|
import React, { useCallback, useMemo, useRef } from 'react'
|
||||||
import { RefreshControl, StyleSheet } from 'react-native'
|
import { RefreshControl, StyleSheet } from 'react-native'
|
||||||
import { FlatList } from 'react-native-gesture-handler'
|
import { FlatList } from 'react-native-gesture-handler'
|
||||||
import { useInfiniteQuery } from 'react-query'
|
import hookRelationships from '@utils/queryHooks/relationships'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
id: Mastodon.Account['id']
|
id: Mastodon.Account['id']
|
||||||
@ -15,8 +14,6 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RelationshipsList: React.FC<Props> = ({ id, type }) => {
|
const RelationshipsList: React.FC<Props> = ({ id, type }) => {
|
||||||
const queryKey: QueryKey.Relationships = ['Relationships', type, { id }]
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
status,
|
status,
|
||||||
data,
|
data,
|
||||||
@ -25,14 +22,18 @@ const RelationshipsList: React.FC<Props> = ({ id, type }) => {
|
|||||||
hasNextPage,
|
hasNextPage,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
isFetchingNextPage
|
isFetchingNextPage
|
||||||
} = useInfiniteQuery(queryKey, relationshipsFetch, {
|
} = hookRelationships({
|
||||||
getNextPageParam: lastPage => {
|
type,
|
||||||
return lastPage.length
|
id,
|
||||||
? {
|
options: {
|
||||||
direction: 'next',
|
getNextPageParam: lastPage => {
|
||||||
id: lastPage[lastPage.length - 1].id
|
return lastPage.length
|
||||||
}
|
? {
|
||||||
: undefined
|
direction: 'next',
|
||||||
|
id: lastPage[lastPage.length - 1].id
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const flattenData = data?.pages ? data.pages.flatMap(d => [...d]) : []
|
const flattenData = data?.pages ? data.pages.flatMap(d => [...d]) : []
|
||||||
|
@ -2,7 +2,7 @@ import { useNavigation } from '@react-navigation/native'
|
|||||||
import ComponentAccount from '@root/components/Account'
|
import ComponentAccount from '@root/components/Account'
|
||||||
import ComponentSeparator from '@root/components/Separator'
|
import ComponentSeparator from '@root/components/Separator'
|
||||||
import TimelineDefault from '@root/components/Timelines/Timeline/Default'
|
import TimelineDefault from '@root/components/Timelines/Timeline/Default'
|
||||||
import { searchFetch } from '@utils/fetches/searchFetch'
|
import hookSearch from '@utils/queryHooks/search'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
@ -17,17 +17,15 @@ import {
|
|||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import { Chase } from 'react-native-animated-spinkit'
|
import { Chase } from 'react-native-animated-spinkit'
|
||||||
import { TextInput } from 'react-native-gesture-handler'
|
import { TextInput } from 'react-native-gesture-handler'
|
||||||
import { useQuery } from 'react-query'
|
|
||||||
|
|
||||||
const ScreenSharedSearch: React.FC = () => {
|
const ScreenSharedSearch: React.FC = () => {
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const [searchTerm, setSearchTerm] = useState<string | undefined>()
|
const [searchTerm, setSearchTerm] = useState<string | undefined>()
|
||||||
const { status, data, refetch } = useQuery(
|
const { status, data, refetch } = hookSearch({
|
||||||
['Search', { term: searchTerm }],
|
term: searchTerm,
|
||||||
searchFetch,
|
options: { enabled: false }
|
||||||
{ enabled: false }
|
})
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateHeaderRight = () =>
|
const updateHeaderRight = () =>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
import { HeaderLeft } from '@components/Header'
|
||||||
import ScreenSharedAccount from '@screens/Shared/Account'
|
import ScreenSharedAccount from '@screens/Shared/Account'
|
||||||
import ScreenSharedAnnouncements from '@screens/Shared/Announcements'
|
import ScreenSharedAnnouncements from '@screens/Shared/Announcements'
|
||||||
import ScreenSharedHashtag from '@screens/Shared/Hashtag'
|
import ScreenSharedHashtag from '@screens/Shared/Hashtag'
|
||||||
@ -9,7 +9,6 @@ import Compose from '@screens/Shared/Compose'
|
|||||||
import ScreenSharedSearch from '@screens/Shared/Search'
|
import ScreenSharedSearch from '@screens/Shared/Search'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { View } from 'react-native'
|
|
||||||
|
|
||||||
const sharedScreens = (Stack: any) => {
|
const sharedScreens = (Stack: any) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
0
src/startup/Main.tsx
Normal file
0
src/startup/Main.tsx
Normal file
12
src/startup/audio.ts
Normal file
12
src/startup/audio.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Audio } from 'expo-av'
|
||||||
|
import log from "./log"
|
||||||
|
|
||||||
|
const audio = () => {
|
||||||
|
log('log', 'audio', 'setting audio playback default options')
|
||||||
|
Audio.setAudioModeAsync({
|
||||||
|
playsInSilentModeIOS: true,
|
||||||
|
interruptionModeIOS: 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default audio
|
41
src/startup/checkSecureStorageVersion.ts
Normal file
41
src/startup/checkSecureStorageVersion.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||||
|
import { persistor } from '@root/store'
|
||||||
|
import log from './log'
|
||||||
|
|
||||||
|
// Used to upgrade/invalidate secure storage
|
||||||
|
|
||||||
|
const dataKey = '@mastodon_app_database_version'
|
||||||
|
const currentVersion = '20210105'
|
||||||
|
|
||||||
|
const checkSecureStorageVersion = async (): Promise<any> => {
|
||||||
|
log(
|
||||||
|
'log',
|
||||||
|
'checkSecureStorageVersion',
|
||||||
|
'Start checking secure storage version'
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
const value = await AsyncStorage.getItem(dataKey)
|
||||||
|
if (value !== currentVersion) {
|
||||||
|
log(
|
||||||
|
'warn',
|
||||||
|
'checkSecureStorageVersion',
|
||||||
|
`Version does not match. Prev: ${value}. Current: ${currentVersion}.`
|
||||||
|
)
|
||||||
|
persistor.purge()
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem(dataKey, currentVersion)
|
||||||
|
} catch (e) {
|
||||||
|
log('error', 'checkSecureStorageVersion', 'Storing storage data error')
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('log', 'checkSecureStorageVersion', 'Storing storage version matched')
|
||||||
|
}
|
||||||
|
return Promise.resolve()
|
||||||
|
} catch (e) {
|
||||||
|
log('error', 'checkSecureStorageVersion', 'Getting storage data error')
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default checkSecureStorageVersion
|
18
src/startup/dev.ts
Normal file
18
src/startup/dev.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import * as Analytics from 'expo-firebase-analytics'
|
||||||
|
import React from 'react'
|
||||||
|
import log from './log'
|
||||||
|
|
||||||
|
const dev = () => {
|
||||||
|
if (__DEV__) {
|
||||||
|
Analytics.setDebugModeEnabled(true)
|
||||||
|
|
||||||
|
log('log', 'devs', 'initializing wdyr')
|
||||||
|
const whyDidYouRender = require('@welldone-software/why-did-you-render')
|
||||||
|
whyDidYouRender(React, {
|
||||||
|
trackHooks: true,
|
||||||
|
hotReloadBufferMs: 1000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default dev
|
37
src/startup/log.ts
Normal file
37
src/startup/log.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import chalk from 'chalk'
|
||||||
|
|
||||||
|
const ctx = new chalk.Instance({ level: 3 })
|
||||||
|
|
||||||
|
const log = (type: 'log' | 'warn' | 'error', func: string, message: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'log':
|
||||||
|
console.log(
|
||||||
|
ctx.bgBlue.white.bold(' Start up ') +
|
||||||
|
' ' +
|
||||||
|
ctx.bgBlueBright.black(` ${func} `) +
|
||||||
|
' ' +
|
||||||
|
message
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case 'warn':
|
||||||
|
console.warn(
|
||||||
|
ctx.bgYellow.white.bold(' Start up ') +
|
||||||
|
' ' +
|
||||||
|
ctx.bgYellowBright.black(` ${func} `) +
|
||||||
|
' ' +
|
||||||
|
message
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case 'error':
|
||||||
|
console.error(
|
||||||
|
ctx.bgRed.white.bold(' Start up ') +
|
||||||
|
' ' +
|
||||||
|
ctx.bgRedBright.black(` ${func} `) +
|
||||||
|
' ' +
|
||||||
|
message
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default log
|
61
src/startup/netInfo.ts
Normal file
61
src/startup/netInfo.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import client from '@api/client'
|
||||||
|
import NetInfo from '@react-native-community/netinfo'
|
||||||
|
import { store } from '@root/store'
|
||||||
|
import { localRemoveInstance } from '@utils/slices/instancesSlice'
|
||||||
|
import log from './log'
|
||||||
|
|
||||||
|
const netInfo = async (): Promise<{
|
||||||
|
connected: boolean
|
||||||
|
corrupted?: string
|
||||||
|
}> => {
|
||||||
|
log('log', 'netInfo', 'initializing')
|
||||||
|
const netInfo = await NetInfo.fetch()
|
||||||
|
const activeIndex = store.getState().instances.local?.activeIndex
|
||||||
|
|
||||||
|
if (netInfo.isConnected) {
|
||||||
|
log('log', 'netInfo', 'network connected')
|
||||||
|
if (activeIndex) {
|
||||||
|
log('log', 'netInfo', 'checking locally stored credentials')
|
||||||
|
return client<Mastodon.Account>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: `accounts/verify_credentials`
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
log('log', 'netInfo', 'local credential check passed')
|
||||||
|
if (
|
||||||
|
res.id !==
|
||||||
|
store.getState().instances.local?.instances[activeIndex].account.id
|
||||||
|
) {
|
||||||
|
log('error', 'netInfo', 'local id does not match remote id')
|
||||||
|
store.dispatch(localRemoveInstance(activeIndex))
|
||||||
|
return Promise.resolve({ connected: true, corruputed: '' })
|
||||||
|
} else {
|
||||||
|
return Promise.resolve({ connected: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
log('error', 'netInfo', 'local credential check failed')
|
||||||
|
if (
|
||||||
|
error.status &&
|
||||||
|
typeof error.status === 'number' &&
|
||||||
|
error.status === 401
|
||||||
|
) {
|
||||||
|
store.dispatch(localRemoveInstance(activeIndex))
|
||||||
|
}
|
||||||
|
return Promise.resolve({
|
||||||
|
connected: true,
|
||||||
|
corrupted: error.data.error
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
log('log', 'netInfo', 'no local credential found')
|
||||||
|
return Promise.resolve({ connected: true })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('warn', 'netInfo', 'network not connected')
|
||||||
|
return Promise.resolve({ connected: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default netInfo
|
15
src/startup/onlineStatus.ts
Normal file
15
src/startup/onlineStatus.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import NetInfo from '@react-native-community/netinfo'
|
||||||
|
import { onlineManager } from 'react-query'
|
||||||
|
import log from './log'
|
||||||
|
|
||||||
|
const onlineStatus = () =>
|
||||||
|
onlineManager.setEventListener(setOnline => {
|
||||||
|
log('log', 'onlineStatus', 'added onlineManager listener')
|
||||||
|
return NetInfo.addEventListener(state => {
|
||||||
|
log('log', 'onlineStatus', `setting online state ${state.isConnected}`)
|
||||||
|
// @ts-ignore
|
||||||
|
setOnline(state.isConnected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export default onlineStatus
|
14
src/startup/sentry.ts
Normal file
14
src/startup/sentry.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import * as Sentry from 'sentry-expo'
|
||||||
|
import log from "./log"
|
||||||
|
|
||||||
|
const sentry = () => {
|
||||||
|
log('log', 'Sentry', 'initializing')
|
||||||
|
Sentry.init({
|
||||||
|
dsn:
|
||||||
|
'https://c9e29aa05f774aca8f36def98244ce04@o389581.ingest.sentry.io/5571975',
|
||||||
|
enableInExpoDevelopment: false,
|
||||||
|
debug: __DEV__
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default sentry
|
37
src/store.ts
37
src/store.ts
@ -1,27 +1,46 @@
|
|||||||
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
|
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||||
|
import {
|
||||||
|
combineReducers,
|
||||||
|
configureStore,
|
||||||
|
getDefaultMiddleware
|
||||||
|
} from '@reduxjs/toolkit'
|
||||||
|
import instancesSlice from '@utils/slices/instancesSlice'
|
||||||
|
import settingsSlice from '@utils/slices/settingsSlice'
|
||||||
import { persistReducer, persistStore } from 'redux-persist'
|
import { persistReducer, persistStore } from 'redux-persist'
|
||||||
import createSecureStore from 'redux-persist-expo-securestore'
|
import createSecureStore from 'redux-persist-expo-securestore'
|
||||||
|
|
||||||
import instancesSlice from '@utils/slices/instancesSlice'
|
|
||||||
import settingsSlice from '@utils/slices/settingsSlice'
|
|
||||||
|
|
||||||
const secureStorage = createSecureStore()
|
const secureStorage = createSecureStore()
|
||||||
|
|
||||||
|
const prefix = 'mastodon_app'
|
||||||
|
|
||||||
const instancesPersistConfig = {
|
const instancesPersistConfig = {
|
||||||
key: 'instances',
|
key: 'instances',
|
||||||
storage: secureStorage
|
prefix,
|
||||||
|
version: 1,
|
||||||
|
storage: secureStorage,
|
||||||
|
debug: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsPersistConfig = {
|
const settingsPersistConfig = {
|
||||||
key: 'settings',
|
key: 'settings',
|
||||||
|
prefix,
|
||||||
storage: secureStorage
|
storage: secureStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rootPersistConfig = {
|
||||||
|
key: 'root',
|
||||||
|
prefix,
|
||||||
|
version: 0,
|
||||||
|
storage: AsyncStorage
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
instances: persistReducer(instancesPersistConfig, instancesSlice),
|
||||||
|
settings: persistReducer(settingsPersistConfig, settingsSlice)
|
||||||
|
})
|
||||||
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: {
|
reducer: persistReducer(rootPersistConfig, rootReducer),
|
||||||
instances: persistReducer(instancesPersistConfig, instancesSlice),
|
|
||||||
settings: persistReducer(settingsPersistConfig, settingsSlice)
|
|
||||||
},
|
|
||||||
middleware: getDefaultMiddleware({
|
middleware: getDefaultMiddleware({
|
||||||
serializableCheck: {
|
serializableCheck: {
|
||||||
ignoredActions: ['persist/PERSIST']
|
ignoredActions: ['persist/PERSIST']
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
import client from '@api/client'
|
|
||||||
|
|
||||||
export const accountFetch = async ({
|
|
||||||
queryKey
|
|
||||||
}: {
|
|
||||||
queryKey: QueryKey.Account
|
|
||||||
}): Promise<Mastodon.Account> => {
|
|
||||||
const [_, { id }] = queryKey
|
|
||||||
|
|
||||||
const res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: `accounts/${id}`
|
|
||||||
})
|
|
||||||
return Promise.resolve(res.body)
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import client from '@api/client'
|
|
||||||
|
|
||||||
export const announcementFetch = async ({
|
|
||||||
queryKey
|
|
||||||
}: {
|
|
||||||
queryKey: QueryKey.Announcements
|
|
||||||
}): Promise<Mastodon.Announcement[]> => {
|
|
||||||
const [_, { showAll }] = queryKey
|
|
||||||
|
|
||||||
const res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: `announcements`,
|
|
||||||
...(showAll && {
|
|
||||||
params: {
|
|
||||||
with_dismissed: 'true'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return Promise.resolve(res.body)
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
import client from '@api/client'
|
|
||||||
|
|
||||||
export const applicationFetch = async ({
|
|
||||||
queryKey
|
|
||||||
}: {
|
|
||||||
queryKey: QueryKey.Application
|
|
||||||
}): Promise<Mastodon.AppOauth> => {
|
|
||||||
const [_, { instanceDomain }] = queryKey
|
|
||||||
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('client_name', 'test_dudu')
|
|
||||||
formData.append('redirect_uris', 'exp://127.0.0.1:19000')
|
|
||||||
formData.append('scopes', 'read write follow push')
|
|
||||||
|
|
||||||
const res = await client({
|
|
||||||
method: 'post',
|
|
||||||
instance: 'remote',
|
|
||||||
instanceDomain,
|
|
||||||
url: `apps`,
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
return Promise.resolve(res.body)
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
import client from '@api/client'
|
|
||||||
|
|
||||||
export const emojisFetch = async (): Promise<Mastodon.Emoji[]> => {
|
|
||||||
const res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: 'custom_emojis'
|
|
||||||
})
|
|
||||||
return Promise.resolve(res.body)
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
import client from '@api/client'
|
|
||||||
|
|
||||||
export const instanceFetch = async ({
|
|
||||||
queryKey
|
|
||||||
}: {
|
|
||||||
queryKey: QueryKey.Instance
|
|
||||||
}): Promise<Mastodon.Instance> => {
|
|
||||||
const [_, { instanceDomain }] = queryKey
|
|
||||||
|
|
||||||
const res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'remote',
|
|
||||||
instanceDomain,
|
|
||||||
url: `instance`
|
|
||||||
})
|
|
||||||
return Promise.resolve(res.body)
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
import client from '@api/client'
|
|
||||||
|
|
||||||
export const listsFetch = async (): Promise<Mastodon.List[]> => {
|
|
||||||
const res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: 'lists'
|
|
||||||
})
|
|
||||||
return Promise.resolve(res.body)
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
import client from '@api/client'
|
|
||||||
|
|
||||||
export const relationshipFetch = async ({
|
|
||||||
queryKey
|
|
||||||
}: {
|
|
||||||
queryKey: QueryKey.Relationship
|
|
||||||
}): Promise<Mastodon.Relationship> => {
|
|
||||||
const [_, { id }] = queryKey
|
|
||||||
|
|
||||||
const res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: `accounts/relationships`,
|
|
||||||
params: {
|
|
||||||
'id[]': id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return Promise.resolve(res.body[0])
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
import client from '@api/client'
|
|
||||||
|
|
||||||
export const relationshipsFetch = async ({
|
|
||||||
queryKey,
|
|
||||||
pageParam
|
|
||||||
}: {
|
|
||||||
queryKey: QueryKey.Relationships
|
|
||||||
pageParam?: { direction: 'next'; id: Mastodon.Status['id'] }
|
|
||||||
}): Promise<Mastodon.Account[]> => {
|
|
||||||
const [_, type, { id }] = queryKey
|
|
||||||
let params: { [key: string]: string } = {}
|
|
||||||
|
|
||||||
if (pageParam) {
|
|
||||||
switch (pageParam.direction) {
|
|
||||||
case 'next':
|
|
||||||
params.max_id = pageParam.id
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: `accounts/${id}/${type}`,
|
|
||||||
params
|
|
||||||
})
|
|
||||||
return Promise.resolve(res.body)
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
import client from '@api/client'
|
|
||||||
|
|
||||||
export const searchFetch = async ({
|
|
||||||
queryKey
|
|
||||||
}: {
|
|
||||||
queryKey: QueryKey.Search
|
|
||||||
}): Promise<
|
|
||||||
| Mastodon.Account[]
|
|
||||||
| Mastodon.Tag[]
|
|
||||||
| Mastodon.Status[]
|
|
||||||
| {
|
|
||||||
accounts: Mastodon.Account[]
|
|
||||||
hashtags: Mastodon.Tag[]
|
|
||||||
statuses: Mastodon.Status[]
|
|
||||||
}
|
|
||||||
> => {
|
|
||||||
const [_, { type, term, limit = 20 }] = queryKey
|
|
||||||
const res = await client({
|
|
||||||
version: 'v2',
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: 'search',
|
|
||||||
params: { ...(type && { type }), q: term, limit }
|
|
||||||
})
|
|
||||||
if (type) {
|
|
||||||
return Promise.resolve(res.body[type])
|
|
||||||
} else {
|
|
||||||
return Promise.resolve(res.body)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,212 +0,0 @@
|
|||||||
import { uniqBy } from 'lodash'
|
|
||||||
|
|
||||||
import client from '@api/client'
|
|
||||||
|
|
||||||
export const timelineFetch = async ({
|
|
||||||
queryKey,
|
|
||||||
pageParam
|
|
||||||
}: {
|
|
||||||
queryKey: QueryKey.Timeline
|
|
||||||
pageParam?: { direction: 'prev' | 'next'; id: Mastodon.Status['id'] }
|
|
||||||
}): Promise<{
|
|
||||||
toots: Mastodon.Status[]
|
|
||||||
pointer?: number
|
|
||||||
pinnedLength?: number
|
|
||||||
}> => {
|
|
||||||
const [page, { account, hashtag, list, toot }] = queryKey
|
|
||||||
let res
|
|
||||||
let params: { [key: string]: string } = {}
|
|
||||||
|
|
||||||
if (pageParam) {
|
|
||||||
switch (pageParam.direction) {
|
|
||||||
case 'prev':
|
|
||||||
if (page === 'Bookmarks' || page === 'Favourites') {
|
|
||||||
params.max_id = pageParam.id
|
|
||||||
} else {
|
|
||||||
params.min_id = pageParam.id
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'next':
|
|
||||||
if (page === 'Bookmarks' || page === 'Favourites') {
|
|
||||||
params.min_id = pageParam.id
|
|
||||||
} else {
|
|
||||||
params.max_id = pageParam.id
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (page) {
|
|
||||||
case 'Following':
|
|
||||||
res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: 'timelines/home',
|
|
||||||
params
|
|
||||||
})
|
|
||||||
return Promise.resolve({ toots: res.body })
|
|
||||||
|
|
||||||
case 'Local':
|
|
||||||
params.local = 'true'
|
|
||||||
res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: 'timelines/public',
|
|
||||||
params
|
|
||||||
})
|
|
||||||
return Promise.resolve({ toots: res.body })
|
|
||||||
|
|
||||||
case 'LocalPublic':
|
|
||||||
res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: 'timelines/public',
|
|
||||||
params
|
|
||||||
})
|
|
||||||
return Promise.resolve({ toots: res.body })
|
|
||||||
|
|
||||||
case 'RemotePublic':
|
|
||||||
res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'remote',
|
|
||||||
url: 'timelines/public',
|
|
||||||
params
|
|
||||||
})
|
|
||||||
return Promise.resolve({ toots: res.body })
|
|
||||||
|
|
||||||
case 'Notifications':
|
|
||||||
res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: 'notifications',
|
|
||||||
params
|
|
||||||
})
|
|
||||||
return Promise.resolve({ toots: res.body })
|
|
||||||
|
|
||||||
case 'Account_Default':
|
|
||||||
if (pageParam && pageParam.direction === 'next') {
|
|
||||||
res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: `accounts/${account}/statuses`,
|
|
||||||
params: {
|
|
||||||
exclude_replies: 'true',
|
|
||||||
...params
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return Promise.resolve({ toots: res.body })
|
|
||||||
} else {
|
|
||||||
res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: `accounts/${account}/statuses`,
|
|
||||||
params: {
|
|
||||||
pinned: 'true'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const pinnedLength = res.body.length
|
|
||||||
let toots: Mastodon.Status[] = res.body
|
|
||||||
res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: `accounts/${account}/statuses`,
|
|
||||||
params: {
|
|
||||||
exclude_replies: 'true'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
toots = uniqBy([...toots, ...res.body], 'id')
|
|
||||||
return Promise.resolve({ toots: toots, pinnedLength })
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'Account_All':
|
|
||||||
res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: `accounts/${account}/statuses`,
|
|
||||||
params
|
|
||||||
})
|
|
||||||
return Promise.resolve({ toots: res.body })
|
|
||||||
|
|
||||||
case 'Account_Media':
|
|
||||||
res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: `accounts/${account}/statuses`,
|
|
||||||
params: {
|
|
||||||
only_media: 'true',
|
|
||||||
...params
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return Promise.resolve({ toots: res.body })
|
|
||||||
|
|
||||||
case 'Hashtag':
|
|
||||||
res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: `timelines/tag/${hashtag}`,
|
|
||||||
params
|
|
||||||
})
|
|
||||||
return Promise.resolve({ toots: res.body })
|
|
||||||
|
|
||||||
case 'Conversations':
|
|
||||||
res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: `conversations`,
|
|
||||||
params
|
|
||||||
})
|
|
||||||
if (pageParam) {
|
|
||||||
// Bug in pull to refresh in conversations
|
|
||||||
res.body = res.body.filter(
|
|
||||||
(b: Mastodon.Conversation) => b.id !== pageParam.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return Promise.resolve({ toots: res.body })
|
|
||||||
|
|
||||||
case 'Bookmarks':
|
|
||||||
res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: `bookmarks`,
|
|
||||||
params
|
|
||||||
})
|
|
||||||
return Promise.resolve({ toots: res.body })
|
|
||||||
|
|
||||||
case 'Favourites':
|
|
||||||
res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: `favourites`,
|
|
||||||
params
|
|
||||||
})
|
|
||||||
return Promise.resolve({ toots: res.body })
|
|
||||||
|
|
||||||
case 'List':
|
|
||||||
res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: `timelines/list/${list}`,
|
|
||||||
params
|
|
||||||
})
|
|
||||||
return Promise.resolve({ toots: res.body })
|
|
||||||
|
|
||||||
case 'Toot':
|
|
||||||
res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: `statuses/${toot}`
|
|
||||||
})
|
|
||||||
const theToot = res.body
|
|
||||||
res = await client({
|
|
||||||
method: 'get',
|
|
||||||
instance: 'local',
|
|
||||||
url: `statuses/${toot}/context`
|
|
||||||
})
|
|
||||||
return Promise.resolve({
|
|
||||||
toots: [...res.body.ancestors, theToot, ...res.body.descendants],
|
|
||||||
pointer: res.body.ancestors.length
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
return Promise.reject()
|
|
||||||
}
|
|
||||||
}
|
|
27
src/utils/queryHooks/account.ts
Normal file
27
src/utils/queryHooks/account.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import client from '@api/client'
|
||||||
|
import { AxiosError } from 'axios'
|
||||||
|
import { useQuery, UseQueryOptions } from 'react-query'
|
||||||
|
|
||||||
|
export type QueryKey = ['Account', { id: Mastodon.Account['id'] }]
|
||||||
|
|
||||||
|
const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
|
||||||
|
const { id } = queryKey[1]
|
||||||
|
|
||||||
|
return client<Mastodon.Account>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: `accounts/${id}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hookAccount = <TData = Mastodon.Account>({
|
||||||
|
options,
|
||||||
|
...queryKeyParams
|
||||||
|
}: QueryKey[1] & {
|
||||||
|
options?: UseQueryOptions<Mastodon.Account, AxiosError, TData>
|
||||||
|
}) => {
|
||||||
|
const queryKey: QueryKey = ['Account', { ...queryKeyParams }]
|
||||||
|
return useQuery(queryKey, queryFunction, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hookAccount
|
35
src/utils/queryHooks/accountCheck.ts
Normal file
35
src/utils/queryHooks/accountCheck.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hookAccountCheck = <TData = Mastodon.Account>({
|
||||||
|
options,
|
||||||
|
...queryKeyParams
|
||||||
|
}: QueryKey[1] & {
|
||||||
|
options?: UseQueryOptions<Mastodon.Account, AxiosError, TData>
|
||||||
|
}) => {
|
||||||
|
const queryKey: QueryKey = ['AccountCheck', { ...queryKeyParams }]
|
||||||
|
return useQuery(queryKey, queryFunction, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hookAccountCheck
|
32
src/utils/queryHooks/announcement.ts
Normal file
32
src/utils/queryHooks/announcement.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import client from '@api/client'
|
||||||
|
import { AxiosError } from 'axios'
|
||||||
|
import { useQuery, UseQueryOptions } from 'react-query'
|
||||||
|
|
||||||
|
type QueryKey = ['Announcements', { showAll?: boolean }]
|
||||||
|
|
||||||
|
const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
|
||||||
|
const { showAll } = queryKey[1]
|
||||||
|
|
||||||
|
return client<Mastodon.Announcement[]>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: `announcements`,
|
||||||
|
...(showAll && {
|
||||||
|
params: {
|
||||||
|
with_dismissed: 'true'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hookAnnouncement = <TData = Mastodon.Announcement[]>({
|
||||||
|
options,
|
||||||
|
...queryKeyParams
|
||||||
|
}: QueryKey[1] & {
|
||||||
|
options?: UseQueryOptions<Mastodon.Announcement[], AxiosError, TData>
|
||||||
|
}) => {
|
||||||
|
const queryKey: QueryKey = ['Announcements', { ...queryKeyParams }]
|
||||||
|
return useQuery(queryKey, queryFunction, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hookAnnouncement
|
34
src/utils/queryHooks/apps.ts
Normal file
34
src/utils/queryHooks/apps.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import client from '@api/client'
|
||||||
|
import { AxiosError } from 'axios'
|
||||||
|
import { useQuery, UseQueryOptions } from 'react-query'
|
||||||
|
|
||||||
|
export type QueryKey = ['Apps', { instanceDomain?: string }]
|
||||||
|
|
||||||
|
const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
|
||||||
|
const { instanceDomain } = queryKey[1]
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('client_name', 'test_dudu')
|
||||||
|
formData.append('redirect_uris', 'exp://127.0.0.1:19000')
|
||||||
|
formData.append('scopes', 'read write follow push')
|
||||||
|
|
||||||
|
return client<Mastodon.Apps>({
|
||||||
|
method: 'post',
|
||||||
|
instance: 'remote',
|
||||||
|
instanceDomain,
|
||||||
|
url: `apps`,
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hookApps = <TData = Mastodon.Apps>({
|
||||||
|
options,
|
||||||
|
...queryKeyParams
|
||||||
|
}: QueryKey[1] & {
|
||||||
|
options?: UseQueryOptions<Mastodon.Apps, AxiosError, TData>
|
||||||
|
}) => {
|
||||||
|
const queryKey: QueryKey = ['Apps', { ...queryKeyParams }]
|
||||||
|
return useQuery(queryKey, queryFunction, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hookApps
|
24
src/utils/queryHooks/emojis.ts
Normal file
24
src/utils/queryHooks/emojis.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import client from '@api/client'
|
||||||
|
import { AxiosError } from 'axios'
|
||||||
|
import { useQuery, UseQueryOptions } from 'react-query'
|
||||||
|
|
||||||
|
type QueryKey = ['Emojis']
|
||||||
|
|
||||||
|
const queryFunction = () => {
|
||||||
|
return client<Mastodon.Emoji[]>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: 'custom_emojis'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hookEmojis = <TData = Mastodon.Emoji[]>({
|
||||||
|
options
|
||||||
|
}: {
|
||||||
|
options?: UseQueryOptions<Mastodon.Emoji[], AxiosError, TData>
|
||||||
|
}) => {
|
||||||
|
const queryKey: QueryKey = ['Emojis']
|
||||||
|
return useQuery(queryKey, queryFunction, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hookEmojis
|
28
src/utils/queryHooks/instance.ts
Normal file
28
src/utils/queryHooks/instance.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import client from '@api/client'
|
||||||
|
import { AxiosError } from 'axios'
|
||||||
|
import { useQuery, UseQueryOptions } from 'react-query'
|
||||||
|
|
||||||
|
export type QueryKey = ['Instance', { instanceDomain?: string }]
|
||||||
|
|
||||||
|
const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
|
||||||
|
const { instanceDomain } = queryKey[1]
|
||||||
|
|
||||||
|
return client<Mastodon.Instance>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'remote',
|
||||||
|
instanceDomain,
|
||||||
|
url: `instance`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hookInstance = <TData = Mastodon.Instance>({
|
||||||
|
options,
|
||||||
|
...queryKeyParams
|
||||||
|
}: QueryKey[1] & {
|
||||||
|
options?: UseQueryOptions<Mastodon.Instance, AxiosError, TData>
|
||||||
|
}) => {
|
||||||
|
const queryKey: QueryKey = ['Instance', { ...queryKeyParams }]
|
||||||
|
return useQuery(queryKey, queryFunction, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hookInstance
|
24
src/utils/queryHooks/lists.ts
Normal file
24
src/utils/queryHooks/lists.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import client from '@api/client'
|
||||||
|
import { AxiosError } from 'axios'
|
||||||
|
import { useQuery, UseQueryOptions } from 'react-query'
|
||||||
|
|
||||||
|
export type QueryKey = ['Lists']
|
||||||
|
|
||||||
|
const queryFunction = () => {
|
||||||
|
return client<Mastodon.List[]>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: 'lists'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hookLists = <TData = Mastodon.List[]>({
|
||||||
|
options
|
||||||
|
}: {
|
||||||
|
options?: UseQueryOptions<Mastodon.List[], AxiosError, TData>
|
||||||
|
}) => {
|
||||||
|
const queryKey: QueryKey = ['Lists']
|
||||||
|
return useQuery(queryKey, queryFunction, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hookLists
|
30
src/utils/queryHooks/relationship.ts
Normal file
30
src/utils/queryHooks/relationship.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import client from '@api/client'
|
||||||
|
import { AxiosError } from 'axios'
|
||||||
|
import { useQuery, UseQueryOptions } from 'react-query'
|
||||||
|
|
||||||
|
export type QueryKey = ['Relationship', { id: Mastodon.Account['id'] }]
|
||||||
|
|
||||||
|
const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
|
||||||
|
const { id } = queryKey[1]
|
||||||
|
|
||||||
|
return client<Mastodon.Relationship>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: `accounts/relationships`,
|
||||||
|
params: {
|
||||||
|
'id[]': id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hookRelationship = <TData = Mastodon.Relationship>({
|
||||||
|
options,
|
||||||
|
...queryKeyParams
|
||||||
|
}: QueryKey[1] & {
|
||||||
|
options?: UseQueryOptions<Mastodon.Relationship, AxiosError, TData>
|
||||||
|
}) => {
|
||||||
|
const queryKey: QueryKey = ['Relationship', { ...queryKeyParams }]
|
||||||
|
return useQuery(queryKey, queryFunction, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hookRelationship
|
46
src/utils/queryHooks/relationships.ts
Normal file
46
src/utils/queryHooks/relationships.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import client from '@api/client'
|
||||||
|
import { AxiosError } from 'axios'
|
||||||
|
import { useInfiniteQuery, UseInfiniteQueryOptions } from 'react-query'
|
||||||
|
|
||||||
|
export type QueryKey = [
|
||||||
|
'Relationships',
|
||||||
|
{ type: 'following' | 'followers'; id: Mastodon.Account['id'] }
|
||||||
|
]
|
||||||
|
|
||||||
|
const queryFunction = ({
|
||||||
|
queryKey,
|
||||||
|
pageParam
|
||||||
|
}: {
|
||||||
|
queryKey: QueryKey
|
||||||
|
pageParam?: { direction: 'next'; id: Mastodon.Status['id'] }
|
||||||
|
}) => {
|
||||||
|
const { type, id } = queryKey[1]
|
||||||
|
let params: { [key: string]: string } = {}
|
||||||
|
|
||||||
|
if (pageParam) {
|
||||||
|
switch (pageParam.direction) {
|
||||||
|
case 'next':
|
||||||
|
params.max_id = pageParam.id
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return client<Mastodon.Account[]>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: `accounts/${id}/${type}`,
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hookRelationships = <TData = Mastodon.Account[]>({
|
||||||
|
options,
|
||||||
|
...queryKeyParams
|
||||||
|
}: QueryKey[1] & {
|
||||||
|
options?: UseInfiniteQueryOptions<Mastodon.Account[], AxiosError, TData>
|
||||||
|
}) => {
|
||||||
|
const queryKey: QueryKey = ['Relationships', { ...queryKeyParams }]
|
||||||
|
return useInfiniteQuery(queryKey, queryFunction, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hookRelationships
|
41
src/utils/queryHooks/search.ts
Normal file
41
src/utils/queryHooks/search.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import client from '@api/client'
|
||||||
|
import { AxiosError } from 'axios'
|
||||||
|
import { useQuery, UseQueryOptions } from 'react-query'
|
||||||
|
|
||||||
|
export type QueryKey = [
|
||||||
|
'Search',
|
||||||
|
{
|
||||||
|
type?: 'accounts' | 'hashtags' | 'statuses'
|
||||||
|
term?: string
|
||||||
|
limit?: number
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
type SearchResult = {
|
||||||
|
accounts: Mastodon.Account[]
|
||||||
|
hashtags: Mastodon.Tag[]
|
||||||
|
statuses: Mastodon.Status[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
|
||||||
|
const { type, term, limit = 20 } = queryKey[1]
|
||||||
|
return client<SearchResult>({
|
||||||
|
version: 'v2',
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: 'search',
|
||||||
|
params: { ...(type && { type }), ...(term && { q: term }), limit }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hookSearch = <TData = SearchResult>({
|
||||||
|
options,
|
||||||
|
...queryKeyParams
|
||||||
|
}: QueryKey[1] & {
|
||||||
|
options?: UseQueryOptions<SearchResult, AxiosError, TData>
|
||||||
|
}) => {
|
||||||
|
const queryKey: QueryKey = ['Search', { ...queryKeyParams }]
|
||||||
|
return useQuery(queryKey, queryFunction, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hookSearch
|
291
src/utils/queryHooks/timeline.ts
Normal file
291
src/utils/queryHooks/timeline.ts
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
import client from '@api/client'
|
||||||
|
import { AxiosError } from 'axios'
|
||||||
|
import { uniqBy } from 'lodash'
|
||||||
|
import { useInfiniteQuery, UseInfiniteQueryOptions } from 'react-query'
|
||||||
|
|
||||||
|
export type QueryKeyTimeline = [
|
||||||
|
'Timeline',
|
||||||
|
{
|
||||||
|
page: App.Pages
|
||||||
|
hashtag?: Mastodon.Tag['name']
|
||||||
|
list?: Mastodon.List['id']
|
||||||
|
toot?: Mastodon.Status['id']
|
||||||
|
account?: Mastodon.Account['id']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const queryFunction = async ({
|
||||||
|
queryKey,
|
||||||
|
pageParam
|
||||||
|
}: {
|
||||||
|
queryKey: QueryKeyTimeline
|
||||||
|
pageParam?: { direction: 'prev' | 'next'; id: Mastodon.Status['id'] }
|
||||||
|
}) => {
|
||||||
|
const { page, account, hashtag, list, toot } = queryKey[1]
|
||||||
|
let res
|
||||||
|
let params: { [key: string]: string } = {}
|
||||||
|
|
||||||
|
if (pageParam) {
|
||||||
|
switch (pageParam.direction) {
|
||||||
|
case 'prev':
|
||||||
|
if (page === 'Bookmarks' || page === 'Favourites') {
|
||||||
|
params.max_id = pageParam.id
|
||||||
|
} else {
|
||||||
|
params.min_id = pageParam.id
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'next':
|
||||||
|
if (page === 'Bookmarks' || page === 'Favourites') {
|
||||||
|
params.min_id = pageParam.id
|
||||||
|
} else {
|
||||||
|
params.max_id = pageParam.id
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (page) {
|
||||||
|
case 'Following':
|
||||||
|
res = await client<Mastodon.Status[]>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: 'timelines/home',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
return Promise.resolve({
|
||||||
|
toots: res,
|
||||||
|
pointer: undefined,
|
||||||
|
pinnedLength: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'Local':
|
||||||
|
params.local = 'true'
|
||||||
|
res = await client<Mastodon.Status[]>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: 'timelines/public',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
return Promise.resolve({
|
||||||
|
toots: res,
|
||||||
|
pointer: undefined,
|
||||||
|
pinnedLength: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'LocalPublic':
|
||||||
|
res = await client<Mastodon.Status[]>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: 'timelines/public',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
return Promise.resolve({
|
||||||
|
toots: res,
|
||||||
|
pointer: undefined,
|
||||||
|
pinnedLength: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'RemotePublic':
|
||||||
|
res = await client<Mastodon.Status[]>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'remote',
|
||||||
|
url: 'timelines/public',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
return Promise.resolve({
|
||||||
|
toots: res,
|
||||||
|
pointer: undefined,
|
||||||
|
pinnedLength: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'Notifications':
|
||||||
|
res = await client<Mastodon.Notification[]>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: 'notifications',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
return Promise.resolve({
|
||||||
|
toots: res,
|
||||||
|
pointer: undefined,
|
||||||
|
pinnedLength: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'Account_Default':
|
||||||
|
if (pageParam && pageParam.direction === 'next') {
|
||||||
|
res = await client<Mastodon.Status[]>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: `accounts/${account}/statuses`,
|
||||||
|
params: {
|
||||||
|
exclude_replies: 'true',
|
||||||
|
...params
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return Promise.resolve({
|
||||||
|
toots: res,
|
||||||
|
pointer: undefined,
|
||||||
|
pinnedLength: undefined
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res = await client<Mastodon.Status[]>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: `accounts/${account}/statuses`,
|
||||||
|
params: {
|
||||||
|
pinned: 'true'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const pinnedLength = res.length
|
||||||
|
let toots = res
|
||||||
|
res = await client<Mastodon.Status[]>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: `accounts/${account}/statuses`,
|
||||||
|
params: {
|
||||||
|
exclude_replies: 'true'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
toots = uniqBy([...toots, ...res], 'id')
|
||||||
|
return Promise.resolve({
|
||||||
|
toots: toots,
|
||||||
|
pointer: undefined,
|
||||||
|
pinnedLength
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'Account_All':
|
||||||
|
res = await client<Mastodon.Status[]>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: `accounts/${account}/statuses`,
|
||||||
|
params
|
||||||
|
})
|
||||||
|
return Promise.resolve({
|
||||||
|
toots: res,
|
||||||
|
pointer: undefined,
|
||||||
|
pinnedLength: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'Account_Media':
|
||||||
|
res = await client<Mastodon.Status[]>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: `accounts/${account}/statuses`,
|
||||||
|
params: {
|
||||||
|
only_media: 'true',
|
||||||
|
...params
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return Promise.resolve({
|
||||||
|
toots: res,
|
||||||
|
pointer: undefined,
|
||||||
|
pinnedLength: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'Hashtag':
|
||||||
|
res = await client<Mastodon.Status[]>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: `timelines/tag/${hashtag}`,
|
||||||
|
params
|
||||||
|
})
|
||||||
|
return Promise.resolve({
|
||||||
|
toots: res,
|
||||||
|
pointer: undefined,
|
||||||
|
pinnedLength: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'Conversations':
|
||||||
|
res = await client<Mastodon.Conversation[]>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: `conversations`,
|
||||||
|
params
|
||||||
|
})
|
||||||
|
if (pageParam) {
|
||||||
|
// Bug in pull to refresh in conversations
|
||||||
|
res = res.filter(b => b.id !== pageParam.id)
|
||||||
|
}
|
||||||
|
return Promise.resolve({
|
||||||
|
toots: res,
|
||||||
|
pointer: undefined,
|
||||||
|
pinnedLength: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'Bookmarks':
|
||||||
|
res = await client<Mastodon.Status[]>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: `bookmarks`,
|
||||||
|
params
|
||||||
|
})
|
||||||
|
return Promise.resolve({
|
||||||
|
toots: res,
|
||||||
|
pointer: undefined,
|
||||||
|
pinnedLength: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'Favourites':
|
||||||
|
res = await client<Mastodon.Status[]>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: `favourites`,
|
||||||
|
params
|
||||||
|
})
|
||||||
|
return Promise.resolve({
|
||||||
|
toots: res,
|
||||||
|
pointer: undefined,
|
||||||
|
pinnedLength: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'List':
|
||||||
|
res = await client<Mastodon.Status[]>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: `timelines/list/${list}`,
|
||||||
|
params
|
||||||
|
})
|
||||||
|
return Promise.resolve({
|
||||||
|
toots: res,
|
||||||
|
pointer: undefined,
|
||||||
|
pinnedLength: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'Toot':
|
||||||
|
res = await client<Mastodon.Status>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: `statuses/${toot}`
|
||||||
|
})
|
||||||
|
const theToot = res
|
||||||
|
res = await client<{
|
||||||
|
ancestors: Mastodon.Status[]
|
||||||
|
descendants: Mastodon.Status[]
|
||||||
|
}>({
|
||||||
|
method: 'get',
|
||||||
|
instance: 'local',
|
||||||
|
url: `statuses/${toot}/context`
|
||||||
|
})
|
||||||
|
return Promise.resolve({
|
||||||
|
toots: [...res.ancestors, theToot, ...res.descendants],
|
||||||
|
pointer: res.ancestors.length,
|
||||||
|
pinnedLength: undefined
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Unpromise<T extends Promise<any>> = T extends Promise<infer U> ? U : never
|
||||||
|
const hookTimeline = <TData = Unpromise<ReturnType<typeof queryFunction>>>({
|
||||||
|
options,
|
||||||
|
...queryKeyParams
|
||||||
|
}: QueryKeyTimeline[1] & {
|
||||||
|
options?: UseInfiniteQueryOptions<any, AxiosError, TData>
|
||||||
|
}) => {
|
||||||
|
const queryKey: QueryKeyTimeline = ['Timeline', { ...queryKeyParams }]
|
||||||
|
return useInfiniteQuery(queryKey, queryFunction, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hookTimeline
|
@ -1,70 +1,65 @@
|
|||||||
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
|
|
||||||
|
|
||||||
import { RootState } from '@root/store'
|
|
||||||
import client from '@api/client'
|
import client from '@api/client'
|
||||||
|
import analytics from '@components/analytics'
|
||||||
|
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
|
import { RootState } from '@root/store'
|
||||||
|
import * as AuthSession from 'expo-auth-session'
|
||||||
|
|
||||||
|
export type InstanceLocal = {
|
||||||
|
appData: {
|
||||||
|
clientId: string
|
||||||
|
clientSecret: string
|
||||||
|
}
|
||||||
|
url: string
|
||||||
|
token: string
|
||||||
|
account: {
|
||||||
|
id: Mastodon.Account['id']
|
||||||
|
preferences: Mastodon.Preferences
|
||||||
|
}
|
||||||
|
notification: {
|
||||||
|
unread: boolean
|
||||||
|
latestTime?: Mastodon.Notification['created_at']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type InstancesState = {
|
export type InstancesState = {
|
||||||
local: {
|
local: {
|
||||||
url: string | undefined
|
activeIndex: number | null
|
||||||
token: string | undefined
|
instances: InstanceLocal[]
|
||||||
account: {
|
|
||||||
id: Mastodon.Account['id'] | undefined
|
|
||||||
preferences: Mastodon.Preferences
|
|
||||||
}
|
|
||||||
notification: {
|
|
||||||
unread: boolean
|
|
||||||
latestTime?: Mastodon.Notification['created_at']
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
remote: {
|
remote: {
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialStateLocal: InstancesState['local'] = {
|
export const localUpdateAccountPreferences = createAsyncThunk(
|
||||||
url: undefined,
|
'instances/localUpdateAccountPreferences',
|
||||||
token: undefined,
|
async (): Promise<Mastodon.Preferences> => {
|
||||||
account: {
|
const preferences = await client<Mastodon.Preferences>({
|
||||||
id: undefined,
|
|
||||||
preferences: {
|
|
||||||
'posting:default:visibility': undefined,
|
|
||||||
'posting:default:sensitive': undefined,
|
|
||||||
'posting:default:language': undefined,
|
|
||||||
'reading:expand:media': undefined,
|
|
||||||
'reading:expand:spoilers': undefined
|
|
||||||
}
|
|
||||||
},
|
|
||||||
notification: {
|
|
||||||
unread: false,
|
|
||||||
latestTime: undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const updateLocalAccountPreferences = createAsyncThunk(
|
|
||||||
'instances/updateLocalAccountPreferences',
|
|
||||||
async () => {
|
|
||||||
const { body: preferences } = await client({
|
|
||||||
method: 'get',
|
method: 'get',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: `preferences`
|
url: `preferences`
|
||||||
})
|
})
|
||||||
|
|
||||||
return preferences as Mastodon.Preferences
|
return Promise.resolve(preferences)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const loginLocal = createAsyncThunk(
|
export const localAddInstance = createAsyncThunk(
|
||||||
'instances/loginLocal',
|
'instances/localAddInstance',
|
||||||
async ({
|
async ({
|
||||||
url,
|
url,
|
||||||
token
|
token,
|
||||||
|
appData
|
||||||
}: {
|
}: {
|
||||||
url: InstancesState['local']['url']
|
url: InstanceLocal['url']
|
||||||
token: InstancesState['local']['token']
|
token: InstanceLocal['token']
|
||||||
}) => {
|
appData: InstanceLocal['appData']
|
||||||
const {
|
}): Promise<InstanceLocal> => {
|
||||||
body: { id }
|
const store = require('@root/store')
|
||||||
} = await client({
|
const state = store.getState().instances
|
||||||
|
|
||||||
|
const { id } = await client<Mastodon.Account>({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
instance: 'remote',
|
instance: 'remote',
|
||||||
instanceDomain: url,
|
instanceDomain: url,
|
||||||
@ -72,7 +67,16 @@ export const loginLocal = createAsyncThunk(
|
|||||||
headers: { Authorization: `Bearer ${token}` }
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
})
|
})
|
||||||
|
|
||||||
const { body: preferences } = await client({
|
// Overwrite existing account?
|
||||||
|
// if (
|
||||||
|
// state.local.instances.filter(
|
||||||
|
// instance => instance && instance.account && instance.account.id === id
|
||||||
|
// ).length
|
||||||
|
// ) {
|
||||||
|
// return Promise.reject()
|
||||||
|
// }
|
||||||
|
|
||||||
|
const preferences = await client<Mastodon.Preferences>({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
instance: 'remote',
|
instance: 'remote',
|
||||||
instanceDomain: url,
|
instanceDomain: url,
|
||||||
@ -80,60 +84,153 @@ export const loginLocal = createAsyncThunk(
|
|||||||
headers: { Authorization: `Bearer ${token}` }
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return Promise.resolve({
|
||||||
|
appData,
|
||||||
url,
|
url,
|
||||||
token,
|
token,
|
||||||
account: {
|
account: {
|
||||||
id,
|
id,
|
||||||
preferences
|
preferences
|
||||||
|
},
|
||||||
|
notification: {
|
||||||
|
unread: false
|
||||||
}
|
}
|
||||||
} as InstancesState['local']
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
export const localRemoveInstance = createAsyncThunk(
|
||||||
|
'instances/localRemoveInstance',
|
||||||
|
async (index?: InstancesState['local']['activeIndex']): Promise<number> => {
|
||||||
|
const store = require('@root/store')
|
||||||
|
const local = store.getState().instances.local
|
||||||
|
|
||||||
|
if (index) {
|
||||||
|
return Promise.resolve(index)
|
||||||
|
} else {
|
||||||
|
if (local.activeIndex !== null) {
|
||||||
|
const currentInstance = local.instances[local.activeIndex]
|
||||||
|
|
||||||
|
const 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`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (!revoked) {
|
||||||
|
console.warn('Revoking error')
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(local.activeIndex)
|
||||||
|
} else {
|
||||||
|
throw new Error('Active index invalid, cannot remove instance')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const instancesInitialState: InstancesState = {
|
||||||
|
local: {
|
||||||
|
activeIndex: null,
|
||||||
|
instances: []
|
||||||
|
},
|
||||||
|
remote: {
|
||||||
|
url: 'm.cmx.im'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const instancesSlice = createSlice({
|
const instancesSlice = createSlice({
|
||||||
name: 'instances',
|
name: 'instances',
|
||||||
initialState: {
|
initialState: instancesInitialState,
|
||||||
local: initialStateLocal,
|
|
||||||
remote: {
|
|
||||||
url: 'm.cmx.im'
|
|
||||||
}
|
|
||||||
} as InstancesState,
|
|
||||||
reducers: {
|
reducers: {
|
||||||
resetLocal: state => {
|
localUpdateActiveIndex: (
|
||||||
state.local = initialStateLocal
|
|
||||||
},
|
|
||||||
updateNotification: (
|
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<Partial<InstancesState['local']['notification']>>
|
action: PayloadAction<NonNullable<InstancesState['local']['activeIndex']>>
|
||||||
) => {
|
) => {
|
||||||
state.local.notification = {
|
if (action.payload < state.local.instances.length) {
|
||||||
...state.local.notification,
|
state.local.activeIndex = action.payload
|
||||||
|
} else {
|
||||||
|
throw new Error('Set index cannot be found')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
localUpdateNotification: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<Partial<InstanceLocal['notification']>>
|
||||||
|
) => {
|
||||||
|
state.local.instances[state.local.activeIndex!].notification = {
|
||||||
|
...state.local.instances[state.local.activeIndex!].notification,
|
||||||
...action.payload
|
...action.payload
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
remoteUpdate: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<InstancesState['remote']['url']>
|
||||||
|
) => {
|
||||||
|
state.remote.url = action.payload
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
extraReducers: builder => {
|
extraReducers: builder => {
|
||||||
builder
|
builder
|
||||||
.addCase(loginLocal.fulfilled, (state, action) => {
|
.addCase(localAddInstance.fulfilled, (state, action) => {
|
||||||
state.local = action.payload
|
state.local.instances.push(action.payload)
|
||||||
|
state.local.activeIndex = state.local.instances.length - 1
|
||||||
|
|
||||||
|
analytics('login')
|
||||||
})
|
})
|
||||||
.addCase(updateLocalAccountPreferences.fulfilled, (state, action) => {
|
.addCase(localAddInstance.rejected, (state, action) => {
|
||||||
state.local.account.preferences = action.payload
|
console.error(state.local)
|
||||||
|
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
|
||||||
|
|
||||||
|
analytics('logout')
|
||||||
|
})
|
||||||
|
.addCase(localRemoveInstance.rejected, (state, action) => {
|
||||||
|
console.error(state.local)
|
||||||
|
console.error(action.error)
|
||||||
|
})
|
||||||
|
|
||||||
|
.addCase(localUpdateAccountPreferences.fulfilled, (state, action) => {
|
||||||
|
state.local.instances[state.local.activeIndex!].account.preferences =
|
||||||
|
action.payload
|
||||||
|
})
|
||||||
|
.addCase(localUpdateAccountPreferences.rejected, (_, action) => {
|
||||||
|
console.error(action.error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getLocalUrl = (state: RootState) => state.instances.local.url
|
export const getLocalActiveIndex = ({ instances: { local } }: RootState) =>
|
||||||
export const getLocalToken = (state: RootState) => state.instances.local.token
|
local.activeIndex
|
||||||
export const getLocalNotification = (state: RootState) =>
|
export const getLocalInstances = ({ instances: { local } }: RootState) =>
|
||||||
state.instances.local.notification
|
local.instances
|
||||||
export const getRemoteUrl = (state: RootState) => state.instances.remote.url
|
export const getLocalUrl = ({ instances: { local } }: RootState) =>
|
||||||
export const getLocalAccountId = (state: RootState) =>
|
local.activeIndex ? local.instances[local.activeIndex].url : undefined
|
||||||
state.instances.local.account.id
|
// export const getLocalToken = ({ instances: { local } }: RootState) =>
|
||||||
export const getLocalAccountPreferences = (state: RootState) =>
|
// local && local.activeIndex && local.instances[local.activeIndex].token
|
||||||
state.instances.local.account.preferences
|
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 getRemoteUrl = ({ instances: { remote } }: RootState) => remote.url
|
||||||
|
|
||||||
export const { resetLocal, updateNotification } = instancesSlice.actions
|
export const {
|
||||||
|
localUpdateActiveIndex,
|
||||||
|
localUpdateNotification,
|
||||||
|
remoteUpdate
|
||||||
|
} = instancesSlice.actions
|
||||||
|
|
||||||
export default instancesSlice.reducer
|
export default instancesSlice.reducer
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user