Bluesky app fork with some witchin' additions 💫

Added filter for follow notifications (#2)

authored by

m-doescode and committed by
Aviva Ruben
c5b1dc0b 360587c4

+139 -30
+26
src/screens/Settings/DeerSettings.tsx
··· 24 24 useSetDirectFetchRecords, 25 25 } from '#/state/preferences/direct-fetch-records' 26 26 import { 27 + useHideFollowNotifications, 28 + useSetHideFollowNotifications, 29 + } from '#/state/preferences/hide-follow-notifications' 30 + import { 27 31 useNoAppLabelers, 28 32 useSetNoAppLabelers, 29 33 } from '#/state/preferences/no-app-labelers' ··· 43 47 import * as Dialog from '#/components/Dialog' 44 48 import * as Toggle from '#/components/forms/Toggle' 45 49 import {Atom_Stroke2_Corner0_Rounded as DeerIcon} from '#/components/icons/Atom' 50 + import {Bell_Stroke2_Corner0_Rounded as BellIcon} from '#/components/icons/Bell' 46 51 import {Eye_Stroke2_Corner0_Rounded as VisibilityIcon} from '#/components/icons/Eye' 47 52 import {Earth_Stroke2_Corner2_Rounded as GlobeIcon} from '#/components/icons/Globe' 48 53 import {Lab_Stroke2_Corner0_Rounded as BeakerIcon} from '#/components/icons/Lab' ··· 135 140 136 141 const noDiscoverFallback = useNoDiscoverFallback() 137 142 const setNoDiscoverFallback = useSetNoDiscoverFallback() 143 + 144 + const hideFollowNotifications = useHideFollowNotifications() 145 + const setHideFollowNotifications = useSetHideFollowNotifications() 138 146 139 147 const location = useGeolocation() 140 148 const setLocationControl = Dialog.useDialogControl() ··· 309 317 style={[a.w_full]}> 310 318 <Toggle.LabelText style={[a.flex_1]}> 311 319 <Trans>Do not fall back to discover feed</Trans> 320 + </Toggle.LabelText> 321 + <Toggle.Platform /> 322 + </Toggle.Item> 323 + </SettingsList.Group> 324 + 325 + <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 326 + <SettingsList.ItemIcon icon={BellIcon} /> 327 + <SettingsList.ItemText> 328 + <Trans>Notification Filters</Trans> 329 + </SettingsList.ItemText> 330 + <Toggle.Item 331 + name="hide_follow_notifications" 332 + label={_(msg`Hide follow notifications`)} 333 + value={hideFollowNotifications ?? false} 334 + onChange={value => setHideFollowNotifications(value)} 335 + style={[a.w_full]}> 336 + <Toggle.LabelText style={[a.flex_1]}> 337 + <Trans>Hide follow notifications</Trans> 312 338 </Toggle.LabelText> 313 339 <Toggle.Platform /> 314 340 </Toggle.Item>
+1 -1
src/screens/Settings/NotificationSettings.tsx
··· 2 2 import {msg, Trans} from '@lingui/macro' 3 3 import {useLingui} from '@lingui/react' 4 4 5 - import {AllNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 5 + import {type AllNavigatorParams, type NativeStackScreenProps} from '#/lib/routes/types' 6 6 import {useNotificationFeedQuery} from '#/state/queries/notifications/feed' 7 7 import {useNotificationSettingsMutation} from '#/state/queries/notifications/settings' 8 8 import {atoms as a} from '#/alf'
+2
src/state/persisted/schema.ts
··· 131 131 noAppLabelers: z.boolean().optional(), 132 132 noDiscoverFallback: z.boolean().optional(), 133 133 repostCarouselEnabled: z.boolean().optional(), 134 + hideFollowNotifications: z.boolean().optional(), 134 135 135 136 /** @deprecated */ 136 137 mutedThreads: z.array(z.string()), ··· 191 192 noAppLabelers: false, 192 193 noDiscoverFallback: false, 193 194 repostCarouselEnabled: false, 195 + hideFollowNotifications: false, 194 196 } 195 197 196 198 export function tryParse(rawData: string): Schema | undefined {
+52
src/state/preferences/hide-follow-notifications.tsx
··· 1 + import React from 'react' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + type StateContext = persisted.Schema['hideFollowNotifications'] 6 + type SetContext = (v: persisted.Schema['hideFollowNotifications']) => void 7 + 8 + const stateContext = React.createContext<StateContext>( 9 + persisted.defaults.hideFollowNotifications, 10 + ) 11 + const setContext = React.createContext<SetContext>( 12 + (_: persisted.Schema['hideFollowNotifications']) => {}, 13 + ) 14 + 15 + export function Provider({children}: React.PropsWithChildren<{}>) { 16 + const [state, setState] = React.useState( 17 + persisted.get('hideFollowNotifications'), 18 + ) 19 + 20 + const setStateWrapped = React.useCallback( 21 + (hideFollowNotifications: persisted.Schema['hideFollowNotifications']) => { 22 + setState(hideFollowNotifications) 23 + persisted.write('hideFollowNotifications', hideFollowNotifications) 24 + }, 25 + [setState], 26 + ) 27 + 28 + React.useEffect(() => { 29 + return persisted.onUpdate( 30 + 'hideFollowNotifications', 31 + nextHideFollowNotifications => { 32 + setState(nextHideFollowNotifications) 33 + }, 34 + ) 35 + }, [setStateWrapped]) 36 + 37 + return ( 38 + <stateContext.Provider value={state}> 39 + <setContext.Provider value={setStateWrapped}> 40 + {children} 41 + </setContext.Provider> 42 + </stateContext.Provider> 43 + ) 44 + } 45 + 46 + export function useHideFollowNotifications() { 47 + return React.useContext(stateContext) 48 + } 49 + 50 + export function useSetHideFollowNotifications() { 51 + return React.useContext(setContext) 52 + }
+29 -26
src/state/preferences/index.tsx
··· 8 8 import {Provider as ExternalEmbedsProvider} from './external-embeds-prefs' 9 9 import {Provider as GoLinksProvider} from './go-links-enabled' 10 10 import {Provider as HiddenPostsProvider} from './hidden-posts' 11 + import {Provider as FollowNotificationsProvider} from './hide-follow-notifications' 11 12 import {Provider as InAppBrowserProvider} from './in-app-browser' 12 13 import {Provider as KawaiiProvider} from './kawaii' 13 14 import {Provider as LanguagesProvider} from './languages' ··· 41 42 <AltTextRequiredProvider> 42 43 <GoLinksProvider> 43 44 <NoAppLabelersProvider> 44 - <DirectFetchRecordsProvider> 45 - <ConstellationProvider> 46 - <NoDiscoverProvider> 47 - <LargeAltBadgeProvider> 48 - <ExternalEmbedsProvider> 49 - <HiddenPostsProvider> 50 - <InAppBrowserProvider> 51 - <DisableHapticsProvider> 52 - <AutoplayProvider> 53 - <UsedStarterPacksProvider> 54 - <SubtitlesProvider> 55 - <TrendingSettingsProvider> 56 - <RepostCarouselProvider> 45 + <FollowNotificationsProvider> 46 + <DirectFetchRecordsProvider> 47 + <ConstellationProvider> 48 + <NoDiscoverProvider> 49 + <LargeAltBadgeProvider> 50 + <ExternalEmbedsProvider> 51 + <HiddenPostsProvider> 52 + <InAppBrowserProvider> 53 + <DisableHapticsProvider> 54 + <AutoplayProvider> 55 + <UsedStarterPacksProvider> 56 + <SubtitlesProvider> 57 + <TrendingSettingsProvider> 58 + <RepostCarouselProvider> 57 59 <KawaiiProvider>{children}</KawaiiProvider> 58 - </RepostCarouselProvider> 59 - </TrendingSettingsProvider> 60 - </SubtitlesProvider> 61 - </UsedStarterPacksProvider> 62 - </AutoplayProvider> 63 - </DisableHapticsProvider> 64 - </InAppBrowserProvider> 65 - </HiddenPostsProvider> 66 - </ExternalEmbedsProvider> 67 - </LargeAltBadgeProvider> 68 - </NoDiscoverProvider> 69 - </ConstellationProvider> 70 - </DirectFetchRecordsProvider> 60 + </RepostCarouselProvider> 61 + </TrendingSettingsProvider> 62 + </SubtitlesProvider> 63 + </UsedStarterPacksProvider> 64 + </AutoplayProvider> 65 + </DisableHapticsProvider> 66 + </InAppBrowserProvider> 67 + </HiddenPostsProvider> 68 + </ExternalEmbedsProvider> 69 + </LargeAltBadgeProvider> 70 + </NoDiscoverProvider> 71 + </ConstellationProvider> 72 + </DirectFetchRecordsProvider> 73 + </FollowNotificationsProvider> 71 74 </NoAppLabelersProvider> 72 75 </GoLinksProvider> 73 76 </AltTextRequiredProvider>
+3
src/state/queries/notifications/feed.ts
··· 32 32 useQueryClient, 33 33 } from '@tanstack/react-query' 34 34 35 + import {useHideFollowNotifications} from '#/state/preferences/hide-follow-notifications' 35 36 import {useAgent} from '#/state/session' 36 37 import {useThreadgateHiddenReplyUris} from '#/state/threadgate-hidden-replies' 37 38 import {useModerationOpts} from '../../preferences/moderation-opts' ··· 63 64 const agent = useAgent() 64 65 const queryClient = useQueryClient() 65 66 const moderationOpts = useModerationOpts() 67 + const hideFollowNotifications = useHideFollowNotifications() 66 68 const unreads = useUnreadNotificationsApi() 67 69 const enabled = opts.enabled !== false 68 70 const filter = opts.filter ··· 111 113 cursor: pageParam, 112 114 queryClient, 113 115 moderationOpts, 116 + hideFollowNotifications, 114 117 fetchAdditionalData: true, 115 118 reasons, 116 119 })
+10 -1
src/state/queries/notifications/unread.tsx
··· 10 10 import BroadcastChannel from '#/lib/broadcast' 11 11 import {resetBadgeCount} from '#/lib/notifications/notifications' 12 12 import {logger} from '#/logger' 13 + import {useHideFollowNotifications} from '#/state/preferences/hide-follow-notifications' 13 14 import {useAgent, useSession} from '#/state/session' 14 15 import {useModerationOpts} from '../../preferences/moderation-opts' 15 16 import {truncateAndInvalidate} from '../util' ··· 47 48 const agent = useAgent() 48 49 const queryClient = useQueryClient() 49 50 const moderationOpts = useModerationOpts() 51 + const hideFollowNotifications = useHideFollowNotifications() 50 52 51 53 const [numUnread, setNumUnread] = React.useState('') 52 54 ··· 153 155 limit: 40, 154 156 queryClient, 155 157 moderationOpts, 158 + hideFollowNotifications, 156 159 reasons: [], 157 160 158 161 // only fetch subjects when the page is going to be used ··· 201 204 } 202 205 }, 203 206 } 204 - }, [setNumUnread, queryClient, moderationOpts, agent]) 207 + }, [ 208 + setNumUnread, 209 + queryClient, 210 + moderationOpts, 211 + hideFollowNotifications, 212 + agent, 213 + ]) 205 214 checkUnreadRef.current = api.checkUnread 206 215 207 216 return (
+7 -1
src/state/queries/notifications/util.ts
··· 35 35 limit, 36 36 queryClient, 37 37 moderationOpts, 38 + hideFollowNotifications, 38 39 fetchAdditionalData, 39 40 reasons, 40 41 }: { ··· 43 44 limit: number 44 45 queryClient: QueryClient 45 46 moderationOpts: ModerationOpts | undefined 47 + hideFollowNotifications: boolean | undefined 46 48 fetchAdditionalData: boolean 47 49 reasons: string[] 48 50 }): Promise<{ ··· 59 61 60 62 // filter out notifs by mod rules 61 63 const notifs = res.data.notifications.filter( 62 - notif => !shouldFilterNotif(notif, moderationOpts), 64 + notif => !shouldFilterNotif(notif, moderationOpts, hideFollowNotifications), 63 65 ) 64 66 65 67 // group notifications which are essentially similar (follows, likes on a post) ··· 110 112 export function shouldFilterNotif( 111 113 notif: AppBskyNotificationListNotifications.Notification, 112 114 moderationOpts: ModerationOpts | undefined, 115 + hideFollowNotifications: boolean | undefined, 113 116 ): boolean { 114 117 const containsImperative = !!notif.author.labels?.some(labelIsHideableOffense) 115 118 if (containsImperative) { 119 + return true 120 + } 121 + if (hideFollowNotifications && notif.reason == 'follow') { 116 122 return true 117 123 } 118 124 if (!moderationOpts) {
+9 -1
src/view/screens/DebugMod.tsx
··· 25 25 type NativeStackScreenProps, 26 26 } from '#/lib/routes/types' 27 27 import {useModerationOpts} from '#/state/preferences/moderation-opts' 28 + import {useHideFollowNotifications} from '#/state/preferences/hide-follow-notifications' 28 29 import {moderationOptsOverrideContext} from '#/state/preferences/moderation-opts' 29 30 import {type FeedNotification} from '#/state/queries/notifications/types' 30 31 import { ··· 869 870 moderationOpts: ModerationOpts 870 871 }) { 871 872 const t = useTheme() 872 - if (shouldFilterNotif(notif.notification, moderationOpts)) { 873 + const hideFollowNotifications = useHideFollowNotifications() 874 + if ( 875 + shouldFilterNotif( 876 + notif.notification, 877 + moderationOpts, 878 + hideFollowNotifications, 879 + ) 880 + ) { 873 881 return ( 874 882 <P style={[t.atoms.bg_contrast_25, a.px_lg, a.py_md]}> 875 883 Filtered from the feed