Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/improsocial/impro
An extensible Bluesky client for web impro.social
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391import { 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, };}