Implement blocks (#554)
* Quick fix to prompt * Add blocked accounts screen * Add blocking tools to profile * Blur avis/banners of blocked users * Factor blocking state into moderation dsl * Filter post slices from the feed if any are hidden * Handle various block UIs * Filter in the client on blockedBy * Implement block list * Fix some copy * Bump deps * Fix lint
This commit is contained in:
parent
e68aa75429
commit
a95c03e280
24 changed files with 974 additions and 291 deletions
|
@ -13,6 +13,9 @@ import {updateDataOptimistically} from 'lib/async/revertible'
|
|||
import {PostLabelInfo, PostModeration} from 'lib/labeling/types'
|
||||
import {
|
||||
getEmbedLabels,
|
||||
getEmbedMuted,
|
||||
getEmbedBlocking,
|
||||
getEmbedBlockedBy,
|
||||
filterAccountLabels,
|
||||
filterProfileLabels,
|
||||
getPostModeration,
|
||||
|
@ -30,7 +33,10 @@ export class PostThreadItemModel {
|
|||
// data
|
||||
post: AppBskyFeedDefs.PostView
|
||||
postRecord?: FeedPost.Record
|
||||
parent?: PostThreadItemModel | AppBskyFeedDefs.NotFoundPost
|
||||
parent?:
|
||||
| PostThreadItemModel
|
||||
| AppBskyFeedDefs.NotFoundPost
|
||||
| AppBskyFeedDefs.BlockedPost
|
||||
replies?: (PostThreadItemModel | AppBskyFeedDefs.NotFoundPost)[]
|
||||
richText?: RichText
|
||||
|
||||
|
@ -60,7 +66,18 @@ export class PostThreadItemModel {
|
|||
),
|
||||
accountLabels: filterAccountLabels(this.post.author.labels),
|
||||
profileLabels: filterProfileLabels(this.post.author.labels),
|
||||
isMuted: this.post.author.viewer?.muted || false,
|
||||
isMuted:
|
||||
this.post.author.viewer?.muted ||
|
||||
getEmbedMuted(this.post.embed) ||
|
||||
false,
|
||||
isBlocking:
|
||||
!!this.post.author.viewer?.blocking ||
|
||||
getEmbedBlocking(this.post.embed) ||
|
||||
false,
|
||||
isBlockedBy:
|
||||
!!this.post.author.viewer?.blockedBy ||
|
||||
getEmbedBlockedBy(this.post.embed) ||
|
||||
false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,6 +131,8 @@ export class PostThreadItemModel {
|
|||
this.parent = parentModel
|
||||
} else if (AppBskyFeedDefs.isNotFoundPost(v.parent)) {
|
||||
this.parent = v.parent
|
||||
} else if (AppBskyFeedDefs.isBlockedPost(v.parent)) {
|
||||
this.parent = v.parent
|
||||
}
|
||||
}
|
||||
// replies
|
||||
|
@ -218,6 +237,7 @@ export class PostThreadModel {
|
|||
|
||||
// data
|
||||
thread?: PostThreadItemModel
|
||||
isBlocked = false
|
||||
|
||||
constructor(
|
||||
public rootStore: RootStoreModel,
|
||||
|
@ -377,11 +397,17 @@ export class PostThreadModel {
|
|||
this._replaceAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
console.log(e)
|
||||
this._xIdle(e)
|
||||
}
|
||||
}
|
||||
|
||||
_replaceAll(res: GetPostThread.Response) {
|
||||
this.isBlocked = AppBskyFeedDefs.isBlockedPost(res.data.thread)
|
||||
if (this.isBlocked) {
|
||||
return
|
||||
}
|
||||
pruneReplies(res.data.thread)
|
||||
sortThread(res.data.thread)
|
||||
const thread = new PostThreadItemModel(
|
||||
this.rootStore,
|
||||
|
@ -399,7 +425,20 @@ export class PostThreadModel {
|
|||
type MaybePost =
|
||||
| AppBskyFeedDefs.ThreadViewPost
|
||||
| AppBskyFeedDefs.NotFoundPost
|
||||
| AppBskyFeedDefs.BlockedPost
|
||||
| {[k: string]: unknown; $type: string}
|
||||
function pruneReplies(post: MaybePost) {
|
||||
if (post.replies) {
|
||||
post.replies = (post.replies as MaybePost[]).filter((reply: MaybePost) => {
|
||||
if (reply.blocked) {
|
||||
return false
|
||||
}
|
||||
pruneReplies(reply)
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function sortThread(post: MaybePost) {
|
||||
if (post.notFound) {
|
||||
return
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {
|
||||
AtUri,
|
||||
ComAtprotoLabelDefs,
|
||||
AppBskyActorGetProfile as GetProfile,
|
||||
AppBskyActorProfile,
|
||||
|
@ -23,6 +24,8 @@ export class ProfileViewerModel {
|
|||
muted?: boolean
|
||||
following?: string
|
||||
followedBy?: string
|
||||
blockedBy?: boolean
|
||||
blocking?: string
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this)
|
||||
|
@ -86,6 +89,8 @@ export class ProfileModel {
|
|||
accountLabels: filterAccountLabels(this.labels),
|
||||
profileLabels: filterProfileLabels(this.labels),
|
||||
isMuted: this.viewer?.muted || false,
|
||||
isBlocking: !!this.viewer?.blocking || false,
|
||||
isBlockedBy: !!this.viewer?.blockedBy || false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -185,6 +190,33 @@ export class ProfileModel {
|
|||
await this.refresh()
|
||||
}
|
||||
|
||||
async blockAccount() {
|
||||
const res = await this.rootStore.agent.app.bsky.graph.block.create(
|
||||
{
|
||||
repo: this.rootStore.me.did,
|
||||
},
|
||||
{
|
||||
subject: this.did,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
)
|
||||
this.viewer.blocking = res.uri
|
||||
await this.refresh()
|
||||
}
|
||||
|
||||
async unblockAccount() {
|
||||
if (!this.viewer.blocking) {
|
||||
return
|
||||
}
|
||||
const {rkey} = new AtUri(this.viewer.blocking)
|
||||
await this.rootStore.agent.app.bsky.graph.block.delete({
|
||||
repo: this.rootStore.me.did,
|
||||
rkey,
|
||||
})
|
||||
this.viewer.blocking = undefined
|
||||
await this.refresh()
|
||||
}
|
||||
|
||||
// state transitions
|
||||
// =
|
||||
|
||||
|
|
|
@ -111,6 +111,10 @@ export class NotificationsFeedItemModel {
|
|||
addedInfo?.profileLabels || [],
|
||||
),
|
||||
isMuted: this.author.viewer?.muted || addedInfo?.isMuted || false,
|
||||
isBlocking:
|
||||
!!this.author.viewer?.blocking || addedInfo?.isBlocking || false,
|
||||
isBlockedBy:
|
||||
!!this.author.viewer?.blockedBy || addedInfo?.isBlockedBy || false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,11 @@ import {updateDataOptimistically} from 'lib/async/revertible'
|
|||
import {PostLabelInfo, PostModeration} from 'lib/labeling/types'
|
||||
import {
|
||||
getEmbedLabels,
|
||||
getEmbedMuted,
|
||||
getEmbedBlocking,
|
||||
getEmbedBlockedBy,
|
||||
getPostModeration,
|
||||
mergePostModerations,
|
||||
filterAccountLabels,
|
||||
filterProfileLabels,
|
||||
} from 'lib/labeling/helpers'
|
||||
|
@ -97,7 +101,18 @@ export class PostsFeedItemModel {
|
|||
),
|
||||
accountLabels: filterAccountLabels(this.post.author.labels),
|
||||
profileLabels: filterProfileLabels(this.post.author.labels),
|
||||
isMuted: this.post.author.viewer?.muted || false,
|
||||
isMuted:
|
||||
this.post.author.viewer?.muted ||
|
||||
getEmbedMuted(this.post.embed) ||
|
||||
false,
|
||||
isBlocking:
|
||||
!!this.post.author.viewer?.blocking ||
|
||||
getEmbedBlocking(this.post.embed) ||
|
||||
false,
|
||||
isBlockedBy:
|
||||
!!this.post.author.viewer?.blockedBy ||
|
||||
getEmbedBlockedBy(this.post.embed) ||
|
||||
false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -240,6 +255,10 @@ export class PostsFeedSliceModel {
|
|||
return this.items[0]
|
||||
}
|
||||
|
||||
get moderation() {
|
||||
return mergePostModerations(this.items.map(item => item.moderation))
|
||||
}
|
||||
|
||||
containsUri(uri: string) {
|
||||
return !!this.items.find(item => item.post.uri === uri)
|
||||
}
|
||||
|
@ -265,6 +284,8 @@ export class PostsFeedModel {
|
|||
isRefreshing = false
|
||||
hasNewLatest = false
|
||||
hasLoaded = false
|
||||
isBlocking = false
|
||||
isBlockedBy = false
|
||||
error = ''
|
||||
loadMoreError = ''
|
||||
params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams
|
||||
|
@ -553,6 +574,8 @@ export class PostsFeedModel {
|
|||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasLoaded = true
|
||||
this.isBlocking = error instanceof GetAuthorFeed.BlockedActorError
|
||||
this.isBlockedBy = error instanceof GetAuthorFeed.BlockedByActorError
|
||||
this.error = cleanError(error)
|
||||
this.loadMoreError = cleanError(loadMoreError)
|
||||
if (error) {
|
||||
|
|
106
src/state/models/lists/blocked-accounts.ts
Normal file
106
src/state/models/lists/blocked-accounts.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
import {makeAutoObservable} from 'mobx'
|
||||
import {
|
||||
AppBskyGraphGetBlocks as GetBlocks,
|
||||
AppBskyActorDefs as ActorDefs,
|
||||
} from '@atproto/api'
|
||||
import {RootStoreModel} from '../root-store'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {bundleAsync} from 'lib/async/bundle'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
|
||||
export class BlockedAccountsModel {
|
||||
// state
|
||||
isLoading = false
|
||||
isRefreshing = false
|
||||
hasLoaded = false
|
||||
error = ''
|
||||
hasMore = true
|
||||
loadMoreCursor?: string
|
||||
|
||||
// data
|
||||
blocks: ActorDefs.ProfileView[] = []
|
||||
|
||||
constructor(public rootStore: RootStoreModel) {
|
||||
makeAutoObservable(
|
||||
this,
|
||||
{
|
||||
rootStore: false,
|
||||
},
|
||||
{autoBind: true},
|
||||
)
|
||||
}
|
||||
|
||||
get hasContent() {
|
||||
return this.blocks.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) {
|
||||
return
|
||||
}
|
||||
this._xLoading(replace)
|
||||
try {
|
||||
const res = await this.rootStore.agent.app.bsky.graph.getBlocks({
|
||||
limit: PAGE_SIZE,
|
||||
cursor: replace ? undefined : this.loadMoreCursor,
|
||||
})
|
||||
if (replace) {
|
||||
this._replaceAll(res)
|
||||
} else {
|
||||
this._appendAll(res)
|
||||
}
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(e)
|
||||
}
|
||||
})
|
||||
|
||||
// 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) {
|
||||
this.rootStore.log.error('Failed to fetch user followers', err)
|
||||
}
|
||||
}
|
||||
|
||||
// helper functions
|
||||
// =
|
||||
|
||||
_replaceAll(res: GetBlocks.Response) {
|
||||
this.blocks = []
|
||||
this._appendAll(res)
|
||||
}
|
||||
|
||||
_appendAll(res: GetBlocks.Response) {
|
||||
this.loadMoreCursor = res.data.cursor
|
||||
this.hasMore = !!this.loadMoreCursor
|
||||
this.blocks = this.blocks.concat(res.data.blocks)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue