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
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.xmflsct.app.tooot"
>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
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.SYSTEM_ALERT_WINDOW"/>
<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.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<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"
>
<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">
<!-- [Custom] Expo Notifications -->
<meta-data android:name="expo.modules.notifications.default_notification_icon" android:resource="@drawable/ic_stat_notifications" />
<!-- [Custom] End Expo Notifications -->
<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_RELEASE_CHANNEL" android:value="${releaseChannel}"/>
<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_LAUNCH_WAIT_MS" android:value="0"/>
<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"
>
<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">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>

View File

@ -24,6 +24,7 @@ public class BasePackageList {
new expo.modules.lineargradient.LinearGradientPackage(),
new expo.modules.localization.LocalizationPackage(),
new expo.modules.location.LocationPackage(),
new expo.modules.notifications.NotificationsPackage(),
new expo.modules.permissions.PermissionsPackage(),
new expo.modules.screencapture.ScreenCapturePackage(),
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 { getSettingsLanguage } from '@utils/slices/settingsSlice'
import ThemeManager from '@utils/styles/ThemeManager'
import * as Notifications from 'expo-notifications'
import * as SplashScreen from 'expo-splash-screen'
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 { QueryClient, QueryClientProvider } from 'react-query'
import { Provider } from 'react-redux'
@ -39,6 +40,17 @@ const App: React.FC = () => {
log('log', 'App', 'rendering App')
const [localCorrupt, setLocalCorrupt] = useState<string>()
useEffect(() => {
AppState.addEventListener('change', () => {
Notifications.setBadgeCountAsync(0)
Notifications.dismissAllNotificationsAsync()
})
return () => {
AppState.removeEventListener('change', () => {})
}
}, [])
useEffect(() => {
const delaySplash = async () => {
log('log', 'App', 'delay splash')

View File

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

View File

@ -3,9 +3,9 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { ColorDefinitions } from '@utils/styles/themes'
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 { State, TapGestureHandler } from 'react-native-gesture-handler'
import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler'
export interface Props {
iconFront?: any

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,29 +17,25 @@ import { AppState, Linking } from 'react-native'
const ScreenMeSettingsPush: React.FC = () => {
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 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 alerts = useMemo(() => {

View File

@ -24,6 +24,8 @@ const Collections: React.FC = () => {
onPress={() => navigation.navigate('Tab-Me-Lists')}
/>
)
} else {
return null
}
}, [listsQuery.isSuccess, listsQuery.data, i18n.language])
@ -55,6 +57,8 @@ const Collections: React.FC = () => {
}
/>
)
} else {
return null
}
}, [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 { useNavigation } from '@react-navigation/native'
import i18n from '@root/i18n/i18n'
import androidDefaults from '@utils/slices/instances/push/androidDefaults'
import {
getInstanceActive,
getInstancePush
getInstancePush,
getInstances
} from '@utils/slices/instancesSlice'
import {
changeBrowser,
@ -17,8 +19,10 @@ import {
getSettingsBrowser
} from '@utils/slices/settingsSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import * as Notifications from 'expo-notifications'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native'
import { useDispatch, useSelector } from 'react-redux'
const SettingsApp: React.FC = () => {
@ -28,6 +32,7 @@ const SettingsApp: React.FC = () => {
const { setTheme } = useTheme()
const { t } = useTranslation('meSettings')
const instances = useSelector(getInstances, () => true)
const instanceActive = useSelector(getInstanceActive)
const settingsLanguage = useSelector(getSettingsLanguage)
const settingsTheme = useSelector(getSettingsTheme)
@ -80,9 +85,68 @@ const SettingsApp: React.FC = () => {
new: availableLanguages[buttonIndex]
})
haptics('Success')
// @ts-ignore
dispatch(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
}}
>
{instances[instanceActive].token}
{instances[instanceActive]?.token}
</Text>
<MenuRow
title={'Local active index'}

View File

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

View File

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

View File

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

View File

@ -73,7 +73,7 @@ const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => {
<Text
style={[styles.stat, { color: theme.primary, textAlign: 'center' }]}
children={t('content.summary.followers_count', {
count: account?.followers_count || 0
count: account.followers_count || 0
})}
onPress={() => {
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
})
})
Notifications.setBadgeCountAsync(0)
Notifications.dismissAllNotificationsAsync()
}
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 apiInstance from '@api/instance'
import i18n from '@root/i18n/i18n'
import { RootState } from '@root/store'
import {
getInstance,
@ -8,6 +9,7 @@ import {
} from '@utils/slices/instancesSlice'
import * as Notifications from 'expo-notifications'
import { Platform } from 'react-native'
import androidDefaults from './androidDefaults'
const register1 = async ({
expoToken,
@ -66,17 +68,6 @@ const pushRegister = async (
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 accountFull = `@${instanceAccount.acct}@${instanceUri}`
const serverRes = await register1({
@ -111,25 +102,45 @@ const pushRegister = async (
})
if (Platform.OS === 'android') {
Notifications.setNotificationChannelAsync('follow', {
name: 'Follow',
importance: Notifications.AndroidImportance.DEFAULT
})
Notifications.setNotificationChannelAsync('favourite', {
name: 'Favourite',
importance: Notifications.AndroidImportance.DEFAULT
})
Notifications.setNotificationChannelAsync('reblog', {
name: 'Reblog',
importance: Notifications.AndroidImportance.DEFAULT
})
Notifications.setNotificationChannelAsync('mention', {
name: 'Mention',
importance: Notifications.AndroidImportance.DEFAULT
})
Notifications.setNotificationChannelAsync('poll', {
name: 'Poll',
importance: Notifications.AndroidImportance.DEFAULT
Notifications.setNotificationChannelGroupAsync(accountFull, {
name: accountFull,
...androidDefaults
}).then(group => {
if (group) {
if (instancePush.decode.value === false) {
Notifications.setNotificationChannelAsync(`${group.id}_default`, {
groupId: group.id,
name: i18n.t('meSettingsPush:content.default.heading'),
...androidDefaults
})
} else {
Notifications.setNotificationChannelAsync(`${group.id}_follow`, {
groupId: group.id,
name: i18n.t('meSettingsPush:content.follow.heading'),
...androidDefaults
})
Notifications.setNotificationChannelAsync(`${group.id}_favourite`, {
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 { RootState } from '@root/store'
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 instance = getInstance(state)
const instanceUri = instance?.uri
const instanceAccount = instance?.account
if (!instance?.url || !instance.account.id) {
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
}

View File

@ -1,13 +1,16 @@
import apiGeneral from '@api/general'
import { createAsyncThunk } from '@reduxjs/toolkit'
import i18n from '@root/i18n/i18n'
import { RootState } from '@root/store'
import * as Notifications from 'expo-notifications'
import { Platform } from 'react-native'
import { getInstance, Instance, PUSH_SERVER } from '../instancesSlice'
import androidDefaults from './push/androidDefaults'
export const updateInstancePushDecode = createAsyncThunk(
'instances/updatePushDecode',
async (
disalbe: boolean,
disable: boolean,
{ getState }
): Promise<Instance['push']['decode']['value']> => {
const state = getState() as RootState
@ -30,10 +33,61 @@ export const updateInstancePushDecode = createAsyncThunk(
expoToken,
instanceUrl: instance.url,
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 { ComposeStateDraft } from '@screens/Compose/utils/types'
import { findIndex } from 'lodash'
import { Appearance } from 'react-native'
import addInstance from './instances/add'
import { connectInstancesPush } from './instances/connectPush'
import removeInstance from './instances/remove'

View File

@ -1,8 +1,12 @@
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 Localization from 'expo-localization'
import * as Notifications from 'expo-notifications'
import { pickBy } from 'lodash'
import androidDefaults from './instances/push/androidDefaults'
import { getInstances } from './instancesSlice'
enum availableLanguages {
'zh-Hans',