mirror of
https://github.com/tooot-app/app
synced 2025-02-27 09:07:51 +01:00
Lots of updates
This commit is contained in:
parent
dcb36a682d
commit
4b99813bb7
155
App.tsx
155
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 { persistor, store } from '@root/store'
|
||||
import { resetLocal } from '@root/utils/slices/instancesSlice'
|
||||
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 React, { useCallback, useEffect, useState } from 'react'
|
||||
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 { 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 })
|
||||
const startingLog = (type: 'log' | 'warn' | 'error', message: string) => {
|
||||
switch (type) {
|
||||
case 'log':
|
||||
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
|
||||
}
|
||||
}
|
||||
dev()
|
||||
sentry()
|
||||
audio()
|
||||
onlineStatus()
|
||||
|
||||
if (__DEV__) {
|
||||
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')
|
||||
log('log', 'react-query', 'initializing')
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
startingLog('log', 'setting audio playback default options')
|
||||
Audio.setAudioModeAsync({
|
||||
playsInSilentModeIOS: true,
|
||||
interruptionModeIOS: 1
|
||||
})
|
||||
|
||||
startingLog('log', 'initializing native screen')
|
||||
log('log', 'react-native-screens', 'initializing')
|
||||
enableScreens()
|
||||
|
||||
const App: React.FC = () => {
|
||||
startingLog('log', 'rendering App')
|
||||
const [appLoaded, setAppLoaded] = useState(false)
|
||||
log('log', 'App', 'rendering App')
|
||||
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(() => {
|
||||
const delaySplash = async () => {
|
||||
startingLog('log', 'delay splash')
|
||||
log('log', 'App', 'delay splash')
|
||||
try {
|
||||
await SplashScreen.preventAutoHideAsync()
|
||||
} catch (e) {
|
||||
@ -91,72 +41,29 @@ const App: React.FC = () => {
|
||||
}
|
||||
delaySplash()
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
const hideSplash = async () => {
|
||||
startingLog('log', 'hide splash')
|
||||
|
||||
const onBeforeLift = useCallback(async () => {
|
||||
const netInfoRes = await netInfo()
|
||||
|
||||
if (netInfoRes.corrupted && netInfoRes.corrupted.length) {
|
||||
setLocalCorrupt(netInfoRes.corrupted)
|
||||
}
|
||||
|
||||
log('log', 'App', 'hide splash')
|
||||
try {
|
||||
await SplashScreen.hideAsync()
|
||||
return Promise.resolve()
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
return Promise.reject()
|
||||
}
|
||||
}
|
||||
if (appLoaded) {
|
||||
hideSplash()
|
||||
}
|
||||
}, [appLoaded])
|
||||
|
||||
const onBeforeLift = useCallback(() => {
|
||||
NetInfo.fetch().then(netInfo => {
|
||||
startingLog('log', 'on before lift')
|
||||
const localUrl = store.getState().instances.local.url
|
||||
const localToken = store.getState().instances.local.token
|
||||
if (netInfo.isConnected) {
|
||||
startingLog('log', 'network connected')
|
||||
if (localUrl && localToken) {
|
||||
startingLog('log', 'checking locally stored credentials')
|
||||
client({
|
||||
method: 'get',
|
||||
instance: 'remote',
|
||||
instanceDomain: localUrl,
|
||||
url: `accounts/verify_credentials`,
|
||||
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(
|
||||
bootstrapped => {
|
||||
startingLog('log', 'bootstrapped')
|
||||
if (bootstrapped && appLoaded) {
|
||||
startingLog('log', 'loading actual app :)')
|
||||
log('log', 'App', 'bootstrapped')
|
||||
if (bootstrapped) {
|
||||
log('log', 'App', 'loading actual app :)')
|
||||
require('@root/i18n/i18n')
|
||||
return (
|
||||
<ThemeManager>
|
||||
@ -167,7 +74,7 @@ const App: React.FC = () => {
|
||||
return null
|
||||
}
|
||||
},
|
||||
[appLoaded]
|
||||
[localCorrupt]
|
||||
)
|
||||
|
||||
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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^1.13.2",
|
||||
"@react-native-community/masked-view": "0.1.10",
|
||||
"@react-native-community/netinfo": "^5.9.7",
|
||||
"@react-native-community/segmented-control": "2.2.1",
|
||||
|
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'
|
||||
| '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
|
||||
}
|
||||
|
||||
type AppOauth = {
|
||||
type Apps = {
|
||||
id: string
|
||||
name: string
|
||||
website?: string
|
||||
|
@ -11,39 +11,38 @@ import ScreenLocal from '@screens/Local'
|
||||
import ScreenMe from '@screens/Me'
|
||||
import ScreenNotifications from '@screens/Notifications'
|
||||
import ScreenPublic from '@screens/Public'
|
||||
import { timelineFetch } from '@utils/fetches/timelineFetch'
|
||||
import hookTimeline from '@utils/queryHooks/timeline'
|
||||
import {
|
||||
getLocalActiveIndex,
|
||||
getLocalNotification,
|
||||
getLocalUrl,
|
||||
updateLocalAccountPreferences,
|
||||
updateNotification
|
||||
localUpdateAccountPreferences,
|
||||
localUpdateNotification
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { themes } from '@utils/styles/themes'
|
||||
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 Toast from 'react-native-toast-message'
|
||||
import { useInfiniteQuery } from 'react-query'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
const Tab = createBottomTabNavigator<RootStackParamList>()
|
||||
|
||||
export type RootStackParamList = {
|
||||
'Screen-Local': undefined
|
||||
'Screen-Public': { publicTab: boolean }
|
||||
'Screen-Post': undefined
|
||||
'Screen-Notifications': undefined
|
||||
'Screen-Me': undefined
|
||||
}
|
||||
const Tab = createBottomTabNavigator()
|
||||
|
||||
export interface Props {
|
||||
localCorrupt?: string
|
||||
}
|
||||
|
||||
export const navigationRef = createRef<NavigationContainerRef>()
|
||||
|
||||
const Index: React.FC<Props> = ({ localCorrupt }) => {
|
||||
const dispatch = useDispatch()
|
||||
const localInstance = useSelector(getLocalUrl)
|
||||
const localActiveIndex = useSelector(getLocalActiveIndex)
|
||||
const { mode, theme } = useTheme()
|
||||
enum barStyle {
|
||||
light = 'dark-content',
|
||||
@ -51,7 +50,7 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
||||
}
|
||||
|
||||
const routeNameRef = useRef<string | undefined>()
|
||||
const navigationRef = useRef<NavigationContainerRef>(null)
|
||||
// const navigationRef = useRef<NavigationContainerRef>(null)
|
||||
|
||||
// const isConnected = useNetInfo().isConnected
|
||||
// const [firstRender, setFirstRender] = useState(false)
|
||||
@ -82,14 +81,14 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
||||
// On launch check if there is any unread announcements
|
||||
useEffect(() => {
|
||||
console.log('Checking announcements')
|
||||
localInstance &&
|
||||
client({
|
||||
localActiveIndex !== null &&
|
||||
client<Mastodon.Announcement[]>({
|
||||
method: 'get',
|
||||
instance: 'local',
|
||||
url: `announcements`
|
||||
})
|
||||
.then(({ body }: { body?: Mastodon.Announcement[] }) => {
|
||||
if (body?.filter(announcement => !announcement.read).length) {
|
||||
.then(res => {
|
||||
if (res?.filter(announcement => !announcement.read).length) {
|
||||
navigationRef.current?.navigate('Screen-Shared-Announcements', {
|
||||
showAll: false
|
||||
})
|
||||
@ -99,15 +98,15 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
||||
}, [])
|
||||
|
||||
// On launch check if there is any unread noficiations
|
||||
const queryNotification = useInfiniteQuery(
|
||||
['Notifications', {}],
|
||||
timelineFetch,
|
||||
{
|
||||
enabled: localInstance ? true : false,
|
||||
const queryNotification = hookTimeline({
|
||||
page: 'Notifications',
|
||||
options: {
|
||||
enabled: localActiveIndex !== null ? true : false,
|
||||
refetchInterval: 1000 * 60,
|
||||
refetchIntervalInBackground: true
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const prevNotification = useSelector(getLocalNotification)
|
||||
useEffect(() => {
|
||||
if (queryNotification.data?.pages) {
|
||||
@ -120,7 +119,7 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
||||
|
||||
if (!prevNotification || !prevNotification.latestTime) {
|
||||
dispatch(
|
||||
updateNotification({
|
||||
localUpdateNotification({
|
||||
unread: true
|
||||
})
|
||||
)
|
||||
@ -129,7 +128,7 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
||||
new Date(prevNotification.latestTime) < new Date(latestNotificationTime)
|
||||
) {
|
||||
dispatch(
|
||||
updateNotification({
|
||||
localUpdateNotification({
|
||||
unread: true,
|
||||
latestTime: latestNotificationTime
|
||||
})
|
||||
@ -140,8 +139,8 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
||||
|
||||
// Lazily update users's preferences, for e.g. composing default visibility
|
||||
useEffect(() => {
|
||||
if (localInstance) {
|
||||
dispatch(updateLocalAccountPreferences())
|
||||
if (localActiveIndex !== null) {
|
||||
dispatch(localUpdateAccountPreferences())
|
||||
}
|
||||
}, [])
|
||||
|
||||
@ -204,51 +203,48 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
||||
const tabNavigatorTabBarOptions = useMemo(
|
||||
() => ({
|
||||
activeTintColor: theme.primary,
|
||||
inactiveTintColor: localInstance ? theme.secondary : theme.disabled,
|
||||
inactiveTintColor:
|
||||
localActiveIndex !== null ? theme.secondary : theme.disabled,
|
||||
showLabel: false
|
||||
}),
|
||||
[theme, localInstance]
|
||||
[theme, localActiveIndex]
|
||||
)
|
||||
const tabScreenLocalListeners = useCallback(
|
||||
() => ({
|
||||
tabPress: (e: any) => {
|
||||
if (!localInstance) {
|
||||
if (!(localActiveIndex !== null)) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
}),
|
||||
[localInstance]
|
||||
[localActiveIndex]
|
||||
)
|
||||
const tabScreenComposeListeners = useMemo(
|
||||
() => ({
|
||||
tabPress: (e: any) => {
|
||||
e.preventDefault()
|
||||
if (localInstance) {
|
||||
if (localActiveIndex !== null) {
|
||||
haptics('Medium')
|
||||
navigationRef.current?.navigate('Screen-Shared-Compose')
|
||||
}
|
||||
}
|
||||
}),
|
||||
[localInstance]
|
||||
[localActiveIndex]
|
||||
)
|
||||
const tabScreenComposeComponent = useCallback(() => null, [])
|
||||
const tabScreenNotificationsListeners = useCallback(
|
||||
() => ({
|
||||
tabPress: (e: any) => {
|
||||
if (!localInstance) {
|
||||
if (!(localActiveIndex !== null)) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
}),
|
||||
[localInstance]
|
||||
[localActiveIndex]
|
||||
)
|
||||
const tabScreenNotificationsOptions = useMemo(
|
||||
() => ({
|
||||
tabBarBadge: prevNotification && prevNotification.unread ? '' : undefined,
|
||||
tabBarBadgeStyle: {
|
||||
transform: [{ scale: 0.5 }],
|
||||
backgroundColor: theme.red
|
||||
}
|
||||
tabBarBadge: prevNotification && prevNotification.unread ? '' : undefined
|
||||
}),
|
||||
[theme, prevNotification]
|
||||
)
|
||||
@ -263,7 +259,9 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
||||
onStateChange={navigationContainerOnStateChange}
|
||||
>
|
||||
<Tab.Navigator
|
||||
initialRouteName={localInstance ? 'Screen-Local' : 'Screen-Public'}
|
||||
initialRouteName={
|
||||
localActiveIndex !== null ? 'Screen-Local' : 'Screen-Public'
|
||||
}
|
||||
screenOptions={tabNavigatorScreenOptions}
|
||||
tabBarOptions={tabNavigatorTabBarOptions}
|
||||
>
|
||||
@ -282,7 +280,13 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
||||
name='Screen-Notifications'
|
||||
component={ScreenNotifications}
|
||||
listeners={tabScreenNotificationsListeners}
|
||||
options={tabScreenNotificationsOptions}
|
||||
options={{
|
||||
tabBarBadgeStyle: {
|
||||
transform: [{ scale: 0.5 }],
|
||||
backgroundColor: theme.red
|
||||
},
|
||||
...tabScreenNotificationsOptions
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen name='Screen-Me' component={ScreenMe} />
|
||||
</Tab.Navigator>
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { RootState } from '@root/store'
|
||||
import axios from 'axios'
|
||||
import chalk from 'chalk'
|
||||
|
||||
const ctx = new chalk.Instance({ level: 3 })
|
||||
|
||||
const client = async ({
|
||||
const client = async <T = unknown>({
|
||||
method,
|
||||
instance,
|
||||
localIndex,
|
||||
instanceDomain,
|
||||
version = 'v1',
|
||||
url,
|
||||
@ -16,6 +18,7 @@ const client = async ({
|
||||
}: {
|
||||
method: 'get' | 'post' | 'put' | 'delete'
|
||||
instance: 'local' | 'remote'
|
||||
localIndex?: number
|
||||
instanceDomain?: string
|
||||
version?: 'v1' | 'v2'
|
||||
url: string
|
||||
@ -25,9 +28,30 @@ const client = async ({
|
||||
headers?: { [key: string]: string }
|
||||
body?: FormData
|
||||
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(
|
||||
ctx.bgGreen.bold(' API ') +
|
||||
' ' +
|
||||
domain +
|
||||
' ' +
|
||||
method +
|
||||
ctx.green(' -> ') +
|
||||
@ -35,10 +59,6 @@ const client = async ({
|
||||
(params ? ctx.green(' -> ') : ''),
|
||||
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({
|
||||
timeout: method === 'post' ? 1000 * 60 : 1000 * 15,
|
||||
@ -50,18 +70,13 @@ const client = async ({
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
...(instance === 'local' && {
|
||||
Authorization: `Bearer ${state.local.token}`
|
||||
Authorization: `Bearer ${state.local!.instances[theLocalIndex!].token}`
|
||||
})
|
||||
},
|
||||
...(body && { data: body }),
|
||||
...(onUploadProgress && { onUploadProgress: onUploadProgress })
|
||||
})
|
||||
.then(response =>
|
||||
Promise.resolve({
|
||||
headers: response.headers,
|
||||
body: response.data
|
||||
})
|
||||
)
|
||||
.then(response => Promise.resolve(response.data))
|
||||
.catch(error => {
|
||||
if (error.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
@ -70,7 +85,8 @@ const client = async ({
|
||||
ctx.bold(' API '),
|
||||
ctx.bold('response'),
|
||||
error.response.status,
|
||||
error.response.data.error
|
||||
error.response.data.error,
|
||||
error.request
|
||||
)
|
||||
return Promise.reject(error.response)
|
||||
} else if (error.request) {
|
||||
|
@ -23,6 +23,7 @@ export interface Props {
|
||||
loading?: boolean
|
||||
destructive?: boolean
|
||||
disabled?: boolean
|
||||
active?: boolean
|
||||
|
||||
strokeWidth?: number
|
||||
size?: 'S' | 'M' | 'L'
|
||||
@ -40,6 +41,7 @@ const Button: React.FC<Props> = ({
|
||||
loading = false,
|
||||
destructive = false,
|
||||
disabled = false,
|
||||
active = false,
|
||||
strokeWidth,
|
||||
size = 'M',
|
||||
spacing = 'S',
|
||||
@ -68,6 +70,9 @@ const Button: React.FC<Props> = ({
|
||||
)
|
||||
|
||||
const colorContent = useMemo(() => {
|
||||
if (active) {
|
||||
return theme.blue
|
||||
} else {
|
||||
if (overlay) {
|
||||
return theme.primaryOverlay
|
||||
} else {
|
||||
@ -81,7 +86,30 @@ const Button: React.FC<Props> = ({
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [theme, disabled])
|
||||
const colorBorder = useMemo(() => {
|
||||
if (active) {
|
||||
return theme.blue
|
||||
} else {
|
||||
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])
|
||||
|
||||
const children = useMemo(() => {
|
||||
switch (type) {
|
||||
@ -118,26 +146,7 @@ const Button: React.FC<Props> = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
}, [theme, content, loading, disabled])
|
||||
|
||||
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])
|
||||
}, [theme, content, loading, disabled, active])
|
||||
|
||||
enum spacingMapping {
|
||||
XS = 'S',
|
||||
@ -164,7 +173,7 @@ const Button: React.FC<Props> = ({
|
||||
testID='base'
|
||||
onPress={onPress}
|
||||
children={children}
|
||||
disabled={disabled || loading}
|
||||
disabled={disabled || active || loading}
|
||||
/>
|
||||
</Animated.View>
|
||||
)
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { StyleConstants } from '@root/utils/styles/constants'
|
||||
import React, { createElement } from 'react'
|
||||
import { StyleProp, View, ViewStyle } from 'react-native'
|
||||
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({
|
||||
base: {
|
||||
height: 50
|
||||
minHeight: 50
|
||||
},
|
||||
core: {
|
||||
flex: 1,
|
||||
|
@ -5,16 +5,12 @@ import { useNavigation } from '@react-navigation/native'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { LinearGradient } from 'expo-linear-gradient'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { Pressable, Text, View } from 'react-native'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { Image, Pressable, Text, View } from 'react-native'
|
||||
import HTMLView from 'react-native-htmlview'
|
||||
import Animated, {
|
||||
measure,
|
||||
useAnimatedRef,
|
||||
useAnimatedStyle,
|
||||
useDerivedValue,
|
||||
useSharedValue,
|
||||
withSpring,
|
||||
withTiming
|
||||
} from 'react-native-reanimated'
|
||||
|
||||
@ -40,7 +36,8 @@ const renderNode = ({
|
||||
showFullLink: boolean
|
||||
disableDetails: boolean
|
||||
}) => {
|
||||
if (node.name == 'a') {
|
||||
switch (node.name) {
|
||||
case 'a':
|
||||
const classes = node.attribs.class
|
||||
const href = node.attribs.href
|
||||
if (classes) {
|
||||
@ -65,7 +62,9 @@ const renderNode = ({
|
||||
</Text>
|
||||
)
|
||||
} else if (classes.includes('mention') && mentions) {
|
||||
const accountIndex = mentions.findIndex(mention => mention.url === href)
|
||||
const accountIndex = mentions.findIndex(
|
||||
mention => mention.url === href
|
||||
)
|
||||
return (
|
||||
<Text
|
||||
key={index}
|
||||
@ -89,7 +88,8 @@ const renderNode = ({
|
||||
} else {
|
||||
const domain = href.split(new RegExp(/:\/\/(.[^\/]+)/))
|
||||
// Need example here
|
||||
const content = node.children && node.children[0] && node.children[0].data
|
||||
const content =
|
||||
node.children && node.children[0] && node.children[0].data
|
||||
const shouldBeTag =
|
||||
tags && tags.filter(tag => `#${tag.name}` === content).length > 0
|
||||
return (
|
||||
@ -118,12 +118,12 @@ const renderNode = ({
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (node.name === 'p') {
|
||||
break
|
||||
case 'p':
|
||||
if (!node.children.length) {
|
||||
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 { t } = useTranslation()
|
||||
|
||||
const relationshipQueryKey: QueryKey.Relationship = ['Relationship', { id }]
|
||||
const relationshipQueryKey = ['Relationship', { id }]
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const fireMutation = useCallback(
|
||||
({ type }: { type: 'authorize' | 'reject' }) => {
|
||||
return client({
|
||||
return client<Mastodon.Relationship>({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: `follow_requests/${id}/${type}`
|
||||
@ -29,9 +29,9 @@ const RelationshipIncoming: React.FC<Props> = ({ id }) => {
|
||||
[]
|
||||
)
|
||||
const mutation = useMutation(fireMutation, {
|
||||
onSuccess: ({ body }) => {
|
||||
onSuccess: res => {
|
||||
haptics('Success')
|
||||
queryClient.setQueryData(relationshipQueryKey, body)
|
||||
queryClient.setQueryData(relationshipQueryKey, res)
|
||||
queryClient.refetchQueries(['Notifications'])
|
||||
},
|
||||
onError: (err: any, { type }) => {
|
||||
|
@ -2,10 +2,10 @@ import client from '@api/client'
|
||||
import Button from '@components/Button'
|
||||
import haptics from '@components/haptics'
|
||||
import { toast } from '@components/toast'
|
||||
import { relationshipFetch } from '@utils/fetches/relationshipFetch'
|
||||
import hookRelationship from '@utils/queryHooks/relationship'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMutation, useQuery, useQueryClient } from 'react-query'
|
||||
import { useMutation, useQueryClient } from 'react-query'
|
||||
|
||||
export interface Props {
|
||||
id: Mastodon.Account['id']
|
||||
@ -14,13 +14,13 @@ export interface Props {
|
||||
const RelationshipOutgoing: React.FC<Props> = ({ id }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const relationshipQueryKey: QueryKey.Relationship = ['Relationship', { id }]
|
||||
const query = useQuery(relationshipQueryKey, relationshipFetch)
|
||||
const relationshipQueryKey = ['Relationship', { id }]
|
||||
const query = hookRelationship({ id })
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const fireMutation = useCallback(
|
||||
({ type, state }: { type: 'follow' | 'block'; state: boolean }) => {
|
||||
return client({
|
||||
return client<Mastodon.Relationship>({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: `accounts/${id}/${state ? 'un' : ''}${type}`
|
||||
@ -29,9 +29,9 @@ const RelationshipOutgoing: React.FC<Props> = ({ id }) => {
|
||||
[]
|
||||
)
|
||||
const mutation = useMutation(fireMutation, {
|
||||
onSuccess: ({ body }) => {
|
||||
onSuccess: res => {
|
||||
haptics('Success')
|
||||
queryClient.setQueryData(relationshipQueryKey, body)
|
||||
queryClient.setQueryData(relationshipQueryKey, res)
|
||||
},
|
||||
onError: (err: any, { type }) => {
|
||||
haptics('Error')
|
||||
|
@ -3,7 +3,7 @@ import Timeline from '@components/Timelines/Timeline'
|
||||
import SegmentedControl from '@react-native-community/segmented-control'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
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 React, { useCallback, useState } from 'react'
|
||||
import { Dimensions, StyleSheet, View } from 'react-native'
|
||||
@ -21,7 +21,7 @@ export interface Props {
|
||||
const Timelines: React.FC<Props> = ({ name, content }) => {
|
||||
const navigation = useNavigation()
|
||||
const { mode } = useTheme()
|
||||
const localRegistered = useSelector(getLocalUrl)
|
||||
const localActiveIndex = useSelector(getLocalActiveIndex)
|
||||
const publicDomain = useSelector(getRemoteUrl)
|
||||
const [segment, setSegment] = useState(0)
|
||||
|
||||
@ -30,7 +30,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
||||
}, [])
|
||||
|
||||
const routes = content
|
||||
.filter(p => (localRegistered ? true : p.page === 'RemotePublic'))
|
||||
.filter(p => (localActiveIndex !== null ? true : p.page === 'RemotePublic'))
|
||||
.map(p => ({ key: p.page }))
|
||||
|
||||
const renderScene = useCallback(
|
||||
@ -42,12 +42,12 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
||||
}
|
||||
}) => {
|
||||
return (
|
||||
(localRegistered || route.key === 'RemotePublic') && (
|
||||
(localActiveIndex !== null || route.key === 'RemotePublic') && (
|
||||
<Timeline page={route.key} />
|
||||
)
|
||||
)
|
||||
},
|
||||
[localRegistered]
|
||||
[localActiveIndex]
|
||||
)
|
||||
|
||||
const screenComponent = useCallback(
|
||||
@ -62,7 +62,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
||||
initialLayout={{ width: Dimensions.get('window').width }}
|
||||
/>
|
||||
),
|
||||
[segment, localRegistered]
|
||||
[segment, localActiveIndex]
|
||||
)
|
||||
|
||||
return (
|
||||
@ -71,7 +71,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
||||
name={`Screen-${name}-Root`}
|
||||
options={{
|
||||
headerTitle: name === 'Public' ? publicDomain : '',
|
||||
...(localRegistered && {
|
||||
...(localActiveIndex !== null && {
|
||||
headerCenter: () => (
|
||||
<View style={styles.segmentsContainer}>
|
||||
<SegmentedControl
|
||||
|
@ -3,16 +3,17 @@ import TimelineConversation from '@components/Timelines/Timeline/Conversation'
|
||||
import TimelineDefault from '@components/Timelines/Timeline/Default'
|
||||
import TimelineEmpty from '@components/Timelines/Timeline/Empty'
|
||||
import TimelineEnd from '@root/components/Timelines/Timeline/End'
|
||||
import TimelineHeader from '@components/Timelines/Timeline/Header'
|
||||
import TimelineNotifications from '@components/Timelines/Timeline/Notifications'
|
||||
import { useScrollToTop } from '@react-navigation/native'
|
||||
import { timelineFetch } from '@utils/fetches/timelineFetch'
|
||||
import { updateNotification } from '@utils/slices/instancesSlice'
|
||||
import { localUpdateNotification } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { RefreshControl, StyleSheet } from 'react-native'
|
||||
import { FlatList } from 'react-native-gesture-handler'
|
||||
import { InfiniteData, useInfiniteQuery } from 'react-query'
|
||||
import { InfiniteData } from 'react-query'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import hookTimeline, { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
|
||||
export type TimelineData =
|
||||
| InfiniteData<{
|
||||
@ -41,15 +42,14 @@ const Timeline: React.FC<Props> = ({
|
||||
disableRefresh = false,
|
||||
disableInfinity = false
|
||||
}) => {
|
||||
const queryKey: QueryKey.Timeline = [
|
||||
const queryKeyParams = {
|
||||
page,
|
||||
{
|
||||
...(hashtag && { hashtag }),
|
||||
...(list && { list }),
|
||||
...(toot && { toot }),
|
||||
...(account && { account })
|
||||
}
|
||||
]
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', queryKeyParams]
|
||||
const {
|
||||
status,
|
||||
data,
|
||||
@ -61,7 +61,9 @@ const Timeline: React.FC<Props> = ({
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage
|
||||
} = useInfiniteQuery(queryKey, timelineFetch, {
|
||||
} = hookTimeline({
|
||||
...queryKeyParams,
|
||||
options: {
|
||||
getPreviousPageParam: firstPage => {
|
||||
return firstPage.toots.length
|
||||
? {
|
||||
@ -78,7 +80,9 @@ const Timeline: React.FC<Props> = ({
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const flattenData = data?.pages ? data.pages.flatMap(d => [...d?.toots]) : []
|
||||
const flattenPointer = data?.pages
|
||||
? data.pages.flatMap(d => [d?.pointer])
|
||||
@ -92,9 +96,9 @@ const Timeline: React.FC<Props> = ({
|
||||
useEffect(() => {
|
||||
if (page === 'Notifications' && flattenData.length) {
|
||||
dispatch(
|
||||
updateNotification({
|
||||
localUpdateNotification({
|
||||
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}
|
||||
queryKey={queryKey}
|
||||
index={index}
|
||||
{...(queryKey[0] === 'RemotePublic' && {
|
||||
{...(queryKey[1].page === 'RemotePublic' && {
|
||||
disableDetails: true,
|
||||
disableOnPress: true
|
||||
})}
|
||||
@ -166,6 +170,7 @@ const Timeline: React.FC<Props> = ({
|
||||
() => !disableInfinity && !isFetchingNextPage && fetchNextPage(),
|
||||
[isFetchingNextPage]
|
||||
)
|
||||
const ListHeaderComponent = useCallback(() => <TimelineHeader />, [])
|
||||
const ListFooterComponent = useCallback(
|
||||
() => <TimelineEnd hasNextPage={!disableInfinity ? hasNextPage : false} />,
|
||||
[hasNextPage]
|
||||
@ -207,6 +212,8 @@ const Timeline: React.FC<Props> = ({
|
||||
ListEmptyComponent={flItemEmptyComponent}
|
||||
{...(!disableRefresh && { refreshControl })}
|
||||
ItemSeparatorComponent={ItemSeparatorComponent}
|
||||
{...(queryKey &&
|
||||
queryKey[1].page === 'RemotePublic' && { ListHeaderComponent })}
|
||||
{...(toot && isSuccess && { onScrollToIndexFailed })}
|
||||
maintainVisibleContentPosition={{
|
||||
minIndexForVisible: 0,
|
||||
|
@ -10,27 +10,14 @@ import TimelineActions from '@components/Timelines/Timeline/Shared/Actions'
|
||||
import client from '@root/api/client'
|
||||
import { useMutation, useQueryClient } from 'react-query'
|
||||
import { useTheme } from '@root/utils/styles/ThemeManager'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
|
||||
export interface Props {
|
||||
conversation: Mastodon.Conversation
|
||||
queryKey: QueryKey.Timeline
|
||||
queryKey: QueryKeyTimeline
|
||||
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> = ({
|
||||
conversation,
|
||||
queryKey,
|
||||
@ -39,6 +26,13 @@ const TimelineConversation: React.FC<Props> = ({
|
||||
const { theme } = useTheme()
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const fireMutation = useCallback(() => {
|
||||
return client<Mastodon.Conversation>({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: `conversations/${conversation.id}/read`
|
||||
})
|
||||
}, [])
|
||||
const { mutate } = useMutation(fireMutation, {
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries(queryKey)
|
||||
@ -49,7 +43,7 @@ const TimelineConversation: React.FC<Props> = ({
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
if (conversation.last_status) {
|
||||
conversation.unread && mutate({ id: conversation.id })
|
||||
conversation.unread && mutate()
|
||||
navigation.push('Screen-Shared-Toot', {
|
||||
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 TimelinePoll from '@components/Timelines/Timeline/Shared/Poll'
|
||||
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 React, { useCallback } from 'react'
|
||||
import { Pressable, StyleSheet, View } from 'react-native'
|
||||
@ -15,7 +16,7 @@ import { useSelector } from 'react-redux'
|
||||
|
||||
export interface Props {
|
||||
item: Mastodon.Status
|
||||
queryKey?: QueryKey.Timeline
|
||||
queryKey?: QueryKeyTimeline
|
||||
index: number
|
||||
pinnedLength?: number
|
||||
highlighted?: boolean
|
||||
@ -33,7 +34,7 @@ const TimelineDefault: React.FC<Props> = ({
|
||||
disableDetails = false,
|
||||
disableOnPress = false
|
||||
}) => {
|
||||
const localAccountId = useSelector(getLocalAccountId)
|
||||
const localAccount = useSelector(getLocalAccount)
|
||||
const navigation = useNavigation()
|
||||
|
||||
let actualStatus = item.reblog ? item.reblog : item
|
||||
@ -64,7 +65,7 @@ const TimelineDefault: React.FC<Props> = ({
|
||||
<TimelineHeaderDefault
|
||||
queryKey={disableOnPress ? undefined : queryKey}
|
||||
status={actualStatus}
|
||||
sameAccount={actualStatus.account.id === localAccountId}
|
||||
sameAccount={actualStatus.account.id === localAccount?.id}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@ -88,7 +89,7 @@ const TimelineDefault: React.FC<Props> = ({
|
||||
queryKey={queryKey}
|
||||
poll={actualStatus.poll}
|
||||
reblog={item.reblog ? true : false}
|
||||
sameAccount={actualStatus.account.id === localAccountId}
|
||||
sameAccount={actualStatus.account.id === localAccount?.id}
|
||||
/>
|
||||
)}
|
||||
{!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 TimelinePoll from '@components/Timelines/Timeline/Shared/Poll'
|
||||
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 React, { useCallback } from 'react'
|
||||
import { Pressable, StyleSheet, View } from 'react-native'
|
||||
@ -15,7 +16,7 @@ import { useSelector } from 'react-redux'
|
||||
|
||||
export interface Props {
|
||||
notification: Mastodon.Notification
|
||||
queryKey: QueryKey.Timeline
|
||||
queryKey: QueryKeyTimeline
|
||||
highlighted?: boolean
|
||||
}
|
||||
|
||||
@ -24,7 +25,7 @@ const TimelineNotifications: React.FC<Props> = ({
|
||||
queryKey,
|
||||
highlighted = false
|
||||
}) => {
|
||||
const localAccountId = useSelector(getLocalAccountId)
|
||||
const localAccount = useSelector(getLocalAccount)
|
||||
const navigation = useNavigation()
|
||||
const actualAccount = notification.status
|
||||
? notification.status.account
|
||||
@ -83,7 +84,7 @@ const TimelineNotifications: React.FC<Props> = ({
|
||||
queryKey={queryKey}
|
||||
poll={notification.status.poll}
|
||||
reblog={false}
|
||||
sameAccount={notification.account.id === localAccountId}
|
||||
sameAccount={notification.account.id === localAccount?.id}
|
||||
/>
|
||||
)}
|
||||
{notification.status.media_attachments.length > 0 && (
|
||||
|
@ -4,6 +4,7 @@ import Icon from '@components/Icon'
|
||||
import { TimelineData } from '@components/Timelines/Timeline'
|
||||
import { toast } from '@components/toast'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { findIndex } from 'lodash'
|
||||
@ -13,7 +14,7 @@ import { ActionSheetIOS, Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { useMutation, useQueryClient } from 'react-query'
|
||||
|
||||
export interface Props {
|
||||
queryKey: QueryKey.Timeline
|
||||
queryKey: QueryKeyTimeline
|
||||
status: Mastodon.Status
|
||||
reblog: boolean
|
||||
}
|
||||
@ -36,7 +37,7 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
||||
stateKey: 'favourited' | 'reblogged' | 'bookmarked'
|
||||
state?: boolean
|
||||
}) => {
|
||||
return client({
|
||||
return client<Mastodon.Status>({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: `statuses/${status.id}/${state ? 'un' : ''}${type}`
|
||||
@ -58,7 +59,7 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
||||
let tootIndex = -1
|
||||
const pageIndex = findIndex(old?.pages, page => {
|
||||
const tempIndex = findIndex(page.toots, [
|
||||
queryKey[0] === 'Notifications'
|
||||
queryKey[1].page === 'Notifications'
|
||||
? 'status.id'
|
||||
: reblog
|
||||
? 'reblog.id'
|
||||
@ -75,12 +76,12 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
||||
|
||||
if (pageIndex >= 0 && tootIndex >= 0) {
|
||||
if (
|
||||
(type === 'favourite' && queryKey[0] === 'Favourites') ||
|
||||
(type === 'bookmark' && queryKey[0] === 'Bookmarks')
|
||||
(type === 'favourite' && queryKey[1].page === 'Favourites') ||
|
||||
(type === 'bookmark' && queryKey[1].page === 'Bookmarks')
|
||||
) {
|
||||
old!.pages[pageIndex].toots.splice(tootIndex, 1)
|
||||
} else {
|
||||
if (queryKey[0] === 'Notifications') {
|
||||
if (queryKey[1].page === 'Notifications') {
|
||||
old!.pages[pageIndex].toots[tootIndex].status[stateKey] =
|
||||
typeof state === 'boolean' ? !state : true
|
||||
} else {
|
||||
|
@ -3,9 +3,10 @@ import { Pressable, StyleSheet } from 'react-native'
|
||||
import { Image } from 'react-native-expo-image-cache'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
|
||||
export interface Props {
|
||||
queryKey?: QueryKey.Timeline
|
||||
queryKey?: QueryKeyTimeline
|
||||
account: Mastodon.Account
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ import haptics from '@components/haptics'
|
||||
import Icon from '@components/Icon'
|
||||
import { TimelineData } from '@components/Timelines/Timeline'
|
||||
import { toast } from '@components/toast'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { findIndex } from 'lodash'
|
||||
@ -14,7 +15,7 @@ import HeaderSharedAccount from './HeaderShared/Account'
|
||||
import HeaderSharedCreated from './HeaderShared/Created'
|
||||
|
||||
export interface Props {
|
||||
queryKey: QueryKey.Timeline
|
||||
queryKey: QueryKeyTimeline
|
||||
conversation: Mastodon.Conversation
|
||||
}
|
||||
|
||||
@ -23,7 +24,7 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const fireMutation = useCallback(() => {
|
||||
return client({
|
||||
return client<Mastodon.Conversation>({
|
||||
method: 'delete',
|
||||
instance: 'local',
|
||||
url: `conversations/${conversation.id}`
|
||||
|
@ -13,9 +13,10 @@ import HeaderSharedAccount from './HeaderShared/Account'
|
||||
import HeaderSharedApplication from './HeaderShared/Application'
|
||||
import HeaderSharedCreated from './HeaderShared/Created'
|
||||
import HeaderSharedVisibility from './HeaderShared/Visibility'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
|
||||
export interface Props {
|
||||
queryKey?: QueryKey.Timeline
|
||||
queryKey?: QueryKeyTimeline
|
||||
status: Mastodon.Status
|
||||
sameAccount: boolean
|
||||
}
|
||||
|
@ -2,12 +2,13 @@ import client from '@api/client'
|
||||
import haptics from '@components/haptics'
|
||||
import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu'
|
||||
import { toast } from '@components/toast'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMutation, useQueryClient } from 'react-query'
|
||||
|
||||
export interface Props {
|
||||
queryKey?: QueryKey.Timeline
|
||||
queryKey?: QueryKeyTimeline
|
||||
account: Pick<Mastodon.Account, 'id' | 'acct'>
|
||||
setBottomSheetVisible: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
@ -25,14 +26,14 @@ const HeaderDefaultActionsAccount: React.FC<Props> = ({
|
||||
switch (type) {
|
||||
case 'mute':
|
||||
case 'block':
|
||||
return client({
|
||||
return client<Mastodon.Account>({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: `accounts/${account.id}/${type}`
|
||||
})
|
||||
break
|
||||
case 'reports':
|
||||
return client({
|
||||
return client<Mastodon.Account>({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: `reports`,
|
||||
|
@ -3,12 +3,13 @@ import MenuContainer from '@components/Menu/Container'
|
||||
import MenuHeader from '@components/Menu/Header'
|
||||
import MenuRow from '@components/Menu/Row'
|
||||
import { toast } from '@components/toast'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMutation, useQueryClient } from 'react-query'
|
||||
|
||||
export interface Props {
|
||||
queryKey: QueryKey.Timeline
|
||||
queryKey: QueryKeyTimeline
|
||||
domain: string
|
||||
setBottomSheetVisible: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
@ -21,7 +22,7 @@ const HeaderDefaultActionsDomain: React.FC<Props> = ({
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
const fireMutation = useCallback(() => {
|
||||
return client({
|
||||
return client<{}>({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: `domain_blocks`,
|
||||
|
@ -9,9 +9,10 @@ import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu'
|
||||
import { TimelineData } from '@components/Timelines/Timeline'
|
||||
import { toast } from '@components/toast'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
|
||||
export interface Props {
|
||||
queryKey: QueryKey.Timeline
|
||||
queryKey: QueryKeyTimeline
|
||||
status: Mastodon.Status
|
||||
setBottomSheetVisible: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
@ -30,14 +31,14 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
|
||||
switch (type) {
|
||||
case 'mute':
|
||||
case 'pin':
|
||||
return client({
|
||||
return client<Mastodon.Status>({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: `statuses/${status.id}/${state ? 'un' : ''}${type}`
|
||||
}) // bug in response from Mastodon, but onMutate ignore the error in response
|
||||
break
|
||||
case 'delete':
|
||||
return client({
|
||||
return client<Mastodon.Status>({
|
||||
method: 'delete',
|
||||
instance: 'local',
|
||||
url: `statuses/${status.id}`
|
||||
@ -153,7 +154,7 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
|
||||
),
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
await client({
|
||||
await client<Mastodon.Status>({
|
||||
method: 'delete',
|
||||
instance: 'local',
|
||||
url: `statuses/${status.id}`
|
||||
@ -163,7 +164,7 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
|
||||
setBottomSheetVisible(false)
|
||||
navigation.navigate('Screen-Shared-Compose', {
|
||||
type: 'edit',
|
||||
incomingStatus: res.body
|
||||
incomingStatus: res
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
|
@ -6,6 +6,7 @@ import relativeTime from '@components/relativeTime'
|
||||
import { TimelineData } from '@components/Timelines/Timeline'
|
||||
import { ParseEmojis } from '@root/components/Parse'
|
||||
import { toast } from '@root/components/toast'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { findIndex } from 'lodash'
|
||||
@ -15,7 +16,7 @@ import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { useMutation, useQueryClient } from 'react-query'
|
||||
|
||||
export interface Props {
|
||||
queryKey: QueryKey.Timeline
|
||||
queryKey: QueryKeyTimeline
|
||||
poll: NonNullable<Mastodon.Status['poll']>
|
||||
reblog: boolean
|
||||
sameAccount: boolean
|
||||
@ -45,7 +46,7 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
}
|
||||
})
|
||||
|
||||
return client({
|
||||
return client<Mastodon.Poll>({
|
||||
method: type === 'vote' ? 'post' : 'get',
|
||||
instance: 'local',
|
||||
url: type === 'vote' ? `polls/${poll.id}/votes` : `polls/${poll.id}`,
|
||||
@ -55,7 +56,7 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
[allOptions]
|
||||
)
|
||||
const mutation = useMutation(fireMutation, {
|
||||
onSuccess: ({ body }) => {
|
||||
onSuccess: (res) => {
|
||||
queryClient.cancelQueries(queryKey)
|
||||
|
||||
queryClient.setQueryData<TimelineData>(queryKey, old => {
|
||||
@ -75,9 +76,9 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
|
||||
if (pageIndex >= 0 && tootIndex >= 0) {
|
||||
if (reblog) {
|
||||
old!.pages[pageIndex].toots[tootIndex].reblog!.poll = body
|
||||
old!.pages[pageIndex].toots[tootIndex].reblog!.poll = res
|
||||
} else {
|
||||
old!.pages[pageIndex].toots[tootIndex].poll = body
|
||||
old!.pages[pageIndex].toots[tootIndex].poll = res
|
||||
}
|
||||
}
|
||||
return old
|
||||
|
@ -41,9 +41,7 @@ i18next.use(initReactI18next).init({
|
||||
// react options
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
},
|
||||
|
||||
debug: true
|
||||
}
|
||||
})
|
||||
|
||||
export default i18next
|
||||
|
@ -14,6 +14,7 @@ export default {
|
||||
meLists: require('./screens/meLists').default,
|
||||
meListsList: require('./screens/meListsList').default,
|
||||
meSettings: require('./screens/meSettings').default,
|
||||
meSettingsUpdateRemote: require('./screens/meSettingsUpdateRemote').default,
|
||||
|
||||
sharedAccount: require('./screens/sharedAccount').default,
|
||||
sharedToot: require('./screens/sharedToot').default,
|
||||
|
@ -26,8 +26,13 @@ export default {
|
||||
cancel: '$t(common:buttons.cancel)'
|
||||
}
|
||||
},
|
||||
remote: {
|
||||
heading: '$t(meSettingsUpdateRemote:heading)',
|
||||
description: '外站只能看不能玩'
|
||||
},
|
||||
cache: {
|
||||
heading: '清空缓存'
|
||||
heading: '清空缓存',
|
||||
empty: '暂无缓存'
|
||||
},
|
||||
analytics: {
|
||||
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 { 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()
|
||||
|
||||
@ -34,7 +37,7 @@ const ScreenMe: React.FC = () => {
|
||||
component={ScreenMeConversations}
|
||||
options={({ navigation }: any) => ({
|
||||
headerTitle: t('meConversations:heading'),
|
||||
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
|
||||
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@ -42,7 +45,7 @@ const ScreenMe: React.FC = () => {
|
||||
component={ScreenMeBookmarks}
|
||||
options={({ navigation }: any) => ({
|
||||
headerTitle: t('meBookmarks:heading'),
|
||||
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
|
||||
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@ -50,7 +53,7 @@ const ScreenMe: React.FC = () => {
|
||||
component={ScreenMeFavourites}
|
||||
options={({ navigation }: any) => ({
|
||||
headerTitle: t('meFavourites:heading'),
|
||||
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
|
||||
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@ -58,7 +61,7 @@ const ScreenMe: React.FC = () => {
|
||||
component={ScreenMeLists}
|
||||
options={({ navigation }: any) => ({
|
||||
headerTitle: t('meLists:heading'),
|
||||
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
|
||||
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@ -66,7 +69,7 @@ const ScreenMe: React.FC = () => {
|
||||
component={ScreenMeListsList}
|
||||
options={({ route, navigation }: any) => ({
|
||||
headerTitle: t('meListsList:heading', { list: route.params.title }),
|
||||
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
|
||||
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@ -74,7 +77,24 @@ const ScreenMe: React.FC = () => {
|
||||
component={ScreenMeSettings}
|
||||
options={({ navigation }: any) => ({
|
||||
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 { useNavigation } from '@react-navigation/native'
|
||||
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 { StyleSheet } from 'react-native'
|
||||
import { useQuery } from 'react-query'
|
||||
|
||||
const ScreenMeLists: React.FC = () => {
|
||||
const navigation = useNavigation()
|
||||
const { status, data, refetch } = useQuery(['Lists'], listsFetch)
|
||||
const { status, data, refetch } = hookLists({})
|
||||
|
||||
const children = useMemo(() => {
|
||||
if (status === 'success') {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { useScrollToTop } from '@react-navigation/native'
|
||||
import Collections from '@screens/Me/Root/Collections'
|
||||
import Login from '@screens/Me/Root/Login'
|
||||
import MyInfo from '@screens/Me/Root/MyInfo'
|
||||
import Settings from '@screens/Me/Root/Settings'
|
||||
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 accountInitialState from '@screens/Shared/Account/utils/initialState'
|
||||
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 { useSelector } from 'react-redux'
|
||||
import Animated, {
|
||||
useAnimatedScrollHandler,
|
||||
useSharedValue
|
||||
} from 'react-native-reanimated'
|
||||
import ComponentInstance from '@components/Instance'
|
||||
|
||||
const ScreenMeRoot: React.FC = () => {
|
||||
const localRegistered = useSelector(getLocalUrl)
|
||||
const localActiveIndex = useSelector(getLocalActiveIndex)
|
||||
|
||||
const scrollRef = useRef<Animated.ScrollView>(null)
|
||||
useScrollToTop(scrollRef)
|
||||
@ -36,7 +36,7 @@ const ScreenMeRoot: React.FC = () => {
|
||||
|
||||
return (
|
||||
<AccountContext.Provider value={{ accountState, accountDispatch }}>
|
||||
{localRegistered && data ? (
|
||||
{localActiveIndex !== null && data ? (
|
||||
<AccountNav scrollY={scrollY} account={data} />
|
||||
) : null}
|
||||
<Animated.ScrollView
|
||||
@ -45,10 +45,14 @@ const ScreenMeRoot: React.FC = () => {
|
||||
onScroll={onScroll}
|
||||
scrollEventThrottle={16}
|
||||
>
|
||||
{localRegistered ? <MyInfo setData={setData} /> : <Login />}
|
||||
{localRegistered && <Collections />}
|
||||
{localActiveIndex !== null ? (
|
||||
<MyInfo setData={setData} />
|
||||
) : (
|
||||
<ComponentInstance type='local' />
|
||||
)}
|
||||
{localActiveIndex !== null ? <Collections /> : null}
|
||||
<Settings />
|
||||
{localRegistered && <Logout />}
|
||||
{localActiveIndex !== null ? <Logout /> : null}
|
||||
</Animated.ScrollView>
|
||||
</AccountContext.Provider>
|
||||
)
|
||||
|
@ -3,15 +3,13 @@ import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||
import { useQuery } from 'react-query'
|
||||
import { announcementFetch } from '@root/utils/fetches/announcementsFetch'
|
||||
import hookAnnouncement from '@utils/queryHooks/announcement'
|
||||
|
||||
const Collections: React.FC = () => {
|
||||
const { t } = useTranslation('meRoot')
|
||||
const navigation = useNavigation()
|
||||
|
||||
const queryKey = ['Announcements', { showAll: true }]
|
||||
const { data, isFetching } = useQuery(queryKey, announcementFetch)
|
||||
const { data, isFetching } = hookAnnouncement({ showAll: true })
|
||||
|
||||
const announcementContent = useMemo(() => {
|
||||
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 { useNavigation } from '@react-navigation/native'
|
||||
import analytics from '@root/components/analytics'
|
||||
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 React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -13,7 +11,6 @@ import { useDispatch } from 'react-redux'
|
||||
const Logout: React.FC = () => {
|
||||
const { t } = useTranslation('meRoot')
|
||||
const dispatch = useDispatch()
|
||||
const navigation = useNavigation()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return (
|
||||
@ -36,12 +33,7 @@ const Logout: React.FC = () => {
|
||||
onPress: () => {
|
||||
haptics('Success')
|
||||
queryClient.clear()
|
||||
dispatch(resetLocal())
|
||||
analytics('logout')
|
||||
navigation.navigate('Screen-Public', {
|
||||
screen: 'Screen-Public-Root',
|
||||
params: { publicTab: true }
|
||||
})
|
||||
dispatch(localRemoveInstance())
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -1,9 +1,8 @@
|
||||
import AccountHeader from '@screens/Shared/Account/Header'
|
||||
import AccountInformation from '@screens/Shared/Account/Information'
|
||||
import { accountFetch } from '@utils/fetches/accountFetch'
|
||||
import { getLocalAccountId } from '@utils/slices/instancesSlice'
|
||||
import hookAccount from '@utils/queryHooks/account'
|
||||
import { getLocalAccount } from '@utils/slices/instancesSlice'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useQuery } from 'react-query'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
export interface Props {
|
||||
@ -11,8 +10,9 @@ export interface Props {
|
||||
}
|
||||
|
||||
const MyInfo: React.FC<Props> = ({ setData }) => {
|
||||
const localAccountId = useSelector(getLocalAccountId)
|
||||
const { data } = useQuery(['Account', { id: localAccountId }], accountFetch)
|
||||
const localAccount = useSelector(getLocalAccount)
|
||||
const { data } = hookAccount({ id: localAccount!.id })
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setData(data)
|
||||
@ -22,7 +22,7 @@ const MyInfo: React.FC<Props> = ({ setData }) => {
|
||||
return (
|
||||
<>
|
||||
<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 { useNavigation } from '@react-navigation/native'
|
||||
import haptics from '@root/components/haptics'
|
||||
import { persistor } from '@root/store'
|
||||
import {
|
||||
getLocalActiveIndex,
|
||||
getLocalInstances,
|
||||
getRemoteUrl
|
||||
} from '@root/utils/slices/instancesSlice'
|
||||
import {
|
||||
changeAnalytics,
|
||||
changeBrowser,
|
||||
@ -13,18 +21,63 @@ import {
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
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 { ActionSheetIOS, Button, StyleSheet, Text, View } from 'react-native'
|
||||
import { ActionSheetIOS, StyleSheet, Text } from 'react-native'
|
||||
import { CacheManager } from 'react-native-expo-image-cache'
|
||||
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 navigation = useNavigation()
|
||||
const { t, i18n } = useTranslation('meSettings')
|
||||
const { setTheme, theme } = useTheme()
|
||||
const settingsLanguage = useSelector(getSettingsLanguage)
|
||||
const settingsTheme = useSelector(getSettingsTheme)
|
||||
const settingsBrowser = useSelector(getSettingsBrowser)
|
||||
const settingsRemote = useSelector(getRemoteUrl)
|
||||
const settingsAnalytics = useSelector(getSettingsAnalytics)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
@ -134,9 +187,18 @@ const ScreenMeSettings: React.FC = () => {
|
||||
/>
|
||||
</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
|
||||
title={t('content.cache.heading')}
|
||||
content={cacheSize ? prettyBytes(cacheSize) : '暂无缓存'}
|
||||
content={
|
||||
cacheSize ? prettyBytes(cacheSize) : t('content.cache.empty')
|
||||
}
|
||||
iconBack='ChevronRight'
|
||||
onPress={async () => {
|
||||
await CacheManager.clearCache()
|
||||
@ -144,6 +206,8 @@ const ScreenMeSettings: React.FC = () => {
|
||||
setCacheSize(0)
|
||||
}}
|
||||
/>
|
||||
</MenuContainer>
|
||||
<MenuContainer>
|
||||
<MenuRow
|
||||
title={t('content.analytics.heading')}
|
||||
description={t('content.analytics.description')}
|
||||
@ -160,6 +224,8 @@ const ScreenMeSettings: React.FC = () => {
|
||||
{t('content.version', { version: '1.0.0' })}
|
||||
</Text>
|
||||
</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 sharedScreens from '@screens/Shared/sharedScreens'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { RootState } from '@root/store'
|
||||
import { getLocalActiveIndex } from '@utils/slices/instancesSlice'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
const Stack = createNativeStackNavigator()
|
||||
|
||||
const ScreenNotifications: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const localRegistered = useSelector(
|
||||
(state: RootState) => state.instances.local.url
|
||||
)
|
||||
const localActiveIndex = useSelector(getLocalActiveIndex)
|
||||
|
||||
return (
|
||||
<Stack.Navigator
|
||||
@ -23,7 +20,9 @@ const ScreenNotifications: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name='Screen-Notifications-Root'>
|
||||
{() => (localRegistered ? <Timeline page='Notifications' /> : null)}
|
||||
{() =>
|
||||
localActiveIndex !== null ? <Timeline page='Notifications' /> : null
|
||||
}
|
||||
</Stack.Screen>
|
||||
|
||||
{sharedScreens(Stack)}
|
||||
|
@ -1,14 +1,13 @@
|
||||
import BottomSheet from '@components/BottomSheet'
|
||||
import { HeaderRight } from '@components/Header'
|
||||
import HeaderDefaultActionsAccount from '@components/Timelines/Timeline/Shared/HeaderDefault/ActionsAccount'
|
||||
import { accountFetch } from '@utils/fetches/accountFetch'
|
||||
import { getLocalAccountId } from '@utils/slices/instancesSlice'
|
||||
import hookAccount from '@utils/queryHooks/account'
|
||||
import { getLocalAccount } from '@utils/slices/instancesSlice'
|
||||
import React, { useEffect, useReducer, useRef, useState } from 'react'
|
||||
import Animated, {
|
||||
useAnimatedScrollHandler,
|
||||
useSharedValue
|
||||
} from 'react-native-reanimated'
|
||||
import { useQuery } from 'react-query'
|
||||
import { useSelector } from 'react-redux'
|
||||
import AccountHeader from './Account/Header'
|
||||
import AccountInformation from './Account/Information'
|
||||
@ -36,8 +35,8 @@ const ScreenSharedAccount: React.FC<Props> = ({
|
||||
},
|
||||
navigation
|
||||
}) => {
|
||||
const localAccountId = useSelector(getLocalAccountId)
|
||||
const { data } = useQuery(['Account', { id: account.id }], accountFetch)
|
||||
const localAccount = useSelector(getLocalAccount)
|
||||
const { data } = hookAccount({ id: account.id })
|
||||
|
||||
const scrollY = useSharedValue(0)
|
||||
const [accountState, accountDispatch] = useReducer(
|
||||
@ -85,7 +84,7 @@ const ScreenSharedAccount: React.FC<Props> = ({
|
||||
handleDismiss={() => setBottomSheetVisible(false)}
|
||||
>
|
||||
{/* 添加到列表 */}
|
||||
{localAccountId !== account.id && (
|
||||
{localAccount?.id !== account.id && (
|
||||
<HeaderDefaultActionsAccount
|
||||
account={account}
|
||||
setBottomSheetVisible={setBottomSheetVisible}
|
||||
|
@ -1,24 +1,25 @@
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import React, { createRef, useCallback, useContext, useEffect } from 'react'
|
||||
import { Animated, StyleSheet, View } from 'react-native'
|
||||
import AccountInformationAvatar from './Information/Avatar'
|
||||
import AccountInformationName from './Information/Name'
|
||||
import AccountInformationAccount from './Information/Account'
|
||||
import AccountInformationCreated from './Information/Created'
|
||||
import AccountInformationStats from './Information/Stats'
|
||||
import AccountInformationActions from './Information/Actions'
|
||||
import AccountInformationAvatar from './Information/Avatar'
|
||||
import AccountInformationCreated from './Information/Created'
|
||||
import AccountInformationFields from './Information/Fields'
|
||||
import AccountInformationName from './Information/Name'
|
||||
import AccountInformationNotes from './Information/Notes'
|
||||
import AccountInformationStats from './Information/Stats'
|
||||
import AccountInformationSwitch from './Information/Switch'
|
||||
import AccountContext from './utils/createContext'
|
||||
|
||||
export interface Props {
|
||||
account: Mastodon.Account | undefined
|
||||
disableActions?: boolean
|
||||
ownAccount?: boolean
|
||||
}
|
||||
|
||||
const AccountInformation: React.FC<Props> = ({
|
||||
account,
|
||||
disableActions = false
|
||||
ownAccount = false
|
||||
}) => {
|
||||
const { accountDispatch } = useContext(AccountContext)
|
||||
const shimmerAvatarRef = createRef<any>()
|
||||
@ -59,9 +60,13 @@ const AccountInformation: React.FC<Props> = ({
|
||||
{/* <Text>Moved or not: {account.moved}</Text> */}
|
||||
<View style={styles.avatarAndActions}>
|
||||
<AccountInformationAvatar ref={shimmerAvatarRef} account={account} />
|
||||
{!disableActions ? (
|
||||
<View style={styles.actions}>
|
||||
{ownAccount ? (
|
||||
<AccountInformationSwitch />
|
||||
) : (
|
||||
<AccountInformationActions account={account} />
|
||||
) : null}
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<AccountInformationName ref={shimmerNameRef} account={account} />
|
||||
@ -94,6 +99,10 @@ const styles = StyleSheet.create({
|
||||
avatarAndActions: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
actions: {
|
||||
alignSelf: 'flex-end',
|
||||
flexDirection: 'row'
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -25,7 +25,7 @@ const AccountInformationAccount = forwardRef<ShimmerPlaceholder, Props>(
|
||||
width={StyleConstants.Font.Size.M * 8}
|
||||
height={StyleConstants.Font.Size.M}
|
||||
style={{ marginBottom: StyleConstants.Spacing.L }}
|
||||
shimmerColors={theme.shimmer}
|
||||
shimmerColors={[theme.shimmerDefault, theme.shimmerHighlight, theme.shimmerDefault]}
|
||||
>
|
||||
<View style={styles.account}>
|
||||
<Text
|
||||
|
@ -1,24 +1,20 @@
|
||||
import Button from '@components/Button'
|
||||
import { RelationshipOutgoing } from '@components/Relationship'
|
||||
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 React from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
import { useQuery } from 'react-query'
|
||||
import { StyleSheet } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
account: Mastodon.Account | undefined
|
||||
}
|
||||
|
||||
const AccountInformationActions: React.FC<Props> = ({ account }) => {
|
||||
const Conversation = ({ account }: { account: Mastodon.Account }) => {
|
||||
const navigation = useNavigation()
|
||||
const relationshipQueryKey = ['Relationship', { id: account?.id }]
|
||||
const query = useQuery(relationshipQueryKey, relationshipFetch)
|
||||
const query = hookRelationship({ id: account.id })
|
||||
|
||||
return (
|
||||
<View style={styles.actions}>
|
||||
{query.data && !query.data.blocked_by ? (
|
||||
return query.data && !query.data.blocked_by ? (
|
||||
<Button
|
||||
round
|
||||
type='icon'
|
||||
@ -31,17 +27,19 @@ const AccountInformationActions: React.FC<Props> = ({ account }) => {
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{account && account.id && <RelationshipOutgoing id={account.id} />}
|
||||
</View>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
|
||||
const AccountInformationActions: React.FC<Props> = ({ account }) => {
|
||||
return account && account.id ? (
|
||||
<>
|
||||
<Conversation account={account} />
|
||||
<RelationshipOutgoing id={account.id} />
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
actions: {
|
||||
alignSelf: 'flex-end',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
actionConversation: { marginRight: StyleConstants.Spacing.S }
|
||||
})
|
||||
|
||||
|
@ -24,7 +24,7 @@ const AccountInformationAvatar = forwardRef<ShimmerPlaceholder, Props>(
|
||||
visible={avatarLoaded}
|
||||
width={StyleConstants.Avatar.L}
|
||||
height={StyleConstants.Avatar.L}
|
||||
shimmerColors={theme.shimmer}
|
||||
shimmerColors={[theme.shimmerDefault, theme.shimmerHighlight, theme.shimmerDefault]}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: account?.avatar }}
|
||||
|
@ -26,7 +26,7 @@ const AccountInformationCreated = forwardRef<ShimmerPlaceholder, Props>(
|
||||
width={StyleConstants.Font.Size.S * 8}
|
||||
height={StyleConstants.Font.Size.S}
|
||||
style={{ marginBottom: StyleConstants.Spacing.M }}
|
||||
shimmerColors={theme.shimmer}
|
||||
shimmerColors={[theme.shimmerDefault, theme.shimmerHighlight, theme.shimmerDefault]}
|
||||
>
|
||||
<View style={styles.created}>
|
||||
<Icon
|
||||
|
@ -25,6 +25,7 @@ const AccountInformationFields: React.FC<Props> = ({ account }) => {
|
||||
size={'M'}
|
||||
emojis={account.emojis}
|
||||
showFullLink
|
||||
numberOfLines={3}
|
||||
/>
|
||||
{field.verified_at ? (
|
||||
<Icon
|
||||
@ -41,6 +42,7 @@ const AccountInformationFields: React.FC<Props> = ({ account }) => {
|
||||
size={'M'}
|
||||
emojis={account.emojis}
|
||||
showFullLink
|
||||
numberOfLines={3}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
@ -26,7 +26,7 @@ const AccountInformationName = forwardRef<ShimmerPlaceholder, Props>(
|
||||
width={StyleConstants.Font.Size.L * 8}
|
||||
height={StyleConstants.Font.Size.L}
|
||||
style={styles.name}
|
||||
shimmerColors={theme.shimmer}
|
||||
shimmerColors={[theme.shimmerDefault, theme.shimmerHighlight, theme.shimmerDefault]}
|
||||
>
|
||||
{account ? (
|
||||
<ParseEmojis
|
||||
|
@ -41,7 +41,7 @@ const AccountInformationStats = forwardRef<any, Props>(({ account }, ref) => {
|
||||
visible={account !== undefined}
|
||||
width={StyleConstants.Font.Size.S * 5}
|
||||
height={StyleConstants.Font.Size.S}
|
||||
shimmerColors={theme.shimmer}
|
||||
shimmerColors={[theme.shimmerDefault, theme.shimmerHighlight, theme.shimmerDefault]}
|
||||
>
|
||||
<Text style={[styles.stat, { color: theme.primary }]}>
|
||||
{t('content.summary.statuses_count', {
|
||||
@ -54,7 +54,7 @@ const AccountInformationStats = forwardRef<any, Props>(({ account }, ref) => {
|
||||
visible={account !== undefined}
|
||||
width={StyleConstants.Font.Size.S * 5}
|
||||
height={StyleConstants.Font.Size.S}
|
||||
shimmerColors={theme.shimmer}
|
||||
shimmerColors={[theme.shimmerDefault, theme.shimmerHighlight, theme.shimmerDefault]}
|
||||
>
|
||||
<Text
|
||||
style={[styles.stat, { color: theme.primary, textAlign: 'right' }]}
|
||||
@ -76,7 +76,7 @@ const AccountInformationStats = forwardRef<any, Props>(({ account }, ref) => {
|
||||
visible={account !== undefined}
|
||||
width={StyleConstants.Font.Size.S * 5}
|
||||
height={StyleConstants.Font.Size.S}
|
||||
shimmerColors={theme.shimmer}
|
||||
shimmerColors={[theme.shimmerDefault, theme.shimmerHighlight, theme.shimmerDefault]}
|
||||
>
|
||||
<Text
|
||||
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 relativeTime from '@components/relativeTime'
|
||||
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 { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
@ -19,7 +19,7 @@ import {
|
||||
} from 'react-native'
|
||||
import { FlatList, ScrollView } from 'react-native-gesture-handler'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { useMutation, useQuery } from 'react-query'
|
||||
import { useMutation } from 'react-query'
|
||||
|
||||
const fireMutation = async ({
|
||||
announcementId,
|
||||
@ -34,13 +34,13 @@ const fireMutation = async ({
|
||||
}) => {
|
||||
switch (type) {
|
||||
case 'reaction':
|
||||
return client({
|
||||
return client<{}>({
|
||||
method: me ? 'delete' : 'put',
|
||||
instance: 'local',
|
||||
url: `announcements/${announcementId}/reactions/${name}`
|
||||
})
|
||||
case 'dismiss':
|
||||
return client({
|
||||
return client<{}>({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: `announcements/${announcementId}/dismiss`
|
||||
@ -59,12 +59,14 @@ const ScreenSharedAnnouncements: React.FC = ({
|
||||
const [index, setIndex] = useState(0)
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
const queryKey = ['Announcements', { showAll }]
|
||||
const { data, refetch } = useQuery(queryKey, announcementFetch, {
|
||||
const { data, refetch } = hookAnnouncement({
|
||||
showAll,
|
||||
options: {
|
||||
select: announcements =>
|
||||
announcements.filter(announcement =>
|
||||
showAll ? announcement : !announcement.read
|
||||
)
|
||||
}
|
||||
})
|
||||
const queryMutation = useMutation(fireMutation, {
|
||||
onSettled: () => {
|
||||
|
@ -4,7 +4,7 @@ import { store } from '@root/store'
|
||||
import layoutAnimation from '@root/utils/styles/layoutAnimation'
|
||||
import formatText from '@screens/Shared/Compose/formatText'
|
||||
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 { useTheme } from '@utils/styles/ThemeManager'
|
||||
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 composePost from './Compose/utils/post'
|
||||
import composeReducer from './Compose/utils/reducer'
|
||||
import { ComposeState } from './Compose/utils/types'
|
||||
|
||||
const Stack = createNativeStackNavigator()
|
||||
|
||||
@ -34,7 +33,6 @@ export interface Props {
|
||||
| {
|
||||
type?: 'reply' | 'conversation' | 'edit'
|
||||
incomingStatus: Mastodon.Status
|
||||
visibilityLock?: boolean
|
||||
}
|
||||
| undefined
|
||||
}
|
||||
@ -63,19 +61,21 @@ const Compose: React.FC<Props> = ({ route: { params }, navigation }) => {
|
||||
setHasKeyboard(false)
|
||||
}
|
||||
|
||||
const localAccount = getLocalAccount(store.getState())
|
||||
const [composeState, composeDispatch] = useReducer(
|
||||
composeReducer,
|
||||
params?.type && params?.incomingStatus
|
||||
? composeParseState({
|
||||
type: params.type,
|
||||
incomingStatus: params.incomingStatus,
|
||||
visibilityLock: params.visibilityLock
|
||||
incomingStatus: params.incomingStatus
|
||||
})
|
||||
: {
|
||||
...composeInitialState,
|
||||
visibility: getLocalAccountPreferences(store.getState())[
|
||||
'posting:default:visibility'
|
||||
] as ComposeState['visibility']
|
||||
visibility:
|
||||
localAccount?.preferences &&
|
||||
localAccount.preferences['posting:default:visibility']
|
||||
? localAccount.preferences['posting:default:visibility']
|
||||
: 'public'
|
||||
}
|
||||
)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
@ -97,7 +97,7 @@ const ComposeEditAttachment: React.FC<Props> = ({
|
||||
formData.append('focus', `${focus.current.x},${focus.current.y}`)
|
||||
}
|
||||
|
||||
client({
|
||||
client<Mastodon.Attachment>({
|
||||
method: 'put',
|
||||
instance: 'local',
|
||||
url: `media/${theAttachment.id}`,
|
||||
|
@ -1,131 +1,43 @@
|
||||
import haptics from '@components/haptics'
|
||||
import { ParseEmojis } from '@components/Parse'
|
||||
import { emojisFetch } from '@utils/fetches/emojisFetch'
|
||||
import { searchFetch } from '@utils/fetches/searchFetch'
|
||||
import hookEmojis from '@utils/queryHooks/emojis'
|
||||
import hookSearch from '@utils/queryHooks/search'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { forEach, groupBy, sortBy } from 'lodash'
|
||||
import React, {
|
||||
Dispatch,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo
|
||||
} from 'react'
|
||||
import {
|
||||
View,
|
||||
FlatList,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
Image
|
||||
} from 'react-native'
|
||||
import React, { useCallback, useContext, useEffect, useMemo } from 'react'
|
||||
import { View, FlatList, StyleSheet } from 'react-native'
|
||||
import { Chase } from 'react-native-animated-spinkit'
|
||||
import { useQuery } from 'react-query'
|
||||
import ComposeActions from './Actions'
|
||||
import ComposeRootFooter from './Root/Footer'
|
||||
import ComposeRootHeader from './Root/Header'
|
||||
import updateText from './updateText'
|
||||
import ComposeRootSuggestion from './Root/Suggestion'
|
||||
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 { theme } = useTheme()
|
||||
|
||||
const { composeState, composeDispatch } = useContext(ComposeContext)
|
||||
|
||||
const { isFetching, data, refetch } = useQuery(
|
||||
[
|
||||
'Search',
|
||||
{
|
||||
type: composeState.tag?.type,
|
||||
term: composeState.tag?.text.substring(1)
|
||||
}
|
||||
],
|
||||
searchFetch,
|
||||
{ enabled: false }
|
||||
)
|
||||
const { isFetching, data, refetch } = hookSearch({
|
||||
type:
|
||||
composeState.tag?.type === 'accounts' ||
|
||||
composeState.tag?.type === 'hashtags'
|
||||
? composeState.tag.type
|
||||
: undefined,
|
||||
term: composeState.tag?.text.substring(1),
|
||||
options: { enabled: false }
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (composeState.tag?.text) {
|
||||
if (
|
||||
(composeState.tag?.type === 'accounts' ||
|
||||
composeState.tag?.type === 'hashtags') &&
|
||||
composeState.tag?.text
|
||||
) {
|
||||
refetch()
|
||||
}
|
||||
}, [composeState.tag?.text])
|
||||
}, [composeState.tag])
|
||||
|
||||
const { data: emojisData } = useQuery(['Emojis'], emojisFetch)
|
||||
const { data: emojisData } = hookEmojis({})
|
||||
useEffect(() => {
|
||||
if (emojisData && emojisData.length) {
|
||||
let sortedEmojis: { title: string; data: Mastodon.Emoji[] }[] = []
|
||||
@ -155,7 +67,7 @@ const ComposeRoot: React.FC = () => {
|
||||
|
||||
const listItem = useCallback(
|
||||
({ item }) => (
|
||||
<ListItem
|
||||
<ComposeRootSuggestion
|
||||
item={item}
|
||||
composeState={composeState}
|
||||
composeDispatch={composeDispatch}
|
||||
@ -167,13 +79,14 @@ const ComposeRoot: React.FC = () => {
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
<FlatList
|
||||
renderItem={listItem}
|
||||
ListEmptyComponent={listEmpty}
|
||||
keyboardShouldPersistTaps='handled'
|
||||
ListHeaderComponent={ComposeRootHeader}
|
||||
ListFooterComponent={ComposeRootFooter}
|
||||
ListEmptyComponent={listEmpty}
|
||||
data={data as Mastodon.Account[] & Mastodon.Tag[]}
|
||||
renderItem={listItem}
|
||||
keyExtractor={(item: any) => item.acct || item.name}
|
||||
// @ts-ignore
|
||||
data={data ? data[composeState.tag?.type] : undefined}
|
||||
keyExtractor={({ item }) => item.acct || item.name}
|
||||
/>
|
||||
<ComposeActions />
|
||||
</View>
|
||||
@ -185,46 +98,6 @@ const styles = StyleSheet.create({
|
||||
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: {
|
||||
flex: 1,
|
||||
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'
|
||||
})
|
||||
|
||||
return client({
|
||||
return client<Mastodon.Attachment>({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: 'media',
|
||||
body: formData
|
||||
})
|
||||
.then(({ body }: { body: Mastodon.Attachment }) => {
|
||||
if (body.id) {
|
||||
.then(res => {
|
||||
if (res.id) {
|
||||
composeDispatch({
|
||||
type: 'attachment/upload/end',
|
||||
payload: { remote: body, local: result }
|
||||
payload: { remote: res, local: result }
|
||||
})
|
||||
} else {
|
||||
composeDispatch({
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { store } from '@root/store'
|
||||
import { getLocalAccountPreferences } from '@utils/slices/instancesSlice'
|
||||
import { getLocalAccount } from '@utils/slices/instancesSlice'
|
||||
import composeInitialState from './initialState'
|
||||
import { ComposeState } from './types'
|
||||
|
||||
@ -40,9 +40,10 @@ const composeParseState = ({ type, incomingStatus }: Props): ComposeState => {
|
||||
}),
|
||||
visibility:
|
||||
incomingStatus.visibility ||
|
||||
getLocalAccountPreferences(store.getState())[
|
||||
getLocalAccount(store.getState())?.preferences[
|
||||
'posting:default:visibility'
|
||||
],
|
||||
] ||
|
||||
'public',
|
||||
...(incomingStatus.visibility === 'direct' && { visibilityLock: true })
|
||||
}
|
||||
case 'reply':
|
||||
|
@ -40,7 +40,7 @@ const composePost = async (
|
||||
|
||||
formData.append('visibility', composeState.visibility)
|
||||
|
||||
return client({
|
||||
return client<Mastodon.Status>({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: 'statuses',
|
||||
|
@ -20,7 +20,6 @@ const ScreenSharedRelationships: React.FC<Props> = ({
|
||||
params: { account, initialType }
|
||||
}
|
||||
}) => {
|
||||
console.log(account.id)
|
||||
const { mode } = useTheme()
|
||||
const navigation = useNavigation()
|
||||
|
||||
|
@ -3,11 +3,10 @@ import ComponentSeparator from '@components/Separator'
|
||||
import TimelineEmpty from '@components/Timelines/Timeline/Empty'
|
||||
import TimelineEnd from '@root/components/Timelines/Timeline/End'
|
||||
import { useScrollToTop } from '@react-navigation/native'
|
||||
import { relationshipsFetch } from '@utils/fetches/relationshipsFetch'
|
||||
import React, { useCallback, useMemo, useRef } from 'react'
|
||||
import { RefreshControl, StyleSheet } from 'react-native'
|
||||
import { FlatList } from 'react-native-gesture-handler'
|
||||
import { useInfiniteQuery } from 'react-query'
|
||||
import hookRelationships from '@utils/queryHooks/relationships'
|
||||
|
||||
export interface Props {
|
||||
id: Mastodon.Account['id']
|
||||
@ -15,8 +14,6 @@ export interface Props {
|
||||
}
|
||||
|
||||
const RelationshipsList: React.FC<Props> = ({ id, type }) => {
|
||||
const queryKey: QueryKey.Relationships = ['Relationships', type, { id }]
|
||||
|
||||
const {
|
||||
status,
|
||||
data,
|
||||
@ -25,7 +22,10 @@ const RelationshipsList: React.FC<Props> = ({ id, type }) => {
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage
|
||||
} = useInfiniteQuery(queryKey, relationshipsFetch, {
|
||||
} = hookRelationships({
|
||||
type,
|
||||
id,
|
||||
options: {
|
||||
getNextPageParam: lastPage => {
|
||||
return lastPage.length
|
||||
? {
|
||||
@ -34,6 +34,7 @@ const RelationshipsList: React.FC<Props> = ({ id, type }) => {
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
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 ComponentSeparator from '@root/components/Separator'
|
||||
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 { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { debounce } from 'lodash'
|
||||
@ -17,17 +17,15 @@ import {
|
||||
} from 'react-native'
|
||||
import { Chase } from 'react-native-animated-spinkit'
|
||||
import { TextInput } from 'react-native-gesture-handler'
|
||||
import { useQuery } from 'react-query'
|
||||
|
||||
const ScreenSharedSearch: React.FC = () => {
|
||||
const navigation = useNavigation()
|
||||
const { theme } = useTheme()
|
||||
const [searchTerm, setSearchTerm] = useState<string | undefined>()
|
||||
const { status, data, refetch } = useQuery(
|
||||
['Search', { term: searchTerm }],
|
||||
searchFetch,
|
||||
{ enabled: false }
|
||||
)
|
||||
const { status, data, refetch } = hookSearch({
|
||||
term: searchTerm,
|
||||
options: { enabled: false }
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const updateHeaderRight = () =>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||
import { HeaderLeft } from '@components/Header'
|
||||
import ScreenSharedAccount from '@screens/Shared/Account'
|
||||
import ScreenSharedAnnouncements from '@screens/Shared/Announcements'
|
||||
import ScreenSharedHashtag from '@screens/Shared/Hashtag'
|
||||
@ -9,7 +9,6 @@ import Compose from '@screens/Shared/Compose'
|
||||
import ScreenSharedSearch from '@screens/Shared/Search'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { View } from 'react-native'
|
||||
|
||||
const sharedScreens = (Stack: any) => {
|
||||
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
|
35
src/store.ts
35
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 createSecureStore from 'redux-persist-expo-securestore'
|
||||
|
||||
import instancesSlice from '@utils/slices/instancesSlice'
|
||||
import settingsSlice from '@utils/slices/settingsSlice'
|
||||
|
||||
const secureStorage = createSecureStore()
|
||||
|
||||
const prefix = 'mastodon_app'
|
||||
|
||||
const instancesPersistConfig = {
|
||||
key: 'instances',
|
||||
storage: secureStorage
|
||||
prefix,
|
||||
version: 1,
|
||||
storage: secureStorage,
|
||||
debug: true
|
||||
}
|
||||
|
||||
const settingsPersistConfig = {
|
||||
key: 'settings',
|
||||
prefix,
|
||||
storage: secureStorage
|
||||
}
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
const rootPersistConfig = {
|
||||
key: 'root',
|
||||
prefix,
|
||||
version: 0,
|
||||
storage: AsyncStorage
|
||||
}
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
instances: persistReducer(instancesPersistConfig, instancesSlice),
|
||||
settings: persistReducer(settingsPersistConfig, settingsSlice)
|
||||
},
|
||||
})
|
||||
|
||||
const store = configureStore({
|
||||
reducer: persistReducer(rootPersistConfig, rootReducer),
|
||||
middleware: getDefaultMiddleware({
|
||||
serializableCheck: {
|
||||
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,14 +1,18 @@
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
import { RootState } from '@root/store'
|
||||
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 InstancesState = {
|
||||
local: {
|
||||
url: string | undefined
|
||||
token: string | undefined
|
||||
export type InstanceLocal = {
|
||||
appData: {
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
}
|
||||
url: string
|
||||
token: string
|
||||
account: {
|
||||
id: Mastodon.Account['id'] | undefined
|
||||
id: Mastodon.Account['id']
|
||||
preferences: Mastodon.Preferences
|
||||
}
|
||||
notification: {
|
||||
@ -16,55 +20,46 @@ export type InstancesState = {
|
||||
latestTime?: Mastodon.Notification['created_at']
|
||||
}
|
||||
}
|
||||
|
||||
export type InstancesState = {
|
||||
local: {
|
||||
activeIndex: number | null
|
||||
instances: InstanceLocal[]
|
||||
}
|
||||
|
||||
remote: {
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
||||
const initialStateLocal: InstancesState['local'] = {
|
||||
url: undefined,
|
||||
token: undefined,
|
||||
account: {
|
||||
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({
|
||||
export const localUpdateAccountPreferences = createAsyncThunk(
|
||||
'instances/localUpdateAccountPreferences',
|
||||
async (): Promise<Mastodon.Preferences> => {
|
||||
const preferences = await client<Mastodon.Preferences>({
|
||||
method: 'get',
|
||||
instance: 'local',
|
||||
url: `preferences`
|
||||
})
|
||||
|
||||
return preferences as Mastodon.Preferences
|
||||
return Promise.resolve(preferences)
|
||||
}
|
||||
)
|
||||
|
||||
export const loginLocal = createAsyncThunk(
|
||||
'instances/loginLocal',
|
||||
export const localAddInstance = createAsyncThunk(
|
||||
'instances/localAddInstance',
|
||||
async ({
|
||||
url,
|
||||
token
|
||||
token,
|
||||
appData
|
||||
}: {
|
||||
url: InstancesState['local']['url']
|
||||
token: InstancesState['local']['token']
|
||||
}) => {
|
||||
const {
|
||||
body: { id }
|
||||
} = await client({
|
||||
url: InstanceLocal['url']
|
||||
token: InstanceLocal['token']
|
||||
appData: InstanceLocal['appData']
|
||||
}): Promise<InstanceLocal> => {
|
||||
const store = require('@root/store')
|
||||
const state = store.getState().instances
|
||||
|
||||
const { id } = await client<Mastodon.Account>({
|
||||
method: 'get',
|
||||
instance: 'remote',
|
||||
instanceDomain: url,
|
||||
@ -72,7 +67,16 @@ export const loginLocal = createAsyncThunk(
|
||||
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',
|
||||
instance: 'remote',
|
||||
instanceDomain: url,
|
||||
@ -80,60 +84,153 @@ export const loginLocal = createAsyncThunk(
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
})
|
||||
|
||||
return {
|
||||
return Promise.resolve({
|
||||
appData,
|
||||
url,
|
||||
token,
|
||||
account: {
|
||||
id,
|
||||
preferences
|
||||
},
|
||||
notification: {
|
||||
unread: false
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
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')
|
||||
}
|
||||
}
|
||||
} as InstancesState['local']
|
||||
}
|
||||
)
|
||||
|
||||
const instancesSlice = createSlice({
|
||||
name: 'instances',
|
||||
initialState: {
|
||||
local: initialStateLocal,
|
||||
export const instancesInitialState: InstancesState = {
|
||||
local: {
|
||||
activeIndex: null,
|
||||
instances: []
|
||||
},
|
||||
remote: {
|
||||
url: 'm.cmx.im'
|
||||
}
|
||||
} as InstancesState,
|
||||
}
|
||||
|
||||
const instancesSlice = createSlice({
|
||||
name: 'instances',
|
||||
initialState: instancesInitialState,
|
||||
reducers: {
|
||||
resetLocal: state => {
|
||||
state.local = initialStateLocal
|
||||
},
|
||||
updateNotification: (
|
||||
localUpdateActiveIndex: (
|
||||
state,
|
||||
action: PayloadAction<Partial<InstancesState['local']['notification']>>
|
||||
action: PayloadAction<NonNullable<InstancesState['local']['activeIndex']>>
|
||||
) => {
|
||||
state.local.notification = {
|
||||
...state.local.notification,
|
||||
if (action.payload < state.local.instances.length) {
|
||||
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
|
||||
}
|
||||
},
|
||||
remoteUpdate: (
|
||||
state,
|
||||
action: PayloadAction<InstancesState['remote']['url']>
|
||||
) => {
|
||||
state.remote.url = action.payload
|
||||
}
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder
|
||||
.addCase(loginLocal.fulfilled, (state, action) => {
|
||||
state.local = action.payload
|
||||
.addCase(localAddInstance.fulfilled, (state, action) => {
|
||||
state.local.instances.push(action.payload)
|
||||
state.local.activeIndex = state.local.instances.length - 1
|
||||
|
||||
analytics('login')
|
||||
})
|
||||
.addCase(updateLocalAccountPreferences.fulfilled, (state, action) => {
|
||||
state.local.account.preferences = action.payload
|
||||
.addCase(localAddInstance.rejected, (state, action) => {
|
||||
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 getLocalToken = (state: RootState) => state.instances.local.token
|
||||
export const getLocalNotification = (state: RootState) =>
|
||||
state.instances.local.notification
|
||||
export const getRemoteUrl = (state: RootState) => state.instances.remote.url
|
||||
export const getLocalAccountId = (state: RootState) =>
|
||||
state.instances.local.account.id
|
||||
export const getLocalAccountPreferences = (state: RootState) =>
|
||||
state.instances.local.account.preferences
|
||||
export const getLocalActiveIndex = ({ instances: { local } }: RootState) =>
|
||||
local.activeIndex
|
||||
export const getLocalInstances = ({ instances: { local } }: RootState) =>
|
||||
local.instances
|
||||
export const getLocalUrl = ({ instances: { local } }: RootState) =>
|
||||
local.activeIndex ? local.instances[local.activeIndex].url : undefined
|
||||
// export const getLocalToken = ({ instances: { local } }: RootState) =>
|
||||
// local && local.activeIndex && local.instances[local.activeIndex].token
|
||||
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
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user