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-redux": "^7.2.2",
"react-timeago": "^5.2.0",
"reconnecting-websocket": "^4.4.0",
"redux-persist": "^6.0.0",
"rn-placeholder": "^3.0.3",
"sentry-expo": "^3.0.4",
@ -121,4 +120,4 @@
"typescript": "~4.1.3",
"uri-scheme": "^1.0.67"
}
}
}

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

View File

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

View File

@ -1,22 +1,20 @@
import apiInstance from '@api/instance'
import useWebsocket from '@api/websocket'
import haptics from '@components/haptics'
import Icon from '@components/Icon'
import { displayMessage } from '@components/Message'
import {
BottomTabNavigationOptions,
createBottomTabNavigator
} from '@react-navigation/bottom-tabs'
import { NavigatorScreenParams } from '@react-navigation/native'
import { StackScreenProps } from '@react-navigation/stack'
import { useTimelineQuery } from '@utils/queryHooks/timeline'
import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getPreviousTab } from '@utils/slices/contextsSlice'
import {
getInstanceAccount,
getInstanceActive,
getInstanceNotification,
getInstances,
updateInstanceActive,
updateInstanceNotification
updateInstanceActive
} from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import * as Notifications from 'expo-notifications'
@ -24,6 +22,7 @@ import { findIndex } from 'lodash'
import React, { useCallback, useEffect, useMemo } from 'react'
import { Platform } from 'react-native'
import FastImage from 'react-native-fast-image'
import { useQueryClient } from 'react-query'
import { useDispatch, useSelector } from 'react-redux'
import TabLocal from './Tabs/Local'
import TabMe from './Tabs/Me'
@ -44,7 +43,7 @@ export type ScreenTabsProp = StackScreenProps<
>
const convertNotificationToToot = (
navigation: any,
navigation: StackNavigationProp<Nav.RootStackParamList, 'Screen-Tabs'>,
id: Mastodon.Notification['id']
) => {
apiInstance<Mastodon.Notification>({
@ -70,11 +69,48 @@ const Tab = createBottomTabNavigator<Nav.ScreenTabsStackParamList>()
const ScreenTabs = React.memo(
({ navigation }: ScreenTabsProp) => {
// Push notifications
const queryClient = useQueryClient()
const instances = useSelector(
getInstances,
(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(() => {
const subscription = Notifications.addNotificationResponseReceivedListener(
({ notification }) => {
@ -93,46 +129,13 @@ const ScreenTabs = React.memo(
if (notificationIndex !== -1) {
dispatch(updateInstanceActive(instances[notificationIndex]))
}
if (payloadData?.notification_id) {
convertNotificationToToot(
navigation,
notification.request.content.data.notification_id as string
)
if (payloadData.notification_id) {
convertNotificationToToot(navigation, payloadData.notification_id)
}
}
)
return () => subscription.remove()
// 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])
}, [instances])
const { mode, theme } = useTheme()
const dispatch = useDispatch()
@ -210,33 +213,6 @@ const ScreenTabs = React.memo(
)
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)
return (
@ -252,23 +228,7 @@ const ScreenTabs = React.memo(
component={composeComponent}
listeners={composeListeners}
/>
<Tab.Screen
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-Notifications' component={TabNotifications} />
<Tab.Screen name='Tab-Me' component={TabMe} />
</Tab.Navigator>
)

View File

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

View File

@ -3,10 +3,9 @@ import Timeline from '@components/Timeline'
import TimelineNotifications from '@components/Timeline/Notifications'
import sharedScreens from '@screens/Tabs/Shared/sharedScreens'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { updateInstanceNotification } from '@utils/slices/instancesSlice'
import React, { useCallback, useMemo } from 'react'
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 { useDispatch } from 'react-redux'
@ -39,39 +38,7 @@ const TabNotifications = React.memo(
[]
)
const children = useCallback(
({ navigation }) => (
<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
// }
// }
// ]
}}
/>
),
() => <Timeline queryKey={queryKey} customProps={{ renderItem }} />,
[]
)

View File

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

View File

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

View File

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

View File

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

View File

@ -1,39 +1,37 @@
import { createAsyncThunk } from '@reduxjs/toolkit'
import { RootState } from '@root/store'
import * as AuthSession from 'expo-auth-session'
import { Instance } from '../instancesSlice'
import { updateInstancePush } from './updatePush'
const removeInstance = createAsyncThunk(
'instances/remove',
async (index: number): Promise<number> => {
const { store } = require('@root/store')
const instances = (store.getState() as RootState).instances.instances
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')
}
async (instance: Instance, { dispatch }): Promise<Instance> => {
if (instance.push.global.value) {
dispatch(updateInstancePush(false))
}
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']
preferences: Mastodon.Preferences
}
notification: {
readTime?: Mastodon.Notification['created_at']
latestTime?: Mastodon.Notification['created_at']
}
push:
| {
global: { loading: boolean; value: true }
@ -129,16 +125,6 @@ const instancesSlice = createSlice({
...action.payload
}
},
updateInstanceNotification: (
{ instances },
action: PayloadAction<Partial<Instance['notification']>>
) => {
const activeIndex = findInstanceActive(instances)
instances[activeIndex].notification = {
...instances[activeIndex].notification,
...action.payload
}
},
updateInstanceDraft: (
{ instances },
action: PayloadAction<ComposeStateDraft>
@ -198,7 +184,16 @@ const instancesSlice = createSlice({
})
.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[state.instances.length - 1].active = true)
@ -310,13 +305,6 @@ export const getInstanceAccount = ({ instances: { instances } }: RootState) => {
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) => {
const instanceActive = findInstanceActive(instances)
return instanceActive !== -1 ? instances[instanceActive].push : null
@ -330,7 +318,6 @@ export const getInstanceDrafts = ({ instances: { instances } }: RootState) => {
export const {
updateInstanceActive,
updateInstanceAccount,
updateInstanceNotification,
updateInstanceDraft,
removeInstanceDraft
} = 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"
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:
version "2.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa"