1
0
mirror of https://github.com/tooot-app/app synced 2024-12-22 07:34:06 +01:00

Add admin notifications besides push #535

This commit is contained in:
xmflsct 2022-12-10 22:43:37 +01:00
parent bdbacf579e
commit 213328ef1a
17 changed files with 244 additions and 234 deletions

View File

@ -5,7 +5,7 @@ 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 { Text, View } from 'react-native' import { View } from 'react-native'
import { Flow } from 'react-native-animated-spinkit' import { Flow } from 'react-native-animated-spinkit'
import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler' import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler'

View File

@ -1,2 +1,9 @@
export const PERMISSION_MANAGE_REPORTS = 0x0000000000000010 export const PERMISSION_MANAGE_REPORTS = 0x0000000000000010
export const PERMISSION_MANAGE_USERS = 0x0000000000000400 export const PERMISSION_MANAGE_USERS = 0x0000000000000400
export const checkPermission = (permission: number, permissions?: string | number): boolean =>
permissions
? !!(
(typeof permissions === 'string' ? parseInt(permissions || '0') : permissions) & permission
)
: false

View File

@ -2,19 +2,6 @@
"content": { "content": {
"altText": { "altText": {
"heading": "Alternative Text" "heading": "Alternative Text"
},
"notificationsFilter": {
"heading": "Show notification types",
"content": {
"follow": "$t(screenTabs:me.push.follow.heading)",
"follow_request": "Follow request",
"favourite": "$t(screenTabs:me.push.favourite.heading)",
"reblog": "$t(screenTabs:me.push.reblog.heading)",
"mention": "$t(screenTabs:me.push.mention.heading)",
"poll": "$t(screenTabs:me.push.poll.heading)",
"status": "Toot from subscribed users",
"update": "Reblog has been edited"
}
} }
} }
} }

View File

