1
0
mirror of https://github.com/tooot-app/app synced 2025-01-30 10:14:53 +01:00

With unread notifications

This commit is contained in:
Zhiyuan Zheng 2020-12-24 10:28:51 +01:00
parent 4461cd1fa4
commit b5f267e6bf
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
6 changed files with 109 additions and 32 deletions

View File

@ -67,6 +67,7 @@
"@types/react-redux": "^7.1.12", "@types/react-redux": "^7.1.12",
"@welldone-software/why-did-you-render": "^6.0.3", "@welldone-software/why-did-you-render": "^6.0.3",
"babel-plugin-module-resolver": "^4.1.0", "babel-plugin-module-resolver": "^4.1.0",
"chalk": "^4.1.0",
"typescript": "~4.0.0" "typescript": "~4.0.0"
}, },
"private": true "private": true

View File

@ -6,7 +6,7 @@ import {
} from '@react-navigation/native' } from '@react-navigation/native'
import { enableScreens } from 'react-native-screens' import { enableScreens } from 'react-native-screens'
import React, { useEffect, useRef } from 'react' import React, { useEffect, useMemo, useRef } from 'react'
import { StatusBar } from 'react-native' import { StatusBar } from 'react-native'
import Toast from 'react-native-toast-message' import Toast from 'react-native-toast-message'
import { Feather } from '@expo/vector-icons' import { Feather } from '@expo/vector-icons'
@ -23,12 +23,15 @@ import { toast, toastConfig } from '@components/toast'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { import {
getLocalNotification,
getLocalUrl, getLocalUrl,
updateLocalAccountPreferences updateLocalAccountPreferences,
updateNotification
} from '@utils/slices/instancesSlice' } from '@utils/slices/instancesSlice'
import { useQuery } from 'react-query' import { useInfiniteQuery, useQuery } from 'react-query'
import { announcementFetch } from './utils/fetches/announcementsFetch' import { announcementFetch } from './utils/fetches/announcementsFetch'
import client from './api/client' import client from './api/client'
import { timelineFetch } from './utils/fetches/timelineFetch'
enableScreens() enableScreens()
const Tab = createBottomTabNavigator<RootStackParamList>() const Tab = createBottomTabNavigator<RootStackParamList>()
@ -55,6 +58,7 @@ export const Index: React.FC<Props> = ({ localCorrupt }) => {
dark = 'light-content' dark = 'light-content'
} }
// On launch display login credentials corrupt information
useEffect(() => { useEffect(() => {
const showLocalCorrect = localCorrupt const showLocalCorrect = localCorrupt
? toast({ ? toast({
@ -67,27 +71,66 @@ export const Index: React.FC<Props> = ({ localCorrupt }) => {
return showLocalCorrect return showLocalCorrect
}, [localCorrupt]) }, [localCorrupt])
// On launch check if there is any unread announcements
const navigationRef = useRef<NavigationContainerRef>(null)
useEffect(() => {
localInstance &&
client({
method: 'get',
instance: 'local',
url: `announcements`
})
.then(({ body }: { body?: Mastodon.Announcement[] }) => {
if (body?.filter(announcement => !announcement.read).length) {
navigationRef.current?.navigate('Screen-Shared-Announcements')
}
})
.catch(() => {})
}, [])
// On launch check if there is any unread noficiations
const queryNotification = useInfiniteQuery(
['Notifications', {}] as QueryKey.Timeline,
timelineFetch,
{ enabled: localInstance ? true : false, cacheTime: 1000 * 30 }
)
const prevNotification = useSelector(getLocalNotification)
useEffect(() => {
if (queryNotification.data?.pages) {
const flattenData = queryNotification.data.pages.flatMap(d => [
...d?.toots
])
const latestNotificationTime = flattenData.length
? flattenData[0].created_at
: undefined
if (!prevNotification || !prevNotification.latestTime) {
dispatch(
updateNotification({
unread: true
})
)
} else if (
latestNotificationTime &&
new Date(prevNotification.latestTime) < new Date(latestNotificationTime)
) {
dispatch(
updateNotification({
unread: true,
latestTime: latestNotificationTime
})
)
}
}
}, [queryNotification.data?.pages])
// Lazily update users's preferences, for e.g. composing default visibility
useEffect(() => { useEffect(() => {
if (localInstance) { if (localInstance) {
dispatch(updateLocalAccountPreferences()) dispatch(updateLocalAccountPreferences())
} }
}, []) }, [])
const navigationRef = useRef<NavigationContainerRef>(null)
useEffect(() => {
client({
method: 'get',
instance: 'local',
url: `announcements`
})
.then(({ body }: { body?: Mastodon.Announcement[] }) => {
if (body?.filter(announcement => !announcement.read).length) {
navigationRef.current?.navigate('Screen-Shared-Announcements')
}
})
.catch(() => {})
}, [])
return ( return (
<> <>
<StatusBar barStyle={barStyle[mode]} /> <StatusBar barStyle={barStyle[mode]} />
@ -147,7 +190,7 @@ export const Index: React.FC<Props> = ({ localCorrupt }) => {
<Tab.Screen name='Screen-Public' component={ScreenPublic} /> <Tab.Screen name='Screen-Public' component={ScreenPublic} />
<Tab.Screen <Tab.Screen
name='Screen-Post' name='Screen-Post'
listeners={({ navigation, route }) => ({ listeners={({ navigation }) => ({
tabPress: e => { tabPress: e => {
e.preventDefault() e.preventDefault()
localInstance && localInstance &&
@ -162,6 +205,10 @@ export const Index: React.FC<Props> = ({ localCorrupt }) => {
<Tab.Screen <Tab.Screen
name='Screen-Notifications' name='Screen-Notifications'
component={ScreenNotifications} component={ScreenNotifications}
options={{
tabBarBadge: prevNotification.unread ? '' : undefined,
tabBarBadgeStyle: { transform: [{ scale: 0.5 }] }
}}
listeners={{ listeners={{
tabPress: e => { tabPress: e => {
if (!localInstance) { if (!localInstance) {

View File

@ -1,6 +1,9 @@
import axios from 'axios' import axios from 'axios'
import chalk from 'chalk'
import { store, RootState } from '@root/store' import { store, RootState } from '@root/store'
const ctx = new chalk.Instance({ level: 3 })
const client = async ({ const client = async ({
method, method,
instance, instance,
@ -25,13 +28,13 @@ const client = async ({
onUploadProgress?: (progressEvent: any) => void onUploadProgress?: (progressEvent: any) => void
}): Promise<any> => { }): Promise<any> => {
console.log( console.log(
'API call:', ctx.bgGreen.bold(' API ') +
'Method ->', ' ' +
method, method +
'Endpoint ->', ctx.green(' -> ') +
url, `/${url}` +
'Params ->', (params ? ctx.green(' -> ') : ''),
params params ? params : ''
) )
const state: RootState['instances'] = store.getState().instances const state: RootState['instances'] = store.getState().instances
const domain = const domain =

View File

@ -1,11 +1,12 @@
import React from 'react' import React, { useEffect } from 'react'
import { createNativeStackNavigator } from 'react-native-screens/native-stack' import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import Timeline from '@components/Timelines/Timeline' import Timeline from '@components/Timelines/Timeline'
import sharedScreens from '@screens/Shared/sharedScreens' import sharedScreens from '@screens/Shared/sharedScreens'
import { useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { RootState } from '@root/store' import { RootState } from '@root/store'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { updateNotification } from '@root/utils/slices/instancesSlice'
const Stack = createNativeStackNavigator() const Stack = createNativeStackNavigator()
@ -15,6 +16,12 @@ const ScreenNotifications: React.FC = () => {
(state: RootState) => state.instances.local.url (state: RootState) => state.instances.local.url
) )
// Clear notification unread, but keep the time
const dispatch = useDispatch()
useEffect(() => {
dispatch(updateNotification({ unread: false }))
}, [])
return ( return (
<Stack.Navigator <Stack.Navigator
screenOptions={{ headerTitle: t('notifications:heading') }} screenOptions={{ headerTitle: t('notifications:heading') }}

View File

@ -1,4 +1,4 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
import { RootState } from '@root/store' import { RootState } from '@root/store'
import client from '@api/client' import client from '@api/client'
@ -11,13 +11,17 @@ export type InstancesState = {
id: Mastodon.Account['id'] | undefined id: Mastodon.Account['id'] | undefined
preferences: Mastodon.Preferences preferences: Mastodon.Preferences
} }
notification: {
unread: boolean
latestTime?: Mastodon.Notification['created_at']
}
} }
remote: { remote: {
url: string url: string
} }
} }
const initialStateLocal = { const initialStateLocal: InstancesState['local'] = {
url: undefined, url: undefined,
token: undefined, token: undefined,
account: { account: {
@ -29,6 +33,10 @@ const initialStateLocal = {
'reading:expand:media': undefined, 'reading:expand:media': undefined,
'reading:expand:spoilers': undefined 'reading:expand:spoilers': undefined
} }
},
notification: {
unread: false,
latestTime: undefined
} }
} }
@ -94,6 +102,15 @@ const instancesSlice = createSlice({
reducers: { reducers: {
resetLocal: state => { resetLocal: state => {
state.local = initialStateLocal state.local = initialStateLocal
},
updateNotification: (
state,
action: PayloadAction<Partial<InstancesState['local']['notification']>>
) => {
state.local.notification = {
...state.local.notification,
...action.payload
}
} }
}, },
extraReducers: builder => { extraReducers: builder => {
@ -109,12 +126,14 @@ const instancesSlice = createSlice({
export const getLocalUrl = (state: RootState) => state.instances.local.url export const getLocalUrl = (state: RootState) => state.instances.local.url
export const getLocalToken = (state: RootState) => state.instances.local.token export const getLocalToken = (state: RootState) => state.instances.local.token
export const getLocalNotification = (state: RootState) =>
state.instances.local.notification
export const getRemoteUrl = (state: RootState) => state.instances.remote.url export const getRemoteUrl = (state: RootState) => state.instances.remote.url
export const getLocalAccountId = (state: RootState) => export const getLocalAccountId = (state: RootState) =>
state.instances.local.account.id state.instances.local.account.id
export const getLocalAccountPreferences = (state: RootState) => export const getLocalAccountPreferences = (state: RootState) =>
state.instances.local.account.preferences state.instances.local.account.preferences
export const { resetLocal } = instancesSlice.actions export const { resetLocal, updateNotification } = instancesSlice.actions
export default instancesSlice.reducer export default instancesSlice.reducer

View File

@ -2107,7 +2107,7 @@ chalk@^3.0.0:
ansi-styles "^4.1.0" ansi-styles "^4.1.0"
supports-color "^7.1.0" supports-color "^7.1.0"
chalk@^4.0.0: chalk@^4.0.0, chalk@^4.1.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==