Misc cleanup (#1925)
* Remove unused prefs * Cleanup * Remove my-follows cache * Replace moderationOpts in ProfileCard comp * Replace moderationOpts in FeedSlice * Remove preferences modelzio/stable
parent
e749f2f3a5
commit
0de8d40981
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,6 @@ import {
|
||||||
ComAtprotoServerListAppPasswords,
|
ComAtprotoServerListAppPasswords,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {RootStoreModel} from './root-store'
|
import {RootStoreModel} from './root-store'
|
||||||
import {MyFollowsCache} from './cache/my-follows'
|
|
||||||
import {isObj, hasProp} from 'lib/type-guards'
|
import {isObj, hasProp} from 'lib/type-guards'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
|
||||||
|
@ -18,7 +17,6 @@ export class MeModel {
|
||||||
avatar: string = ''
|
avatar: string = ''
|
||||||
followsCount: number | undefined
|
followsCount: number | undefined
|
||||||
followersCount: number | undefined
|
followersCount: number | undefined
|
||||||
follows: MyFollowsCache
|
|
||||||
invites: ComAtprotoServerDefs.InviteCode[] = []
|
invites: ComAtprotoServerDefs.InviteCode[] = []
|
||||||
appPasswords: ComAtprotoServerListAppPasswords.AppPassword[] = []
|
appPasswords: ComAtprotoServerListAppPasswords.AppPassword[] = []
|
||||||
lastProfileStateUpdate = Date.now()
|
lastProfileStateUpdate = Date.now()
|
||||||
|
@ -33,11 +31,9 @@ export class MeModel {
|
||||||
{rootStore: false, serialize: false, hydrate: false},
|
{rootStore: false, serialize: false, hydrate: false},
|
||||||
{autoBind: true},
|
{autoBind: true},
|
||||||
)
|
)
|
||||||
this.follows = new MyFollowsCache(this.rootStore)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.follows.clear()
|
|
||||||
this.rootStore.profiles.cache.clear()
|
this.rootStore.profiles.cache.clear()
|
||||||
this.rootStore.posts.cache.clear()
|
this.rootStore.posts.cache.clear()
|
||||||
this.did = ''
|
this.did = ''
|
||||||
|
|
|
@ -15,7 +15,6 @@ import {ProfilesCache} from './cache/profiles-view'
|
||||||
import {PostsCache} from './cache/posts'
|
import {PostsCache} from './cache/posts'
|
||||||
import {LinkMetasCache} from './cache/link-metas'
|
import {LinkMetasCache} from './cache/link-metas'
|
||||||
import {MeModel} from './me'
|
import {MeModel} from './me'
|
||||||
import {PreferencesModel} from './ui/preferences'
|
|
||||||
import {resetToTab} from '../../Navigation'
|
import {resetToTab} from '../../Navigation'
|
||||||
import {ImageSizesCache} from './cache/image-sizes'
|
import {ImageSizesCache} from './cache/image-sizes'
|
||||||
import {reset as resetNavigation} from '../../Navigation'
|
import {reset as resetNavigation} from '../../Navigation'
|
||||||
|
@ -39,7 +38,6 @@ export class RootStoreModel {
|
||||||
appInfo?: AppInfo
|
appInfo?: AppInfo
|
||||||
session = new SessionModel(this)
|
session = new SessionModel(this)
|
||||||
shell = new ShellUiModel(this)
|
shell = new ShellUiModel(this)
|
||||||
preferences = new PreferencesModel(this)
|
|
||||||
me = new MeModel(this)
|
me = new MeModel(this)
|
||||||
handleResolutions = new HandleResolutionsCache()
|
handleResolutions = new HandleResolutionsCache()
|
||||||
profiles = new ProfilesCache(this)
|
profiles = new ProfilesCache(this)
|
||||||
|
@ -64,7 +62,6 @@ export class RootStoreModel {
|
||||||
return {
|
return {
|
||||||
appInfo: this.appInfo,
|
appInfo: this.appInfo,
|
||||||
me: this.me.serialize(),
|
me: this.me.serialize(),
|
||||||
preferences: this.preferences.serialize(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,9 +76,6 @@ export class RootStoreModel {
|
||||||
if (hasProp(v, 'me')) {
|
if (hasProp(v, 'me')) {
|
||||||
this.me.hydrate(v.me)
|
this.me.hydrate(v.me)
|
||||||
}
|
}
|
||||||
if (hasProp(v, 'preferences')) {
|
|
||||||
this.preferences.hydrate(v.preferences)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -1,9 +1,8 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View, StyleSheet, ActivityIndicator} from 'react-native'
|
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 {Button} from '#/view/com/util/forms/Button'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {SuggestedActor} from 'state/models/discovery/suggested-actors'
|
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||||
import {sanitizeHandle} from 'lib/strings/handles'
|
import {sanitizeHandle} from 'lib/strings/handles'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
|
@ -21,7 +20,7 @@ import {
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
profile: SuggestedActor
|
profile: AppBskyActorDefs.ProfileViewBasic
|
||||||
dataUpdatedAt: number
|
dataUpdatedAt: number
|
||||||
moderation: ProfileModeration
|
moderation: ProfileModeration
|
||||||
onFollowStateChange: (props: {
|
onFollowStateChange: (props: {
|
||||||
|
@ -67,7 +66,7 @@ export function ProfileCard({
|
||||||
onFollowStateChange,
|
onFollowStateChange,
|
||||||
moderation,
|
moderation,
|
||||||
}: {
|
}: {
|
||||||
profile: Shadow<SuggestedActor>
|
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
|
||||||
moderation: ProfileModeration
|
moderation: ProfileModeration
|
||||||
onFollowStateChange: (props: {
|
onFollowStateChange: (props: {
|
||||||
did: string
|
did: string
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
FeedParams,
|
FeedParams,
|
||||||
usePostFeedQuery,
|
usePostFeedQuery,
|
||||||
} from '#/state/queries/post-feed'
|
} from '#/state/queries/post-feed'
|
||||||
|
import {useModerationOpts} from '#/state/queries/preferences'
|
||||||
|
|
||||||
const LOADING_ITEM = {_reactKey: '__loading__'}
|
const LOADING_ITEM = {_reactKey: '__loading__'}
|
||||||
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
||||||
|
@ -71,6 +72,7 @@ export function Feed({
|
||||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||||
const checkForNewRef = React.useRef<(() => void) | null>(null)
|
const checkForNewRef = React.useRef<(() => void) | null>(null)
|
||||||
|
|
||||||
|
const moderationOpts = useModerationOpts()
|
||||||
const opts = React.useMemo(() => ({enabled}), [enabled])
|
const opts = React.useMemo(() => ({enabled}), [enabled])
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
|
@ -115,7 +117,7 @@ export function Feed({
|
||||||
|
|
||||||
const feedItems = React.useMemo(() => {
|
const feedItems = React.useMemo(() => {
|
||||||
let arr: any[] = []
|
let arr: any[] = []
|
||||||
if (isFetched) {
|
if (isFetched && moderationOpts) {
|
||||||
if (isError && isEmpty) {
|
if (isError && isEmpty) {
|
||||||
arr = arr.concat([ERROR_ITEM])
|
arr = arr.concat([ERROR_ITEM])
|
||||||
}
|
}
|
||||||
|
@ -133,7 +135,7 @@ export function Feed({
|
||||||
arr.push(LOADING_ITEM)
|
arr.push(LOADING_ITEM)
|
||||||
}
|
}
|
||||||
return arr
|
return arr
|
||||||
}, [isFetched, isError, isEmpty, data])
|
}, [isFetched, isError, isEmpty, data, moderationOpts])
|
||||||
|
|
||||||
// events
|
// events
|
||||||
// =
|
// =
|
||||||
|
@ -195,7 +197,14 @@ export function Feed({
|
||||||
} else if (item === LOADING_ITEM) {
|
} else if (item === LOADING_ITEM) {
|
||||||
return <PostFeedLoadingPlaceholder />
|
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,
|
feed,
|
||||||
|
@ -204,6 +213,7 @@ export function Feed({
|
||||||
onPressTryAgain,
|
onPressTryAgain,
|
||||||
onPressRetryLoadMore,
|
onPressRetryLoadMore,
|
||||||
renderEmptyState,
|
renderEmptyState,
|
||||||
|
moderationOpts,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -2,30 +2,28 @@ import React from 'react'
|
||||||
import {StyleSheet, View} from 'react-native'
|
import {StyleSheet, View} from 'react-native'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {FeedPostSlice} from '#/state/queries/post-feed'
|
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 {Link} from '../util/Link'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
import Svg, {Circle, Line} from 'react-native-svg'
|
import Svg, {Circle, Line} from 'react-native-svg'
|
||||||
import {FeedItem} from './FeedItem'
|
import {FeedItem} from './FeedItem'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
import {makeProfileLink} from 'lib/routes/links'
|
||||||
import {useStores} from '#/state'
|
|
||||||
|
|
||||||
export const FeedSlice = observer(function FeedSliceImpl({
|
export const FeedSlice = observer(function FeedSliceImpl({
|
||||||
slice,
|
slice,
|
||||||
dataUpdatedAt,
|
dataUpdatedAt,
|
||||||
ignoreFilterFor,
|
ignoreFilterFor,
|
||||||
|
moderationOpts,
|
||||||
}: {
|
}: {
|
||||||
slice: FeedPostSlice
|
slice: FeedPostSlice
|
||||||
dataUpdatedAt: number
|
dataUpdatedAt: number
|
||||||
ignoreFilterFor?: string
|
ignoreFilterFor?: string
|
||||||
|
moderationOpts: ModerationOpts
|
||||||
}) {
|
}) {
|
||||||
const store = useStores()
|
|
||||||
const moderations = React.useMemo(() => {
|
const moderations = React.useMemo(() => {
|
||||||
return slice.items.map(item =>
|
return slice.items.map(item => moderatePost(item.post, moderationOpts))
|
||||||
moderatePost(item.post, store.preferences.moderationOpts),
|
}, [slice, moderationOpts])
|
||||||
)
|
|
||||||
}, [slice, store.preferences.moderationOpts])
|
|
||||||
|
|
||||||
// apply moderation filter
|
// apply moderation filter
|
||||||
for (let i = 0; i < slice.items.length; i++) {
|
for (let i = 0; i < slice.items.length; i++) {
|
||||||
|
|
|
@ -11,7 +11,6 @@ import {Text} from '../util/text/Text'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useStores} from 'state/index'
|
|
||||||
import {FollowButton} from './FollowButton'
|
import {FollowButton} from './FollowButton'
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||||
import {sanitizeHandle} from 'lib/strings/handles'
|
import {sanitizeHandle} from 'lib/strings/handles'
|
||||||
|
@ -158,19 +157,26 @@ const FollowersList = observer(function FollowersListImpl({
|
||||||
}: {
|
}: {
|
||||||
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||||
}) {
|
}) {
|
||||||
const store = useStores()
|
|
||||||
const pal = usePalette('default')
|
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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const followersWithMods = followers
|
|
||||||
.map(f => ({
|
|
||||||
f,
|
|
||||||
mod: moderateProfile(f, store.preferences.moderationOpts),
|
|
||||||
}))
|
|
||||||
.filter(({mod}) => !mod.account.filter)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.followedBy}>
|
<View style={styles.followedBy}>
|
||||||
<Text
|
<Text
|
||||||
|
|
Loading…
Reference in New Issue