mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Filter posts containing mute words from search and notifications (#5599)

* Filter mute words from search

* Filter mute words from notifications

* Do no filter search if using from filter

authored by

Eric Bailey and committed by
GitHub
1db39ed1 fc82d2f6

+170 -18
+86 -17
src/state/queries/notifications/feed.ts
··· 17 17 */ 18 18 19 19 import {useCallback, useEffect, useMemo, useRef} from 'react' 20 - import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api' 20 + import { 21 + AppBskyActorDefs, 22 + AppBskyFeedDefs, 23 + AppBskyFeedPost, 24 + AtUri, 25 + } from '@atproto/api' 21 26 import { 22 27 InfiniteData, 23 28 QueryClient, ··· 26 31 useQueryClient, 27 32 } from '@tanstack/react-query' 28 33 34 + import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' 29 35 import {useAgent} from '#/state/session' 30 36 import {useThreadgateHiddenReplyUris} from '#/state/threadgate-hidden-replies' 31 37 import {useModerationOpts} from '../../preferences/moderation-opts' ··· 67 73 68 74 const selectArgs = useMemo(() => { 69 75 return { 76 + moderationOpts, 70 77 hiddenReplyUris, 71 78 } 72 - }, [hiddenReplyUris]) 79 + }, [moderationOpts, hiddenReplyUris]) 80 + const lastRun = useRef<{ 81 + data: InfiniteData<FeedPage> 82 + args: typeof selectArgs 83 + result: InfiniteData<FeedPage> 84 + } | null>(null) 73 85 74 86 const query = useInfiniteQuery< 75 87 FeedPage, ··· 111 123 enabled, 112 124 select: useCallback( 113 125 (data: InfiniteData<FeedPage>) => { 114 - const {hiddenReplyUris} = selectArgs 126 + const {moderationOpts, hiddenReplyUris} = selectArgs 127 + 128 + // Keep track of the last run and whether we can reuse 129 + // some already selected pages from there. 130 + let reusedPages = [] 131 + if (lastRun.current) { 132 + const { 133 + data: lastData, 134 + args: lastArgs, 135 + result: lastResult, 136 + } = lastRun.current 137 + let canReuse = true 138 + for (let key in selectArgs) { 139 + if (selectArgs.hasOwnProperty(key)) { 140 + if ((selectArgs as any)[key] !== (lastArgs as any)[key]) { 141 + // Can't do reuse anything if any input has changed. 142 + canReuse = false 143 + break 144 + } 145 + } 146 + } 147 + if (canReuse) { 148 + for (let i = 0; i < data.pages.length; i++) { 149 + if (data.pages[i] && lastData.pages[i] === data.pages[i]) { 150 + reusedPages.push(lastResult.pages[i]) 151 + continue 152 + } 153 + // Stop as soon as pages stop matching up. 154 + break 155 + } 156 + } 157 + } 115 158 116 159 // override 'isRead' using the first page's returned seenAt 117 160 // we do this because the `markAllRead()` call above will ··· 124 167 } 125 168 } 126 169 127 - data = { 170 + const result = { 128 171 ...data, 129 - pages: data.pages.map(page => { 130 - return { 131 - ...page, 132 - items: page.items.filter(item => { 133 - const isHiddenReply = 134 - item.type === 'reply' && 135 - item.subjectUri && 136 - hiddenReplyUris.has(item.subjectUri) 137 - return !isHiddenReply 138 - }), 139 - } 140 - }), 172 + pages: [ 173 + ...reusedPages, 174 + ...data.pages.slice(reusedPages.length).map(page => { 175 + return { 176 + ...page, 177 + items: page.items 178 + .filter(item => { 179 + const isHiddenReply = 180 + item.type === 'reply' && 181 + item.subjectUri && 182 + hiddenReplyUris.has(item.subjectUri) 183 + return !isHiddenReply 184 + }) 185 + .filter(item => { 186 + if ( 187 + item.type === 'reply' || 188 + item.type === 'mention' || 189 + item.type === 'quote' 190 + ) { 191 + /* 192 + * The `isPostView` check will fail here bc we don't have 193 + * a `$type` field on the `subject`. But if the nested 194 + * `record` is a post, we know it's a post view. 195 + */ 196 + if (AppBskyFeedPost.isRecord(item.subject?.record)) { 197 + const mod = moderatePost(item.subject, moderationOpts!) 198 + if (mod.ui('contentList').filter) { 199 + return false 200 + } 201 + } 202 + } 203 + return true 204 + }), 205 + } 206 + }), 207 + ], 141 208 } 142 209 143 - return data 210 + lastRun.current = {data, result, args: selectArgs} 211 + 212 + return result 144 213 }, 145 214 [selectArgs], 146 215 ),
+84 -1
src/state/queries/search-posts.ts
··· 1 + import React from 'react' 1 2 import { 2 3 AppBskyActorDefs, 3 4 AppBskyFeedDefs, ··· 11 12 useInfiniteQuery, 12 13 } from '@tanstack/react-query' 13 14 15 + import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' 16 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 14 17 import {useAgent} from '#/state/session' 15 18 import { 16 19 didOrHandleUriMatches, ··· 35 38 enabled?: boolean 36 39 }) { 37 40 const agent = useAgent() 41 + const moderationOpts = useModerationOpts() 42 + const selectArgs = React.useMemo( 43 + () => ({ 44 + isSearchingSpecificUser: /from:(\w+)/.test(query), 45 + moderationOpts, 46 + }), 47 + [query, moderationOpts], 48 + ) 49 + const lastRun = React.useRef<{ 50 + data: InfiniteData<AppBskyFeedSearchPosts.OutputSchema> 51 + args: typeof selectArgs 52 + result: InfiniteData<AppBskyFeedSearchPosts.OutputSchema> 53 + } | null>(null) 54 + 38 55 return useInfiniteQuery< 39 56 AppBskyFeedSearchPosts.OutputSchema, 40 57 Error, ··· 54 71 }, 55 72 initialPageParam: undefined, 56 73 getNextPageParam: lastPage => lastPage.cursor, 57 - enabled, 74 + enabled: enabled ?? !!moderationOpts, 75 + select: React.useCallback( 76 + (data: InfiniteData<AppBskyFeedSearchPosts.OutputSchema>) => { 77 + const {moderationOpts, isSearchingSpecificUser} = selectArgs 78 + 79 + /* 80 + * If a user applies the `from:<user>` filter, don't apply any 81 + * moderation. Note that if we add any more filtering logic below, we 82 + * may need to adjust this. 83 + */ 84 + if (isSearchingSpecificUser) { 85 + return data 86 + } 87 + 88 + // Keep track of the last run and whether we can reuse 89 + // some already selected pages from there. 90 + let reusedPages = [] 91 + if (lastRun.current) { 92 + const { 93 + data: lastData, 94 + args: lastArgs, 95 + result: lastResult, 96 + } = lastRun.current 97 + let canReuse = true 98 + for (let key in selectArgs) { 99 + if (selectArgs.hasOwnProperty(key)) { 100 + if ((selectArgs as any)[key] !== (lastArgs as any)[key]) { 101 + // Can't do reuse anything if any input has changed. 102 + canReuse = false 103 + break 104 + } 105 + } 106 + } 107 + if (canReuse) { 108 + for (let i = 0; i < data.pages.length; i++) { 109 + if (data.pages[i] && lastData.pages[i] === data.pages[i]) { 110 + reusedPages.push(lastResult.pages[i]) 111 + continue 112 + } 113 + // Stop as soon as pages stop matching up. 114 + break 115 + } 116 + } 117 + } 118 + 119 + const result = { 120 + ...data, 121 + pages: [ 122 + ...reusedPages, 123 + ...data.pages.slice(reusedPages.length).map(page => { 124 + return { 125 + ...page, 126 + posts: page.posts.filter(post => { 127 + const mod = moderatePost(post, moderationOpts!) 128 + return !mod.ui('contentList').filter 129 + }), 130 + } 131 + }), 132 + ], 133 + } 134 + 135 + lastRun.current = {data, result, args: selectArgs} 136 + 137 + return result 138 + }, 139 + [selectArgs], 140 + ), 58 141 }) 59 142 } 60 143