Improve push experience

This commit is contained in:
Zhiyuan Zheng 2021-03-02 01:17:06 +01:00
parent 82cefdc80c
commit ea018a71fa
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
38 changed files with 417 additions and 218 deletions

View File

@ -1,8 +1,5 @@
<manifest <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.xmflsct.app.tooot">
xmlns:tools="http://schemas.android.com/tools"
package="com.xmflsct.app.tooot"
>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/> <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
@ -12,30 +9,17 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application <application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:requestLegacyExternalStorage="true">
android:name=".MainApplication" <!-- [Custom] Expo Notifications -->
android:label="@string/app_name" <meta-data android:name="expo.modules.notifications.default_notification_icon" android:resource="@drawable/ic_stat_notifications" />
android:icon="@mipmap/ic_launcher" <!-- [Custom] End Expo Notifications -->
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="true"
android:theme="@style/AppTheme"
android:requestLegacyExternalStorage="true"
>
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://exp.host/@xmflsct/tooot"/> <meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://exp.host/@xmflsct/tooot"/>
<meta-data android:name="expo.modules.updates.EXPO_SDK_VERSION" android:value="${expoSDK}"/> <meta-data android:name="expo.modules.updates.EXPO_SDK_VERSION" android:value="${expoSDK}"/>
<meta-data android:name="expo.modules.updates.EXPO_RELEASE_CHANNEL" android:value="${releaseChannel}"/> <meta-data android:name="expo.modules.updates.EXPO_RELEASE_CHANNEL" android:value="${releaseChannel}"/>
<meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/> <meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/> <meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/> <meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<activity <activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:screenOrientation="portrait">
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.App.SplashScreen"
android:screenOrientation="portrait"
>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>

View File

