Misc cleanup (#1925)

* Remove unused prefs

* Cleanup

* Remove my-follows cache

* Replace moderationOpts in ProfileCard comp

* Replace moderationOpts in FeedSlice

* Remove preferences model
zio/stable
Eric Bailey 2023-11-15 20:35:58 -06:00 committed by GitHub
parent e749f2f3a5
commit 0de8d40981
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 37 additions and 907 deletions

View File

@ -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<string, FollowInfo> = {}
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<FollowState> {
// 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)
}
}
}

View File

@ -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<string, ProfileViewFollows> = 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
})
}
})
}

View File

@ -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})
}
}
}

View File

@ -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<string> = 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
}

View File

@ -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')
}
}
}

View File

@ -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 = ''

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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<SuggestedActor>
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
moderation: ProfileModeration
onFollowStateChange: (props: {
did: string

View File

@ -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 <PostFeedLoadingPlaceholder />
}
return <FeedSlice slice={item} dataUpdatedAt={dataUpdatedAt} />
return (
<FeedSlice
slice={item}
dataUpdatedAt={dataUpdatedAt}
// we check for this before creating the feedItems array
moderationOpts={moderationOpts!}
/>
)
},
[
feed,
@ -204,6 +213,7 @@ export function Feed({
onPressTryAgain,
onPressRetryLoadMore,
renderEmptyState,
moderationOpts,
],
)

View File

@ -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++) {

View File

@ -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 (
<View style={styles.followedBy}>
<Text