1
0
mirror of https://github.com/tooot-app/app synced 2025-04-22 22:27:37 +02:00

Ready for push feature

This commit is contained in:
Zhiyuan Zheng 2021-03-04 01:03:53 +01:00
parent a4a6e9316b
commit cc02626adb
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
17 changed files with 255 additions and 135 deletions

View File

@ -8,7 +8,7 @@ ensure_env_vars(
VERSIONS = read_json( json_path: "./package.json" )[:versions] VERSIONS = read_json( json_path: "./package.json" )[:versions]
ENVIRONMENT = ENV["TOOOT_ENVIRONMENT"] ENVIRONMENT = ENV["TOOOT_ENVIRONMENT"]
VERSION = "#{VERSIONS[:major]}.#{VERSIONS[:minor]}" VERSION = "#{VERSIONS[:major]}.#{VERSIONS[:minor]}"
RELEASE_CHANNEL = "#{VERSIONS[:major]}-#{ENVIRONMENT}" RELEASE_CHANNEL = "#{VERSIONS[:major]}-#{VERSIONS[:minor]}-#{ENVIRONMENT}"
BUILD_NUMBER = ENV["GITHUB_RUN_NUMBER"] BUILD_NUMBER = ENV["GITHUB_RUN_NUMBER"]
GITHUB_REPO = "tooot-app/app" GITHUB_REPO = "tooot-app/app"
case ENVIRONMENT case ENVIRONMENT

View File

@ -1,9 +1,9 @@
{ {
"name": "tooot", "name": "tooot",
"versions": { "versions": {
"native": "210201", "native": "210304",
"major": 0, "major": 0,
"minor": 5, "minor": 6,
"patch": 0, "patch": 0,
"expo": "40.0.0" "expo": "40.0.0"
}, },

View File

@ -8,10 +8,12 @@ import ScreenAnnouncements from '@screens/Announcements'
import ScreenCompose from '@screens/Compose' import ScreenCompose from '@screens/Compose'
import ScreenImagesViewer from '@screens/ImagesViewer' import ScreenImagesViewer from '@screens/ImagesViewer'
import ScreenTabs from '@screens/Tabs' import ScreenTabs from '@screens/Tabs'
import pushUseConnect from '@utils/push/useConnect'
import pushUseReceive from '@utils/push/useReceive'
import pushUseRespond from '@utils/push/useRespond'
import { updatePreviousTab } from '@utils/slices/contextsSlice' import { updatePreviousTab } from '@utils/slices/contextsSlice'
import { connectInstancesPush } from '@utils/slices/instances/connectPush'
import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences' import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences'
import { getInstanceActive } from '@utils/slices/instancesSlice' import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { themes } from '@utils/styles/themes' import { themes } from '@utils/styles/themes'
import * as Analytics from 'expo-firebase-analytics' import * as Analytics from 'expo-firebase-analytics'
@ -20,6 +22,7 @@ import React, { createRef, useCallback, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert, Platform, StatusBar } from 'react-native' import { Alert, Platform, StatusBar } from 'react-native'
import { createNativeStackNavigator } from 'react-native-screens/native-stack' import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import { useQueryClient } from 'react-query'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import * as Sentry from 'sentry-expo' import * as Sentry from 'sentry-expo'
@ -56,10 +59,15 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
// } // }
// }, [isConnected, firstRender]) // }, [isConnected, firstRender])
// Update Expo Token to server // Push hooks
useEffect(() => { const instances = useSelector(
dispatch(connectInstancesPush({ mode, t })) getInstances,
}, []) (prev, next) => prev.length === next.length
)
const queryClient = useQueryClient()
pushUseConnect({ navigationRef, mode, t, instances, dispatch })
pushUseReceive({ navigationRef, queryClient, instances })
pushUseRespond({ navigationRef, queryClient, instances, dispatch })
// Prevent screenshot alert // Prevent screenshot alert
useEffect(() => { useEffect(() => {

View File

@ -56,8 +56,7 @@ const displayMessage = ({
onPress, onPress,
...(mode && ...(mode &&
type && { type && {
renderFlashMessageIcon: props => { renderFlashMessageIcon: () => {
console.log(props)
return ( return (
<Icon <Icon
name={iconMapping[type]} name={iconMapping[type]}
@ -92,7 +91,7 @@ const Message = React.memo(
...StyleConstants.FontStyle.M, ...StyleConstants.FontStyle.M,
fontWeight: StyleConstants.Font.Weight.Bold fontWeight: StyleConstants.Font.Weight.Bold
}} }}
textStyle={{ color: theme.primary, ...StyleConstants.FontStyle.M }} textStyle={{ color: theme.primary, ...StyleConstants.FontStyle.S }}
// @ts-ignore // @ts-ignore
textProps={{ numberOfLines: 2 }} textProps={{ numberOfLines: 2 }}
/> />

View File

@ -209,7 +209,6 @@ const TimelineActions: React.FC<Props> = ({
: iconColorAction(status.reblogged) : iconColorAction(status.reblogged)
} }
size={StyleConstants.Font.Size.L} size={StyleConstants.Font.Size.L}
strokeWidth={status.reblogged ? 3 : undefined}
/> />
{status.reblogs_count > 0 && ( {status.reblogs_count > 0 && (
<Text <Text
@ -233,7 +232,6 @@ const TimelineActions: React.FC<Props> = ({
name='Heart' name='Heart'
color={iconColorAction(status.favourited)} color={iconColorAction(status.favourited)}
size={StyleConstants.Font.Size.L} size={StyleConstants.Font.Size.L}
strokeWidth={status.favourited ? 3 : undefined}
/> />
{status.favourites_count > 0 && ( {status.favourites_count > 0 && (
<Text <Text
@ -257,7 +255,6 @@ const TimelineActions: React.FC<Props> = ({
name='Bookmark' name='Bookmark'
color={iconColorAction(status.bookmarked)} color={iconColorAction(status.bookmarked)}
size={StyleConstants.Font.Size.L} size={StyleConstants.Font.Size.L}
strokeWidth={status.bookmarked ? 3 : undefined}
/> />
), ),
[status.bookmarked] [status.bookmarked]

View File

@ -1,6 +1,10 @@
export default { export default {
heading: 'Push Notification', heading: 'Push Notification',
content: { content: {
enable: {
direct: 'Enable push notification',
settings: 'Enable in settings'
},
global: { global: {
heading: 'Enable push notification', heading: 'Enable push notification',
description: "Messages are routed through tooot's server" description: "Messages are routed through tooot's server"
@ -10,6 +14,9 @@ export default {
description: description:
"Messages routed through tooot's server are encrypted, but you can choose to decode the message on the server. Our server source code is open source, and no log policy." "Messages routed through tooot's server are encrypted, but you can choose to decode the message on the server. Our server source code is open source, and no log policy."
}, },
default: {
heading: 'Default' // Android notification channel name only
},
follow: { follow: {
heading: 'New follower' heading: 'New follower'
}, },
@ -26,5 +33,9 @@ export default {
heading: 'Poll updates' heading: 'Poll updates'
}, },
howitworks: 'Learn how routing works' howitworks: 'Learn how routing works'
},
error: {
message: 'Push service error',
description: 'Please re-enable push notification in settings'
} }
} }

View File

@ -35,7 +35,7 @@ export default {
howitworks: '了解通知消息转发如何工作' howitworks: '了解通知消息转发如何工作'
}, },
error: { error: {
message: '推送服务错误', message: '推送服务错误',
description: '请在设置中重新尝试启用推送通知' description: '请在设置中重新尝试启用推送通知'
} }
} }

View File

@ -7,19 +7,19 @@ import {
import { NavigatorScreenParams } from '@react-navigation/native' import { NavigatorScreenParams } from '@react-navigation/native'
import { StackScreenProps } from '@react-navigation/stack' import { StackScreenProps } from '@react-navigation/stack'
import { getPreviousTab } from '@utils/slices/contextsSlice' import { getPreviousTab } from '@utils/slices/contextsSlice'
import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice' import {
getInstanceAccount,
getInstanceActive
} from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useMemo } from 'react' import React, { useCallback, useMemo } from 'react'
import { Platform } from 'react-native' import { Platform } from 'react-native'
import FastImage from 'react-native-fast-image' import FastImage from 'react-native-fast-image'
import { useQueryClient } from 'react-query' import { useSelector } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import TabLocal from './Tabs/Local' import TabLocal from './Tabs/Local'
import TabMe from './Tabs/Me' import TabMe from './Tabs/Me'
import TabNotifications from './Tabs/Notifications' import TabNotifications from './Tabs/Notifications'
import TabPublic from './Tabs/Public' import TabPublic from './Tabs/Public'
import pushReceive from './Tabs/utils/pushReceive'
import pushRespond from './Tabs/utils/pushRespond'
export type ScreenTabsParamList = { export type ScreenTabsParamList = {
'Tab-Local': NavigatorScreenParams<Nav.TabLocalStackParamList> 'Tab-Local': NavigatorScreenParams<Nav.TabLocalStackParamList>
@ -40,18 +40,12 @@ const ScreenTabs = React.memo(
({ navigation }: ScreenTabsProp) => { ({ navigation }: ScreenTabsProp) => {
const { mode, theme } = useTheme() const { mode, theme } = useTheme()
const queryClient = useQueryClient()
const dispatch = useDispatch()
const instanceActive = useSelector(getInstanceActive) const instanceActive = useSelector(getInstanceActive)
const instances = useSelector( const instanceAccount = useSelector(
getInstances, getInstanceAccount,
(prev, next) => prev.length === next.length (prev, next) => prev?.avatarStatic === next?.avatarStatic
) )
pushReceive({ navigation, queryClient, instances })
pushRespond({ navigation, queryClient, instances, dispatch })
const screenOptions = useCallback( const screenOptions = useCallback(
({ route }): BottomTabNavigationOptions => ({ ({ route }): BottomTabNavigationOptions => ({
tabBarVisible: instanceActive !== -1, tabBarVisible: instanceActive !== -1,
@ -77,7 +71,7 @@ const ScreenTabs = React.memo(
return instanceActive !== -1 ? ( return instanceActive !== -1 ? (
<FastImage <FastImage
source={{ source={{
uri: instances[instanceActive].account.avatarStatic uri: instanceAccount?.avatarStatic
}} }}
style={{ style={{
width: size, width: size,
@ -99,7 +93,7 @@ const ScreenTabs = React.memo(
} }
} }
}), }),
[instances, instanceActive] [instanceAccount, instanceActive]
) )
const tabBarOptions = useMemo( const tabBarOptions = useMemo(
() => ({ () => ({

View File

@ -1,31 +0,0 @@
import apiInstance from '@api/instance'
import { StackNavigationProp } from '@react-navigation/stack'
const pushNavigate = (
navigation: StackNavigationProp<Nav.RootStackParamList, 'Screen-Tabs'>,
id?: Mastodon.Notification['id']
) => {
// @ts-ignore
navigation.navigate('Tab-Notifications', {
screen: 'Tab-Notifications-Root'
})
if (!id) {
return
}
apiInstance<Mastodon.Notification>({
method: 'get',
url: `notifications/${id}`
}).then(({ body }) => {
if (body.status) {
// @ts-ignore
navigation.navigate('Tab-Notifications', {
screen: 'Tab-Shared-Toot',
params: { toot: body.status }
})
}
})
}
export default pushNavigate

View File

@ -5,10 +5,11 @@ import {
configureStore, configureStore,
getDefaultMiddleware getDefaultMiddleware
} from '@reduxjs/toolkit' } from '@reduxjs/toolkit'
import { InstancesV3 } from '@utils/migrations/instances/v3'
import contextsSlice from '@utils/slices/contextsSlice' import contextsSlice from '@utils/slices/contextsSlice'
import instancesSlice from '@utils/slices/instancesSlice' import instancesSlice from '@utils/slices/instancesSlice'
import settingsSlice from '@utils/slices/settingsSlice' import settingsSlice from '@utils/slices/settingsSlice'
import { persistReducer, persistStore } from 'redux-persist' import { createMigrate, persistReducer, persistStore } from 'redux-persist'
const secureStorage = createSecureStore() const secureStorage = createSecureStore()
@ -20,10 +21,40 @@ const contextsPersistConfig = {
storage: AsyncStorage storage: AsyncStorage
} }
const instancesMigration = {
4: (state: InstancesV3) => {
return {
instances: state.local.instances.map((instance, index) => {
// @ts-ignore
delete instance.notification
return {
...instance,
active: state.local.activeIndex === index,
push: {
global: { loading: false, value: false },
decode: { loading: false, value: false },
alerts: {
follow: { loading: false, value: true },
favourite: { loading: false, value: true },
reblog: { loading: false, value: true },
mention: { loading: false, value: true },
poll: { loading: false, value: true }
},
keys: undefined
}
}
})
}
}
}
const instancesPersistConfig = { const instancesPersistConfig = {
key: 'instances', key: 'instances',
prefix, prefix,
storage: secureStorage storage: secureStorage,
version: 4,
// @ts-ignore
migrate: createMigrate(instancesMigration)
} }
const settingsPersistConfig = { const settingsPersistConfig = {

View File

@ -0,0 +1,33 @@
type InstanceLocal = {
appData: {
clientId: string
clientSecret: string
}
url: string
token: string
uri: Mastodon.Instance['uri']
urls: Mastodon.Instance['urls']
max_toot_chars: number
account: {
id: Mastodon.Account['id']
acct: Mastodon.Account['acct']
avatarStatic: Mastodon.Account['avatar_static']
preferences: Mastodon.Preferences
}
notification: {
readTime?: Mastodon.Notification['created_at']
latestTime?: Mastodon.Notification['created_at']
}
drafts: any[]
}
export type InstancesV3 = {
local: {
activeIndex: number | null
instances: InstanceLocal[]
}
remote: {
url: string
}
}

View File

@ -0,0 +1,91 @@
import apiGeneral from '@api/general'
import { displayMessage } from '@components/Message'
import { NavigationContainerRef } from '@react-navigation/native'
import { Dispatch } from '@reduxjs/toolkit'
import {
disableAllPushes,
Instance,
PUSH_SERVER
} from '@utils/slices/instancesSlice'
import * as Notifications from 'expo-notifications'
import { useEffect } from 'react'
import { TFunction } from 'react-i18next'
export interface Params {
navigationRef: React.RefObject<NavigationContainerRef>
mode: 'light' | 'dark'
t: TFunction<'common'>
instances: Instance[]
dispatch: Dispatch<any>
}
const pushUseConnect = ({
navigationRef,
mode,
t,
instances,
dispatch
}: Params) => {
return useEffect(() => {
const connect = async () => {
const expoToken = (
await Notifications.getExpoPushTokenAsync({
experienceId: '@xmflsct/tooot'
})
).data
apiGeneral({
method: 'post',
domain: PUSH_SERVER,
url: 'v1/connect',
body: {
expoToken
}
}).catch(() => {
displayMessage({
mode,
type: 'error',
duration: 'long',
message: t('meSettingsPush:error.message'),
description: t('meSettingsPush:error.description'),
onPress: () => {
navigationRef.current?.navigate('Screen-Tabs', {
screen: 'Tab-Me',
params: {
screen: 'Tab-Me-Root'
}
})
navigationRef.current?.navigate('Screen-Tabs', {
screen: 'Tab-Me',
params: {
screen: 'Tab-Me-Settings'
}
})
}
})
dispatch(disableAllPushes())
instances.forEach(instance => {
if (instance.push.global.value) {
apiGeneral<{}>({
method: 'delete',
domain: instance.url,
url: 'api/v1/push/subscription',
headers: {
Authorization: `Bearer ${instance.token}`
}
}).catch(() => console.log('error!!!'))
}
})
})
}
const pushEnabled = instances.filter(instance => instance.push.global.value)
if (pushEnabled.length) {
connect()
}
}, [instances])
}
export default pushUseConnect

View File

@ -0,0 +1,35 @@
import apiInstance from '@api/instance'
import { NavigationContainerRef } from '@react-navigation/native'
const pushUseNavigate = (
navigationRef: React.RefObject<NavigationContainerRef>,
id?: Mastodon.Notification['id']
) => {
navigationRef.current?.navigate('Screen-Tabs', {
screen: 'Tab-Notifications',
params: {
screen: 'Tab-Notifications-Root'
}
})
if (!id) {
return
}
apiInstance<Mastodon.Notification>({
method: 'get',
url: `notifications/${id}`
}).then(({ body }) => {
if (body.status) {
navigationRef.current?.navigate('Screen-Tabs', {
screen: 'Tab-Notifications',
params: {
screen: 'Tab-Shared-Toot',
params: { toot: body.status }
}
})
}
})
}
export default pushUseNavigate

View File

@ -1,5 +1,5 @@
import { displayMessage } from '@components/Message' import { displayMessage } from '@components/Message'
import { StackNavigationProp } from '@react-navigation/stack' import { NavigationContainerRef } from '@react-navigation/native'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { Instance, updateInstanceActive } from '@utils/slices/instancesSlice' import { Instance, updateInstanceActive } from '@utils/slices/instancesSlice'
import * as Notifications from 'expo-notifications' import * as Notifications from 'expo-notifications'
@ -7,15 +7,15 @@ import { findIndex } from 'lodash'
import { useEffect } from 'react' import { useEffect } from 'react'
import { QueryClient } from 'react-query' import { QueryClient } from 'react-query'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import pushNavigate from './pushNavigate' import pushUseNavigate from './useNavigate'
export interface Params { export interface Params {
navigation: StackNavigationProp<Nav.RootStackParamList, 'Screen-Tabs'> navigationRef: React.RefObject<NavigationContainerRef>
queryClient: QueryClient queryClient: QueryClient
instances: Instance[] instances: Instance[]
} }
const pushReceive = ({ navigation, queryClient, instances }: Params) => { const pushUseReceive = ({ navigationRef, queryClient, instances }: Params) => {
const dispatch = useDispatch() const dispatch = useDispatch()
return useEffect(() => { return useEffect(() => {
@ -46,7 +46,7 @@ const pushReceive = ({ navigation, queryClient, instances }: Params) => {
if (notificationIndex !== -1) { if (notificationIndex !== -1) {
dispatch(updateInstanceActive(instances[notificationIndex])) dispatch(updateInstanceActive(instances[notificationIndex]))
} }
pushNavigate(navigation, payloadData.notification_id) pushUseNavigate(navigationRef, payloadData.notification_id)
} }
}) })
} }
@ -55,4 +55,4 @@ const pushReceive = ({ navigation, queryClient, instances }: Params) => {
}, [instances]) }, [instances])
} }
export default pushReceive export default pushUseReceive

View File

@ -1,4 +1,4 @@
import { StackNavigationProp } from '@react-navigation/stack' import { NavigationContainerRef } from '@react-navigation/native'
import { Dispatch } from '@reduxjs/toolkit' import { Dispatch } from '@reduxjs/toolkit'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { Instance, updateInstanceActive } from '@utils/slices/instancesSlice' import { Instance, updateInstanceActive } from '@utils/slices/instancesSlice'
@ -6,17 +6,17 @@ import * as Notifications from 'expo-notifications'
import { findIndex } from 'lodash' import { findIndex } from 'lodash'
import { useEffect } from 'react' import { useEffect } from 'react'
import { QueryClient } from 'react-query' import { QueryClient } from 'react-query'
import pushNavigate from './pushNavigate' import pushUseNavigate from './useNavigate'
export interface Params { export interface Params {
navigation: StackNavigationProp<Nav.RootStackParamList, 'Screen-Tabs'> navigationRef: React.RefObject<NavigationContainerRef>
queryClient: QueryClient queryClient: QueryClient
instances: Instance[] instances: Instance[]
dispatch: Dispatch<any> dispatch: Dispatch<any>
} }
const pushRespond = ({ const pushUseRespond = ({
navigation, navigationRef,
queryClient, queryClient,
instances, instances,
dispatch dispatch
@ -44,11 +44,11 @@ const pushRespond = ({
if (notificationIndex !== -1) { if (notificationIndex !== -1) {
dispatch(updateInstanceActive(instances[notificationIndex])) dispatch(updateInstanceActive(instances[notificationIndex]))
} }
pushNavigate(navigation, payloadData.notification_id) pushUseNavigate(navigationRef, payloadData.notification_id)
} }
) )
return () => subscription.remove() return () => subscription.remove()
}, [instances]) }, [instances])
} }
export default pushRespond export default pushUseRespond

View File

@ -1,38 +0,0 @@
import apiGeneral from '@api/general'
import { createAsyncThunk } from '@reduxjs/toolkit'
import { RootState } from '@root/store'
import * as Notifications from 'expo-notifications'
import { TFunction } from 'react-i18next'
import { PUSH_SERVER } from '../instancesSlice'
export const connectInstancesPush = createAsyncThunk(
'instances/connectPush',
async (
{ mode, t }: { mode: 'light' | 'dark'; t: TFunction<'common'> },
{ getState }
): Promise<any> => {
const state = getState() as RootState
const pushEnabled = state.instances.instances.filter(
instance => instance.push.global.value
)
if (pushEnabled.length) {
const expoToken = (
await Notifications.getExpoPushTokenAsync({
experienceId: '@xmflsct/tooot'
})
).data
return apiGeneral({
method: 'post',
domain: PUSH_SERVER,
url: 'v1/connect',
body: {
expoToken
}
})
} else {
return Promise.resolve()
}
}
)

View File

@ -1,11 +1,9 @@
import analytics from '@components/analytics' import analytics from '@components/analytics'
import { displayMessage } from '@components/Message'
import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { RootState } from '@root/store' import { RootState } from '@root/store'
import { ComposeStateDraft } from '@screens/Compose/utils/types' import { ComposeStateDraft } from '@screens/Compose/utils/types'
import { findIndex } from 'lodash' import { findIndex } from 'lodash'
import addInstance from './instances/add' import addInstance from './instances/add'
import { connectInstancesPush } from './instances/connectPush'
import removeInstance from './instances/remove' import removeInstance from './instances/remove'
import { updateAccountPreferences } from './instances/updateAccountPreferences' import { updateAccountPreferences } from './instances/updateAccountPreferences'
import { updateInstancePush } from './instances/updatePush' import { updateInstancePush } from './instances/updatePush'
@ -150,6 +148,13 @@ const instancesSlice = createSlice({
instances[activeIndex].drafts = instances[activeIndex].drafts?.filter( instances[activeIndex].drafts = instances[activeIndex].drafts?.filter(
draft => draft.timestamp !== action.payload draft => draft.timestamp !== action.payload
) )
},
disableAllPushes: ({ instances }) => {
instances = instances.map(instance => {
let newInstance = instance
newInstance.push.global.value = false
return newInstance
})
} }
}, },
extraReducers: builder => { extraReducers: builder => {
@ -266,22 +271,6 @@ const instancesSlice = createSlice({
action.meta.arg.changed action.meta.arg.changed
].loading = true ].loading = true
}) })
// If Expo token does not exist on the server, disable all existing ones
.addCase(connectInstancesPush.rejected, (state, action) => {
state.instances = state.instances.map(instance => {
let newInstance = instance
newInstance.push.global.value = false
displayMessage({
mode: action.meta.arg.mode,
type: 'error',
autoHide: false,
message: action.meta.arg.t('meSettingsPush:error.message'),
description: action.meta.arg.t('meSettingsPush:error.description')
})
return newInstance
})
})
} }
}) })
@ -337,7 +326,8 @@ export const {
updateInstanceActive, updateInstanceActive,
updateInstanceAccount, updateInstanceAccount,
updateInstanceDraft, updateInstanceDraft,
removeInstanceDraft removeInstanceDraft,
disableAllPushes
} = instancesSlice.actions } = instancesSlice.actions
export default instancesSlice.reducer export default instancesSlice.reducer