1
0
mirror of https://github.com/tooot-app/app synced 2025-02-27 09:07:51 +01:00

Removed webhook notification

This commit is contained in:
Zhiyuan Zheng 2021-03-01 00:28:14 +01:00
parent b20b75f22e
commit 32aaf08574
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
14 changed files with 114 additions and 260 deletions

View File

@ -83,7 +83,6 @@
"react-query": "^3.12.0", "react-query": "^3.12.0",
"react-redux": "^7.2.2", "react-redux": "^7.2.2",
"react-timeago": "^5.2.0", "react-timeago": "^5.2.0",
"reconnecting-websocket": "^4.4.0",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"rn-placeholder": "^3.0.3", "rn-placeholder": "^3.0.3",
"sentry-expo": "^3.0.4", "sentry-expo": "^3.0.4",

View File

@ -1,56 +0,0 @@
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import {
getInstance,
updateInstanceNotification
} from '@utils/slices/instancesSlice'
import { useEffect, useRef } from 'react'
import { useQueryClient } from 'react-query'
import { useDispatch, useSelector } from 'react-redux'
import ReconnectingWebSocket from 'reconnecting-websocket'
const useWebsocket = ({
stream,
event
}: {
stream: Mastodon.WebSocketStream
event: 'update' | 'delete' | 'notification'
}) => {
const queryClient = useQueryClient()
const dispatch = useDispatch()
const localInstance = useSelector(
getInstance,
(prev, next) =>
prev?.urls.streaming_api === next?.urls.streaming_api &&
prev?.token === next?.token
)
const rws = useRef<ReconnectingWebSocket>()
useEffect(() => {
if (!localInstance) {
return
}
rws.current = new ReconnectingWebSocket(
`${localInstance.urls.streaming_api}/api/v1/streaming?stream=${stream}&access_token=${localInstance.token}`
)
rws.current.addEventListener('message', ({ data }) => {
const message: Mastodon.WebSocket = JSON.parse(data)
if (message.event === event) {
switch (message.event) {
case 'notification':
const payload: Mastodon.Notification = JSON.parse(message.payload)
dispatch(
updateInstanceNotification({ latestTime: payload.created_at })
)
const queryKey: QueryKeyTimeline = [
'Timeline',
{ page: 'Notifications' }
]
queryClient.invalidateQueries(queryKey)
break
}
}
})
}, [localInstance?.urls.streaming_api, localInstance?.token])
}
export default useWebsocket

View File

