Skip to content

Commit

Permalink
Filter posts containing mute words from search and notifications (#5599)
Browse files Browse the repository at this point in the history
* Filter mute words from search

* Filter mute words from notifications

* Do no filter search if using from filter
  • Loading branch information
estrattonbailey authored Oct 8, 2024
1 parent fc82d2f commit 1db39ed
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 18 deletions.
103 changes: 86 additions & 17 deletions src/state/queries/notifications/feed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
*/

import {useCallback, useEffect, useMemo, useRef} from 'react'
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
import {
AppBskyActorDefs,
AppBskyFeedDefs,
AppBskyFeedPost,
AtUri,
} from '@atproto/api'
import {
InfiniteData,
QueryClient,
Expand All @@ -26,6 +31,7 @@ import {
useQueryClient,
} from '@tanstack/react-query'

import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {useAgent} from '#/state/session'
import {useThreadgateHiddenReplyUris} from '#/state/threadgate-hidden-replies'
import {useModerationOpts} from '../../preferences/moderation-opts'
Expand Down Expand Up @@ -67,9 +73,15 @@ export function useNotificationFeedQuery(opts?: {

const selectArgs = useMemo(() => {
return {
moderationOpts,
hiddenReplyUris,
}
}, [hiddenReplyUris])
}, [moderationOpts, hiddenReplyUris])
const lastRun = useRef<{
data: InfiniteData<FeedPage>
args: typeof selectArgs
result: InfiniteData<FeedPage>
} | null>(null)

const query = useInfiniteQuery<
FeedPage,
Expand Down Expand Up @@ -111,7 +123,38 @@ export function useNotificationFeedQuery(opts?: {
enabled,
select: useCallback(
(data: InfiniteData<FeedPage>) => {
const {hiddenReplyUris} = selectArgs
const {moderationOpts, hiddenReplyUris} = selectArgs

// Keep track of the last run and whether we can reuse
// some already selected pages from there.
let reusedPages = []
if (lastRun.current) {
const {
data: lastData,
args: lastArgs,
result: lastResult,
} = lastRun.current
let canReuse = true
for (let key in selectArgs) {
if (selectArgs.hasOwnProperty(key)) {
if ((selectArgs as any)[key] !== (lastArgs as any)[key]) {
// Can't do reuse anything if any input has changed.
canReuse = false
break
}
}
}
if (canReuse) {
for (let i = 0; i < data.pages.length; i++) {
if (data.pages[i] && lastData.pages[i] === data.pages[i]) {
reusedPages.push(lastResult.pages[i])
continue
}
// Stop as soon as pages stop matching up.
break
}
}
}

// override 'isRead' using the first page's returned seenAt
// we do this because the `markAllRead()` call above will
Expand All @@ -124,23 +167,49 @@ export function useNotificationFeedQuery(opts?: {
}
}

data = {
const result = {
...data,
pages: data.pages.map(page => {
return {
...page,
items: page.items.filter(item => {
const isHiddenReply =
item.type === 'reply' &&
item.subjectUri &&
hiddenReplyUris.has(item.subjectUri)
return !isHiddenReply
}),
}
}),
pages: [
...reusedPages,
...data.pages.slice(reusedPages.length).map(page => {
return {
...page,
items: page.items
.filter(item => {
const isHiddenReply =
item.type === 'reply' &&
item.subjectUri &&
hiddenReplyUris.has(item.subjectUri)
return !isHiddenReply
})
.filter(item => {
if (
item.type === 'reply' ||
item.type === 'mention' ||
item.type === 'quote'
) {
/*
* The `isPostView` check will fail here bc we don't have
* a `$type` field on the `subject`. But if the nested
* `record` is a post, we know it's a post view.
*/
if (AppBskyFeedPost.isRecord(item.subject?.record)) {
const mod = moderatePost(item.subject, moderationOpts!)
if (mod.ui('contentList').filter) {
return false
}
}
}
return true
}),
}
}),
],
}

return data
lastRun.current = {data, result, args: selectArgs}

return result
},
[selectArgs],
),
Expand Down
85 changes: 84 additions & 1 deletion src/state/queries/search-posts.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from 'react'
import {
AppBskyActorDefs,
AppBskyFeedDefs,
Expand All @@ -11,6 +12,8 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'

import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useAgent} from '#/state/session'
import {
didOrHandleUriMatches,
Expand All @@ -35,6 +38,20 @@ export function useSearchPostsQuery({
enabled?: boolean
}) {
const agent = useAgent()
const moderationOpts = useModerationOpts()
const selectArgs = React.useMemo(
() => ({
isSearchingSpecificUser: /from:(\w+)/.test(query),
moderationOpts,
}),
[query, moderationOpts],
)
const lastRun = React.useRef<{
data: InfiniteData<AppBskyFeedSearchPosts.OutputSchema>
args: typeof selectArgs
result: InfiniteData<AppBskyFeedSearchPosts.OutputSchema>
} | null>(null)

return useInfiniteQuery<
AppBskyFeedSearchPosts.OutputSchema,
Error,
Expand All @@ -54,7 +71,73 @@ export function useSearchPostsQuery({
},
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
enabled,
enabled: enabled ?? !!moderationOpts,
select: React.useCallback(
(data: InfiniteData<AppBskyFeedSearchPosts.OutputSchema>) => {
const {moderationOpts, isSearchingSpecificUser} = selectArgs

/*
* If a user applies the `from:<user>` filter, don't apply any
* moderation. Note that if we add any more filtering logic below, we
* may need to adjust this.
*/
if (isSearchingSpecificUser) {
return data
}

// Keep track of the last run and whether we can reuse
// some already selected pages from there.
let reusedPages = []
if (lastRun.current) {
const {
data: lastData,
args: lastArgs,
result: lastResult,
} = lastRun.current
let canReuse = true
for (let key in selectArgs) {
if (selectArgs.hasOwnProperty(key)) {
if ((selectArgs as any)[key] !== (lastArgs as any)[key]) {
// Can't do reuse anything if any input has changed.
canReuse = false
break
}
}
}
if (canReuse) {
for (let i = 0; i < data.pages.length; i++) {
if (data.pages[i] && lastData.pages[i] === data.pages[i]) {
reusedPages.push(lastResult.pages[i])
continue
}
// Stop as soon as pages stop matching up.
break
}
}
}

const result = {
...data,
pages: [
...reusedPages,
...data.pages.slice(reusedPages.length).map(page => {
return {
...page,
posts: page.posts.filter(post => {
const mod = moderatePost(post, moderationOpts!)
return !mod.ui('contentList').filter
}),
}
}),
],
}

lastRun.current = {data, result, args: selectArgs}

return result
},
[selectArgs],
),
})
}

Expand Down

0 comments on commit 1db39ed

Please sign in to comment.