Improve push experience
@ -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"/>
|
||||
|
@ -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(),
|
||||
|
BIN
android/app/src/main/res/drawable-hdpi/ic_stat_notifications.png
Normal file
After Width: | Height: | Size: 577 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_stat_notifications.png
Normal file
After Width: | Height: | Size: 379 B |
After Width: | Height: | Size: 892 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.5 KiB |
14
src/App.tsx
@ -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')
|
||||
|
@ -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({
|
||||
|
@ -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
|
||||
|
@ -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: '了解通知消息转发如何工作'
|
||||
},
|
||||
|
@ -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}
|
||||
|
@ -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:
|
||||
|
@ -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 />
|
||||
</>
|
||||
|
@ -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(
|
||||
() => ({
|
||||
|
@ -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(() => {
|
||||
|
@ -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])
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -25,7 +25,7 @@ const SettingsDev: React.FC = () => {
|
||||
color: theme.primary
|
||||
}}
|
||||
>
|
||||
{instances[instanceActive].token}
|
||||
{instances[instanceActive]?.token}
|
||||
</Text>
|
||||
<MenuRow
|
||||
title={'Local active index'}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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',
|
||||
|
@ -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', {
|
||||
|
31
src/screens/Tabs/utils/pushNavigate.ts
Normal 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
|
58
src/screens/Tabs/utils/pushReceive.ts
Normal 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
|
54
src/screens/Tabs/utils/pushRespond.ts
Normal 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
|
@ -10,8 +10,6 @@ const push = () => {
|
||||
shouldSetBadge: false
|
||||
})
|
||||
})
|
||||
Notifications.setBadgeCountAsync(0)
|
||||
Notifications.dismissAllNotificationsAsync()
|
||||
}
|
||||
|
||||
export default push
|
||||
|
11
src/utils/slices/instances/push/androidDefaults.ts
Normal 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
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
)
|
||||
|
@ -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'
|
||||
|
@ -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',
|
||||
|