@ -24,9 +24,22 @@
} }
}, },
"notifications": { "notifications": {
"filter": { "filters": {
"accessibilityLabel": "Filter", "accessibilityLabel": "Filter",
"accessibilityHint": "Filter shown notifications' types" "accessibilityHint": "Filter shown notifications' types",
"title": "Show notifications",
"options": {
"follow": "$t(screenTabs:me.push.follow.heading)",
"follow_request": "Follow request",
"favourite": "$t(screenTabs:me.push.favourite.heading)",
"reblog": "$t(screenTabs:me.push.reblog.heading)",
"mention": "$t(screenTabs:me.push.mention.heading)",
"poll": "$t(screenTabs:me.push.poll.heading)",
"status": "Toot from subscribed users",
"update": "Reblog has been edited",
"admin.sign_up": "$t(screenTabs:me.push.admin.sign_up.heading)",
"admin.report": "$t(screenTabs:me.push.admin.report.heading)"
}
} }
}, },
"me": { "me": {

View File

@ -15,7 +15,6 @@ import Animated, {
} from 'react-native-reanimated' } from 'react-native-reanimated'
import { useSafeAreaInsets } from 'react-native-safe-area-context' import { useSafeAreaInsets } from 'react-native-safe-area-context'
import ActionsAltText from './Actions/AltText' import ActionsAltText from './Actions/AltText'
import ActionsNotificationsFilter from './Actions/NotificationsFilter'
const ScreenActions = ({ const ScreenActions = ({
route: { params }, route: { params },
@ -53,8 +52,6 @@ const ScreenActions = ({
const actions = () => { const actions = () => {
switch (params.type) { switch (params.type) {
case 'notifications_filter':
return <ActionsNotificationsFilter />
case 'alt_text': case 'alt_text':
return <ActionsAltText text={params.text} /> return <ActionsAltText text={params.text} />
} }

View File

@ -1,114 +0,0 @@
import Button from '@components/Button'
import MenuContainer from '@components/Menu/Container'
import MenuHeader from '@components/Menu/Header'
import MenuRow from '@components/Menu/Row'
import { useNavigation } from '@react-navigation/native'
import {
checkInstanceFeature,
getInstanceNotificationsFilter,
updateInstanceNotificationsFilter
} from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { useQueryClient } from '@tanstack/react-query'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { useAppDispatch } from '@root/store'
const ActionsNotificationsFilter: React.FC = () => {
const navigation = useNavigation()
const { t } = useTranslation('screenActions')
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }]
const queryClient = useQueryClient()
const dispatch = useAppDispatch()
const instanceNotificationsFilter = useSelector(
getInstanceNotificationsFilter
)
if (!instanceNotificationsFilter) {
navigation.goBack()
return null
}
const hasTypeStatus = useSelector(
checkInstanceFeature('notification_type_status')
)
const hasTypeUpdate = useSelector(
checkInstanceFeature('notification_type_update')
)
const options = useMemo(() => {
return (
instanceNotificationsFilter &&
(
[
'follow',
'follow_request',
'favourite',
'reblog',
'mention',
'poll',
'status',
'update'
] as [
'follow',
'follow_request',
'favourite',
'reblog',
'mention',
'poll',
'status',
'update'
]
)
.filter(type => {
switch (type) {
case 'status':
return hasTypeStatus
case 'update':
return hasTypeUpdate
default:
return true
}
})
.map(type => (
<MenuRow
key={type}
title={t(`content.notificationsFilter.content.${type}`)}
switchValue={instanceNotificationsFilter[type]}
switchOnValueChange={() =>
dispatch(
updateInstanceNotificationsFilter({
...instanceNotificationsFilter,
[type]: !instanceNotificationsFilter[type]
})
)
}
/>
))
)
}, [instanceNotificationsFilter, hasTypeStatus, hasTypeUpdate])
return (
<>
<MenuContainer>
<MenuHeader heading={t(`content.notificationsFilter.heading`)} />
{options}
</MenuContainer>
<Button
type='text'
content={t('common:buttons.apply')}
onPress={() => {
queryClient.resetQueries(queryKey)
}}
style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
}}
/>
</>
)
}
export default ActionsNotificationsFilter

View File

@ -3,17 +3,13 @@ import Icon from '@components/Icon'
import { MenuContainer, MenuRow } from '@components/Menu' import { MenuContainer, MenuRow } from '@components/Menu'
import CustomText from '@components/Text' import CustomText from '@components/Text'
import browserPackage from '@helpers/browserPackage' import browserPackage from '@helpers/browserPackage'
import { checkPermission } from '@helpers/permissions'
import { useAppDispatch } from '@root/store' import { useAppDispatch } from '@root/store'
import { isDevelopment } from '@utils/checkEnvironment' import { isDevelopment } from '@utils/checkEnvironment'
import { useAppsQuery } from '@utils/queryHooks/apps' import { useAppsQuery } from '@utils/queryHooks/apps'
import { useProfileQuery } from '@utils/queryHooks/profile' import { useProfileQuery } from '@utils/queryHooks/profile'
import { getExpoToken, retrieveExpoToken } from '@utils/slices/appSlice' import { getExpoToken, retrieveExpoToken } from '@utils/slices/appSlice'
import { import { PUSH_ADMIN, PUSH_DEFAULT, setChannels } from '@utils/slices/instances/push/utils'
checkPushAdminPermission,
PUSH_ADMIN,
PUSH_DEFAULT,
setChannels
} from '@utils/slices/instances/push/utils'
import { updateInstancePush } from '@utils/slices/instances/updatePush' import { updateInstancePush } from '@utils/slices/instances/updatePush'
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert' import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode' import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode'
@ -88,7 +84,6 @@ const TabMePush: React.FC = () => {
switchOnValueChange={() => switchOnValueChange={() =>
dispatch( dispatch(
updateInstancePushAlert({ updateInstancePushAlert({
changed: alert,
alerts: { alerts: {
...instancePush?.alerts, ...instancePush?.alerts,
[alert]: instancePush?.alerts[alert] [alert]: instancePush?.alerts[alert]
@ -104,7 +99,7 @@ const TabMePush: React.FC = () => {
const adminAlerts = () => const adminAlerts = () =>
profileQuery.data?.role?.permissions profileQuery.data?.role?.permissions
? PUSH_ADMIN.map(({ type, permission }) => ? PUSH_ADMIN.map(({ type, permission }) =>
checkPushAdminPermission(permission, profileQuery.data.role?.permissions) ? ( checkPermission(permission, profileQuery.data.role?.permissions) ? (
<MenuRow <MenuRow
key={type} key={type}
title={t(`me.push.${type}.heading`)} title={t(`me.push.${type}.heading`)}
@ -113,7 +108,6 @@ const TabMePush: React.FC = () => {
switchOnValueChange={() => switchOnValueChange={() =>
dispatch( dispatch(
updateInstancePushAlert({ updateInstancePushAlert({
changed: type,
alerts: { alerts: {
...instancePush?.alerts, ...instancePush?.alerts,
[type]: instancePush?.alerts[type] [type]: instancePush?.alerts[type]

View File

@ -1,73 +0,0 @@
import { HeaderCenter, HeaderRight } from '@components/Header'
import Timeline from '@components/Timeline'
import TimelineNotifications from '@components/Timeline/Notifications'
import navigationRef from '@helpers/navigationRef'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { TabNotificationsStackParamList } from '@utils/navigation/navigators'
import usePopToTop from '@utils/navigation/usePopToTop'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native'
import TabShared from './Shared'
const Stack = createNativeStackNavigator<TabNotificationsStackParamList>()
const TabNotifications = React.memo(
() => {
const { t, i18n } = useTranslation('screenTabs')
const screenOptionsRoot = useMemo(
() => ({
title: t('tabs.notifications.name'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={t('tabs.notifications.name')} />
}),
headerRight: () => (
<HeaderRight
accessibilityLabel={t('notifications.filter.accessibilityLabel')}
accessibilityHint={t('notifications.filter.accessibilityHint')}
content='Filter'
onPress={() =>
navigationRef.navigate('Screen-Actions', {
type: 'notifications_filter'
})
}
/>
)
}),
[i18n.language]
)
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }]
const children = useCallback(
() => (
<Timeline
queryKey={queryKey}
customProps={{
renderItem: ({ item }) => (
<TimelineNotifications notification={item} queryKey={queryKey} />
)
}}
/>
),
[]
)
usePopToTop()
return (
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
<Stack.Screen
name='Tab-Notifications-Root'
children={children}
options={screenOptionsRoot}
/>
{TabShared({ Stack })}
</Stack.Navigator>
)
},
() => true
)
export default TabNotifications

View File

@ -0,0 +1,138 @@
import { HeaderLeft, HeaderRight } from '@components/Header'
import { MenuContainer, MenuRow } from '@components/Menu'
import {
checkPermission,
PERMISSION_MANAGE_REPORTS,
PERMISSION_MANAGE_USERS
} from '@helpers/permissions'
import { useAppDispatch } from '@root/store'
import { useQueryClient } from '@tanstack/react-query'
import { TabNotificationsStackScreenProps } from '@utils/navigation/navigators'
import { useProfileQuery } from '@utils/queryHooks/profile'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import {
checkInstanceFeature,
getInstanceNotificationsFilter,
updateInstanceNotificationsFilter
} from '@utils/slices/instancesSlice'
import { isEqual } from 'lodash'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler'
import { useSelector } from 'react-redux'
export const NOTIFICATIONS_FILTERS_DEFAULT: [
'follow',
'follow_request',
'favourite',
'reblog',
'mention',
'poll',
'status',
'update'
] = ['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'poll', 'status', 'update']
export const NOTIFICATIONS_FILTERS_ADMIN: {
type: 'admin.sign_up' | 'admin.report'
permission: number
}[] = [
{ type: 'admin.sign_up', permission: PERMISSION_MANAGE_USERS },
{ type: 'admin.report', permission: PERMISSION_MANAGE_REPORTS }
]
const TabNotificationsFilters: React.FC<
TabNotificationsStackScreenProps<'Tab-Notifications-Filters'>
> = ({ navigation }) => {
const { t } = useTranslation('screenTabs')
const hasTypeStatus = useSelector(checkInstanceFeature('notification_type_status'))
const hasTypeUpdate = useSelector(checkInstanceFeature('notification_type_update'))
const dispatch = useAppDispatch()
const instanceNotificationsFilter = useSelector(getInstanceNotificationsFilter)
const [filters, setFilters] = useState(instanceNotificationsFilter)
const queryClient = useQueryClient()
useEffect(() => {
const changed = !isEqual(instanceNotificationsFilter, filters)
navigation.setOptions({
title: t('notifications.filters.title'),
headerLeft: () => (
<HeaderLeft
content='ChevronDown'
onPress={() => {
if (changed) {
Alert.alert(t('common:discard.title'), t('common:discard.message'), [
{
text: t('common:buttons.discard'),
style: 'destructive',
onPress: () => navigation.goBack()
},
{
text: t('common:buttons.cancel'),
style: 'default'
}
])
} else {
navigation.goBack()
}
}}
/>
),
headerRight: () => (
<HeaderRight
type='text'
content={t('common:buttons.apply')}
onPress={() => {
if (changed) {
dispatch(updateInstanceNotificationsFilter(filters))
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }]
queryClient.invalidateQueries({ queryKey })
}
navigation.goBack()
}}
/>
)
})
}, [filters])
const profileQuery = useProfileQuery()
return (
<ScrollView style={{ flex: 1 }}>
<MenuContainer>
{NOTIFICATIONS_FILTERS_DEFAULT.filter(type => {
switch (type) {
case 'status':
return hasTypeStatus
case 'update':
return hasTypeUpdate
default:
return true
}
}).map((type, index) => (
<MenuRow
key={index}
title={t(`notifications.filters.options.${type}`)}
switchValue={filters[type]}
switchOnValueChange={() => setFilters({ ...filters, [type]: !filters[type] })}
/>
))}
{NOTIFICATIONS_FILTERS_ADMIN.filter(({ permission }) =>
checkPermission(permission, profileQuery.data?.role?.permissions)
).map(({ type }) => (
<MenuRow
key={type}
title={t(`notifications.filters.options.${type}`)}
switchValue={filters[type]}
switchOnValueChange={() => setFilters({ ...filters, [type]: !filters[type] })}
/>
))}
</MenuContainer>
</ScrollView>
)
}
export default TabNotificationsFilters

View File

@ -0,0 +1,61 @@
import { HeaderRight } from '@components/Header'
import Timeline from '@components/Timeline'
import TimelineNotifications from '@components/Timeline/Notifications'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { ScreenTabsScreenProps, TabNotificationsStackParamList } from '@utils/navigation/navigators'
import usePopToTop from '@utils/navigation/usePopToTop'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React from 'react'
import { useTranslation } from 'react-i18next'
import TabShared from '../Shared'
import TabNotificationsFilters from './Filters'
const Stack = createNativeStackNavigator<TabNotificationsStackParamList>()
const Root = () => {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }]
return (
<Timeline
queryKey={queryKey}
customProps={{
renderItem: ({ item }) => <TimelineNotifications notification={item} queryKey={queryKey} />
}}
/>
)
}
const TabNotifications = ({ navigation }: ScreenTabsScreenProps<'Tab-Notifications'>) => {
const { t } = useTranslation('screenTabs')
usePopToTop()
return (
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
<Stack.Screen
name='Tab-Notifications-Root'
component={Root}
options={{
title: t('tabs.notifications.name'),
headerRight: () => (
<HeaderRight
accessibilityLabel={t('notifications.filters.accessibilityLabel')}
accessibilityHint={t('notifications.filters.accessibilityHint')}
content='Filter'
onPress={() =>
navigation.navigate('Tab-Notifications', { screen: 'Tab-Notifications-Filters' })
}
/>
)
}}
/>
<Stack.Screen
name='Tab-Notifications-Filters'
component={TabNotificationsFilters}
options={{ presentation: 'modal', gestureEnabled: false }}
/>
{TabShared({ Stack })}
</Stack.Navigator>
)
}
export default TabNotifications

View File

@ -6,15 +6,10 @@ import TabSharedHistory from '@screens/Tabs/Shared/History'
import TabSharedSearch from '@screens/Tabs/Shared/Search' import TabSharedSearch from '@screens/Tabs/Shared/Search'
import TabSharedToot from '@screens/Tabs/Shared/Toot' import TabSharedToot from '@screens/Tabs/Shared/Toot'
import TabSharedUsers from '@screens/Tabs/Shared/Users' import TabSharedUsers from '@screens/Tabs/Shared/Users'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next'
import TabSharedAccountInLists from './AccountInLists' import TabSharedAccountInLists from './AccountInLists'
const TabShared = ({ Stack }: { Stack: ReturnType<typeof createNativeStackNavigator> }) => { const TabShared = ({ Stack }: { Stack: ReturnType<typeof createNativeStackNavigator> }) => {
const { colors, mode } = useTheme()
const { t } = useTranslation('screenTabs')
return ( return (
<Stack.Group> <Stack.Group>
<Stack.Screen <Stack.Screen

View File

@ -136,6 +136,11 @@ const instancesMigration = {
return { return {
...instance, ...instance,
mePage: { ...instance.mePage, followedTags: { shown: false } }, mePage: { ...instance.mePage, followedTags: { shown: false } },
notifications_filter: {
...instance.notifications_filter,
'admin.sign_up': true,
'admin.report': true
},
push: { push: {
...instance.push, ...instance.push,
global: instance.push.global.value, global: instance.push.global.value,

View File

@ -29,6 +29,8 @@ export type InstanceV11 = {
poll: boolean poll: boolean
status: boolean status: boolean
update: boolean update: boolean
'admin.sign_up': boolean
'admin.report': boolean
} }
push: { push: {
global: boolean global: boolean

View File

@ -136,7 +136,10 @@ export type TabPublicStackParamList = {
export type TabNotificationsStackParamList = { export type TabNotificationsStackParamList = {
'Tab-Notifications-Root': undefined 'Tab-Notifications-Root': undefined
'Tab-Notifications-Filters': undefined
} & TabSharedStackParamList } & TabSharedStackParamList
export type TabNotificationsStackScreenProps<T extends keyof TabNotificationsStackParamList> =
NativeStackScreenProps<TabNotificationsStackParamList, T>
export type TabMeStackParamList = { export type TabMeStackParamList = {
'Tab-Me-Root': undefined 'Tab-Me-Root': undefined

View File

@ -87,7 +87,9 @@ const addInstance = createAsyncThunk(
mention: true, mention: true,
poll: true, poll: true,
status: true, status: true,
update: true update: true,
'admin.sign_up': true,
'admin.report': true
}, },
push: { push: {
global: false, global: false,

View File

@ -1,4 +1,8 @@
import { PERMISSION_MANAGE_REPORTS, PERMISSION_MANAGE_USERS } from '@helpers/permissions' import {
checkPermission,
PERMISSION_MANAGE_REPORTS,
PERMISSION_MANAGE_USERS
} from '@helpers/permissions'
import queryClient from '@helpers/queryClient' import queryClient from '@helpers/queryClient'
import i18n from '@root/i18n/i18n' import i18n from '@root/i18n/i18n'
import { InstanceLatest } from '@utils/migrations/instances/migration' import { InstanceLatest } from '@utils/migrations/instances/migration'
@ -20,16 +24,6 @@ export const PUSH_ADMIN: { type: 'admin.sign_up' | 'admin.report'; permission: n
{ type: 'admin.report', permission: PERMISSION_MANAGE_REPORTS } { type: 'admin.report', permission: PERMISSION_MANAGE_REPORTS }
] ]
export const checkPushAdminPermission = (
permission: number,
permissions?: string | number
): boolean =>
permissions
? !!(
(typeof permissions === 'string' ? parseInt(permissions || '0') : permissions) & permission
)
: false
export const setChannels = async (instance: InstanceLatest) => { export const setChannels = async (instance: InstanceLatest) => {
const account = `@${instance.account.acct}@${instance.uri}` const account = `@${instance.account.acct}@${instance.uri}`
@ -68,7 +62,7 @@ export const setChannels = async (instance: InstanceLatest) => {
await setChannel(push) await setChannel(push)
} }
for (const { type, permission } of PUSH_ADMIN) { for (const { type, permission } of PUSH_ADMIN) {
if (checkPushAdminPermission(permission, profileQuery.role?.permissions)) { if (checkPermission(permission, profileQuery.role?.permissions)) {
await setChannel(type) await setChannel(type)
} }
} }

View File

@ -7,7 +7,6 @@ export const updateInstancePushAlert = createAsyncThunk(
async ({ async ({
alerts alerts
}: { }: {
changed: keyof InstanceLatest['push']['alerts']
alerts: InstanceLatest['push']['alerts'] alerts: InstanceLatest['push']['alerts']
}): Promise<InstanceLatest['push']['alerts']> => { }): Promise<InstanceLatest['push']['alerts']> => {
const formData = new FormData() const formData = new FormData()