@ -24,6 +24,7 @@ public class BasePackageList {
new expo.modules.lineargradient.LinearGradientPackage(), new expo.modules.lineargradient.LinearGradientPackage(),
new expo.modules.localization.LocalizationPackage(), new expo.modules.localization.LocalizationPackage(),
new expo.modules.location.LocationPackage(), new expo.modules.location.LocationPackage(),
new expo.modules.notifications.NotificationsPackage(),
new expo.modules.permissions.PermissionsPackage(), new expo.modules.permissions.PermissionsPackage(),
new expo.modules.screencapture.ScreenCapturePackage(), new expo.modules.screencapture.ScreenCapturePackage(),
new expo.modules.securestore.SecureStorePackage(), new expo.modules.securestore.SecureStorePackage(),

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -10,9 +10,10 @@ import sentry from '@root/startup/sentry'
import { persistor, store } from '@root/store' import { persistor, store } from '@root/store'
import { getSettingsLanguage } from '@utils/slices/settingsSlice' import { getSettingsLanguage } from '@utils/slices/settingsSlice'
import ThemeManager from '@utils/styles/ThemeManager' import ThemeManager from '@utils/styles/ThemeManager'
import * as Notifications from 'expo-notifications'
import * as SplashScreen from 'expo-splash-screen' import * as SplashScreen from 'expo-splash-screen'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { LogBox, Platform } from 'react-native' import { AppState, LogBox, Platform } from 'react-native'
import { enableScreens } from 'react-native-screens' import { enableScreens } from 'react-native-screens'
import { QueryClient, QueryClientProvider } from 'react-query' import { QueryClient, QueryClientProvider } from 'react-query'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
@ -39,6 +40,17 @@ const App: React.FC = () => {
log('log', 'App', 'rendering App') log('log', 'App', 'rendering App')
const [localCorrupt, setLocalCorrupt] = useState<string>() const [localCorrupt, setLocalCorrupt] = useState<string>()
useEffect(() => {
AppState.addEventListener('change', () => {
Notifications.setBadgeCountAsync(0)
Notifications.dismissAllNotificationsAsync()
})
return () => {
AppState.removeEventListener('change', () => {})
}
}, [])
useEffect(() => { useEffect(() => {
const delaySplash = async () => { const delaySplash = async () => {
log('log', 'App', 'delay splash') log('log', 'App', 'delay splash')

View File

@ -109,14 +109,11 @@ const GracefullyImage = React.memo(
</Pressable> </Pressable>
) )
}, },
(prev, next) => { (prev, next) =>
let skipUpdate = true prev.hidden === next.hidden &&
skipUpdate = prev.hidden === next.hidden prev.uri.preview === next.uri.preview &&
skipUpdate = prev.uri.preview === next.uri.preview prev.uri.original === next.uri.original &&
skipUpdate = prev.uri.original === next.uri.original prev.uri.remote === next.uri.remote
skipUpdate = prev.uri.remote === next.uri.remote
return false
}
) )
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@ -3,9 +3,9 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { ColorDefinitions } from '@utils/styles/themes' import { ColorDefinitions } from '@utils/styles/themes'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { StyleSheet, Switch, Text, View } from 'react-native' import { StyleSheet, Text, View } from 'react-native'
import { Flow } from 'react-native-animated-spinkit' import { Flow } from 'react-native-animated-spinkit'
import { State, TapGestureHandler } from 'react-native-gesture-handler' import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler'
export interface Props { export interface Props {
iconFront?: any iconFront?: any

View File

@ -14,6 +14,9 @@ export default {
description: description:
'经由tooot服务器中转的通知消息已被加密但可以允许tooot服务器解密并转发消息。tooot消息服务器源码开源且不留存任何日志。' '经由tooot服务器中转的通知消息已被加密但可以允许tooot服务器解密并转发消息。tooot消息服务器源码开源且不留存任何日志。'
}, },
default: {
heading: '默认通知' // Android notification channel name only
},
follow: { follow: {
heading: '新关注者' heading: '新关注者'
}, },
@ -24,10 +27,10 @@ export default {
heading: '嘟文被转嘟' heading: '嘟文被转嘟'
}, },
mention: { mention: {
heading: '提及你' heading: '嘟文提及你'
}, },
poll: { poll: {
heading: '投票' heading: '投票更新'
}, },
howitworks: '了解通知消息转发如何工作' howitworks: '了解通知消息转发如何工作'
}, },

View File

@ -319,7 +319,7 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
<KeyboardAvoidingView <KeyboardAvoidingView
style={styles.base} style={styles.base}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'android' ? 63 : 0} keyboardVerticalOffset={Platform.OS === 'android' ? 30 : 0}
> >
<SafeAreaView <SafeAreaView
style={styles.base} style={styles.base}

View File

