From 0de8d40981fecdeaec92307bafe121ccb2091b45 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 15 Nov 2023 20:35:58 -0600 Subject: [PATCH] Misc cleanup (#1925) * Remove unused prefs * Cleanup * Remove my-follows cache * Replace moderationOpts in ProfileCard comp * Replace moderationOpts in FeedSlice * Remove preferences model --- src/state/models/cache/my-follows.ts | 137 ------------- src/state/models/discovery/foafs.ts | 132 ------------- .../models/discovery/suggested-actors.ts | 151 --------------- .../models/discovery/user-autocomplete.ts | 143 -------------- src/state/models/feeds/post.ts | 181 ------------------ src/state/models/me.ts | 4 - src/state/models/root-store.ts | 6 - src/state/models/ui/preferences.ts | 129 ------------- .../onboarding/RecommendedFollowsItem.tsx | 7 +- src/view/com/posts/Feed.tsx | 16 +- src/view/com/posts/FeedSlice.tsx | 12 +- src/view/com/profile/ProfileCard.tsx | 26 ++- 12 files changed, 37 insertions(+), 907 deletions(-) delete mode 100644 src/state/models/cache/my-follows.ts delete mode 100644 src/state/models/discovery/foafs.ts delete mode 100644 src/state/models/discovery/suggested-actors.ts delete mode 100644 src/state/models/discovery/user-autocomplete.ts delete mode 100644 src/state/models/feeds/post.ts delete mode 100644 src/state/models/ui/preferences.ts diff --git a/src/state/models/cache/my-follows.ts b/src/state/models/cache/my-follows.ts deleted file mode 100644 index e1e8af50..00000000 --- a/src/state/models/cache/my-follows.ts +++ /dev/null @@ -1,137 +0,0 @@ -import {makeAutoObservable} from 'mobx' -import { - AppBskyActorDefs, - AppBskyGraphGetFollows as GetFollows, - moderateProfile, -} from '@atproto/api' -import {RootStoreModel} from '../root-store' -import {bundleAsync} from 'lib/async/bundle' - -const MAX_SYNC_PAGES = 10 -const SYNC_TTL = 60e3 * 10 // 10 minutes - -type Profile = AppBskyActorDefs.ProfileViewBasic | AppBskyActorDefs.ProfileView - -export enum FollowState { - Following, - NotFollowing, - Unknown, -} - -export interface FollowInfo { - did: string - followRecordUri: string | undefined - handle: string - displayName: string | undefined - avatar: string | undefined -} - -/** - * This model is used to maintain a synced local cache of the user's - * follows. It should be periodically refreshed and updated any time - * the user makes a change to their follows. - */ -export class MyFollowsCache { - // data - byDid: Record = {} - lastSync = 0 - - constructor(public rootStore: RootStoreModel) { - makeAutoObservable( - this, - { - rootStore: false, - }, - {autoBind: true}, - ) - } - - // public api - // = - - clear() { - this.byDid = {} - } - - /** - * Syncs a subset of the user's follows - * for performance reasons, caps out at 1000 follows - */ - syncIfNeeded = bundleAsync(async () => { - if (this.lastSync > Date.now() - SYNC_TTL) { - return - } - - let cursor - for (let i = 0; i < MAX_SYNC_PAGES; i++) { - const res: GetFollows.Response = await this.rootStore.agent.getFollows({ - actor: this.rootStore.me.did, - cursor, - limit: 100, - }) - res.data.follows = res.data.follows.filter( - profile => - !moderateProfile(profile, this.rootStore.preferences.moderationOpts) - .account.filter, - ) - this.hydrateMany(res.data.follows) - if (!res.data.cursor) { - break - } - cursor = res.data.cursor - } - - this.lastSync = Date.now() - }) - - getFollowState(did: string): FollowState { - if (typeof this.byDid[did] === 'undefined') { - return FollowState.Unknown - } - if (typeof this.byDid[did].followRecordUri === 'string') { - return FollowState.Following - } - return FollowState.NotFollowing - } - - async fetchFollowState(did: string): Promise { - // TODO: can we get a more efficient method for this? getProfile fetches more data than we need -prf - const res = await this.rootStore.agent.getProfile({actor: did}) - this.hydrate(did, res.data) - return this.getFollowState(did) - } - - getFollowUri(did: string): string { - const v = this.byDid[did] - if (v && typeof v.followRecordUri === 'string') { - return v.followRecordUri - } - throw new Error('Not a followed user') - } - - addFollow(did: string, info: FollowInfo) { - this.byDid[did] = info - } - - removeFollow(did: string) { - if (this.byDid[did]) { - this.byDid[did].followRecordUri = undefined - } - } - - hydrate(did: string, profile: Profile) { - this.byDid[did] = { - did, - followRecordUri: profile.viewer?.following, - handle: profile.handle, - displayName: profile.displayName, - avatar: profile.avatar, - } - } - - hydrateMany(profiles: Profile[]) { - for (const profile of profiles) { - this.hydrate(profile.did, profile) - } - } -} diff --git a/src/state/models/discovery/foafs.ts b/src/state/models/discovery/foafs.ts deleted file mode 100644 index 4a647dcf..00000000 --- a/src/state/models/discovery/foafs.ts +++ /dev/null @@ -1,132 +0,0 @@ -import {AppBskyActorDefs} from '@atproto/api' -import {makeAutoObservable, runInAction} from 'mobx' -import sampleSize from 'lodash.samplesize' -import {bundleAsync} from 'lib/async/bundle' -import {RootStoreModel} from '../root-store' - -export type RefWithInfoAndFollowers = AppBskyActorDefs.ProfileViewBasic & { - followers: AppBskyActorDefs.ProfileView[] -} - -export type ProfileViewFollows = AppBskyActorDefs.ProfileView & { - follows: AppBskyActorDefs.ProfileViewBasic[] -} - -export class FoafsModel { - isLoading = false - hasData = false - sources: string[] = [] - foafs: Map = new Map() // FOAF stands for Friend of a Friend - popular: RefWithInfoAndFollowers[] = [] - - constructor(public rootStore: RootStoreModel) { - makeAutoObservable(this) - } - - get hasContent() { - if (this.popular.length > 0) { - return true - } - for (const foaf of this.foafs.values()) { - if (foaf.follows.length) { - return true - } - } - return false - } - - fetch = bundleAsync(async () => { - try { - this.isLoading = true - - // fetch some of the user's follows - await this.rootStore.me.follows.syncIfNeeded() - - // grab 10 of the users followed by the user - runInAction(() => { - this.sources = sampleSize( - Object.keys(this.rootStore.me.follows.byDid), - 10, - ) - }) - if (this.sources.length === 0) { - return - } - runInAction(() => { - this.foafs.clear() - this.popular.length = 0 - }) - - // fetch their profiles - const profiles = await this.rootStore.agent.getProfiles({ - actors: this.sources, - }) - - // fetch their follows - const results = await Promise.allSettled( - this.sources.map(source => - this.rootStore.agent.getFollows({actor: source}), - ), - ) - - // store the follows and construct a "most followed" set - const popular: RefWithInfoAndFollowers[] = [] - for (let i = 0; i < results.length; i++) { - const res = results[i] - if (res.status === 'fulfilled') { - this.rootStore.me.follows.hydrateMany(res.value.data.follows) - } - const profile = profiles.data.profiles[i] - const source = this.sources[i] - if (res.status === 'fulfilled' && profile) { - // filter out inappropriate suggestions - res.value.data.follows = res.value.data.follows.filter(follow => { - const viewer = follow.viewer - if (viewer) { - if ( - viewer.following || - viewer.muted || - viewer.mutedByList || - viewer.blockedBy || - viewer.blocking - ) { - return false - } - } - if (follow.did === this.rootStore.me.did) { - return false - } - return true - }) - - runInAction(() => { - this.foafs.set(source, { - ...profile, - follows: res.value.data.follows, - }) - }) - for (const follow of res.value.data.follows) { - let item = popular.find(p => p.did === follow.did) - if (!item) { - item = {...follow, followers: []} - popular.push(item) - } - item.followers.push(profile) - } - } - } - - popular.sort((a, b) => b.followers.length - a.followers.length) - runInAction(() => { - this.popular = popular.filter(p => p.followers.length > 1).slice(0, 20) - }) - this.hasData = true - } catch (e) { - console.error('Failed to fetch FOAFs', e) - } finally { - runInAction(() => { - this.isLoading = false - }) - } - }) -} diff --git a/src/state/models/discovery/suggested-actors.ts b/src/state/models/discovery/suggested-actors.ts deleted file mode 100644 index 450786c2..00000000 --- a/src/state/models/discovery/suggested-actors.ts +++ /dev/null @@ -1,151 +0,0 @@ -import {makeAutoObservable, runInAction} from 'mobx' -import {AppBskyActorDefs, moderateProfile} from '@atproto/api' -import {RootStoreModel} from '../root-store' -import {cleanError} from 'lib/strings/errors' -import {bundleAsync} from 'lib/async/bundle' -import {logger} from '#/logger' - -const PAGE_SIZE = 30 - -export type SuggestedActor = - | AppBskyActorDefs.ProfileViewBasic - | AppBskyActorDefs.ProfileView - -export class SuggestedActorsModel { - // state - pageSize = PAGE_SIZE - isLoading = false - isRefreshing = false - hasLoaded = false - loadMoreCursor: string | undefined = undefined - error = '' - hasMore = false - lastInsertedAtIndex = -1 - - // data - suggestions: SuggestedActor[] = [] - - constructor(public rootStore: RootStoreModel, opts?: {pageSize?: number}) { - if (opts?.pageSize) { - this.pageSize = opts.pageSize - } - makeAutoObservable( - this, - { - rootStore: false, - }, - {autoBind: true}, - ) - } - - get hasContent() { - return this.suggestions.length > 0 - } - - get hasError() { - return this.error !== '' - } - - get isEmpty() { - return this.hasLoaded && !this.hasContent - } - - // public api - // = - - async refresh() { - return this.loadMore(true) - } - - loadMore = bundleAsync(async (replace: boolean = false) => { - if (replace) { - this.hasMore = true - this.loadMoreCursor = undefined - } - if (!this.hasMore) { - return - } - this._xLoading(replace) - try { - const res = await this.rootStore.agent.app.bsky.actor.getSuggestions({ - limit: 25, - cursor: this.loadMoreCursor, - }) - let {actors, cursor} = res.data - actors = actors.filter( - actor => - !moderateProfile(actor, this.rootStore.preferences.moderationOpts) - .account.filter, - ) - this.rootStore.me.follows.hydrateMany(actors) - - runInAction(() => { - if (replace) { - this.suggestions = [] - } - this.loadMoreCursor = cursor - this.hasMore = !!cursor - this.suggestions = this.suggestions.concat( - actors.filter(actor => { - const viewer = actor.viewer - if (viewer) { - if ( - viewer.following || - viewer.muted || - viewer.mutedByList || - viewer.blockedBy || - viewer.blocking - ) { - return false - } - } - if (actor.did === this.rootStore.me.did) { - return false - } - return true - }), - ) - }) - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } - }) - - async insertSuggestionsByActor(actor: string, indexToInsertAt: number) { - // fetch suggestions - const res = - await this.rootStore.agent.app.bsky.graph.getSuggestedFollowsByActor({ - actor: actor, - }) - const {suggestions: moreSuggestions} = res.data - this.rootStore.me.follows.hydrateMany(moreSuggestions) - // dedupe - const toInsert = moreSuggestions.filter( - s => !this.suggestions.find(s2 => s2.did === s.did), - ) - // insert - this.suggestions.splice(indexToInsertAt + 1, 0, ...toInsert) - // update index - this.lastInsertedAtIndex = indexToInsertAt - } - - // state transitions - // = - - _xLoading(isRefreshing = false) { - this.isLoading = true - this.isRefreshing = isRefreshing - this.error = '' - } - - _xIdle(err?: any) { - this.isLoading = false - this.isRefreshing = false - this.hasLoaded = true - this.error = cleanError(err) - if (err) { - logger.error('Failed to fetch suggested actors', {error: err}) - } - } -} diff --git a/src/state/models/discovery/user-autocomplete.ts b/src/state/models/discovery/user-autocomplete.ts deleted file mode 100644 index f28869e8..00000000 --- a/src/state/models/discovery/user-autocomplete.ts +++ /dev/null @@ -1,143 +0,0 @@ -import {makeAutoObservable, runInAction} from 'mobx' -import {AppBskyActorDefs} from '@atproto/api' -import AwaitLock from 'await-lock' -import {RootStoreModel} from '../root-store' -import {isInvalidHandle} from 'lib/strings/handles' - -type ProfileViewBasic = AppBskyActorDefs.ProfileViewBasic - -export class UserAutocompleteModel { - // state - isLoading = false - isActive = false - prefix = '' - lock = new AwaitLock() - - // data - knownHandles: Set = new Set() - _suggestions: ProfileViewBasic[] = [] - - constructor(public rootStore: RootStoreModel) { - makeAutoObservable( - this, - { - rootStore: false, - knownHandles: false, - }, - {autoBind: true}, - ) - } - - get follows(): ProfileViewBasic[] { - return Object.values(this.rootStore.me.follows.byDid).map(item => ({ - did: item.did, - handle: item.handle, - displayName: item.displayName, - avatar: item.avatar, - })) - } - - get suggestions(): ProfileViewBasic[] { - if (!this.isActive) { - return [] - } - return this._suggestions - } - - // public api - // = - - async setup() { - this.isLoading = true - await this.rootStore.me.follows.syncIfNeeded() - runInAction(() => { - for (const did in this.rootStore.me.follows.byDid) { - const info = this.rootStore.me.follows.byDid[did] - if (!isInvalidHandle(info.handle)) { - this.knownHandles.add(info.handle) - } - } - this.isLoading = false - }) - } - - setActive(v: boolean) { - this.isActive = v - } - - async setPrefix(prefix: string) { - const origPrefix = prefix.trim().toLocaleLowerCase() - this.prefix = origPrefix - await this.lock.acquireAsync() - try { - if (this.prefix) { - if (this.prefix !== origPrefix) { - return // another prefix was set before we got our chance - } - - // reset to follow results - this._computeSuggestions([]) - - // ask backend - const res = await this.rootStore.agent.searchActorsTypeahead({ - term: this.prefix, - limit: 8, - }) - this._computeSuggestions(res.data.actors) - - // update known handles - runInAction(() => { - for (const u of res.data.actors) { - this.knownHandles.add(u.handle) - } - }) - } else { - runInAction(() => { - this._computeSuggestions([]) - }) - } - } finally { - this.lock.release() - } - } - - // internal - // = - - _computeSuggestions(searchRes: AppBskyActorDefs.ProfileViewBasic[] = []) { - if (this.prefix) { - const items: ProfileViewBasic[] = [] - for (const item of this.follows) { - if (prefixMatch(this.prefix, item)) { - items.push(item) - } - if (items.length >= 8) { - break - } - } - for (const item of searchRes) { - if (!items.find(item2 => item2.handle === item.handle)) { - items.push({ - did: item.did, - handle: item.handle, - displayName: item.displayName, - avatar: item.avatar, - }) - } - } - this._suggestions = items - } else { - this._suggestions = this.follows - } - } -} - -function prefixMatch(prefix: string, info: ProfileViewBasic): boolean { - if (info.handle.includes(prefix)) { - return true - } - if (info.displayName?.toLocaleLowerCase().includes(prefix)) { - return true - } - return false -} diff --git a/src/state/models/feeds/post.ts b/src/state/models/feeds/post.ts deleted file mode 100644 index 4fa1213b..00000000 --- a/src/state/models/feeds/post.ts +++ /dev/null @@ -1,181 +0,0 @@ -import {makeAutoObservable} from 'mobx' -import { - AppBskyFeedPost as FeedPost, - AppBskyFeedDefs, - RichText, - moderatePost, - PostModeration, -} from '@atproto/api' -import {RootStoreModel} from '../root-store' -import {updateDataOptimistically} from 'lib/async/revertible' -import {track} from 'lib/analytics/analytics' -import {hackAddDeletedEmbed} from 'lib/api/hack-add-deleted-embed' -import {logger} from '#/logger' - -type FeedViewPost = AppBskyFeedDefs.FeedViewPost -type ReasonRepost = AppBskyFeedDefs.ReasonRepost -type PostView = AppBskyFeedDefs.PostView - -export class PostsFeedItemModel { - // ui state - _reactKey: string = '' - - // data - post: PostView - postRecord?: FeedPost.Record - reply?: FeedViewPost['reply'] - reason?: FeedViewPost['reason'] - richText?: RichText - - constructor( - public rootStore: RootStoreModel, - _reactKey: string, - v: FeedViewPost, - ) { - this._reactKey = _reactKey - this.post = v.post - if (FeedPost.isRecord(this.post.record)) { - const valid = FeedPost.validateRecord(this.post.record) - if (valid.success) { - hackAddDeletedEmbed(this.post) - this.postRecord = this.post.record - this.richText = new RichText(this.postRecord, {cleanNewlines: true}) - } else { - this.postRecord = undefined - this.richText = undefined - logger.warn('Received an invalid app.bsky.feed.post record', { - error: valid.error, - }) - } - } else { - this.postRecord = undefined - this.richText = undefined - logger.warn( - 'app.bsky.feed.getTimeline or app.bsky.feed.getAuthorFeed served an unexpected record type', - {record: this.post.record}, - ) - } - this.reply = v.reply - this.reason = v.reason - makeAutoObservable(this, {rootStore: false}) - } - - get uri() { - return this.post.uri - } - - get parentUri() { - return this.postRecord?.reply?.parent.uri - } - - get rootUri(): string { - if (typeof this.postRecord?.reply?.root.uri === 'string') { - return this.postRecord?.reply?.root.uri - } - return this.post.uri - } - - get moderation(): PostModeration { - return moderatePost(this.post, this.rootStore.preferences.moderationOpts) - } - - copy(v: FeedViewPost) { - this.post = v.post - this.reply = v.reply - this.reason = v.reason - } - - copyMetrics(v: FeedViewPost) { - this.post.replyCount = v.post.replyCount - this.post.repostCount = v.post.repostCount - this.post.likeCount = v.post.likeCount - this.post.viewer = v.post.viewer - } - - get reasonRepost(): ReasonRepost | undefined { - if (this.reason?.$type === 'app.bsky.feed.defs#reasonRepost') { - return this.reason as ReasonRepost - } - } - - async toggleLike() { - this.post.viewer = this.post.viewer || {} - try { - if (this.post.viewer.like) { - // unlike - const url = this.post.viewer.like - await updateDataOptimistically( - this.post, - () => { - this.post.likeCount = (this.post.likeCount || 0) - 1 - this.post.viewer!.like = undefined - }, - () => this.rootStore.agent.deleteLike(url), - ) - track('Post:Unlike') - } else { - // like - await updateDataOptimistically( - this.post, - () => { - this.post.likeCount = (this.post.likeCount || 0) + 1 - this.post.viewer!.like = 'pending' - }, - () => this.rootStore.agent.like(this.post.uri, this.post.cid), - res => { - this.post.viewer!.like = res.uri - }, - ) - track('Post:Like') - } - } catch (error) { - logger.error('Failed to toggle like', {error}) - } - } - - async toggleRepost() { - this.post.viewer = this.post.viewer || {} - try { - if (this.post.viewer?.repost) { - // unrepost - const url = this.post.viewer.repost - await updateDataOptimistically( - this.post, - () => { - this.post.repostCount = (this.post.repostCount || 0) - 1 - this.post.viewer!.repost = undefined - }, - () => this.rootStore.agent.deleteRepost(url), - ) - track('Post:Unrepost') - } else { - // repost - await updateDataOptimistically( - this.post, - () => { - this.post.repostCount = (this.post.repostCount || 0) + 1 - this.post.viewer!.repost = 'pending' - }, - () => this.rootStore.agent.repost(this.post.uri, this.post.cid), - res => { - this.post.viewer!.repost = res.uri - }, - ) - track('Post:Repost') - } - } catch (error) { - logger.error('Failed to toggle repost', {error}) - } - } - - async delete() { - try { - await this.rootStore.agent.deletePost(this.post.uri) - this.rootStore.emitPostDeleted(this.post.uri) - } catch (error) { - logger.error('Failed to delete post', {error}) - } finally { - track('Post:Delete') - } - } -} diff --git a/src/state/models/me.ts b/src/state/models/me.ts index 427b0e35..586be4f4 100644 --- a/src/state/models/me.ts +++ b/src/state/models/me.ts @@ -4,7 +4,6 @@ import { ComAtprotoServerListAppPasswords, } from '@atproto/api' import {RootStoreModel} from './root-store' -import {MyFollowsCache} from './cache/my-follows' import {isObj, hasProp} from 'lib/type-guards' import {logger} from '#/logger' @@ -18,7 +17,6 @@ export class MeModel { avatar: string = '' followsCount: number | undefined followersCount: number | undefined - follows: MyFollowsCache invites: ComAtprotoServerDefs.InviteCode[] = [] appPasswords: ComAtprotoServerListAppPasswords.AppPassword[] = [] lastProfileStateUpdate = Date.now() @@ -33,11 +31,9 @@ export class MeModel { {rootStore: false, serialize: false, hydrate: false}, {autoBind: true}, ) - this.follows = new MyFollowsCache(this.rootStore) } clear() { - this.follows.clear() this.rootStore.profiles.cache.clear() this.rootStore.posts.cache.clear() this.did = '' diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts index 288e8b8e..6d7c2c12 100644 --- a/src/state/models/root-store.ts +++ b/src/state/models/root-store.ts @@ -15,7 +15,6 @@ import {ProfilesCache} from './cache/profiles-view' import {PostsCache} from './cache/posts' import {LinkMetasCache} from './cache/link-metas' import {MeModel} from './me' -import {PreferencesModel} from './ui/preferences' import {resetToTab} from '../../Navigation' import {ImageSizesCache} from './cache/image-sizes' import {reset as resetNavigation} from '../../Navigation' @@ -39,7 +38,6 @@ export class RootStoreModel { appInfo?: AppInfo session = new SessionModel(this) shell = new ShellUiModel(this) - preferences = new PreferencesModel(this) me = new MeModel(this) handleResolutions = new HandleResolutionsCache() profiles = new ProfilesCache(this) @@ -64,7 +62,6 @@ export class RootStoreModel { return { appInfo: this.appInfo, me: this.me.serialize(), - preferences: this.preferences.serialize(), } } @@ -79,9 +76,6 @@ export class RootStoreModel { if (hasProp(v, 'me')) { this.me.hydrate(v.me) } - if (hasProp(v, 'preferences')) { - this.preferences.hydrate(v.preferences) - } } } diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts deleted file mode 100644 index 3a7fcf6c..00000000 --- a/src/state/models/ui/preferences.ts +++ /dev/null @@ -1,129 +0,0 @@ -import {makeAutoObservable} from 'mobx' -import { - LabelPreference as APILabelPreference, - BskyThreadViewPreference, -} from '@atproto/api' -import {isObj, hasProp} from 'lib/type-guards' -import {RootStoreModel} from '../root-store' -import {ModerationOpts} from '@atproto/api' - -// TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf -export type LabelPreference = APILabelPreference | 'show' -export type ThreadViewPreference = BskyThreadViewPreference & { - lab_treeViewEnabled?: boolean | undefined -} - -export class LabelPreferencesModel { - nsfw: LabelPreference = 'hide' - nudity: LabelPreference = 'warn' - suggestive: LabelPreference = 'warn' - gore: LabelPreference = 'warn' - hate: LabelPreference = 'hide' - spam: LabelPreference = 'hide' - impersonation: LabelPreference = 'warn' - - constructor() { - makeAutoObservable(this, {}, {autoBind: true}) - } -} - -export class PreferencesModel { - contentLabels = new LabelPreferencesModel() - savedFeeds: string[] = [] - pinnedFeeds: string[] = [] - - constructor(public rootStore: RootStoreModel) { - makeAutoObservable(this, {}, {autoBind: true}) - } - - serialize() { - return { - contentLabels: this.contentLabels, - savedFeeds: this.savedFeeds, - pinnedFeeds: this.pinnedFeeds, - } - } - - /** - * The function hydrates an object with properties related to content languages, labels, saved feeds, - * and pinned feeds that it gets from the parameter `v` (probably local storage) - * @param {unknown} v - the data object to hydrate from - */ - hydrate(v: unknown) { - if (isObj(v)) { - // check if content labels in preferences exist, then hydrate - if (hasProp(v, 'contentLabels') && typeof v.contentLabels === 'object') { - Object.assign(this.contentLabels, v.contentLabels) - } - // check if saved feeds in preferences, then hydrate - if ( - hasProp(v, 'savedFeeds') && - Array.isArray(v.savedFeeds) && - typeof v.savedFeeds.every(item => typeof item === 'string') - ) { - this.savedFeeds = v.savedFeeds - } - // check if pinned feeds in preferences exist, then hydrate - if ( - hasProp(v, 'pinnedFeeds') && - Array.isArray(v.pinnedFeeds) && - typeof v.pinnedFeeds.every(item => typeof item === 'string') - ) { - this.pinnedFeeds = v.pinnedFeeds - } - } - } - - // moderation - // = - - /** - * @deprecated use `getModerationOpts` from '#/state/queries/preferences/moderation' instead - */ - get moderationOpts(): ModerationOpts { - return { - userDid: this.rootStore.session.currentSession?.did || '', - adultContentEnabled: false, - labels: { - // TEMP translate old settings until this UI can be migrated -prf - porn: tempfixLabelPref(this.contentLabels.nsfw), - sexual: tempfixLabelPref(this.contentLabels.suggestive), - nudity: tempfixLabelPref(this.contentLabels.nudity), - nsfl: tempfixLabelPref(this.contentLabels.gore), - corpse: tempfixLabelPref(this.contentLabels.gore), - gore: tempfixLabelPref(this.contentLabels.gore), - torture: tempfixLabelPref(this.contentLabels.gore), - 'self-harm': tempfixLabelPref(this.contentLabels.gore), - 'intolerant-race': tempfixLabelPref(this.contentLabels.hate), - 'intolerant-gender': tempfixLabelPref(this.contentLabels.hate), - 'intolerant-sexual-orientation': tempfixLabelPref( - this.contentLabels.hate, - ), - 'intolerant-religion': tempfixLabelPref(this.contentLabels.hate), - intolerant: tempfixLabelPref(this.contentLabels.hate), - 'icon-intolerant': tempfixLabelPref(this.contentLabels.hate), - spam: tempfixLabelPref(this.contentLabels.spam), - impersonation: tempfixLabelPref(this.contentLabels.impersonation), - scam: 'warn', - }, - labelers: [ - { - labeler: { - did: '', - displayName: 'Bluesky Social', - }, - labels: {}, - }, - ], - } - } -} - -// TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf -// TODO do we need this? -function tempfixLabelPref(pref: LabelPreference): APILabelPreference { - if (pref === 'show') { - return 'ignore' - } - return pref -} diff --git a/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx b/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx index 144cc6cd..19292388 100644 --- a/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx +++ b/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx @@ -1,9 +1,8 @@ import React from 'react' import {View, StyleSheet, ActivityIndicator} from 'react-native' -import {ProfileModeration} from '@atproto/api' +import {ProfileModeration, AppBskyActorDefs} from '@atproto/api' import {Button} from '#/view/com/util/forms/Button' import {usePalette} from 'lib/hooks/usePalette' -import {SuggestedActor} from 'state/models/discovery/suggested-actors' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {s} from 'lib/styles' @@ -21,7 +20,7 @@ import { import {logger} from '#/logger' type Props = { - profile: SuggestedActor + profile: AppBskyActorDefs.ProfileViewBasic dataUpdatedAt: number moderation: ProfileModeration onFollowStateChange: (props: { @@ -67,7 +66,7 @@ export function ProfileCard({ onFollowStateChange, moderation, }: { - profile: Shadow + profile: Shadow moderation: ProfileModeration onFollowStateChange: (props: { did: string diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 5c9d1ad2..74bff1ab 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -24,6 +24,7 @@ import { FeedParams, usePostFeedQuery, } from '#/state/queries/post-feed' +import {useModerationOpts} from '#/state/queries/preferences' const LOADING_ITEM = {_reactKey: '__loading__'} const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} @@ -71,6 +72,7 @@ export function Feed({ const [isPTRing, setIsPTRing] = React.useState(false) const checkForNewRef = React.useRef<(() => void) | null>(null) + const moderationOpts = useModerationOpts() const opts = React.useMemo(() => ({enabled}), [enabled]) const { data, @@ -115,7 +117,7 @@ export function Feed({ const feedItems = React.useMemo(() => { let arr: any[] = [] - if (isFetched) { + if (isFetched && moderationOpts) { if (isError && isEmpty) { arr = arr.concat([ERROR_ITEM]) } @@ -133,7 +135,7 @@ export function Feed({ arr.push(LOADING_ITEM) } return arr - }, [isFetched, isError, isEmpty, data]) + }, [isFetched, isError, isEmpty, data, moderationOpts]) // events // = @@ -195,7 +197,14 @@ export function Feed({ } else if (item === LOADING_ITEM) { return } - return + return ( + + ) }, [ feed, @@ -204,6 +213,7 @@ export function Feed({ onPressTryAgain, onPressRetryLoadMore, renderEmptyState, + moderationOpts, ], ) diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx index c33c6028..fad9f9b4 100644 --- a/src/view/com/posts/FeedSlice.tsx +++ b/src/view/com/posts/FeedSlice.tsx @@ -2,30 +2,28 @@ import React from 'react' import {StyleSheet, View} from 'react-native' import {observer} from 'mobx-react-lite' import {FeedPostSlice} from '#/state/queries/post-feed' -import {AtUri, moderatePost} from '@atproto/api' +import {AtUri, moderatePost, ModerationOpts} from '@atproto/api' import {Link} from '../util/Link' import {Text} from '../util/text/Text' import Svg, {Circle, Line} from 'react-native-svg' import {FeedItem} from './FeedItem' import {usePalette} from 'lib/hooks/usePalette' import {makeProfileLink} from 'lib/routes/links' -import {useStores} from '#/state' export const FeedSlice = observer(function FeedSliceImpl({ slice, dataUpdatedAt, ignoreFilterFor, + moderationOpts, }: { slice: FeedPostSlice dataUpdatedAt: number ignoreFilterFor?: string + moderationOpts: ModerationOpts }) { - const store = useStores() const moderations = React.useMemo(() => { - return slice.items.map(item => - moderatePost(item.post, store.preferences.moderationOpts), - ) - }, [slice, store.preferences.moderationOpts]) + return slice.items.map(item => moderatePost(item.post, moderationOpts)) + }, [slice, moderationOpts]) // apply moderation filter for (let i = 0; i < slice.items.length; i++) { diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index eeee17d4..2f359018 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -11,7 +11,6 @@ import {Text} from '../util/text/Text' import {UserAvatar} from '../util/UserAvatar' import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' -import {useStores} from 'state/index' import {FollowButton} from './FollowButton' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' @@ -158,19 +157,26 @@ const FollowersList = observer(function FollowersListImpl({ }: { followers?: AppBskyActorDefs.ProfileView[] | undefined }) { - const store = useStores() const pal = usePalette('default') - if (!followers?.length) { + const moderationOpts = useModerationOpts() + + const followersWithMods = React.useMemo(() => { + if (!followers || !moderationOpts) { + return [] + } + + return followers + .map(f => ({ + f, + mod: moderateProfile(f, moderationOpts), + })) + .filter(({mod}) => !mod.account.filter) + }, [followers, moderationOpts]) + + if (!followersWithMods?.length) { return null } - const followersWithMods = followers - .map(f => ({ - f, - mod: moderateProfile(f, store.preferences.moderationOpts), - })) - .filter(({mod}) => !mod.account.filter) - return (