Ready for push feature

This commit is contained in:
Zhiyuan Zheng 2021-03-04 01:03:53 +01:00
parent a4a6e9316b
commit cc02626adb
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
17 changed files with 255 additions and 135 deletions

View File

@ -8,7 +8,7 @@ ensure_env_vars(
VERSIONS = read_json( json_path: "./package.json" )[:versions]
ENVIRONMENT = ENV["TOOOT_ENVIRONMENT"]
VERSION = "#{VERSIONS[:major]}.#{VERSIONS[:minor]}"
RELEASE_CHANNEL = "#{VERSIONS[:major]}-#{ENVIRONMENT}"
RELEASE_CHANNEL = "#{VERSIONS[:major]}-#{VERSIONS[:minor]}-#{ENVIRONMENT}"
BUILD_NUMBER = ENV["GITHUB_RUN_NUMBER"]
GITHUB_REPO = "tooot-app/app"
case ENVIRONMENT

View File

@ -1,9 +1,9 @@
{
"name": "tooot",
"versions": {
"native": "210201",
"native": "210304",
"major": 0,
"minor": 5,
"minor": 6,
"patch": 0,
"expo": "40.0.0"
},

View File

@ -8,10 +8,12 @@ import ScreenAnnouncements from '@screens/Announcements'
import ScreenCompose from '@screens/Compose'
import ScreenImagesViewer from '@screens/ImagesViewer'
import ScreenTabs from '@screens/Tabs'
import pushUseConnect from '@utils/push/useConnect'
import pushUseReceive from '@utils/push/useReceive'
import pushUseRespond from '@utils/push/useRespond'
import { updatePreviousTab } from '@utils/slices/contextsSlice'
import { connectInstancesPush } from '@utils/slices/instances/connectPush'
import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences'
import { getInstanceActive } from '@utils/slices/instancesSlice'
import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import { themes } from '@utils/styles/themes'
import * as Analytics from 'expo-firebase-analytics'
@ -20,6 +22,7 @@ import React, { createRef, useCallback, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert, Platform, StatusBar } from 'react-native'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import { useQueryClient } from 'react-query'
import { useDispatch, useSelector } from 'react-redux'
import * as Sentry from 'sentry-expo'
@ -56,10 +59,15 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
// }
// }, [isConnected, firstRender])
// Update Expo Token to server
useEffect(() => {
dispatch(connectInstancesPush({ mode, t }))
}, [])
// Push hooks
const instances = useSelector(
getInstances,
(prev, next) => prev.length === next.length
)
const queryClient = useQueryClient()
pushUseConnect({ navigationRef, mode, t, instances, dispatch })
pushUseReceive({ navigationRef, queryClient, instances })
pushUseRespond({ navigationRef, queryClient, instances, dispatch })
// Prevent screenshot alert
useEffect(() => {

View File

@ -56,8 +56,7 @@ const displayMessage = ({
onPress,
...(mode &&
type && {
renderFlashMessageIcon: props => {
console.log(props)
renderFlashMessageIcon: () => {
return (
<Icon
name={iconMapping[type]}
@ -92,7 +91,7 @@ const Message = React.memo(
...StyleConstants.FontStyle.M,
fontWeight: StyleConstants.Font.Weight.Bold
}}
textStyle={{ color: theme.primary, ...StyleConstants.FontStyle.M }}
textStyle={{ color: theme.primary, ...StyleConstants.FontStyle.S }}
// @ts-ignore
textProps={{ numberOfLines: 2 }}
/>

View File

@ -209,7 +209,6 @@ const TimelineActions: React.FC<Props> = ({
: iconColorAction(status.reblogged)
}
size={StyleConstants.Font.Size.L}
strokeWidth={status.reblogged ? 3 : undefined}
/>
{status.reblogs_count > 0 && (
<Text
@ -233,7 +232,6 @@ const TimelineActions: React.FC<Props> = ({
name='Heart'
color={iconColorAction(status.favourited)}
size={StyleConstants.Font.Size.L}
strokeWidth={status.favourited ? 3 : undefined}
/>
{status.favourites_count > 0 && (
<Text
@ -257,7 +255,6 @@ const TimelineActions: React.FC<Props> = ({
name='Bookmark'
color={iconColorAction(status.bookmarked)}
size={StyleConstants.Font.Size.L}
strokeWidth={status.bookmarked ? 3 : undefined}
/>
),
[status.bookmarked]

View File

@ -1,6 +1,10 @@
export default {
heading: 'Push Notification',
content: {
enable: {
direct: 'Enable push notification',
settings: 'Enable in settings'
},
global: {
heading: 'Enable push notification',
description: "Messages are routed through tooot's server"
@ -10,6 +14,9 @@ export default {
description:
"Messages routed through tooot's server are encrypted, but you can choose to decode the message on the server. Our server source code is open source, and no log policy."
},
default: {
heading: 'Default' // Android notification channel name only
},
follow: {
heading: 'New follower'
},
@ -26,5 +33,9 @@ export default {
heading: 'Poll updates'
},
howitworks: 'Learn how routing works'
},
error: {
message: 'Push service error',
description: 'Please re-enable push notification in settings'
}
}

View File

@ -35,7 +35,7 @@ export default {
howitworks: '了解通知消息转发如何工作'
},
error: {
message: '推送服务错误',
message: '推送服务错误',
description: '请在设置中重新尝试启用推送通知'
}
}

View File

@ -7,19 +7,19 @@ import {
import { NavigatorScreenParams } from '@react-navigation/native'
import { StackScreenProps } from '@react-navigation/stack'
import { getPreviousTab } from '@utils/slices/contextsSlice'
import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
import {
getInstanceAccount,
getInstanceActive
} from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useMemo } from 'react'
import { Platform } from 'react-native'
import FastImage from 'react-native-fast-image'
import { useQueryClient } from 'react-query'
import { useDispatch, useSelector } from 'react-redux'
import { useSelector } from 'react-redux'
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>
@ -40,18 +40,12 @@ const ScreenTabs = React.memo(
({ navigation }: ScreenTabsProp) => {
const { mode, theme } = useTheme()
const queryClient = useQueryClient()
const dispatch = useDispatch()
const instanceActive = useSelector(getInstanceActive)
const instances = useSelector(
getInstances,
(prev, next) => prev.length === next.length
const instanceAccount = useSelector(
getInstanceAccount,
(prev, next) => prev?.avatarStatic === next?.avatarStatic
)
pushReceive({ navigation, queryClient, instances })
pushRespond({ navigation, queryClient, instances, dispatch })
const screenOptions = useCallback(
({ route }): BottomTabNavigationOptions => ({
tabBarVisible: instanceActive !== -1,
@ -77,7 +71,7 @@ const ScreenTabs = React.memo(
return instanceActive !== -1 ? (
<FastImage
source={{
uri: instances[instanceActive].account.avatarStatic
uri: instanceAccount?.avatarStatic
}}
style={{
width: size,
@ -99,7 +93,7 @@ const ScreenTabs = React.memo(
}
}
}),
[instances, instanceActive]
[instanceAccount, instanceActive]
)
const tabBarOptions = useMemo(
() => ({

View File

@ -1,31 +0,0 @@
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

@ -5,10 +5,11 @@ import {
configureStore,
getDefaultMiddleware
} from '@reduxjs/toolkit'
import { InstancesV3 } from '@utils/migrations/instances/v3'
import contextsSlice from '@utils/slices/contextsSlice'
import instancesSlice from '@utils/slices/instancesSlice'
import settingsSlice from '@utils/slices/settingsSlice'
import { persistReducer, persistStore } from 'redux-persist'
import { createMigrate, persistReducer, persistStore } from 'redux-persist'
const secureStorage = createSecureStore()
@ -20,10 +21,40 @@ const contextsPersistConfig = {
storage: AsyncStorage
}
const instancesMigration = {
4: (state: InstancesV3) => {
return {
instances: state.local.instances.map((instance, index) => {
// @ts-ignore
delete instance.notification
return {
...instance,
active: state.local.activeIndex === index,
push: {
global: { loading: false, value: false },
decode: { loading: false, value: false },
alerts: {
follow: { loading: false, value: true },
favourite: { loading: false, value: true },
reblog: { loading: false, value: true },
mention: { loading: false, value: true },
poll: { loading: false, value: true }
},
keys: undefined
}
}
})
}
}
}
const instancesPersistConfig = {
key: 'instances',
prefix,
storage: secureStorage
storage: secureStorage,
version: 4,
// @ts-ignore
migrate: createMigrate(instancesMigration)
}
const settingsPersistConfig = {

View File

@ -0,0 +1,33 @@
type InstanceLocal = {
appData: {
clientId: string
clientSecret: string
}
url: string
token: string
uri: Mastodon.Instance['uri']
urls: Mastodon.Instance['urls']
max_toot_chars: number
account: {
id: Mastodon.Account['id']
acct: Mastodon.Account['acct']
avatarStatic: Mastodon.Account['avatar_static']
preferences: Mastodon.Preferences
}
notification: {
readTime?: Mastodon.Notification['created_at']
latestTime?: Mastodon.Notification['created_at']
}
drafts: any[]
}
export type InstancesV3 = {
local: {
activeIndex: number | null
instances: InstanceLocal[]
}
remote: {
url: string
}
}

View File

@ -0,0 +1,91 @@
import apiGeneral from '@api/general'
import { displayMessage } from '@components/Message'
import { NavigationContainerRef } from '@react-navigation/native'
import { Dispatch } from '@reduxjs/toolkit'
import {
disableAllPushes,
Instance,
PUSH_SERVER
} from '@utils/slices/instancesSlice'
import * as Notifications from 'expo-notifications'
import { useEffect } from 'react'
import { TFunction } from 'react-i18next'
export interface Params {
navigationRef: React.RefObject<NavigationContainerRef>
mode: 'light' | 'dark'
t: TFunction<'common'>
instances: Instance[]
dispatch: Dispatch<any>
}
const pushUseConnect = ({
navigationRef,
mode,
t,
instances,
dispatch
}: Params) => {
return useEffect(() => {
const connect = async () => {
const expoToken = (
await Notifications.getExpoPushTokenAsync({
experienceId: '@xmflsct/tooot'
})
).data
apiGeneral({
method: 'post',
domain: PUSH_SERVER,
url: 'v1/connect',
body: {
expoToken
}
}).catch(() => {
displayMessage({
mode,
type: 'error',
duration: 'long',
message: t('meSettingsPush:error.message'),
description: t('meSettingsPush:error.description'),
onPress: () => {
navigationRef.current?.navigate('Screen-Tabs', {
screen: 'Tab-Me',
params: {
screen: 'Tab-Me-Root'
}
})
navigationRef.current?.navigate('Screen-Tabs', {
screen: 'Tab-Me',
params: {
screen: 'Tab-Me-Settings'
}
})
}
})
dispatch(disableAllPushes())
instances.forEach(instance => {
if (instance.push.global.value) {
apiGeneral<{}>({
method: 'delete',
domain: instance.url,
url: 'api/v1/push/subscription',
headers: {
Authorization: `Bearer ${instance.token}`
}
}).catch(() => console.log('error!!!'))
}
})
})
}
const pushEnabled = instances.filter(instance => instance.push.global.value)
if (pushEnabled.length) {
connect()
}
}, [instances])
}
export default pushUseConnect

View File

@ -0,0 +1,35 @@
import apiInstance from '@api/instance'
import { NavigationContainerRef } from '@react-navigation/native'
const pushUseNavigate = (
navigationRef: React.RefObject<NavigationContainerRef>,
id?: Mastodon.Notification['id']
) => {
navigationRef.current?.navigate('Screen-Tabs', {
screen: 'Tab-Notifications',
params: {
screen: 'Tab-Notifications-Root'
}
})
if (!id) {
return
}
apiInstance<Mastodon.Notification>({
method: 'get',
url: `notifications/${id}`
}).then(({ body }) => {
if (body.status) {
navigationRef.current?.navigate('Screen-Tabs', {
screen: 'Tab-Notifications',
params: {
screen: 'Tab-Shared-Toot',
params: { toot: body.status }
}
})
}
})
}
export default pushUseNavigate

View File

@ -1,5 +1,5 @@
import { displayMessage } from '@components/Message'
import { StackNavigationProp } from '@react-navigation/stack'
import { NavigationContainerRef } from '@react-navigation/native'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { Instance, updateInstanceActive } from '@utils/slices/instancesSlice'
import * as Notifications from 'expo-notifications'
@ -7,15 +7,15 @@ import { findIndex } from 'lodash'
import { useEffect } from 'react'
import { QueryClient } from 'react-query'
import { useDispatch } from 'react-redux'
import pushNavigate from './pushNavigate'
import pushUseNavigate from './useNavigate'
export interface Params {
navigation: StackNavigationProp<Nav.RootStackParamList, 'Screen-Tabs'>
navigationRef: React.RefObject<NavigationContainerRef>
queryClient: QueryClient
instances: Instance[]
}
const pushReceive = ({ navigation, queryClient, instances }: Params) => {
const pushUseReceive = ({ navigationRef, queryClient, instances }: Params) => {
const dispatch = useDispatch()
return useEffect(() => {
@ -46,7 +46,7 @@ const pushReceive = ({ navigation, queryClient, instances }: Params) => {
if (notificationIndex !== -1) {
dispatch(updateInstanceActive(instances[notificationIndex]))
}
pushNavigate(navigation, payloadData.notification_id)
pushUseNavigate(navigationRef, payloadData.notification_id)
}
})
}
@ -55,4 +55,4 @@ const pushReceive = ({ navigation, queryClient, instances }: Params) => {
}, [instances])
}
export default pushReceive
export default pushUseReceive

View File

@ -1,4 +1,4 @@
import { StackNavigationProp } from '@react-navigation/stack'
import { NavigationContainerRef } from '@react-navigation/native'
import { Dispatch } from '@reduxjs/toolkit'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { Instance, updateInstanceActive } from '@utils/slices/instancesSlice'
@ -6,17 +6,17 @@ import * as Notifications from 'expo-notifications'
import { findIndex } from 'lodash'
import { useEffect } from 'react'
import { QueryClient } from 'react-query'
import pushNavigate from './pushNavigate'
import pushUseNavigate from './useNavigate'
export interface Params {
navigation: StackNavigationProp<Nav.RootStackParamList, 'Screen-Tabs'>
navigationRef: React.RefObject<NavigationContainerRef>
queryClient: QueryClient
instances: Instance[]
dispatch: Dispatch<any>
}
const pushRespond = ({
navigation,
const pushUseRespond = ({
navigationRef,
queryClient,
instances,
dispatch
@ -44,11 +44,11 @@ const pushRespond = ({
if (notificationIndex !== -1) {
dispatch(updateInstanceActive(instances[notificationIndex]))
}
pushNavigate(navigation, payloadData.notification_id)
pushUseNavigate(navigationRef, payloadData.notification_id)
}
)
return () => subscription.remove()
}, [instances])
}
export default pushRespond
export default pushUseRespond

View File

@ -1,38 +0,0 @@
import apiGeneral from '@api/general'
import { createAsyncThunk } from '@reduxjs/toolkit'
import { RootState } from '@root/store'
import * as Notifications from 'expo-notifications'
import { TFunction } from 'react-i18next'
import { PUSH_SERVER } from '../instancesSlice'
export const connectInstancesPush = createAsyncThunk(
'instances/connectPush',
async (
{ mode, t }: { mode: 'light' | 'dark'; t: TFunction<'common'> },
{ getState }
): Promise<any> => {
const state = getState() as RootState
const pushEnabled = state.instances.instances.filter(
instance => instance.push.global.value
)
if (pushEnabled.length) {
const expoToken = (
await Notifications.getExpoPushTokenAsync({
experienceId: '@xmflsct/tooot'
})
).data
return apiGeneral({
method: 'post',
domain: PUSH_SERVER,
url: 'v1/connect',
body: {
expoToken
}
})
} else {
return Promise.resolve()
}
}
)

View File

@ -1,11 +1,9 @@
import analytics from '@components/analytics'
import { displayMessage } from '@components/Message'
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { RootState } from '@root/store'
import { ComposeStateDraft } from '@screens/Compose/utils/types'
import { findIndex } from 'lodash'
import addInstance from './instances/add'
import { connectInstancesPush } from './instances/connectPush'
import removeInstance from './instances/remove'
import { updateAccountPreferences } from './instances/updateAccountPreferences'
import { updateInstancePush } from './instances/updatePush'
@ -150,6 +148,13 @@ const instancesSlice = createSlice({
instances[activeIndex].drafts = instances[activeIndex].drafts?.filter(
draft => draft.timestamp !== action.payload
)
},
disableAllPushes: ({ instances }) => {
instances = instances.map(instance => {
let newInstance = instance
newInstance.push.global.value = false
return newInstance
})
}
},
extraReducers: builder => {
@ -266,22 +271,6 @@ const instancesSlice = createSlice({
action.meta.arg.changed
].loading = true
})
// If Expo token does not exist on the server, disable all existing ones
.addCase(connectInstancesPush.rejected, (state, action) => {
state.instances = state.instances.map(instance => {
let newInstance = instance
newInstance.push.global.value = false
displayMessage({
mode: action.meta.arg.mode,
type: 'error',
autoHide: false,
message: action.meta.arg.t('meSettingsPush:error.message'),
description: action.meta.arg.t('meSettingsPush:error.description')
})
return newInstance
})
})
}
})
@ -337,7 +326,8 @@ export const {
updateInstanceActive,
updateInstanceAccount,
updateInstanceDraft,
removeInstanceDraft
removeInstanceDraft,
disableAllPushes
} = instancesSlice.actions
export default instancesSlice.reducer