@ -7,6 +7,7 @@ import FlashMessage, { showMessage } from 'react-native-flash-message'
import haptics from './haptics' import haptics from './haptics'
const displayMessage = ({ const displayMessage = ({
duration = 'short',
autoHide = true, autoHide = true,
message, message,
description, description,
@ -15,6 +16,7 @@ const displayMessage = ({
type type
}: }:
| { | {
duration?: 'short' | 'long'
autoHide?: boolean autoHide?: boolean
message: string message: string
description?: string description?: string
@ -23,6 +25,7 @@ const displayMessage = ({
type?: undefined type?: undefined
} }
| { | {
duration?: 'short' | 'long'
autoHide?: boolean autoHide?: boolean
message: string message: string
description?: string description?: string
@ -46,6 +49,7 @@ const displayMessage = ({
} }
showMessage({ showMessage({
duration: duration === 'short' ? 1500 : 3000,
autoHide, autoHide,
message, message,
description, description,
@ -80,7 +84,7 @@ const Message = React.memo(
backgroundColor: theme.background, backgroundColor: theme.background,
shadowColor: theme.primary, shadowColor: theme.primary,
shadowOffset: { width: 0, height: 0 }, shadowOffset: { width: 0, height: 0 },
shadowOpacity: mode === 'light' ? 0.16 : 0.32, shadowOpacity: mode === 'light' ? 0.16 : 0.24,
shadowRadius: 4 shadowRadius: 4
}} }}
titleStyle={{ titleStyle={{

View File

@ -1,6 +1,7 @@
import ComponentSeparator from '@components/Separator' import ComponentSeparator from '@components/Separator'
import { useScrollToTop } from '@react-navigation/native' import { useScrollToTop } from '@react-navigation/native'
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
import { getInstanceActive } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useCallback, useRef } from 'react' import React, { RefObject, useCallback, useRef } from 'react'
@ -15,6 +16,7 @@ import Animated, {
useAnimatedScrollHandler, useAnimatedScrollHandler,
useSharedValue useSharedValue
} from 'react-native-reanimated' } from 'react-native-reanimated'
import { useSelector } from 'react-redux'
import TimelineEmpty from './Timeline/Empty' import TimelineEmpty from './Timeline/Empty'
import TimelineFooter from './Timeline/Footer' import TimelineFooter from './Timeline/Footer'
import TimelineRefresh, { import TimelineRefresh, {
@ -40,6 +42,9 @@ const Timeline: React.FC<Props> = ({
disableInfinity = false, disableInfinity = false,
customProps customProps
}) => { }) => {
// Switching account update timeline
useSelector(getInstanceActive)
const { theme } = useTheme() const { theme } = useTheme()
const { const {

View File

@ -1,22 +1,20 @@
import apiInstance from '@api/instance' import apiInstance from '@api/instance'
import useWebsocket from '@api/websocket'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { displayMessage } from '@components/Message'
import { import {
BottomTabNavigationOptions, BottomTabNavigationOptions,
createBottomTabNavigator createBottomTabNavigator
} from '@react-navigation/bottom-tabs' } from '@react-navigation/bottom-tabs'
import { NavigatorScreenParams } from '@react-navigation/native' import { NavigatorScreenParams } from '@react-navigation/native'
import { StackScreenProps } from '@react-navigation/stack' import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack'
import { useTimelineQuery } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getPreviousTab } from '@utils/slices/contextsSlice' import { getPreviousTab } from '@utils/slices/contextsSlice'
import { import {
getInstanceAccount, getInstanceAccount,
getInstanceActive, getInstanceActive,
getInstanceNotification,
getInstances, getInstances,
updateInstanceActive, updateInstanceActive
updateInstanceNotification
} from '@utils/slices/instancesSlice' } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import * as Notifications from 'expo-notifications' import * as Notifications from 'expo-notifications'
@ -24,6 +22,7 @@ import { findIndex } from 'lodash'
import React, { useCallback, useEffect, useMemo } from 'react' import React, { useCallback, useEffect, 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 { useDispatch, 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'
@ -44,7 +43,7 @@ export type ScreenTabsProp = StackScreenProps<
> >
const convertNotificationToToot = ( const convertNotificationToToot = (
navigation: any, navigation: StackNavigationProp<Nav.RootStackParamList, 'Screen-Tabs'>,
id: Mastodon.Notification['id'] id: Mastodon.Notification['id']
) => { ) => {
apiInstance<Mastodon.Notification>({ apiInstance<Mastodon.Notification>({
@ -70,11 +69,48 @@ const Tab = createBottomTabNavigator<Nav.ScreenTabsStackParamList>()
const ScreenTabs = React.memo( const ScreenTabs = React.memo(
({ navigation }: ScreenTabsProp) => { ({ navigation }: ScreenTabsProp) => {
// Push notifications // Push notifications
const queryClient = useQueryClient()
const instances = useSelector( const instances = useSelector(
getInstances, getInstances,
(prev, next) => prev.length === next.length (prev, next) => prev.length === next.length
) )
const lastNotificationResponse = Notifications.useLastNotificationResponse() useEffect(() => {
const subscription = Notifications.addNotificationReceivedListener(
notification => {
const queryKey: QueryKeyTimeline = [
'Timeline',
{ page: 'Notifications' }
]
queryClient.invalidateQueries(queryKey)
const payloadData = notification.request.content.data as {
notification_id?: string
instanceUrl: string
accountId: string
}
const notificationIndex = findIndex(
instances,
instance =>
instance.url === payloadData.instanceUrl &&
instance.account.id === payloadData.accountId
)
if (notificationIndex !== -1 && payloadData.notification_id) {
displayMessage({
duration: 'long',
message: notification.request.content.title!,
description: notification.request.content.body!,
onPress: () =>
convertNotificationToToot(
navigation,
// @ts-ignore Typescript is wrong
payloadData.notification_id
)
})
}
}
)
return () => subscription.remove()
}, [instances])
useEffect(() => { useEffect(() => {
const subscription = Notifications.addNotificationResponseReceivedListener( const subscription = Notifications.addNotificationResponseReceivedListener(
({ notification }) => { ({ notification }) => {
@ -93,46 +129,13 @@ const ScreenTabs = React.memo(
if (notificationIndex !== -1) { if (notificationIndex !== -1) {
dispatch(updateInstanceActive(instances[notificationIndex])) dispatch(updateInstanceActive(instances[notificationIndex]))
} }
if (payloadData?.notification_id) { if (payloadData.notification_id) {
convertNotificationToToot( convertNotificationToToot(navigation, payloadData.notification_id)
navigation,
notification.request.content.data.notification_id as string
)
} }
} }
) )
return () => subscription.remove() return () => subscription.remove()
}, [instances])
// if (
// lastNotificationResponse &&
// lastNotificationResponse.actionIdentifier ===
// Notifications.DEFAULT_ACTION_IDENTIFIER
// ) {
// const payloadData = lastNotificationResponse.notification.request
// .content.data as {
// notification_id?: string
// instanceUrl: string
// accountId: string
// }
// const notificationIndex = findIndex(
// instances,
// instance =>
// instance.url === payloadData.instanceUrl &&
// instance.account.id === payloadData.accountId
// )
// if (notificationIndex !== -1) {
// dispatch(updateInstanceActive(instances[notificationIndex]))
// }
// if (payloadData?.notification_id) {
// convertNotificationToToot(
// navigation,
// lastNotificationResponse.notification.request.content.data
// .notification_id as string
// )
// }
// }
}, [instances, lastNotificationResponse])
const { mode, theme } = useTheme() const { mode, theme } = useTheme()
const dispatch = useDispatch() const dispatch = useDispatch()
@ -210,33 +213,6 @@ const ScreenTabs = React.memo(
) )
const composeComponent = useCallback(() => null, []) const composeComponent = useCallback(() => null, [])
// On launch check if there is any unread noficiations
useTimelineQuery({
page: 'Notifications',
options: {
enabled: instanceActive !== -1,
notifyOnChangeProps: [],
select: data => {
if (data.pages[0].body.length) {
dispatch(
updateInstanceNotification({
// @ts-ignore
latestTime: data.pages[0].body[0].created_at
})
)
}
return data
}
}
})
useWebsocket({ stream: 'user', event: 'notification' })
const localNotification = useSelector(
getInstanceNotification,
(prev, next) =>
prev?.readTime === next?.readTime &&
prev?.latestTime === next?.latestTime
)
const previousTab = useSelector(getPreviousTab, () => true) const previousTab = useSelector(getPreviousTab, () => true)
return ( return (
@ -252,23 +228,7 @@ const ScreenTabs = React.memo(
component={composeComponent} component={composeComponent}
listeners={composeListeners} listeners={composeListeners}
/> />
<Tab.Screen <Tab.Screen name='Tab-Notifications' component={TabNotifications} />
name='Tab-Notifications'
component={TabNotifications}
options={{
tabBarBadge: localNotification?.latestTime
? !localNotification.readTime ||
new Date(localNotification.readTime) <
new Date(localNotification.latestTime)
? ''
: undefined
: undefined,
tabBarBadgeStyle: {
transform: [{ scale: 0.5 }],
backgroundColor: theme.red
}
}}
/>
<Tab.Screen name='Tab-Me' component={TabMe} /> <Tab.Screen name='Tab-Me' component={TabMe} />
</Tab.Navigator> </Tab.Navigator>
) )

View File

@ -1,7 +1,7 @@
import Button from '@components/Button' import Button from '@components/Button'
import haptics from '@root/components/haptics' import haptics from '@root/components/haptics'
import removeInstance from '@utils/slices/instances/remove' import removeInstance from '@utils/slices/instances/remove'
import { getInstanceActive } from '@utils/slices/instancesSlice' import { getInstance, getInstanceActive } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -13,7 +13,7 @@ const Logout: React.FC = () => {
const { t } = useTranslation('meRoot') const { t } = useTranslation('meRoot')
const dispatch = useDispatch() const dispatch = useDispatch()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const instanceActive = useSelector(getInstanceActive) const instance = useSelector(getInstance)
return ( return (
<Button <Button
@ -33,9 +33,11 @@ const Logout: React.FC = () => {
text: t('content.logout.alert.buttons.logout'), text: t('content.logout.alert.buttons.logout'),
style: 'destructive' as const, style: 'destructive' as const,
onPress: () => { onPress: () => {
haptics('Success') if (instance) {
queryClient.clear() haptics('Success')
dispatch(removeInstance(instanceActive)) queryClient.clear()
dispatch(removeInstance(instance))
}
} }
}, },
{ {

View File

@ -3,10 +3,9 @@ import Timeline from '@components/Timeline'
import TimelineNotifications from '@components/Timeline/Notifications' import TimelineNotifications from '@components/Timeline/Notifications'
import sharedScreens from '@screens/Tabs/Shared/sharedScreens' import sharedScreens from '@screens/Tabs/Shared/sharedScreens'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { updateInstanceNotification } from '@utils/slices/instancesSlice'
import React, { useCallback, useMemo } from 'react' import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Platform, ViewToken } from 'react-native' import { Platform } from 'react-native'
import { createNativeStackNavigator } from 'react-native-screens/native-stack' import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
@ -39,39 +38,7 @@ const TabNotifications = React.memo(
[] []
) )
const children = useCallback( const children = useCallback(
({ navigation }) => ( () => <Timeline queryKey={queryKey} customProps={{ renderItem }} />,
<Timeline
queryKey={queryKey}
customProps={{
renderItem
// viewabilityConfigCallbackPairs: [
// {
// onViewableItemsChanged: ({
// viewableItems
// }: {
// viewableItems: ViewToken[]
// }) => {
// if (
// navigation.isFocused() &&
// viewableItems.length &&
// viewableItems[0].index === 0
// ) {
// dispatch(
// updateInstanceNotification({
// readTime: viewableItems[0].item.created_at
// })
// )
// }
// },
// viewabilityConfig: {
// minimumViewTime: 100,
// itemVisiblePercentThreshold: 60
// }
// }
// ]
}}
/>
),
[] []
) )

View File

@ -3,7 +3,7 @@ import NetInfo from '@react-native-community/netinfo'
import { store } from '@root/store' import { store } from '@root/store'
import removeInstance from '@utils/slices/instances/remove' import removeInstance from '@utils/slices/instances/remove'
import { import {
getInstanceActive, getInstance,
updateInstanceAccount updateInstanceAccount
} from '@utils/slices/instancesSlice' } from '@utils/slices/instancesSlice'
import log from './log' import log from './log'
@ -14,11 +14,11 @@ const netInfo = async (): Promise<{
}> => { }> => {
log('log', 'netInfo', 'initializing') log('log', 'netInfo', 'initializing')
const netInfo = await NetInfo.fetch() const netInfo = await NetInfo.fetch()
const activeIndex = getInstanceActive(store.getState()) const instance = getInstance(store.getState())
if (netInfo.isConnected) { if (netInfo.isConnected) {
log('log', 'netInfo', 'network connected') log('log', 'netInfo', 'network connected')
if (activeIndex !== -1) { if (instance) {
log('log', 'netInfo', 'checking locally stored credentials') log('log', 'netInfo', 'checking locally stored credentials')
return apiInstance<Mastodon.Account>({ return apiInstance<Mastodon.Account>({
method: 'get', method: 'get',
@ -26,12 +26,9 @@ const netInfo = async (): Promise<{
}) })
.then(res => { .then(res => {
log('log', 'netInfo', 'local credential check passed') log('log', 'netInfo', 'local credential check passed')
if ( if (res.body.id !== instance.account.id) {
res.body.id !==
store.getState().instances.instances[activeIndex].account.id
) {
log('error', 'netInfo', 'local id does not match remote id') log('error', 'netInfo', 'local id does not match remote id')
store.dispatch(removeInstance(activeIndex)) store.dispatch(removeInstance(instance))
return Promise.resolve({ connected: true, corruputed: '' }) return Promise.resolve({ connected: true, corruputed: '' })
} else { } else {
store.dispatch( store.dispatch(
@ -50,7 +47,7 @@ const netInfo = async (): Promise<{
typeof error.status === 'number' && typeof error.status === 'number' &&
error.status === 401 error.status === 401
) { ) {
store.dispatch(removeInstance(activeIndex)) store.dispatch(removeInstance(instance))
} }
return Promise.resolve({ return Promise.resolve({
connected: true, connected: true,

View File

@ -5,11 +5,13 @@ const push = () => {
log('log', 'Push', 'initializing') log('log', 'Push', 'initializing')
Notifications.setNotificationHandler({ Notifications.setNotificationHandler({
handleNotification: async () => ({ handleNotification: async () => ({
shouldShowAlert: true, shouldShowAlert: false,
shouldPlaySound: false, shouldPlaySound: false,
shouldSetBadge: false shouldSetBadge: false
}) })
}) })
Notifications.setBadgeCountAsync(0)
Notifications.dismissAllNotificationsAsync()
} }
export default push export default push

View File

@ -70,10 +70,6 @@ const addInstance = createAsyncThunk(
avatarStatic: avatar_static, avatarStatic: avatar_static,
preferences preferences
}, },
notification: {
readTime: undefined,
latestTime: undefined
},
push: { push: {
global: { loading: false, value: false }, global: { loading: false, value: false },
decode: { loading: false, value: false }, decode: { loading: false, value: false },

View File

@ -85,8 +85,6 @@ const pushRegister = async (
accountId, accountId,
accountFull accountFull
}) })
console.log('endpoint', serverRes.body.endpoint)
console.log('token', instance?.token)
const alerts = instancePush.alerts const alerts = instancePush.alerts
const formData = new FormData() const formData = new FormData()

View File

@ -1,39 +1,37 @@
import { createAsyncThunk } from '@reduxjs/toolkit' import { createAsyncThunk } from '@reduxjs/toolkit'
import { RootState } from '@root/store'
import * as AuthSession from 'expo-auth-session' import * as AuthSession from 'expo-auth-session'
import { Instance } from '../instancesSlice'
import { updateInstancePush } from './updatePush'
const removeInstance = createAsyncThunk( const removeInstance = createAsyncThunk(
'instances/remove', 'instances/remove',
async (index: number): Promise<number> => { async (instance: Instance, { dispatch }): Promise<Instance> => {
const { store } = require('@root/store') if (instance.push.global.value) {
const instances = (store.getState() as RootState).instances.instances dispatch(updateInstancePush(false))
if (index !== -1) {
const currentInstance = instances[index]
let revoked = undefined
try {
revoked = await AuthSession.revokeAsync(
{
clientId: currentInstance.appData.clientId,
clientSecret: currentInstance.appData.clientSecret,
token: currentInstance.token,
scopes: ['read', 'write', 'follow', 'push']
},
{
revocationEndpoint: `https://${currentInstance.url}/oauth/revoke`
}
)
} catch {
console.warn('Revoking error')
}
if (!revoked) {
console.warn('Revoking error')
}
} }
return Promise.resolve(index) let revoked = undefined
try {
revoked = await AuthSession.revokeAsync(
{
clientId: instance.appData.clientId,
clientSecret: instance.appData.clientSecret,
token: instance.token,
scopes: ['read', 'write', 'follow', 'push']
},
{
revocationEndpoint: `https://${instance.url}/oauth/revoke`
}
)
} catch {
console.warn('Revoking error')
}
if (!revoked) {
console.warn('Revoking error')
}
return Promise.resolve(instance)
} }
) )

View File

@ -29,10 +29,6 @@ export type Instance = {
avatarStatic: Mastodon.Account['avatar_static'] avatarStatic: Mastodon.Account['avatar_static']
preferences: Mastodon.Preferences preferences: Mastodon.Preferences
} }
notification: {
readTime?: Mastodon.Notification['created_at']
latestTime?: Mastodon.Notification['created_at']
}
push: push:
| { | {
global: { loading: boolean; value: true } global: { loading: boolean; value: true }
@ -129,16 +125,6 @@ const instancesSlice = createSlice({
...action.payload ...action.payload
} }
}, },
updateInstanceNotification: (
{ instances },
action: PayloadAction<Partial<Instance['notification']>>
) => {
const activeIndex = findInstanceActive(instances)
instances[activeIndex].notification = {
...instances[activeIndex].notification,
...action.payload
}
},
updateInstanceDraft: ( updateInstanceDraft: (
{ instances }, { instances },
action: PayloadAction<ComposeStateDraft> action: PayloadAction<ComposeStateDraft>
@ -198,7 +184,16 @@ const instancesSlice = createSlice({
}) })
.addCase(removeInstance.fulfilled, (state, action) => { .addCase(removeInstance.fulfilled, (state, action) => {
state.instances.splice(action.payload, 1) state.instances = state.instances.filter(instance => {
if (
instance.url === action.payload.url &&
instance.account.id === action.payload.account.id
) {
return false
} else {
return true
}
})
state.instances.length && state.instances.length &&
(state.instances[state.instances.length - 1].active = true) (state.instances[state.instances.length - 1].active = true)
@ -310,13 +305,6 @@ export const getInstanceAccount = ({ instances: { instances } }: RootState) => {
return instanceActive !== -1 ? instances[instanceActive].account : null return instanceActive !== -1 ? instances[instanceActive].account : null
} }
export const getInstanceNotification = ({
instances: { instances }
}: RootState) => {
const instanceActive = findInstanceActive(instances)
return instanceActive !== -1 ? instances[instanceActive].notification : null
}
export const getInstancePush = ({ instances: { instances } }: RootState) => { export const getInstancePush = ({ instances: { instances } }: RootState) => {
const instanceActive = findInstanceActive(instances) const instanceActive = findInstanceActive(instances)
return instanceActive !== -1 ? instances[instanceActive].push : null return instanceActive !== -1 ? instances[instanceActive].push : null
@ -330,7 +318,6 @@ export const getInstanceDrafts = ({ instances: { instances } }: RootState) => {
export const { export const {
updateInstanceActive, updateInstanceActive,
updateInstanceAccount, updateInstanceAccount,
updateInstanceNotification,
updateInstanceDraft, updateInstanceDraft,
removeInstanceDraft removeInstanceDraft
} = instancesSlice.actions } = instancesSlice.actions

View File

@ -8916,11 +8916,6 @@ realpath-native@^2.0.0:
resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-2.0.0.tgz#7377ac429b6e1fd599dc38d08ed942d0d7beb866" resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-2.0.0.tgz#7377ac429b6e1fd599dc38d08ed942d0d7beb866"
integrity sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q== integrity sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==
reconnecting-websocket@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783"
integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==
redent@^2.0.0: redent@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa" resolved "https://registry.yarnpkg.com/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa"