tooot/src/utils/storage/actions.ts

372 lines
11 KiB
TypeScript

import { displayMessage } from '@components/Message'
import i18n from '@i18n/index'
import apiGeneral from '@utils/api/general'
import navigationRef from '@utils/navigation/navigationRef'
import { queryClient } from '@utils/queryHooks'
import log from '@utils/startup/log'
import { storage } from '@utils/storage'
import { Platform } from 'react-native'
import {
MMKV,
useMMKVBoolean,
useMMKVListener,
useMMKVNumber,
useMMKVObject,
useMMKVString
} from 'react-native-mmkv'
import { StorageAccount } from './account'
import { StorageGlobal } from './global'
export const getGlobalStorage = {
string: <T extends keyof StorageGlobal>(key: T) =>
storage.global.getString(key) as NonNullable<StorageGlobal[T]> extends string
? StorageGlobal[T]
: never,
number: <T extends keyof StorageGlobal>(key: T) =>
storage.global.getNumber(key) as NonNullable<StorageGlobal[T]> extends number
? StorageGlobal[T]
: never,
boolean: <T extends keyof StorageGlobal>(key: T) =>
storage.global.getBoolean(key) as NonNullable<StorageGlobal[T]> extends boolean
? StorageGlobal[T]
: never,
object: <T extends keyof StorageGlobal>(key: T) => {
const value = storage.global.getString(key)
if (value?.length) {
try {
return JSON.parse(value) as NonNullable<StorageGlobal[T]> extends object
? StorageGlobal[T]
: never
} catch {
return undefined
}
} else {
return undefined
}
}
}
export const useGlobalStorage = {
string: <T extends keyof StorageGlobal>(key: T) =>
useMMKVString(key, storage.global) as NonNullable<StorageGlobal[T]> extends string
? [StorageGlobal[T], (valud: StorageGlobal[T]) => void]
: never,
number: <T extends keyof StorageGlobal>(key: T) => {
if (Platform.OS === 'ios') {
return useMMKVNumber(key, storage.global) as NonNullable<StorageGlobal[T]> extends number
? [StorageGlobal[T], (valud: StorageGlobal[T]) => void]
: never
} else {
const [num, setNum] = useMMKVString(key, storage.global)
// @ts-ignore
return [parseInt(num), v => setNum(v.toString())] as NonNullable<
StorageGlobal[T]
> extends number
? [StorageGlobal[T], (valud: StorageGlobal[T]) => void]
: never
}
},
boolean: <T extends keyof StorageGlobal>(key: T) =>
useMMKVBoolean(key, storage.global) as NonNullable<StorageGlobal[T]> extends boolean
? [StorageGlobal[T], (valud: StorageGlobal[T]) => void]
: never,
object: <T extends keyof StorageGlobal>(key: T) =>
useMMKVObject(key, storage.global) as NonNullable<StorageGlobal[T]> extends object
? [StorageGlobal[T], (valud: StorageGlobal[T]) => void]
: never
}
export const setGlobalStorage = <T extends keyof StorageGlobal>(
key: T,
value: StorageGlobal[T]
) => {
const checkValue = (): string | number | boolean => {
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return value
} else {
return JSON.stringify(value)
}
}
if (value !== undefined) {
storage.global.set(key, checkValue())
} else {
storage.global.delete(key)
}
}
export const useGlobalStorageListener = (key: keyof StorageGlobal, func: () => void) =>
useMMKVListener(keyChanged => {
if (keyChanged === key) func()
})
export const getAccountStorage = {
string: <T extends keyof StorageAccount>(key: T) =>
storage.account?.getString(key) as NonNullable<StorageAccount[T]> extends string
? StorageAccount[T]
: never,
number: <T extends keyof StorageAccount>(key: T) =>
storage.account?.getNumber(key) as NonNullable<StorageAccount[T]> extends number
? StorageAccount[T]
: never,
boolean: <T extends keyof StorageAccount>(key: T) =>
storage.account?.getBoolean(key) as NonNullable<StorageAccount[T]> extends boolean
? StorageAccount[T]
: never,
object: <T extends keyof StorageAccount>(key: T) => {
const value = storage.account?.getString(key)
if (value?.length) {
try {
return JSON.parse(value) as NonNullable<StorageAccount[T]> extends object
? StorageAccount[T]
: never
} catch {
return undefined
}
} else {
return undefined
}
}
}
export const useAccountStorage = {
string: <T extends keyof StorageAccount>(key: T) =>
useMMKVString(key, storage.account) as NonNullable<StorageAccount[T]> extends string
? [StorageAccount[T], (valud: StorageAccount[T]) => void]
: never,
number: <T extends keyof StorageAccount>(key: T) =>
useMMKVNumber(key, storage.account) as NonNullable<StorageAccount[T]> extends number
? [StorageAccount[T], (valud: StorageAccount[T]) => void]
: never,
boolean: <T extends keyof StorageAccount>(key: T) =>
useMMKVBoolean(key, storage.account) as NonNullable<StorageAccount[T]> extends boolean
? [StorageAccount[T], (valud: StorageAccount[T]) => void]
: never,
object: <T extends keyof StorageAccount>(key: T) =>
useMMKVObject(key, storage.account) as NonNullable<StorageAccount[T]> extends object
? [StorageAccount[T], (valud: StorageAccount[T]) => void]
: never
}
export const setAccountStorage = <T extends keyof StorageAccount>(
kvs: { key: T; value: StorageAccount[T] }[],
account?: string
) => {
let temp: MMKV
if (account) {
temp = new MMKV({ id: account })
} else {
if (!storage.account) {
return null
}
temp = storage.account
}
for (const { key, value } of kvs) {
const checkValue = (): string | number | boolean => {
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
switch (key) {
case 'version':
return value.match(new RegExp(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)/))?.[0] || '0'
default:
return value
}
} else {
return JSON.stringify(value)
}
}
if (value !== undefined) {
temp.set(key, checkValue())
} else {
temp.delete(key)
}
}
}
export const getAccountDetails = <T extends Array<keyof StorageAccount>>(
keys: T,
account?: string
): Pick<StorageAccount, T[number]> | null => {
let temp: MMKV
if (account) {
temp = new MMKV({ id: account })
} else {
if (!storage.account) {
return null
}
temp = storage.account
}
const result = {}
for (const key of keys) {
switch (key) {
case 'auth.clientId':
case 'auth.clientSecret':
case 'auth.token':
case 'auth.domain':
case 'auth.account.acct':
case 'auth.account.domain':
case 'auth.account.id':
case 'auth.account.avatar_static':
// @ts-ignore
result[key] = temp.getString(key)
break
case 'preferences':
case 'notifications':
case 'push':
case 'page_local':
case 'page_me':
case 'drafts':
case 'emojis_frequent':
const value = temp.getString(key)
if (value?.length) {
// @ts-ignore
result[key] = JSON.parse(value)
} else {
// @ts-ignore
result[key] = undefined
}
break
}
}
// @ts-ignore
return result
}
export const generateAccountKey = ({
domain,
id
}: {
domain: Mastodon.Instance<'v1'>['uri'] | Mastodon.Instance<'v2'>['domain']
id: Mastodon.Account['id']
}) => `${domain}/${id}`
export const setAccount = async (account: string) => {
const temp = new MMKV({ id: account })
const token = temp.getString('auth.token')
const domain = temp.getString('auth.domain')
if (!token || !domain) {
await removeAccount(account)
return
}
log('log', 'setAccount', `binding storage of ${account}`)
storage.account = temp
setGlobalStorage('account.active', account)
await queryClient.resetQueries()
queryClient.clear()
await apiGeneral<Mastodon.Account>({
method: 'get',
domain,
url: 'api/v1/accounts/verify_credentials',
headers: {
Authorization: `Bearer ${token}`
}
})
.then(res => res.body)
.then(async a => {
storage.account?.set('auth.account.acct', a.acct)
storage.account?.set('auth.account.avatar_static', a.avatar_static)
log('log', 'setAccount', 'update details')
})
.catch(async error => {
if (error?.status && error.status == 401) {
log('log', 'setAccount', `unauthorised ${account}`)
await removeAccount(account)
}
})
}
export const removeAccount = async (account: string, warning: boolean = true) => {
const temp = new MMKV({ id: account })
if (warning) {
const acct = temp.getString('auth.account.acct')
const domain = temp.getString('auth.account.domain')
displayMessage({
message: i18n.t('screens:localCorrupt.message'),
...(acct && domain && { description: `@${acct}@${domain}` }),
type: 'danger'
})
}
// @ts-ignore
navigationRef.navigate('Screen-Tabs', { screen: 'Tab-Me' })
const revokeDetails = {
domain: temp.getString('auth.domain'),
client_id: temp.getString('auth.clientId'),
client_secret: temp.getString('auth.clientSecret'),
token: temp.getString('auth.token')
}
if (
revokeDetails.domain &&
revokeDetails.client_id &&
revokeDetails.client_secret &&
revokeDetails.token
) {
apiGeneral({
method: 'post',
domain: revokeDetails.domain,
url: 'oauth/revoke',
body: revokeDetails
})
}
const currAccounts: NonNullable<StorageGlobal['accounts']> =
getGlobalStorage.object('accounts') || []
const nextAccounts: NonNullable<StorageGlobal['accounts']> = currAccounts.filter(
a => a !== account
)
storage.global.set('accounts', JSON.stringify(nextAccounts))
if (nextAccounts.length) {
log('log', 'removeAccount', `trying next account ${nextAccounts[nextAccounts.length - 1]}`)
await setAccount(nextAccounts[nextAccounts.length - 1])
} else {
log('log', 'removeAccount', 'setting to undefined')
await queryClient.resetQueries()
queryClient.clear()
storage.account = undefined
setGlobalStorage('account.active', undefined)
}
new MMKV({ id: account }).clearAll()
}
export type ReadableAccountType = {
avatar_static: string
acct: string
key: string
active: boolean
}
export const getReadableAccounts = (): ReadableAccountType[] => {
const accountActive = getGlobalStorage.string('account.active')
const accounts = getGlobalStorage.object('accounts')
return (
accounts?.map(account => {
const details = getAccountDetails(
[
'auth.account.avatar_static',
'auth.account.acct',
'auth.account.domain',
'auth.domain',
'auth.account.id'
],
account
)
if (details) {
return {
avatar_static: details['auth.account.avatar_static'],
acct: `@${details['auth.account.acct']}@${details['auth.account.domain']}`,
key: generateAccountKey({
domain: details['auth.domain'],
id: details['auth.account.id']
}),
active: account === accountActive
}
} else {
return { avatar_static: '', acct: '', key: '', active: false }
}
}) || []
).filter(a => a.acct.length)
}