Lists updates: curate lists and blocklists (#1689)

* Add lists screen

* Update Lists screen and List create/edit modal to support curate lists

* Rework the ProfileList screen and add curatelist support

* More ProfileList progress

* Update list modals

* Rename mutelists to modlists

* Layout updates/fixes

* More layout fixes

* Modal fixes

* List list screen updates

* Update feed page to give more info

* Layout fixes to ListAddUser modal

* Layout fixes to FlatList and Feed on desktop

* Layout fix to LoadLatestBtn on Web

* Handle did resolution before showing the ProfileList screen

* Rename the CustomFeed routes to ProfileFeed for consistency

* Fix layout issues with the pager and feeds

* Factor out some common code

* Fix UIs for mobile

* Fix user list rendering

* Fix: dont bubble custom feed errors in the merge feed

* Refactor feed models to reduce usage of the SavedFeeds model

* Replace CustomFeedModel with FeedSourceModel which abstracts feed-generators and lists

* Add the ability to pin lists

* Add pinned lists to mobile

* Remove dead code

* Rework the ProfileScreenHeader to create more real-estate for action buttons

* Improve layout behavior on web mobile breakpoints

* Refactor feed & list pages to use new Tabs layout component

* Refactor to ProfileSubpageHeader

* Implement modlist block and mute

* Switch to new api and just modify state on modlist actions

* Fix some UI overflows

* Fix: dont show edit buttons on lists you dont own

* Fix alignment issue on long titles

* Improve loading and error states for feeds & lists

* Update list dropdown icons for ios

* Fetch feed display names in the mergefeed

* Improve rendering off offline feeds in the feed-listing page

* Update Feeds listing UI to react to changes in saved/pinned state

* Refresh list and feed on posts tab press

* Fix pinned feed ordering UI

* Fixes to list pinning

* Remove view=simple qp

* Add list to feed tuners

* Render richtext

* Add list href

* Add 'view avatar'

* Remove unused import

* Fix missing import

* Correctly reflect block by list state

* Replace the <Tabs> component with the more effective <PagerWithHeader> component

* Improve the responsiveness of the PagerWithHeader

* Fix visual jank in the feed loading state

* Improve performance of the PagerWithHeader

* Fix a case that would cause the header to animate too aggressively

* Add the ability to scroll to top by tapping the selected tab

* Fix unit test runner

* Update modlists test

* Add curatelist tests

* Fix: remove link behavior in ListAddUser modal

* Fix some layout jank in the PagerWithHeader on iOS

* Simplify ListItems header rendering

* Wait for the appview to recognize the list before proceeding with list creation

* Fix glitch in the onPageSelecting index of the Pager

* Fix until()

* Copy fix

Co-authored-by: Eric Bailey <git@esb.lol>

---------

Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
Paul Frazee 2023-11-01 16:15:40 -07:00 committed by GitHub
parent f9944b55e2
commit f57a8cf8ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
87 changed files with 4090 additions and 1988 deletions

View file

@ -3,7 +3,7 @@ import {AppBskyFeedGetActorFeeds as GetActorFeeds} from '@atproto/api'
import {RootStoreModel} from '../root-store'
import {bundleAsync} from 'lib/async/bundle'
import {cleanError} from 'lib/strings/errors'
import {CustomFeedModel} from '../feeds/custom-feed'
import {FeedSourceModel} from '../content/feed-source'
const PAGE_SIZE = 30
@ -17,7 +17,7 @@ export class ActorFeedsModel {
loadMoreCursor?: string
// data
feeds: CustomFeedModel[] = []
feeds: FeedSourceModel[] = []
constructor(
public rootStore: RootStoreModel,
@ -114,7 +114,9 @@ export class ActorFeedsModel {
this.loadMoreCursor = res.data.cursor
this.hasMore = !!this.loadMoreCursor
for (const f of res.data.feeds) {
this.feeds.push(new CustomFeedModel(this.rootStore, f))
const model = new FeedSourceModel(this.rootStore, f.uri)
model.hydrateFeedGenerator(f)
this.feeds.push(model)
}
}
}

View file

@ -1,12 +1,9 @@
import {makeAutoObservable} from 'mobx'
import {
AppBskyGraphGetLists as GetLists,
AppBskyGraphGetListMutes as GetListMutes,
AppBskyGraphDefs as GraphDefs,
} from '@atproto/api'
import {AppBskyGraphDefs as GraphDefs} from '@atproto/api'
import {RootStoreModel} from '../root-store'
import {cleanError} from 'lib/strings/errors'
import {bundleAsync} from 'lib/async/bundle'
import {accumulate} from 'lib/async/accumulate'
const PAGE_SIZE = 30
@ -25,7 +22,7 @@ export class ListsListModel {
constructor(
public rootStore: RootStoreModel,
public source: 'my-modlists' | string,
public source: 'mine' | 'my-curatelists' | 'my-modlists' | string,
) {
makeAutoObservable(
this,
@ -48,6 +45,26 @@ export class ListsListModel {
return this.hasLoaded && !this.hasContent
}
get curatelists() {
return this.lists.filter(
list => list.purpose === 'app.bsky.graph.defs#curatelist',
)
}
get isCuratelistsEmpty() {
return this.hasLoaded && this.curatelists.length === 0
}
get modlists() {
return this.lists.filter(
list => list.purpose === 'app.bsky.graph.defs#modlist',
)
}
get isModlistsEmpty() {
return this.hasLoaded && this.modlists.length === 0
}
/**
* Removes posts from the feed upon deletion.
*/
@ -76,44 +93,85 @@ export class ListsListModel {
}
this._xLoading(replace)
try {
let res: GetLists.Response
if (this.source === 'my-modlists') {
res = {
success: true,
headers: {},
data: {
subject: undefined,
lists: [],
},
let cursor: string | undefined
let lists: GraphDefs.ListView[] = []
if (
this.source === 'mine' ||
this.source === 'my-curatelists' ||
this.source === 'my-modlists'
) {
const promises = [
accumulate(cursor =>
this.rootStore.agent.app.bsky.graph
.getLists({
actor: this.rootStore.me.did,
cursor,
limit: 50,
})
.then(res => ({cursor: res.data.cursor, items: res.data.lists})),
),
]
if (this.source === 'my-modlists') {
promises.push(
accumulate(cursor =>
this.rootStore.agent.app.bsky.graph
.getListMutes({
cursor,
limit: 50,
})
.then(res => ({
cursor: res.data.cursor,
items: res.data.lists,
})),
),
)
promises.push(
accumulate(cursor =>
this.rootStore.agent.app.bsky.graph
.getListBlocks({
cursor,
limit: 50,
})
.then(res => ({
cursor: res.data.cursor,
items: res.data.lists,
})),
),
)
}
const [res1, res2] = await Promise.all([
fetchAllUserLists(this.rootStore, this.rootStore.me.did),
fetchAllMyMuteLists(this.rootStore),
])
for (let list of res1.data.lists) {
if (list.purpose === 'app.bsky.graph.defs#modlist') {
res.data.lists.push(list)
}
}
for (let list of res2.data.lists) {
if (
list.purpose === 'app.bsky.graph.defs#modlist' &&
!res.data.lists.find(l => l.uri === list.uri)
) {
res.data.lists.push(list)
const resultset = await Promise.all(promises)
for (const res of resultset) {
for (let list of res) {
if (
this.source === 'my-curatelists' &&
list.purpose !== 'app.bsky.graph.defs#curatelist'
) {
continue
}
if (
this.source === 'my-modlists' &&
list.purpose !== 'app.bsky.graph.defs#modlist'
) {
continue
}
if (!lists.find(l => l.uri === list.uri)) {
lists.push(list)
}
}
}
} else {
res = await this.rootStore.agent.app.bsky.graph.getLists({
const res = await this.rootStore.agent.app.bsky.graph.getLists({
actor: this.source,
limit: PAGE_SIZE,
cursor: replace ? undefined : this.loadMoreCursor,
})
lists = res.data.lists
cursor = res.data.cursor
}
if (replace) {
this._replaceAll(res)
this._replaceAll({lists, cursor})
} else {
this._appendAll(res)
this._appendAll({lists, cursor})
}
this._xIdle()
} catch (e: any) {
@ -156,75 +214,28 @@ export class ListsListModel {
// helper functions
// =
_replaceAll(res: GetLists.Response | GetListMutes.Response) {
_replaceAll({
lists,
cursor,
}: {
lists: GraphDefs.ListView[]
cursor: string | undefined
}) {
this.lists = []
this._appendAll(res)
this._appendAll({lists, cursor})
}
_appendAll(res: GetLists.Response | GetListMutes.Response) {
this.loadMoreCursor = res.data.cursor
_appendAll({
lists,
cursor,
}: {
lists: GraphDefs.ListView[]
cursor: string | undefined
}) {
this.loadMoreCursor = cursor
this.hasMore = !!this.loadMoreCursor
this.lists = this.lists.concat(
res.data.lists.map(list => ({...list, _reactKey: list.uri})),
lists.map(list => ({...list, _reactKey: list.uri})),
)
}
}
async function fetchAllUserLists(
store: RootStoreModel,
did: string,
): Promise<GetLists.Response> {
let acc: GetLists.Response = {
success: true,
headers: {},
data: {
subject: undefined,
lists: [],
},
}
let cursor
for (let i = 0; i < 100; i++) {
const res: GetLists.Response = await store.agent.app.bsky.graph.getLists({
actor: did,
cursor,
limit: 50,
})
cursor = res.data.cursor
acc.data.lists = acc.data.lists.concat(res.data.lists)
if (!cursor) {
break
}
}
return acc
}
async function fetchAllMyMuteLists(
store: RootStoreModel,
): Promise<GetListMutes.Response> {
let acc: GetListMutes.Response = {
success: true,
headers: {},
data: {
subject: undefined,
lists: [],
},
}
let cursor
for (let i = 0; i < 100; i++) {
const res: GetListMutes.Response =
await store.agent.app.bsky.graph.getListMutes({
cursor,
limit: 50,
})
cursor = res.data.cursor
acc.data.lists = acc.data.lists.concat(res.data.lists)
if (!cursor) {
break
}
}
return acc
}