Basic notification working

This commit is contained in:
Zhiyuan Zheng 2021-02-28 17:41:21 +01:00
parent 4eea2bf58c
commit 78898059cb
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
12 changed files with 246 additions and 137 deletions

View File

@ -118,6 +118,7 @@
"react-navigation": "^4.4.3",
"react-navigation-stack": "^2.10.2",
"react-test-renderer": "^16.13.1",
"typescript": "~4.1.3"
"typescript": "~4.1.3",
"uri-scheme": "^1.0.67"
}
}
}

View File

@ -71,7 +71,7 @@ declare namespace Nav {
'Tab-Local': undefined
'Tab-Public': undefined
'Tab-Compose': undefined
'Tab-Notifications': { id?: Mastodon.Notification['id'] }
'Tab-Notifications': undefined
'Tab-Me': undefined
}

View File

@ -8,18 +8,17 @@ import ScreenAnnouncements from '@screens/Announcements'
import ScreenCompose from '@screens/Compose'
import ScreenImagesViewer from '@screens/ImagesViewer'
import ScreenTabs from '@screens/Tabs'
import { useAnnouncementQuery } from '@utils/queryHooks/announcement'
import { updatePreviousTab } from '@utils/slices/contextsSlice'
import { connectInstancesPush } from '@utils/slices/instances/connectPush'
import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences'
import { getInstanceActive } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import { themes } from '@utils/styles/themes'
import * as Analytics from 'expo-firebase-analytics'
import * as Notifications from 'expo-notifications'
import { addScreenshotListener } from 'expo-screen-capture'
import React, { createRef, useCallback, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert, Linking, Platform, StatusBar } from 'react-native'
import { Alert, Platform, StatusBar } from 'react-native'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import Toast from 'react-native-toast-message'
import { useDispatch, useSelector } from 'react-redux'
@ -27,35 +26,6 @@ import * as Sentry from 'sentry-expo'
const Stack = createNativeStackNavigator<Nav.RootStackParamList>()
const linking = {
prefixes: ['tooot://', 'https://tooot.app'],
config: {
screens: {
'Screen-Tabs': {
screens: {
'Tab-Notifications': 'push/:id'
}
}
}
},
subscribe (listener: (arg0: string) => any) {
const onReceiveURL = ({ url }: { url: string }) => listener(url)
Linking.addEventListener('url', onReceiveURL)
const subscription = Notifications.addNotificationResponseReceivedListener(
response => {
const url = response.notification.request.content.data.url
console.log(url)
url && typeof url === 'string' && listener(url)
}
)
return () => {
Linking.removeEventListener('url', onReceiveURL)
subscription.remove()
}
}
}
export interface Props {
localCorrupt?: string
}
@ -87,6 +57,11 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
// }
// }, [isConnected, firstRender])
// Update Expo Token to server
useEffect(() => {
dispatch(connectInstancesPush())
}, [])
// Prevent screenshot alert
useEffect(() => {
const screenshotListener = addScreenshotListener(() =>
@ -116,23 +91,6 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
return showLocalCorrect()
}, [localCorrupt])
// On launch check if there is any unread announcements
useAnnouncementQuery({
showAll: false,
options: {
notifyOnChangeProps: [],
select: announcements =>
announcements.filter(announcement => !announcement.read),
onSuccess: data => {
if (data.length) {
navigationRef.current?.navigate('Screen-Announcements', {
showAll: false
})
}
}
}
})
// Lazily update users's preferences, for e.g. composing default visibility
useEffect(() => {
if (instanceActive !== -1) {
@ -175,7 +133,6 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
theme={themes[mode]}
onReady={navigationContainerOnReady}
onStateChange={navigationContainerOnStateChange}
linking={linking}
>
<Stack.Navigator initialRouteName='Screen-Tabs'>
<Stack.Screen

View File

@ -6,7 +6,10 @@ import * as WebBrowser from 'expo-web-browser'
const openLink = async (url: string) => {
switch (getSettingsBrowser(store.getState())) {
case 'internal':
await WebBrowser.openBrowserAsync(url)
await WebBrowser.openBrowserAsync(url, {
dismissButtonStyle: 'close',
enableBarCollapsing: true
})
break
case 'external':
await Linking.openURL(url)

View File

@ -1,3 +1,4 @@
import apiInstance from '@api/instance'
import useWebsocket from '@api/websocket'
import haptics from '@components/haptics'
import Icon from '@components/Icon'
@ -13,10 +14,14 @@ import {
getInstanceAccount,
getInstanceActive,
getInstanceNotification,
getInstances,
updateInstanceActive,
updateInstanceNotification
} from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useMemo } from 'react'
import * as Notifications from 'expo-notifications'
import { findIndex } from 'lodash'
import React, { useCallback, useEffect, useMemo } from 'react'
import { Platform } from 'react-native'
import FastImage from 'react-native-fast-image'
import { useDispatch, useSelector } from 'react-redux'
@ -38,10 +43,97 @@ export type ScreenTabsProp = StackScreenProps<
'Screen-Tabs'
>
const convertNotificationToToot = (
navigation: any,
id: Mastodon.Notification['id']
) => {
apiInstance<Mastodon.Notification>({
method: 'get',
url: `notifications/${id}`
}).then(({ body }) => {
// @ts-ignore
navigation.navigate('Tab-Notifications', {
screen: 'Tab-Notifications-Root'
})
if (body.status) {
// @ts-ignore
navigation.navigate('Tab-Notifications', {
screen: 'Tab-Shared-Toot',
params: { toot: body.status }
})
}
})
}
const Tab = createBottomTabNavigator<Nav.ScreenTabsStackParamList>()
const ScreenTabs = React.memo(
({ navigation }: ScreenTabsProp) => {
// Push notifications
const instances = useSelector(
getInstances,
(prev, next) => prev.length === next.length
)
const lastNotificationResponse = Notifications.useLastNotificationResponse()
useEffect(() => {
const subscription = Notifications.addNotificationResponseReceivedListener(
({ notification }) => {
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) {
dispatch(updateInstanceActive(instances[notificationIndex]))
}
if (payloadData?.notification_id) {
convertNotificationToToot(
navigation,
notification.request.content.data.notification_id as string
)
}
}
)
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])
const { mode, theme } = useTheme()
const dispatch = useDispatch()
const instanceActive = useSelector(getInstanceActive)

View File

@ -1,12 +1,16 @@
import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React from 'react'
import React, { useCallback } from 'react'
const ScreenMeBookmarks = React.memo(
() => {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Bookmarks' }]
return <Timeline queryKey={queryKey} />
const renderItem = useCallback(
({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
[]
)
return <Timeline queryKey={queryKey} customProps={{ renderItem }} />
},
() => true
)

View File

@ -1,12 +1,17 @@
import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React from 'react'
import React, { useCallback } from 'react'
const ScreenMeFavourites = React.memo(
() => {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Favourites' }]
const renderItem = useCallback(
({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
[]
)
return <Timeline queryKey={queryKey} />
return <Timeline queryKey={queryKey} customProps={{ renderItem }} />
},
() => true
)

View File

@ -1,18 +1,17 @@
import { MenuRow } from '@components/Menu'
import TimelineEmpty from '@components/Timeline/Empty'
import { StackScreenProps } from '@react-navigation/stack'
import { useListsQuery } from '@utils/queryHooks/lists'
import React, { useMemo } from 'react'
import React from 'react'
const ScreenMeLists: React.FC<StackScreenProps<
Nav.TabMeStackParamList,
'Tab-Me-Switch'
'Tab-Me-Lists'
>> = ({ navigation }) => {
const { status, data, refetch } = useListsQuery({})
const { data } = useListsQuery({})
const children = useMemo(() => {
if (status === 'success') {
return data?.map((d: Mastodon.List, i: number) => (
return (
<>
{data?.map((d: Mastodon.List, i: number) => (
<MenuRow
key={i}
iconFront='List'
@ -24,13 +23,9 @@ const ScreenMeLists: React.FC<StackScreenProps<
})
}
/>
))
} else {
return <TimelineEmpty status={status} refetch={refetch} />
}
}, [status])
return <>{children}</>
))}
</>
)
}
export default ScreenMeLists

View File

@ -1,6 +1,7 @@
import { MenuContainer, MenuRow } from '@components/Menu'
import { useNavigation } from '@react-navigation/native'
import { useAnnouncementQuery } from '@utils/queryHooks/announcement'
import { useListsQuery } from '@utils/queryHooks/lists'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@ -8,26 +9,54 @@ const Collections: React.FC = () => {
const { t, i18n } = useTranslation('meRoot')
const navigation = useNavigation()
const { data, isFetching } = useAnnouncementQuery({
showAll: true
})
const announcementContent = useMemo(() => {
if (data) {
if (data.length === 0) {
return t('content.collections.announcements.content.empty')
} else {
const amount = data.filter(announcement => !announcement.read).length
if (amount) {
return t('content.collections.announcements.content.unread', {
amount
})
} else {
return t('content.collections.announcements.content.read')
}
}
const listsQuery = useListsQuery({
options: {
notifyOnChangeProps: []
}
}, [data, i18n.language])
})
const rowLists = useMemo(() => {
if (listsQuery.isSuccess && listsQuery.data?.length) {
return (
<MenuRow
iconFront='List'
iconBack='ChevronRight'
title={t('content.collections.lists')}
onPress={() => navigation.navigate('Tab-Me-Lists')}
/>
)
}
}, [listsQuery.isSuccess, listsQuery.data, i18n.language])
const announcementsQuery = useAnnouncementQuery({
showAll: true,
options: {
notifyOnChangeProps: []
}
})
const rowAnnouncements = useMemo(() => {
if (announcementsQuery.isSuccess && announcementsQuery.data?.length) {
const amount = announcementsQuery.data.filter(
announcement => !announcement.read
).length
return (
<MenuRow
iconFront='Clipboard'
iconBack='ChevronRight'
title={t('content.collections.announcements.heading')}
content={
amount
? t('content.collections.announcements.content.unread', {
amount
})
: t('content.collections.announcements.content.read')
}
onPress={() =>
navigation.navigate('Screen-Announcements', { showAll: true })
}
/>
)
}
}, [announcementsQuery.isSuccess, announcementsQuery.data, i18n.language])
return (
<MenuContainer>
@ -49,24 +78,8 @@ const Collections: React.FC = () => {
title={t('content.collections.favourites')}
onPress={() => navigation.navigate('Tab-Me-Favourites')}
/>
<MenuRow
iconFront='List'
iconBack='ChevronRight'
title={t('content.collections.lists')}
onPress={() => navigation.navigate('Tab-Me-Lists')}
/>
<MenuRow
iconFront='Clipboard'
iconBack={data && data.length === 0 ? undefined : 'ChevronRight'}
title={t('content.collections.announcements.heading')}
content={announcementContent}
loading={isFetching}
onPress={() =>
data &&
data.length &&
navigation.navigate('Screen-Announcements', { showAll: true })
}
/>
{rowLists}
{rowAnnouncements}
</MenuContainer>
)
}

View File

@ -43,32 +43,32 @@ const TabNotifications = React.memo(
<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
}
}
]
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

@ -0,0 +1,34 @@
import apiGeneral from '@api/general'
import { createAsyncThunk } from '@reduxjs/toolkit'
import { RootState } from '@root/store'
import * as Notifications from 'expo-notifications'
import { PUSH_SERVER } from '../instancesSlice'
export const connectInstancesPush = createAsyncThunk(
'instances/connectPush',
async (_, { 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

@ -10286,6 +10286,11 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
uri-scheme@^1.0.67:
version "1.0.67"
resolved "https://registry.yarnpkg.com/uri-scheme/-/uri-scheme-1.0.67.tgz#a18c8752489967783eb94784b9d08adaf124d9eb"
integrity sha512-q0xH1d4w3fMaEJfpmA+mXN+L9fzV8bBZz96llWLG4TXO98vbMeXptDhvasQP30y/okwsQnO5gbVYJmeZW2OWIw==
urix@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"