From 78898059cb7fdea30aac2f51112606440fe48c19 Mon Sep 17 00:00:00 2001 From: Zhiyuan Zheng Date: Sun, 28 Feb 2021 17:41:21 +0100 Subject: [PATCH] Basic notification working --- package.json | 5 +- src/@types/react-navigation.d.ts | 2 +- src/Screens.tsx | 57 ++------------ src/components/openLink.ts | 5 +- src/screens/Tabs.tsx | 94 ++++++++++++++++++++++- src/screens/Tabs/Me/Bookmarks.tsx | 10 ++- src/screens/Tabs/Me/Favourites.tsx | 9 ++- src/screens/Tabs/Me/Lists.tsx | 23 +++--- src/screens/Tabs/Me/Root/Collections.tsx | 87 ++++++++++++--------- src/screens/Tabs/Notifications.tsx | 52 ++++++------- src/utils/slices/instances/connectPush.ts | 34 ++++++++ yarn.lock | 5 ++ 12 files changed, 246 insertions(+), 137 deletions(-) create mode 100644 src/utils/slices/instances/connectPush.ts diff --git a/package.json b/package.json index d5bb0594..8e183596 100644 --- a/package.json +++ b/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/src/@types/react-navigation.d.ts b/src/@types/react-navigation.d.ts index a9a01b8f..9006d853 100644 --- a/src/@types/react-navigation.d.ts +++ b/src/@types/react-navigation.d.ts @@ -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 } diff --git a/src/Screens.tsx b/src/Screens.tsx index fa3bc2bf..12f29d29 100644 --- a/src/Screens.tsx +++ b/src/Screens.tsx @@ -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() -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 = ({ 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 = ({ 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 = ({ localCorrupt }) => { theme={themes[mode]} onReady={navigationContainerOnReady} onStateChange={navigationContainerOnStateChange} - linking={linking} > { 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) diff --git a/src/screens/Tabs.tsx b/src/screens/Tabs.tsx index 2cbb2b00..bae16bb8 100644 --- a/src/screens/Tabs.tsx +++ b/src/screens/Tabs.tsx @@ -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({ + 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() 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) diff --git a/src/screens/Tabs/Me/Bookmarks.tsx b/src/screens/Tabs/Me/Bookmarks.tsx index 3272771d..20cf512a 100644 --- a/src/screens/Tabs/Me/Bookmarks.tsx +++ b/src/screens/Tabs/Me/Bookmarks.tsx @@ -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 + const renderItem = useCallback( + ({ item }) => , + [] + ) + return }, () => true ) diff --git a/src/screens/Tabs/Me/Favourites.tsx b/src/screens/Tabs/Me/Favourites.tsx index 53a5e304..58bcfe2a 100644 --- a/src/screens/Tabs/Me/Favourites.tsx +++ b/src/screens/Tabs/Me/Favourites.tsx @@ -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 }) => , + [] + ) - return + return }, () => true ) diff --git a/src/screens/Tabs/Me/Lists.tsx b/src/screens/Tabs/Me/Lists.tsx index 1bf7e7c2..352a4256 100644 --- a/src/screens/Tabs/Me/Lists.tsx +++ b/src/screens/Tabs/Me/Lists.tsx @@ -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> = ({ 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) => ( - )) - } else { - return - } - }, [status]) - - return <>{children} + ))} + + ) } export default ScreenMeLists diff --git a/src/screens/Tabs/Me/Root/Collections.tsx b/src/screens/Tabs/Me/Root/Collections.tsx index da4891de..b70f9061 100644 --- a/src/screens/Tabs/Me/Root/Collections.tsx +++ b/src/screens/Tabs/Me/Root/Collections.tsx @@ -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 ( + 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 ( + + navigation.navigate('Screen-Announcements', { showAll: true }) + } + /> + ) + } + }, [announcementsQuery.isSuccess, announcementsQuery.data, i18n.language]) return ( @@ -49,24 +78,8 @@ const Collections: React.FC = () => { title={t('content.collections.favourites')} onPress={() => navigation.navigate('Tab-Me-Favourites')} /> - navigation.navigate('Tab-Me-Lists')} - /> - - data && - data.length && - navigation.navigate('Screen-Announcements', { showAll: true }) - } - /> + {rowLists} + {rowAnnouncements} ) } diff --git a/src/screens/Tabs/Notifications.tsx b/src/screens/Tabs/Notifications.tsx index b6c7f773..4577e782 100644 --- a/src/screens/Tabs/Notifications.tsx +++ b/src/screens/Tabs/Notifications.tsx @@ -43,32 +43,32 @@ const TabNotifications = React.memo( { - 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 + // } + // } + // ] }} /> ), diff --git a/src/utils/slices/instances/connectPush.ts b/src/utils/slices/instances/connectPush.ts new file mode 100644 index 00000000..37735f6e --- /dev/null +++ b/src/utils/slices/instances/connectPush.ts @@ -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 => { + 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() + } + } +) diff --git a/yarn.lock b/yarn.lock index e8bcbf2c..2965d42a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"