import { isBlockingUser, isBlockedByViewer, isBlockedPost, getQuotedPost, getBlockedQuote, getReplyAuthors, getRootUri, isSelfOrFollowing, getMutedQuote, isMutedPost, isEmptyPost, doHideAuthorOnUnauthenticated, } from "/js/dataHelpers.js"; class FeedFilter { filterFeedItems(feedItems) { throw new Error("Not implemented"); } static compose(...filters) { return new ComposedFeedFilter(filters); } } class ComposedFeedFilter extends FeedFilter { constructor(filters) { super(); this.filters = filters; } filterFeedItems(feedItems) { return this.filters.reduce( (acc, filter) => filter.filterFeedItems(acc), feedItems, ); } } function isRepost(feedItem) { return feedItem.reason?.$type === "app.bsky.feed.defs#reasonRepost"; } function hasHiddenBadgeLabel(post) { return post?.badgeLabels?.some((badge) => badge.visibility === "hide"); } class FilterByFollowing extends FeedFilter { constructor(currentUser) { super(); this.currentUser = currentUser; } filterFeedItems(feedItems) { // Filter the feed items to only show posts from self or people you follow // Logic stolen from social-app: https://github.com/bluesky-social/social-app/blob/185fd39092cd4c43db060439b03c6c49be60a34e/src/lib/api/feed-manip.ts#L324 if (!this.currentUser) { return feedItems; } const userDid = this.currentUser.did; const filteredFeedItems = []; for (const feedItem of feedItems) { // Show all non-reply posts if (!feedItem.reply) { filteredFeedItems.push(feedItem); continue; } const author = feedItem.post.author; if (!author) { continue; } const { parentAuthor, grandparentAuthor, rootAuthor } = getReplyAuthors( feedItem.reply, ); if (!isSelfOrFollowing(author, userDid)) { // Only show replies from self or people you follow. continue; } if ( parentAuthor?.did === author.did || rootAuthor?.did === author.did || grandparentAuthor?.did === author.did ) { // Always show self-threads. filteredFeedItems.push(feedItem); continue; } // From this point on we need at least one more reason to show it. if (parentAuthor && isSelfOrFollowing(parentAuthor, userDid)) { filteredFeedItems.push(feedItem); continue; } if (grandparentAuthor && isSelfOrFollowing(grandparentAuthor, userDid)) { filteredFeedItems.push(feedItem); continue; } if (rootAuthor && isSelfOrFollowing(rootAuthor, userDid)) { filteredFeedItems.push(feedItem); continue; } } return filteredFeedItems; } } class FilterReposts extends FeedFilter { filterFeedItems(feedItems) { return feedItems.filter((item) => !isRepost(item)); } } class FilterReplies extends FeedFilter { filterFeedItems(feedItems) { return feedItems.filter((item) => { // Allow reposts to be replies if (isRepost(item)) { return true; } return !item.reply; }); } } class FilterQuotePosts extends FeedFilter { filterFeedItems(feedItems) { return feedItems.filter((item) => { // Allow reposts to be quoted posts if (isRepost(item)) { return true; } return !getQuotedPost(item.post); }); } } class DedupeFeed extends FeedFilter { constructor({ includeReposts = true } = {}) { super(); this.includeReposts = includeReposts; } filterFeedItems(feedItems) { const rootUris = new Set(); const dedupedFeedItems = []; for (const item of feedItems) { if (isRepost(item) && !this.includeReposts) { dedupedFeedItems.push(item); continue; } const rootUri = getRootUri(item); if (rootUris.has(rootUri)) { continue; } rootUris.add(rootUri); dedupedFeedItems.push(item); } return dedupedFeedItems; } } class FilterBlockedQuotes extends FeedFilter { filterFeedItems(feedItems) { return feedItems.filter((item) => { const blockedQuote = getBlockedQuote(item.post); if ( blockedQuote && (isBlockingUser(blockedQuote) || isBlockedByViewer(blockedQuote)) ) { return false; } return true; }); } } class FilterMutedQuotes extends FeedFilter { filterFeedItems(feedItems) { return feedItems.filter((item) => !getMutedQuote(item.post)); } } class FilterMutedPosts extends FeedFilter { filterFeedItems(feedItems) { // Filter out muted posts, including the reply context. return feedItems.filter((item) => { if (isMutedPost(item.post)) { return false; } if (item.reply?.parent && isMutedPost(item.reply.parent)) { return false; } if (item.reply?.root && isMutedPost(item.reply.root)) { return false; } return true; }); } } class FilterBlockedPosts extends FeedFilter { filterFeedItems(feedItems) { // The appview omits these on the server-side, this filter is in place // just for client-side updates return feedItems.filter((item) => !isBlockedPost(item.post)); } } class FilterEmptyPosts extends FeedFilter { constructor({ checkParents = true } = {}) { super(); this.checkParents = checkParents; } filterFeedItems(feedItems) { const filteredFeedItems = []; for (const item of feedItems) { if (isEmptyPost(item.post)) { continue; } if (this.checkParents) { if (item.reply?.parent && isEmptyPost(item.reply.parent)) { continue; } if (item.reply?.root && isEmptyPost(item.reply.root)) { continue; } } filteredFeedItems.push(item); } return filteredFeedItems; } } class FilterUnauthorizedPosts extends FeedFilter { constructor(isAuthenticated) { super(); this.isAuthenticated = isAuthenticated; } filterFeedItems(feedItems) { if (this.isAuthenticated) { return feedItems; } return feedItems.filter((item) => { if (item.post.author && doHideAuthorOnUnauthenticated(item.post.author)) { return false; } const quotedPost = getQuotedPost(item.post); if ( quotedPost?.author && doHideAuthorOnUnauthenticated(quotedPost.author) ) { return false; } return true; }); } } class FilterHiddenPosts extends FeedFilter { filterFeedItems(feedItems) { return feedItems.filter((item) => { if (item.post.viewer?.isHidden) { return false; } // Also filter hidden quotes const quotedPost = getQuotedPost(item.post); if (quotedPost && quotedPost.isHidden) { return false; } return true; }); } } class FilterContentLabeledPosts extends FeedFilter { filterFeedItems(feedItems) { return feedItems.filter((item) => { const contentLabel = item.post.contentLabel; if (contentLabel?.visibility === "hide") { return false; } if (hasHiddenBadgeLabel(item.post)) { return false; } const quotedPost = getQuotedPost(item.post); if (quotedPost?.contentLabel?.visibility === "hide") { return false; } if (hasHiddenBadgeLabel(quotedPost)) { return false; } return true; }); } } class FilterPluginFilteredPosts extends FeedFilter { constructor(pluginFilteredFeedItems) { super(); this.pluginFilteredFeedItems = pluginFilteredFeedItems; } filterFeedItems(feedItems) { return feedItems.filter( (item) => this.pluginFilteredFeedItems[item.post.uri] !== false, ); } } export function filterFollowingFeed( feed, currentUser, preferences, pluginFilteredFeedItems, ) { const followingFeedPreference = preferences.getFollowingFeedPreference(); const filter = FeedFilter.compose( new FilterByFollowing(currentUser), ...(followingFeedPreference?.hideReposts ? [new FilterReposts()] : []), ...(followingFeedPreference?.hideReplies ? [new FilterReplies()] : []), ...(followingFeedPreference?.hideQuotePosts ? [new FilterQuotePosts()] : []), new FilterEmptyPosts(), new DedupeFeed(), new FilterBlockedQuotes(), new FilterBlockedPosts(), new FilterMutedQuotes(), new FilterMutedPosts(), new FilterHiddenPosts(), new FilterContentLabeledPosts(), new FilterPluginFilteredPosts(pluginFilteredFeedItems), ); return { feed: filter.filterFeedItems(feed.feed), cursor: feed.cursor, }; } export function filterAlgorithmicFeed( feed, isAuthenticated, pluginFilteredFeedItems, ) { const filter = FeedFilter.compose( new FilterEmptyPosts(), new FilterBlockedQuotes(), new FilterBlockedPosts(), new DedupeFeed(), new FilterMutedQuotes(), new FilterMutedPosts(), new FilterHiddenPosts(), new FilterContentLabeledPosts(), new FilterUnauthorizedPosts(isAuthenticated), new FilterPluginFilteredPosts(pluginFilteredFeedItems), ); return { feed: filter.filterFeedItems(feed.feed), cursor: feed.cursor, }; } export function filterBookmarksFeed(feed) { return { feed: new FilterEmptyPosts().filterFeedItems(feed.feed), cursor: feed.cursor, }; } export function filterAuthorFeed(feed, isAuthenticated) { const filter = FeedFilter.compose( new FilterEmptyPosts({ checkParents: false }), new DedupeFeed({ includeReposts: false }), new FilterHiddenPosts(), new FilterContentLabeledPosts(), new FilterUnauthorizedPosts(isAuthenticated), ); return { feed: filter.filterFeedItems(feed.feed), cursor: feed.cursor, }; }