@ -95,7 +95,7 @@ const ComposeAttachments: React.FC = () => {
useEffect(() => { useEffect(() => {
if ( if (
snapToOffsets.length > snapToOffsets.length >
(prevOffsets.current ? prevOffsets.current?.length : 0) (prevOffsets.current ? prevOffsets.current.length : 0)
) { ) {
flatListRef.current?.scrollToOffset({ flatListRef.current?.scrollToOffset({
offset: offset:

View File

@ -18,13 +18,11 @@ const ComposeRootHeader: React.FC = () => {
return ( return (
<> <>
{instanceActive !== -1 && {instanceActive !== -1 && localInstances.length > 1 && (
localInstances.length && <View style={styles.postingAs}>
localInstances.length > 1 && ( <ComposePostingAs />
<View style={styles.postingAs}> </View>
<ComposePostingAs /> )}
</View>
)}
{composeState.spoiler.active ? <ComposeSpoilerInput /> : null} {composeState.spoiler.active ? <ComposeSpoilerInput /> : null}
<ComposeTextInput /> <ComposeTextInput />
</> </>

View File

@ -1,25 +1,15 @@
import apiInstance from '@api/instance'
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 { StackNavigationProp, StackScreenProps } from '@react-navigation/stack' import { StackScreenProps } from '@react-navigation/stack'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getPreviousTab } from '@utils/slices/contextsSlice' import { getPreviousTab } from '@utils/slices/contextsSlice'
import { import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
getInstanceAccount,
getInstanceActive,
getInstances,
updateInstanceActive
} from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import * as Notifications from 'expo-notifications' import React, { useCallback, useMemo } from 'react'
import { findIndex } from 'lodash'
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 { useQueryClient } from 'react-query'
@ -28,6 +18,8 @@ import TabLocal from './Tabs/Local'
import TabMe from './Tabs/Me' import TabMe from './Tabs/Me'
import TabNotifications from './Tabs/Notifications' import TabNotifications from './Tabs/Notifications'
import TabPublic from './Tabs/Public' import TabPublic from './Tabs/Public'
import pushReceive from './Tabs/utils/pushReceive'
import pushRespond from './Tabs/utils/pushRespond'
export type ScreenTabsParamList = { export type ScreenTabsParamList = {
'Tab-Local': NavigatorScreenParams<Nav.TabLocalStackParamList> 'Tab-Local': NavigatorScreenParams<Nav.TabLocalStackParamList>
@ -42,108 +34,23 @@ export type ScreenTabsProp = StackScreenProps<
'Screen-Tabs' 'Screen-Tabs'
> >
const convertNotificationToToot = (
navigation: StackNavigationProp<Nav.RootStackParamList, 'Screen-Tabs'>,
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 Tab = createBottomTabNavigator<Nav.ScreenTabsStackParamList>()
const ScreenTabs = React.memo( const ScreenTabs = React.memo(
({ navigation }: ScreenTabsProp) => { ({ navigation }: ScreenTabsProp) => {
// Push notifications const { mode, theme } = useTheme()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const dispatch = useDispatch()
const instanceActive = useSelector(getInstanceActive)
const instances = useSelector( const instances = useSelector(
getInstances, getInstances,
(prev, next) => prev.length === next.length (prev, next) => prev.length === next.length
) )
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( pushReceive({ navigation, queryClient, instances })
instances, pushRespond({ navigation, queryClient, instances, dispatch })
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 }) => {
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, payloadData.notification_id)
}
}
)
return () => subscription.remove()
}, [instances])
const { mode, theme } = useTheme()
const dispatch = useDispatch()
const instanceActive = useSelector(getInstanceActive)
const localAccount = useSelector(
getInstanceAccount,
(prev, next) => prev?.avatarStatic === next?.avatarStatic
)
const screenOptions = useCallback( const screenOptions = useCallback(
({ route }): BottomTabNavigationOptions => ({ ({ route }): BottomTabNavigationOptions => ({
@ -169,7 +76,9 @@ const ScreenTabs = React.memo(
case 'Tab-Me': case 'Tab-Me':
return instanceActive !== -1 ? ( return instanceActive !== -1 ? (
<FastImage <FastImage
source={{ uri: localAccount?.avatarStatic }} source={{
uri: instances[instanceActive].account.avatarStatic
}}
style={{ style={{
width: size, width: size,
height: size, height: size,
@ -190,7 +99,7 @@ const ScreenTabs = React.memo(
} }
} }
}), }),
[instanceActive, localAccount?.avatarStatic] [instances, instanceActive]
) )
const tabBarOptions = useMemo( const tabBarOptions = useMemo(
() => ({ () => ({

View File

@ -17,29 +17,25 @@ import { AppState, Linking } from 'react-native'
const ScreenMeSettingsPush: React.FC = () => { const ScreenMeSettingsPush: React.FC = () => {
const { t } = useTranslation('meSettingsPush') const { t } = useTranslation('meSettingsPush')
const [appStateVisible, setAppStateVisible] = useState(AppState.currentState)
useEffect(() => {
AppState.addEventListener('change', state => setAppStateVisible(state))
return () => {
AppState.removeEventListener('change', state => setAppStateVisible(state))
}
}, [])
const [pushEnabled, setPushEnabled] = useState<boolean>()
const [pushCanAskAgain, setPushCanAskAgain] = useState<boolean>()
useEffect(() => {
const checkPush = async () => {
const settings = await Notifications.getPermissionsAsync()
layoutAnimation()
setPushEnabled(settings.granted)
setPushCanAskAgain(settings.canAskAgain)
}
checkPush()
}, [appStateVisible])
const dispatch = useDispatch() const dispatch = useDispatch()
const instancePush = useSelector(getInstancePush) const instancePush = useSelector(getInstancePush)
const [pushEnabled, setPushEnabled] = useState<boolean>()
const [pushCanAskAgain, setPushCanAskAgain] = useState<boolean>()
const checkPush = async () => {
const settings = await Notifications.getPermissionsAsync()
layoutAnimation()
setPushEnabled(settings.granted)
setPushCanAskAgain(settings.canAskAgain)
}
useEffect(() => {
checkPush()
AppState.addEventListener('change', () => checkPush())
return () => {
AppState.removeEventListener('change', () => {})
}
}, [])
const isLoading = instancePush?.global.loading || instancePush?.decode.loading const isLoading = instancePush?.global.loading || instancePush?.decode.loading
const alerts = useMemo(() => { const alerts = useMemo(() => {

View File

@ -24,6 +24,8 @@ const Collections: React.FC = () => {
onPress={() => navigation.navigate('Tab-Me-Lists')} onPress={() => navigation.navigate('Tab-Me-Lists')}
/> />
) )
} else {
return null
} }
}, [listsQuery.isSuccess, listsQuery.data, i18n.language]) }, [listsQuery.isSuccess, listsQuery.data, i18n.language])
@ -55,6 +57,8 @@ const Collections: React.FC = () => {
} }
/> />
) )
} else {
return null
} }
}, [announcementsQuery.isSuccess, announcementsQuery.data, i18n.language]) }, [announcementsQuery.isSuccess, announcementsQuery.data, i18n.language])

View File

@ -4,9 +4,11 @@ import { MenuContainer, MenuRow } from '@components/Menu'
import { useActionSheet } from '@expo/react-native-action-sheet' import { useActionSheet } from '@expo/react-native-action-sheet'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import i18n from '@root/i18n/i18n' import i18n from '@root/i18n/i18n'
import androidDefaults from '@utils/slices/instances/push/androidDefaults'
import { import {
getInstanceActive, getInstanceActive,
getInstancePush getInstancePush,
getInstances
} from '@utils/slices/instancesSlice' } from '@utils/slices/instancesSlice'
import { import {
changeBrowser, changeBrowser,
@ -17,8 +19,10 @@ import {
getSettingsBrowser getSettingsBrowser
} from '@utils/slices/settingsSlice' } from '@utils/slices/settingsSlice'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import * as Notifications from 'expo-notifications'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
const SettingsApp: React.FC = () => { const SettingsApp: React.FC = () => {
@ -28,6 +32,7 @@ const SettingsApp: React.FC = () => {
const { setTheme } = useTheme() const { setTheme } = useTheme()
const { t } = useTranslation('meSettings') const { t } = useTranslation('meSettings')
const instances = useSelector(getInstances, () => true)
const instanceActive = useSelector(getInstanceActive) const instanceActive = useSelector(getInstanceActive)
const settingsLanguage = useSelector(getSettingsLanguage) const settingsLanguage = useSelector(getSettingsLanguage)
const settingsTheme = useSelector(getSettingsTheme) const settingsTheme = useSelector(getSettingsTheme)
@ -80,9 +85,68 @@ const SettingsApp: React.FC = () => {
new: availableLanguages[buttonIndex] new: availableLanguages[buttonIndex]
}) })
haptics('Success') haptics('Success')
// @ts-ignore // @ts-ignore
dispatch(changeLanguage(availableLanguages[buttonIndex])) dispatch(changeLanguage(availableLanguages[buttonIndex]))
i18n.changeLanguage(availableLanguages[buttonIndex]) i18n.changeLanguage(availableLanguages[buttonIndex])
// Update Android notification channel language
if (Platform.OS === 'android') {
instances.forEach(instance => {
const accountFull = `@${instance.account.acct}@${instance.uri}`
if (instance.push.decode.value === false) {
Notifications.setNotificationChannelAsync(
`${accountFull}_default`,
{
groupId: accountFull,
name: t('meSettingsPush:content.default.heading'),
...androidDefaults
}
)
} else {
Notifications.setNotificationChannelAsync(
`${accountFull}_follow`,
{
groupId: accountFull,
name: t('meSettingsPush:content.follow.heading'),
...androidDefaults
}
)
Notifications.setNotificationChannelAsync(
`${accountFull}_favourite`,
{
groupId: accountFull,
name: t('meSettingsPush:content.favourite.heading'),
...androidDefaults
}
)
Notifications.setNotificationChannelAsync(
`${accountFull}_reblog`,
{
groupId: accountFull,
name: t('meSettingsPush:content.reblog.heading'),
...androidDefaults
}
)
Notifications.setNotificationChannelAsync(
`${accountFull}_mention`,
{
groupId: accountFull,
name: t('meSettingsPush:content.mention.heading'),
...androidDefaults
}
)
Notifications.setNotificationChannelAsync(
`${accountFull}_poll`,
{
groupId: accountFull,
name: t('meSettingsPush:content.poll.heading'),
...androidDefaults
}
)
}
})
}
} }
} }
) )

View File

@ -25,7 +25,7 @@ const SettingsDev: React.FC = () => {
color: theme.primary color: theme.primary
}} }}
> >
{instances[instanceActive].token} {instances[instanceActive]?.token}
</Text> </Text>
<MenuRow <MenuRow
title={'Local active index'} title={'Local active index'}

View File

@ -2,7 +2,7 @@ import { HeaderCenter, HeaderLeft } from '@components/Header'
import { StackScreenProps } from '@react-navigation/stack' import { StackScreenProps } from '@react-navigation/stack'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Platform, StyleSheet } 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 ScreenMeSwitchRoot from './Switch/Root' import ScreenMeSwitchRoot from './Switch/Root'
@ -37,6 +37,4 @@ const ScreenMeSwitch: React.FC<StackScreenProps<
) )
} }
const styles = StyleSheet.create({})
export default ScreenMeSwitch export default ScreenMeSwitch

View File

@ -92,7 +92,11 @@ const styles = StyleSheet.create({
export default React.memo(AccountInformation, (prev, next) => { export default React.memo(AccountInformation, (prev, next) => {
let skipUpdate = true let skipUpdate = true
skipUpdate = prev.account?.id === next.account?.id if (prev.account?.id !== next.account?.id) {
skipUpdate = prev.account?.acct === next.account?.acct skipUpdate = false
}
if (prev.account?.acct === next.account?.acct) {
skipUpdate = false
}
return skipUpdate return skipUpdate
}) })

View File

@ -33,7 +33,7 @@ const AccountInformationCreated: React.FC<Props> = ({ account }) => {
}} }}
> >
{t('content.created_at', { {t('content.created_at', {
date: new Date(account?.created_at || '').toLocaleDateString( date: new Date(account.created_at || '').toLocaleDateString(
i18n.language, i18n.language,
{ {
year: 'numeric', year: 'numeric',

View File

@ -73,7 +73,7 @@ const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => {
<Text <Text
style={[styles.stat, { color: theme.primary, textAlign: 'center' }]} style={[styles.stat, { color: theme.primary, textAlign: 'center' }]}
children={t('content.summary.followers_count', { children={t('content.summary.followers_count', {
count: account?.followers_count || 0 count: account.followers_count || 0
})} })}
onPress={() => { onPress={() => {
analytics('account_stats_followers_press', { analytics('account_stats_followers_press', {

View File

@ -0,0 +1,31 @@
import apiInstance from '@api/instance'
import { StackNavigationProp } from '@react-navigation/stack'
const pushNavigate = (
navigation: StackNavigationProp<Nav.RootStackParamList, 'Screen-Tabs'>,
id?: Mastodon.Notification['id']
) => {
// @ts-ignore
navigation.navigate('Tab-Notifications', {
screen: 'Tab-Notifications-Root'
})
if (!id) {
return
}
apiInstance<Mastodon.Notification>({
method: 'get',
url: `notifications/${id}`
}).then(({ body }) => {
if (body.status) {
// @ts-ignore
navigation.navigate('Tab-Notifications', {
screen: 'Tab-Shared-Toot',
params: { toot: body.status }
})
}
})
}
export default pushNavigate

View File

@ -0,0 +1,58 @@
import { displayMessage } from '@components/Message'
import { StackNavigationProp } from '@react-navigation/stack'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { Instance, updateInstanceActive } from '@utils/slices/instancesSlice'
import * as Notifications from 'expo-notifications'
import { findIndex } from 'lodash'
import { useEffect } from 'react'
import { QueryClient } from 'react-query'
import { useDispatch } from 'react-redux'
import pushNavigate from './pushNavigate'
export interface Params {
navigation: StackNavigationProp<Nav.RootStackParamList, 'Screen-Tabs'>
queryClient: QueryClient
instances: Instance[]
}
const pushReceive = ({ navigation, queryClient, instances }: Params) => {
const dispatch = useDispatch()
return 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
)
displayMessage({
duration: 'long',
message: notification.request.content.title!,
description: notification.request.content.body!,
onPress: () => {
if (notificationIndex !== -1) {
dispatch(updateInstanceActive(instances[notificationIndex]))
}
pushNavigate(navigation, payloadData.notification_id)
}
})
}
)
return () => subscription.remove()
}, [instances])
}
export default pushReceive

View File

@ -0,0 +1,54 @@
import { StackNavigationProp } from '@react-navigation/stack'
import { Dispatch } from '@reduxjs/toolkit'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { Instance, updateInstanceActive } from '@utils/slices/instancesSlice'
import * as Notifications from 'expo-notifications'
import { findIndex } from 'lodash'
import { useEffect } from 'react'
import { QueryClient } from 'react-query'
import pushNavigate from './pushNavigate'
export interface Params {
navigation: StackNavigationProp<Nav.RootStackParamList, 'Screen-Tabs'>
queryClient: QueryClient
instances: Instance[]
dispatch: Dispatch<any>
}
const pushRespond = ({
navigation,
queryClient,
instances,
dispatch
}: Params) => {
return useEffect(() => {
const subscription = Notifications.addNotificationResponseReceivedListener(
({ 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) {
dispatch(updateInstanceActive(instances[notificationIndex]))
}
pushNavigate(navigation, payloadData.notification_id)
}
)
return () => subscription.remove()
}, [instances])
}
export default pushRespond

View File

@ -10,8 +10,6 @@ const push = () => {
shouldSetBadge: false shouldSetBadge: false
}) })
}) })
Notifications.setBadgeCountAsync(0)
Notifications.dismissAllNotificationsAsync()
} }
export default push export default push

View File

@ -0,0 +1,11 @@
import * as Notifications from 'expo-notifications'
const androidDefaults = {
importance: Notifications.AndroidImportance.DEFAULT,
bypassDnd: false,
showBadge: true,
enableLights: true,
enableVibrate: true
}
export default androidDefaults

View File

@ -1,5 +1,6 @@
import apiGeneral from '@api/general' import apiGeneral from '@api/general'
import apiInstance from '@api/instance' import apiInstance from '@api/instance'
import i18n from '@root/i18n/i18n'
import { RootState } from '@root/store' import { RootState } from '@root/store'
import { import {
getInstance, getInstance,
@ -8,6 +9,7 @@ import {
} from '@utils/slices/instancesSlice' } from '@utils/slices/instancesSlice'
import * as Notifications from 'expo-notifications' import * as Notifications from 'expo-notifications'
import { Platform } from 'react-native' import { Platform } from 'react-native'
import androidDefaults from './androidDefaults'
const register1 = async ({ const register1 = async ({
expoToken, expoToken,
@ -66,17 +68,6 @@ const pushRegister = async (
return Promise.reject() return Promise.reject()
} }
const { status: existingStatus } = await Notifications.getPermissionsAsync()
let finalStatus = existingStatus
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync()
finalStatus = status
}
if (finalStatus !== 'granted') {
alert('Failed to get push token for push notification!')
return Promise.reject()
}
const accountId = instanceAccount.id const accountId = instanceAccount.id
const accountFull = `@${instanceAccount.acct}@${instanceUri}` const accountFull = `@${instanceAccount.acct}@${instanceUri}`
const serverRes = await register1({ const serverRes = await register1({
@ -111,25 +102,45 @@ const pushRegister = async (
}) })
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
Notifications.setNotificationChannelAsync('follow', { Notifications.setNotificationChannelGroupAsync(accountFull, {
name: 'Follow', name: accountFull,
importance: Notifications.AndroidImportance.DEFAULT ...androidDefaults
}) }).then(group => {
Notifications.setNotificationChannelAsync('favourite', { if (group) {
name: 'Favourite', if (instancePush.decode.value === false) {
importance: Notifications.AndroidImportance.DEFAULT Notifications.setNotificationChannelAsync(`${group.id}_default`, {
}) groupId: group.id,
Notifications.setNotificationChannelAsync('reblog', { name: i18n.t('meSettingsPush:content.default.heading'),
name: 'Reblog', ...androidDefaults
importance: Notifications.AndroidImportance.DEFAULT })
}) } else {
Notifications.setNotificationChannelAsync('mention', { Notifications.setNotificationChannelAsync(`${group.id}_follow`, {
name: 'Mention', groupId: group.id,
importance: Notifications.AndroidImportance.DEFAULT name: i18n.t('meSettingsPush:content.follow.heading'),
}) ...androidDefaults
Notifications.setNotificationChannelAsync('poll', { })
name: 'Poll', Notifications.setNotificationChannelAsync(`${group.id}_favourite`, {
importance: Notifications.AndroidImportance.DEFAULT groupId: group.id,
name: i18n.t('meSettingsPush:content.favourite.heading'),
...androidDefaults
})
Notifications.setNotificationChannelAsync(`${group.id}_reblog`, {
groupId: group.id,
name: i18n.t('meSettingsPush:content.reblog.heading'),
...androidDefaults
})
Notifications.setNotificationChannelAsync(`${group.id}_mention`, {
groupId: group.id,
name: i18n.t('meSettingsPush:content.mention.heading'),
...androidDefaults
})
Notifications.setNotificationChannelAsync(`${group.id}_poll`, {
groupId: group.id,
name: i18n.t('meSettingsPush:content.poll.heading'),
...androidDefaults
})
}
}
}) })
} }

View File

@ -2,9 +2,13 @@ import apiGeneral from '@api/general'
import apiInstance from '@api/instance' import apiInstance from '@api/instance'
import { RootState } from '@root/store' import { RootState } from '@root/store'
import { getInstance, PUSH_SERVER } from '@utils/slices/instancesSlice' import { getInstance, PUSH_SERVER } from '@utils/slices/instancesSlice'
import * as Notifications from 'expo-notifications'
import { Platform } from 'react-native'
const pushUnregister = async (state: RootState, expoToken: string) => { const pushUnregister = async (state: RootState, expoToken: string) => {
const instance = getInstance(state) const instance = getInstance(state)
const instanceUri = instance?.uri
const instanceAccount = instance?.account
if (!instance?.url || !instance.account.id) { if (!instance?.url || !instance.account.id) {
return Promise.reject() return Promise.reject()
@ -26,6 +30,11 @@ const pushUnregister = async (state: RootState, expoToken: string) => {
} }
}) })
if (Platform.OS === 'android') {
const accountFull = `@${instanceAccount?.acct}@${instanceUri}`
Notifications.deleteNotificationChannelGroupAsync(accountFull)
}
return return
} }

View File

@ -1,13 +1,16 @@
import apiGeneral from '@api/general' import apiGeneral from '@api/general'
import { createAsyncThunk } from '@reduxjs/toolkit' import { createAsyncThunk } from '@reduxjs/toolkit'
import i18n from '@root/i18n/i18n'
import { RootState } from '@root/store' import { RootState } from '@root/store'
import * as Notifications from 'expo-notifications' import * as Notifications from 'expo-notifications'
import { Platform } from 'react-native'
import { getInstance, Instance, PUSH_SERVER } from '../instancesSlice' import { getInstance, Instance, PUSH_SERVER } from '../instancesSlice'
import androidDefaults from './push/androidDefaults'
export const updateInstancePushDecode = createAsyncThunk( export const updateInstancePushDecode = createAsyncThunk(
'instances/updatePushDecode', 'instances/updatePushDecode',
async ( async (
disalbe: boolean, disable: boolean,
{ getState } { getState }
): Promise<Instance['push']['decode']['value']> => { ): Promise<Instance['push']['decode']['value']> => {
const state = getState() as RootState const state = getState() as RootState
@ -30,10 +33,61 @@ export const updateInstancePushDecode = createAsyncThunk(
expoToken, expoToken,
instanceUrl: instance.url, instanceUrl: instance.url,
accountId: instance.account.id, accountId: instance.account.id,
...(disalbe && { keys: instance.push.keys }) ...(disable && { keys: instance.push.keys })
} }
}) })
return Promise.resolve(disalbe) if (Platform.OS === 'android') {
const accountFull = `@${instance.account.acct}@${instance.uri}`
switch (disable) {
case true:
Notifications.deleteNotificationChannelAsync(`${accountFull}_default`)
Notifications.setNotificationChannelAsync(`${accountFull}_follow`, {
groupId: accountFull,
name: i18n.t('meSettingsPush:content.follow.heading'),
...androidDefaults
})
Notifications.setNotificationChannelAsync(
`${accountFull}_favourite`,
{
groupId: accountFull,
name: i18n.t('meSettingsPush:content.favourite.heading'),
...androidDefaults
}
)
Notifications.setNotificationChannelAsync(`${accountFull}_reblog`, {
groupId: accountFull,
name: i18n.t('meSettingsPush:content.reblog.heading'),
...androidDefaults
})
Notifications.setNotificationChannelAsync(`${accountFull}_mention`, {
groupId: accountFull,
name: i18n.t('meSettingsPush:content.mention.heading'),
...androidDefaults
})
Notifications.setNotificationChannelAsync(`${accountFull}_poll`, {
groupId: accountFull,
name: i18n.t('meSettingsPush:content.poll.heading'),
...androidDefaults
})
break
case false:
Notifications.setNotificationChannelAsync(`${accountFull}_default`, {
groupId: accountFull,
name: i18n.t('meSettingsPush:content.default.heading'),
...androidDefaults
})
Notifications.deleteNotificationChannelAsync(`${accountFull}_follow`)
Notifications.deleteNotificationChannelAsync(
`${accountFull}_favourite`
)
Notifications.deleteNotificationChannelAsync(`${accountFull}_reblog`)
Notifications.deleteNotificationChannelAsync(`${accountFull}_mention`)
Notifications.deleteNotificationChannelAsync(`${accountFull}_poll`)
break
}
}
return Promise.resolve(disable)
} }
) )

View File

@ -4,7 +4,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { RootState } from '@root/store' import { RootState } from '@root/store'
import { ComposeStateDraft } from '@screens/Compose/utils/types' import { ComposeStateDraft } from '@screens/Compose/utils/types'
import { findIndex } from 'lodash' import { findIndex } from 'lodash'
import { Appearance } from 'react-native'
import addInstance from './instances/add' import addInstance from './instances/add'
import { connectInstancesPush } from './instances/connectPush' import { connectInstancesPush } from './instances/connectPush'
import removeInstance from './instances/remove' import removeInstance from './instances/remove'

View File

@ -1,8 +1,12 @@
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit' import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
import { RootState } from '@root/store' import i18n from '@root/i18n/i18n'
import { RootState, store } from '@root/store'
import * as Analytics from 'expo-firebase-analytics' import * as Analytics from 'expo-firebase-analytics'
import * as Localization from 'expo-localization' import * as Localization from 'expo-localization'
import * as Notifications from 'expo-notifications'
import { pickBy } from 'lodash' import { pickBy } from 'lodash'
import androidDefaults from './instances/push/androidDefaults'
import { getInstances } from './instancesSlice'
enum availableLanguages { enum availableLanguages {
'zh-Hans', 'zh-Hans',