Add 'add to list' tool
This commit is contained in:
@@ -0,0 +1,112 @@
|
|||||||
|
import {makeAutoObservable} from 'mobx'
|
||||||
|
import {AtUri, AppBskyGraphListitem} from '@atproto/api'
|
||||||
|
import {runInAction} from 'mobx'
|
||||||
|
import {RootStoreModel} from '../root-store'
|
||||||
|
|
||||||
|
const PAGE_SIZE = 100
|
||||||
|
interface Membership {
|
||||||
|
uri: string
|
||||||
|
value: AppBskyGraphListitem.Record
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ListMembershipModel {
|
||||||
|
// data
|
||||||
|
memberships: Membership[] = []
|
||||||
|
|
||||||
|
constructor(public rootStore: RootStoreModel, public subject: string) {
|
||||||
|
makeAutoObservable(
|
||||||
|
this,
|
||||||
|
{
|
||||||
|
rootStore: false,
|
||||||
|
},
|
||||||
|
{autoBind: true},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// public api
|
||||||
|
// =
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
// NOTE
|
||||||
|
// this approach to determining list membership is too inefficient to work at any scale
|
||||||
|
// it needs to be replaced with server side list membership queries
|
||||||
|
// -prf
|
||||||
|
let cursor
|
||||||
|
let records = []
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
const res = await this.rootStore.agent.app.bsky.graph.listitem.list({
|
||||||
|
repo: this.rootStore.me.did,
|
||||||
|
cursor,
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
records = records.concat(
|
||||||
|
res.records.filter(record => record.value.subject === this.subject),
|
||||||
|
)
|
||||||
|
cursor = res.cursor
|
||||||
|
if (!cursor) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runInAction(() => {
|
||||||
|
this.memberships = records
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getMembership(listUri: string) {
|
||||||
|
return this.memberships.find(m => m.value.list === listUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
isMember(listUri: string) {
|
||||||
|
return !!this.getMembership(listUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
async add(listUri: string) {
|
||||||
|
if (this.isMember(listUri)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await this.rootStore.agent.app.bsky.graph.listitem.create(
|
||||||
|
{
|
||||||
|
repo: this.rootStore.me.did,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: this.subject,
|
||||||
|
list: listUri,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const {rkey} = new AtUri(res.uri)
|
||||||
|
const record = await this.rootStore.agent.app.bsky.graph.listitem.get({
|
||||||
|
repo: this.rootStore.me.did,
|
||||||
|
rkey,
|
||||||
|
})
|
||||||
|
runInAction(() => {
|
||||||
|
this.memberships = this.memberships.concat([record])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(listUri: string) {
|
||||||
|
const membership = this.getMembership(listUri)
|
||||||
|
if (!membership) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const {rkey} = new AtUri(membership.uri)
|
||||||
|
await this.rootStore.agent.app.bsky.graph.listitem.delete({
|
||||||
|
repo: this.rootStore.me.did,
|
||||||
|
rkey,
|
||||||
|
})
|
||||||
|
runInAction(() => {
|
||||||
|
this.memberships = this.memberships.filter(m => m.value.list !== listUri)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTo(uris: string) {
|
||||||
|
for (const uri of uris) {
|
||||||
|
await this.add(uri)
|
||||||
|
}
|
||||||
|
for (const membership of this.memberships) {
|
||||||
|
if (!uris.includes(membership.value.list)) {
|
||||||
|
await this.remove(membership.value.list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import {makeAutoObservable} from 'mobx'
|
import {makeAutoObservable} from 'mobx'
|
||||||
import {
|
import {
|
||||||
AppBskyGraphGetList as GetList,
|
AppBskyGraphGetList as GetList,
|
||||||
AppBskyActorDefs,
|
AppBskyGraphDefs as GraphDefs,
|
||||||
AppBskyGraphDefs,
|
|
||||||
AppBskyGraphList,
|
AppBskyGraphList,
|
||||||
AppBskyRichtextFacet,
|
|
||||||
RichText,
|
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
import * as apilib from 'lib/api/index'
|
import * as apilib from 'lib/api/index'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
|
import {bundleAsync} from 'lib/async/bundle'
|
||||||
|
|
||||||
|
const PAGE_SIZE = 30
|
||||||
|
|
||||||
export class ListModel {
|
export class ListModel {
|
||||||
// state
|
// state
|
||||||
@@ -18,21 +18,13 @@ export class ListModel {
|
|||||||
isRefreshing = false
|
isRefreshing = false
|
||||||
hasLoaded = false
|
hasLoaded = false
|
||||||
error = ''
|
error = ''
|
||||||
params: GetList.QueryParams
|
loadMoreError = ''
|
||||||
|
hasMore = true
|
||||||
|
loadMoreCursor?: string
|
||||||
|
|
||||||
// data
|
// data
|
||||||
uri: string
|
list: GraphDefs.ListView | null = null
|
||||||
creator: AppBskyActorDefs.ProfileView
|
items: GraphDefs.ListItemView[] = []
|
||||||
name: string
|
|
||||||
purpose: AppBskyGraphDefs.ListPurpose
|
|
||||||
description?: string
|
|
||||||
descriptionFacets?: AppBskyRichtextFacet.Main[]
|
|
||||||
avatar?: string
|
|
||||||
viewer?: AppBskyGraphDefs.ListViewerState
|
|
||||||
indexedAt?: string
|
|
||||||
|
|
||||||
// added data
|
|
||||||
descriptionRichText?: RichText = new RichText({text: ''})
|
|
||||||
|
|
||||||
static async createModList(
|
static async createModList(
|
||||||
rootStore: RootStoreModel,
|
rootStore: RootStoreModel,
|
||||||
@@ -65,20 +57,18 @@ export class ListModel {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(public rootStore: RootStoreModel, params: GetList.QueryParams) {
|
constructor(public rootStore: RootStoreModel, public uri: string) {
|
||||||
makeAutoObservable(
|
makeAutoObservable(
|
||||||
this,
|
this,
|
||||||
{
|
{
|
||||||
rootStore: false,
|
rootStore: false,
|
||||||
params: false,
|
|
||||||
},
|
},
|
||||||
{autoBind: true},
|
{autoBind: true},
|
||||||
)
|
)
|
||||||
this.params = params
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasContent() {
|
get hasContent() {
|
||||||
return this.uri !== ''
|
return this.items.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasError() {
|
get hasError() {
|
||||||
@@ -92,12 +82,39 @@ export class ListModel {
|
|||||||
// public api
|
// public api
|
||||||
// =
|
// =
|
||||||
|
|
||||||
async setup() {
|
async refresh() {
|
||||||
await this._load()
|
return this.loadMore(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
async refresh() {
|
loadMore = bundleAsync(async (replace: boolean = false) => {
|
||||||
await this._load(true)
|
if (!replace && !this.hasMore) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this._xLoading(replace)
|
||||||
|
try {
|
||||||
|
const res = await this.rootStore.agent.app.bsky.graph.getList({
|
||||||
|
list: this.uri,
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
cursor: replace ? undefined : this.loadMoreCursor,
|
||||||
|
})
|
||||||
|
if (replace) {
|
||||||
|
this._replaceAll(res)
|
||||||
|
} else {
|
||||||
|
this._appendAll(res)
|
||||||
|
}
|
||||||
|
this._xIdle()
|
||||||
|
} catch (e: any) {
|
||||||
|
this._xIdle(replace ? e : undefined, !replace ? e : undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to load more again after a failure
|
||||||
|
*/
|
||||||
|
async retryLoadMore() {
|
||||||
|
this.loadMoreError = ''
|
||||||
|
this.hasMore = true
|
||||||
|
return this.loadMore()
|
||||||
}
|
}
|
||||||
|
|
||||||
// state transitions
|
// state transitions
|
||||||
@@ -109,43 +126,34 @@ export class ListModel {
|
|||||||
this.error = ''
|
this.error = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
_xIdle(err?: any) {
|
_xIdle(err?: any, loadMoreErr?: any) {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
this.isRefreshing = false
|
this.isRefreshing = false
|
||||||
this.hasLoaded = true
|
this.hasLoaded = true
|
||||||
this.error = cleanError(err)
|
this.error = cleanError(err)
|
||||||
|
this.loadMoreError = cleanError(loadMoreErr)
|
||||||
if (err) {
|
if (err) {
|
||||||
this.rootStore.log.error('Failed to fetch profile', err)
|
this.rootStore.log.error('Failed to fetch user items', err)
|
||||||
|
}
|
||||||
|
if (loadMoreErr) {
|
||||||
|
this.rootStore.log.error('Failed to fetch user items', loadMoreErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// loader functions
|
// helper functions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
async _load(isRefreshing = false) {
|
_replaceAll(res: GetList.Response) {
|
||||||
this._xLoading(isRefreshing)
|
this.items = []
|
||||||
try {
|
this._appendAll(res)
|
||||||
const res = await this.rootStore.agent.app.bsky.graph.getList(this.params)
|
|
||||||
this._replaceAll(res)
|
|
||||||
this._xIdle()
|
|
||||||
} catch (e: any) {
|
|
||||||
this._xIdle(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_replaceAll(res: GetList.Response) {
|
_appendAll(res: GetList.Response) {
|
||||||
this.uri = res.data.list.uri
|
this.loadMoreCursor = res.data.cursor
|
||||||
this.creator = res.data.list.creator
|
this.hasMore = !!this.loadMoreCursor
|
||||||
this.name = res.data.list.name
|
this.list = res.data.list
|
||||||
this.purpose = res.data.list.purpose
|
this.items = this.items.concat(
|
||||||
this.description = res.data.list.description
|
res.data.items.map(item => ({...item, _reactKey: item.subject})),
|
||||||
this.descriptionFacets = res.data.list.descriptionFacets
|
)
|
||||||
this.avatar = res.data.list.avatar
|
|
||||||
this.viewer = res.data.list.viewer
|
|
||||||
this.indexedAt = res.data.list.indexedAt
|
|
||||||
this.descriptionRichText = new RichText({
|
|
||||||
text: this.description || '',
|
|
||||||
facets: this.descriptionFacets,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ export interface CreateMuteListModal {
|
|||||||
onCreate?: (uri: string) => void
|
onCreate?: (uri: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ListAddUserModal {
|
||||||
|
name: 'list-add-user'
|
||||||
|
subject: string
|
||||||
|
displayName: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface CropImageModal {
|
export interface CropImageModal {
|
||||||
name: 'crop-image'
|
name: 'crop-image'
|
||||||
uri: string
|
uri: string
|
||||||
@@ -104,6 +110,7 @@ export type Modal =
|
|||||||
| ReportAccountModal
|
| ReportAccountModal
|
||||||
| ReportPostModal
|
| ReportPostModal
|
||||||
| CreateMuteListModal
|
| CreateMuteListModal
|
||||||
|
| ListAddUserModal
|
||||||
|
|
||||||
// Posts
|
// Posts
|
||||||
| AltTextImageModal
|
| AltTextImageModal
|
||||||
|
|||||||
@@ -152,27 +152,4 @@ const styles = StyleSheet.create({
|
|||||||
marginLeft: 6,
|
marginLeft: 6,
|
||||||
paddingHorizontal: 14,
|
paddingHorizontal: 14,
|
||||||
},
|
},
|
||||||
|
|
||||||
followedBy: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingLeft: 54,
|
|
||||||
paddingRight: 20,
|
|
||||||
marginBottom: 10,
|
|
||||||
marginTop: -6,
|
|
||||||
},
|
|
||||||
followedByAviContainer: {
|
|
||||||
width: 24,
|
|
||||||
height: 36,
|
|
||||||
},
|
|
||||||
followedByAvi: {
|
|
||||||
width: 36,
|
|
||||||
height: 36,
|
|
||||||
borderRadius: 18,
|
|
||||||
padding: 2,
|
|
||||||
},
|
|
||||||
followsByDesc: {
|
|
||||||
flex: 1,
|
|
||||||
paddingRight: 10,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import React, {MutableRefObject} from 'react'
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
RefreshControl,
|
||||||
|
StyleProp,
|
||||||
|
StyleSheet,
|
||||||
|
View,
|
||||||
|
ViewStyle,
|
||||||
|
} from 'react-native'
|
||||||
|
import {observer} from 'mobx-react-lite'
|
||||||
|
import {FlatList} from '../util/Views'
|
||||||
|
import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||||
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
|
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
||||||
|
import {ListModel} from 'state/models/content/list'
|
||||||
|
import {useAnalytics} from 'lib/analytics'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {s} from 'lib/styles'
|
||||||
|
|
||||||
|
const LOADING_ITEM = {_reactKey: '__loading__'}
|
||||||
|
const EMPTY_ITEM = {_reactKey: '__empty__'}
|
||||||
|
const ERROR_ITEM = {_reactKey: '__error__'}
|
||||||
|
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
|
||||||
|
|
||||||
|
export const ListItems = observer(
|
||||||
|
({
|
||||||
|
list,
|
||||||
|
style,
|
||||||
|
scrollElRef,
|
||||||
|
onPressTryAgain,
|
||||||
|
renderEmptyState,
|
||||||
|
testID,
|
||||||
|
headerOffset = 0,
|
||||||
|
}: {
|
||||||
|
list: ListModel
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
|
scrollElRef?: MutableRefObject<FlatList<any> | null>
|
||||||
|
onPressTryAgain?: () => void
|
||||||
|
renderEmptyState?: () => JSX.Element
|
||||||
|
testID?: string
|
||||||
|
headerOffset?: number
|
||||||
|
}) => {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const {track} = useAnalytics()
|
||||||
|
const [isRefreshing, setIsRefreshing] = React.useState(false)
|
||||||
|
|
||||||
|
const data = React.useMemo(() => {
|
||||||
|
let items: any[] = []
|
||||||
|
if (list.hasLoaded) {
|
||||||
|
if (list.hasError) {
|
||||||
|
items = items.concat([ERROR_ITEM])
|
||||||
|
}
|
||||||
|
if (list.isEmpty) {
|
||||||
|
items = items.concat([EMPTY_ITEM])
|
||||||
|
} else {
|
||||||
|
items = items.concat(list.items)
|
||||||
|
}
|
||||||
|
if (list.loadMoreError) {
|
||||||
|
items = items.concat([LOAD_MORE_ERROR_ITEM])
|
||||||
|
}
|
||||||
|
} else if (list.isLoading) {
|
||||||
|
items = items.concat([LOADING_ITEM])
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}, [
|
||||||
|
list.hasError,
|
||||||
|
list.hasLoaded,
|
||||||
|
list.isLoading,
|
||||||
|
list.isEmpty,
|
||||||
|
list.items,
|
||||||
|
list.loadMoreError,
|
||||||
|
])
|
||||||
|
|
||||||
|
// events
|
||||||
|
// =
|
||||||
|
|
||||||
|
const onRefresh = React.useCallback(async () => {
|
||||||
|
track('Lists:onRefresh')
|
||||||
|
setIsRefreshing(true)
|
||||||
|
try {
|
||||||
|
await list.refresh()
|
||||||
|
} catch (err) {
|
||||||
|
list.rootStore.log.error('Failed to refresh lists', err)
|
||||||
|
}
|
||||||
|
setIsRefreshing(false)
|
||||||
|
}, [list, track, setIsRefreshing])
|
||||||
|
|
||||||
|
const onEndReached = React.useCallback(async () => {
|
||||||
|
track('Lists:onEndReached')
|
||||||
|
try {
|
||||||
|
await list.loadMore()
|
||||||
|
} catch (err) {
|
||||||
|
list.rootStore.log.error('Failed to load more lists', err)
|
||||||
|
}
|
||||||
|
}, [list, track])
|
||||||
|
|
||||||
|
const onPressRetryLoadMore = React.useCallback(() => {
|
||||||
|
list.retryLoadMore()
|
||||||
|
}, [list])
|
||||||
|
|
||||||
|
// rendering
|
||||||
|
// =
|
||||||
|
|
||||||
|
const renderItem = React.useCallback(
|
||||||
|
({item}: {item: any}) => {
|
||||||
|
if (item === EMPTY_ITEM) {
|
||||||
|
if (renderEmptyState) {
|
||||||
|
return renderEmptyState()
|
||||||
|
}
|
||||||
|
return <View />
|
||||||
|
} else if (item === ERROR_ITEM) {
|
||||||
|
return (
|
||||||
|
<ErrorMessage
|
||||||
|
message={list.error}
|
||||||
|
onPressTryAgain={onPressTryAgain}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else if (item === LOAD_MORE_ERROR_ITEM) {
|
||||||
|
return (
|
||||||
|
<LoadMoreRetryBtn
|
||||||
|
label="There was an issue fetching the list. Tap here to try again."
|
||||||
|
onPress={onPressRetryLoadMore}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else if (item === LOADING_ITEM) {
|
||||||
|
return <ProfileCardFeedLoadingPlaceholder />
|
||||||
|
}
|
||||||
|
return <View /> // TODO
|
||||||
|
},
|
||||||
|
[list, onPressTryAgain, onPressRetryLoadMore],
|
||||||
|
)
|
||||||
|
|
||||||
|
const Footer = React.useCallback(
|
||||||
|
() =>
|
||||||
|
list.isLoading ? (
|
||||||
|
<View style={styles.feedFooter}>
|
||||||
|
<ActivityIndicator />
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View />
|
||||||
|
),
|
||||||
|
[list],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View testID={testID} style={style}>
|
||||||
|
{data.length > 0 && (
|
||||||
|
<FlatList
|
||||||
|
testID={testID ? `${testID}-flatlist` : undefined}
|
||||||
|
ref={scrollElRef}
|
||||||
|
data={data}
|
||||||
|
keyExtractor={item => item._reactKey}
|
||||||
|
renderItem={renderItem}
|
||||||
|
ListFooterComponent={Footer}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={isRefreshing}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
tintColor={pal.colors.text}
|
||||||
|
titleColor={pal.colors.text}
|
||||||
|
progressViewOffset={headerOffset}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
contentContainerStyle={s.contentContainer}
|
||||||
|
style={{paddingTop: headerOffset}}
|
||||||
|
onEndReached={onEndReached}
|
||||||
|
onEndReachedThreshold={0.6}
|
||||||
|
removeClippedSubviews={true}
|
||||||
|
contentOffset={{x: 0, y: headerOffset * -1}}
|
||||||
|
// @ts-ignore our .web version only -prf
|
||||||
|
desktopFixedHeight
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
feedFooter: {paddingTop: 20},
|
||||||
|
})
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
FontAwesomeIconStyle,
|
FontAwesomeIconStyle,
|
||||||
} from '@fortawesome/react-native-fontawesome'
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {AppBskyGraphDefs as GraphDefs} from '@atproto/api'
|
||||||
import {FlatList} from '../util/Views'
|
import {FlatList} from '../util/Views'
|
||||||
import {ListCard} from './ListCard'
|
import {ListCard} from './ListCard'
|
||||||
import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||||
@@ -23,7 +24,6 @@ import {ListsListModel} from 'state/models/lists/lists-list'
|
|||||||
import {useAnalytics} from 'lib/analytics'
|
import {useAnalytics} from 'lib/analytics'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {isDesktopWeb} from 'platform/detection'
|
|
||||||
|
|
||||||
const LOADING_ITEM = {_reactKey: '__loading__'}
|
const LOADING_ITEM = {_reactKey: '__loading__'}
|
||||||
const CREATENEW_ITEM = {_reactKey: '__loading__'}
|
const CREATENEW_ITEM = {_reactKey: '__loading__'}
|
||||||
@@ -34,19 +34,23 @@ const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
|
|||||||
export const ListsList = observer(
|
export const ListsList = observer(
|
||||||
({
|
({
|
||||||
listsList,
|
listsList,
|
||||||
|
showAddBtns,
|
||||||
style,
|
style,
|
||||||
scrollElRef,
|
scrollElRef,
|
||||||
onPressTryAgain,
|
onPressTryAgain,
|
||||||
onPressCreateNew,
|
onPressCreateNew,
|
||||||
|
renderItem,
|
||||||
renderEmptyState,
|
renderEmptyState,
|
||||||
testID,
|
testID,
|
||||||
headerOffset = 0,
|
headerOffset = 0,
|
||||||
}: {
|
}: {
|
||||||
listsList: ListsListModel
|
listsList: ListsListModel
|
||||||
|
showAddBtns?: boolean
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
scrollElRef?: MutableRefObject<FlatList<any> | null>
|
scrollElRef?: MutableRefObject<FlatList<any> | null>
|
||||||
onPressCreateNew: () => void
|
onPressCreateNew: () => void
|
||||||
onPressTryAgain?: () => void
|
onPressTryAgain?: () => void
|
||||||
|
renderItem?: (list: GraphDefs.ListView) => JSX.Element
|
||||||
renderEmptyState?: () => JSX.Element
|
renderEmptyState?: () => JSX.Element
|
||||||
testID?: string
|
testID?: string
|
||||||
headerOffset?: number
|
headerOffset?: number
|
||||||
@@ -64,7 +68,7 @@ export const ListsList = observer(
|
|||||||
if (listsList.isEmpty) {
|
if (listsList.isEmpty) {
|
||||||
items = items.concat([EMPTY_ITEM])
|
items = items.concat([EMPTY_ITEM])
|
||||||
} else {
|
} else {
|
||||||
if (isDesktopWeb) {
|
if (showAddBtns) {
|
||||||
items = items.concat([CREATENEW_ITEM])
|
items = items.concat([CREATENEW_ITEM])
|
||||||
}
|
}
|
||||||
items = items.concat(listsList.lists)
|
items = items.concat(listsList.lists)
|
||||||
@@ -115,7 +119,7 @@ export const ListsList = observer(
|
|||||||
// rendering
|
// rendering
|
||||||
// =
|
// =
|
||||||
|
|
||||||
const renderItem = React.useCallback(
|
const renderItemInner = React.useCallback(
|
||||||
({item}: {item: any}) => {
|
({item}: {item: any}) => {
|
||||||
if (item === EMPTY_ITEM) {
|
if (item === EMPTY_ITEM) {
|
||||||
if (renderEmptyState) {
|
if (renderEmptyState) {
|
||||||
@@ -141,9 +145,15 @@ export const ListsList = observer(
|
|||||||
} else if (item === LOADING_ITEM) {
|
} else if (item === LOADING_ITEM) {
|
||||||
return <ProfileCardFeedLoadingPlaceholder />
|
return <ProfileCardFeedLoadingPlaceholder />
|
||||||
}
|
}
|
||||||
return <ListCard list={item} />
|
return renderItem ? renderItem(item) : <ListCard list={item} />
|
||||||
},
|
},
|
||||||
[listsList, onPressTryAgain, onPressRetryLoadMore, onPressCreateNew],
|
[
|
||||||
|
listsList,
|
||||||
|
onPressTryAgain,
|
||||||
|
onPressRetryLoadMore,
|
||||||
|
onPressCreateNew,
|
||||||
|
renderItem,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
const Footer = React.useCallback(
|
const Footer = React.useCallback(
|
||||||
@@ -166,7 +176,7 @@ export const ListsList = observer(
|
|||||||
ref={scrollElRef}
|
ref={scrollElRef}
|
||||||
data={data}
|
data={data}
|
||||||
keyExtractor={item => item._reactKey}
|
keyExtractor={item => item._reactKey}
|
||||||
renderItem={renderItem}
|
renderItem={renderItemInner}
|
||||||
ListFooterComponent={Footer}
|
ListFooterComponent={Footer}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
@@ -193,16 +203,13 @@ export const ListsList = observer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
function CreateNewItem({onPress}: {onPress: () => void}) {
|
function CreateNewItem({onPress}: {onPress: () => void}) {
|
||||||
const palInverted = usePalette('inverted')
|
const pal = usePalette('default')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.createNewContainer]}>
|
<View style={[styles.createNewContainer]}>
|
||||||
<Button type="inverted" onPress={onPress} style={styles.createNewButton}>
|
<Button type="default" onPress={onPress} style={styles.createNewButton}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon="plus" style={pal.text as FontAwesomeIconStyle} />
|
||||||
icon="plus"
|
<Text type="button" style={pal.text}>
|
||||||
style={palInverted.text as FontAwesomeIconStyle}
|
|
||||||
/>
|
|
||||||
<Text type="button" style={palInverted.text}>
|
|
||||||
New Mute-list
|
New Mute-list
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -214,8 +221,9 @@ const styles = StyleSheet.create({
|
|||||||
createNewContainer: {
|
createNewContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 16,
|
|
||||||
paddingHorizontal: 18,
|
paddingHorizontal: 18,
|
||||||
|
paddingTop: 18,
|
||||||
|
paddingBottom: 16,
|
||||||
},
|
},
|
||||||
createNewButton: {
|
createNewButton: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
import React, {useCallback} from 'react'
|
||||||
|
import * as Toast from '../util/Toast'
|
||||||
|
import {Pressable, StyleSheet, View} from 'react-native'
|
||||||
|
import {AppBskyGraphDefs as GraphDefs} from '@atproto/api'
|
||||||
|
import {
|
||||||
|
FontAwesomeIcon,
|
||||||
|
FontAwesomeIconStyle,
|
||||||
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {Text} from '../util/text/Text'
|
||||||
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
|
import {ListsList} from '../lists/ListsList'
|
||||||
|
import {ListsListModel} from 'state/models/lists/lists-list'
|
||||||
|
import {ListMembershipModel} from 'state/models/content/list-membership'
|
||||||
|
import {EmptyStateWithButton} from '../util/EmptyStateWithButton'
|
||||||
|
import {Button} from '../util/forms/Button'
|
||||||
|
import {useStores} from 'state/index'
|
||||||
|
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||||
|
import {s} from 'lib/styles'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {isDesktopWeb, isAndroid} from 'platform/detection'
|
||||||
|
|
||||||
|
export const snapPoints = ['fullscreen']
|
||||||
|
|
||||||
|
export function Component({
|
||||||
|
subject,
|
||||||
|
displayName,
|
||||||
|
}: {
|
||||||
|
subject: string
|
||||||
|
displayName: string
|
||||||
|
}) {
|
||||||
|
const store = useStores()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const palPrimary = usePalette('primary')
|
||||||
|
const palInverted = usePalette('inverted')
|
||||||
|
const [selected, setSelected] = React.useState([])
|
||||||
|
|
||||||
|
const listsList: ListsListModel = React.useMemo(
|
||||||
|
() => new ListsListModel(store, store.me.did),
|
||||||
|
[store],
|
||||||
|
)
|
||||||
|
const memberships: ListMembershipModel = React.useMemo(
|
||||||
|
() => new ListMembershipModel(store, subject),
|
||||||
|
[store, subject],
|
||||||
|
)
|
||||||
|
React.useEffect(() => {
|
||||||
|
listsList.refresh()
|
||||||
|
memberships.fetch().then(
|
||||||
|
() => {
|
||||||
|
setSelected(memberships.memberships.map(m => m.value.list))
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
store.log.error('Failed to fetch memberships', {err})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}, [listsList])
|
||||||
|
|
||||||
|
const onPressCancel = useCallback(() => {
|
||||||
|
store.shell.closeModal()
|
||||||
|
}, [store])
|
||||||
|
|
||||||
|
const onPressSave = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await memberships.updateTo(selected)
|
||||||
|
store.shell.closeModal()
|
||||||
|
} catch (err) {
|
||||||
|
store.log.error('Failed to update memberships', {err})
|
||||||
|
}
|
||||||
|
}, [store, selected])
|
||||||
|
|
||||||
|
const onPressNewMuteList = useCallback(() => {
|
||||||
|
store.shell.openModal({
|
||||||
|
name: 'create-mute-list',
|
||||||
|
onCreate: (uri: string) => {
|
||||||
|
listsList.refresh()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, [store, listsList])
|
||||||
|
|
||||||
|
const onToggleSelected = useCallback(
|
||||||
|
(uri: string) => {
|
||||||
|
if (selected.includes(uri)) {
|
||||||
|
setSelected(selected.filter(uri2 => uri2 !== uri))
|
||||||
|
} else {
|
||||||
|
setSelected([...selected, uri])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[selected, setSelected],
|
||||||
|
)
|
||||||
|
|
||||||
|
const renderItem = useCallback(
|
||||||
|
(list: GraphDefs.ListView) => {
|
||||||
|
const isSelected = selected.includes(list.uri)
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
style={[styles.listItem, pal.border]}
|
||||||
|
onPress={() => onToggleSelected(list.uri)}>
|
||||||
|
<View style={styles.listItemAvi}>
|
||||||
|
<UserAvatar size={40} avatar={list.avatar} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.listItemContent}>
|
||||||
|
<Text
|
||||||
|
type="lg"
|
||||||
|
style={[s.bold, pal.text]}
|
||||||
|
numberOfLines={1}
|
||||||
|
lineHeight={1.2}>
|
||||||
|
{sanitizeDisplayName(list.name)}
|
||||||
|
</Text>
|
||||||
|
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
|
||||||
|
{list.purpose === 'app.bsky.graph.defs#modlist' && 'Mute list'} by{' '}
|
||||||
|
{list.creator.did === store.me.did
|
||||||
|
? 'you'
|
||||||
|
: `@${list.creator.handle}`}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
isSelected
|
||||||
|
? [styles.checkbox, palPrimary.border, palPrimary.view]
|
||||||
|
: [styles.checkbox, pal.borderDark]
|
||||||
|
}>
|
||||||
|
{isSelected && (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="check"
|
||||||
|
style={palInverted.text as FontAwesomeIconStyle}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[pal, palPrimary, palInverted, onToggleSelected],
|
||||||
|
)
|
||||||
|
|
||||||
|
const renderEmptyState = React.useCallback(() => {
|
||||||
|
return (
|
||||||
|
<EmptyStateWithButton
|
||||||
|
icon="users-slash"
|
||||||
|
message="You can subscribe to mute-lists to automatically mute all of the users they include. Mute-lists are public but your subscription to a mute-list is private."
|
||||||
|
buttonLabel="New Mute List"
|
||||||
|
onPress={onPressNewMuteList}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}, [onPressNewMuteList])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View testID="listAddRemoveUserModal" style={s.hContentRegion}>
|
||||||
|
<Text style={[styles.title, pal.text]}>Add {displayName} to lists</Text>
|
||||||
|
<ListsList
|
||||||
|
listsList={listsList}
|
||||||
|
showAddBtns
|
||||||
|
onPressCreateNew={onPressNewMuteList}
|
||||||
|
renderItem={renderItem}
|
||||||
|
renderEmptyState={renderEmptyState}
|
||||||
|
style={[styles.list, pal.border]}
|
||||||
|
/>
|
||||||
|
<View style={[styles.btns, pal.border]}>
|
||||||
|
<Button
|
||||||
|
testID="cancelBtn"
|
||||||
|
type="default"
|
||||||
|
onPress={onPressCancel}
|
||||||
|
style={styles.footerBtn}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Cancel this modal"
|
||||||
|
accessibilityHint=""
|
||||||
|
onAccessibilityEscape={onPressCancel}
|
||||||
|
label="Cancel"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
testID="saveBtn"
|
||||||
|
type="primary"
|
||||||
|
onPress={onPressSave}
|
||||||
|
style={styles.footerBtn}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Save these changes"
|
||||||
|
accessibilityHint=""
|
||||||
|
onAccessibilityEscape={onPressSave}
|
||||||
|
label="Save Changes"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
paddingHorizontal: isDesktopWeb ? 0 : 16,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
textAlign: 'center',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: 24,
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
flex: 1,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
},
|
||||||
|
btns: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 10,
|
||||||
|
paddingTop: 10,
|
||||||
|
paddingBottom: isAndroid ? 10 : 0,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
},
|
||||||
|
footerBtn: {
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
paddingVertical: 12,
|
||||||
|
},
|
||||||
|
|
||||||
|
listItem: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderTopWidth: 1,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 10,
|
||||||
|
},
|
||||||
|
listItemAvi: {
|
||||||
|
width: 54,
|
||||||
|
paddingLeft: 4,
|
||||||
|
paddingTop: 8,
|
||||||
|
paddingBottom: 10,
|
||||||
|
},
|
||||||
|
listItemContent: {
|
||||||
|
flex: 1,
|
||||||
|
paddingRight: 10,
|
||||||
|
paddingTop: 10,
|
||||||
|
paddingBottom: 10,
|
||||||
|
},
|
||||||
|
checkbox: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderWidth: 1,
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
borderRadius: 6,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -13,6 +13,7 @@ import * as ServerInputModal from './ServerInput'
|
|||||||
import * as ReportPostModal from './ReportPost'
|
import * as ReportPostModal from './ReportPost'
|
||||||
import * as RepostModal from './Repost'
|
import * as RepostModal from './Repost'
|
||||||
import * as CreateMuteListModal from './CreateMuteList'
|
import * as CreateMuteListModal from './CreateMuteList'
|
||||||
|
import * as ListAddRemoveUserModal from './ListAddUser'
|
||||||
import * as AltImageModal from './AltImage'
|
import * as AltImageModal from './AltImage'
|
||||||
import * as ReportAccountModal from './ReportAccount'
|
import * as ReportAccountModal from './ReportAccount'
|
||||||
import * as DeleteAccountModal from './DeleteAccount'
|
import * as DeleteAccountModal from './DeleteAccount'
|
||||||
@@ -70,6 +71,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
|
|||||||
} else if (activeModal?.name === 'create-mute-list') {
|
} else if (activeModal?.name === 'create-mute-list') {
|
||||||
snapPoints = CreateMuteListModal.snapPoints
|
snapPoints = CreateMuteListModal.snapPoints
|
||||||
element = <CreateMuteListModal.Component {...activeModal} />
|
element = <CreateMuteListModal.Component {...activeModal} />
|
||||||
|
} else if (activeModal?.name === 'list-add-user') {
|
||||||
|
snapPoints = ListAddRemoveUserModal.snapPoints
|
||||||
|
element = <ListAddRemoveUserModal.Component {...activeModal} />
|
||||||
} else if (activeModal?.name === 'delete-account') {
|
} else if (activeModal?.name === 'delete-account') {
|
||||||
snapPoints = DeleteAccountModal.snapPoints
|
snapPoints = DeleteAccountModal.snapPoints
|
||||||
element = <DeleteAccountModal.Component />
|
element = <DeleteAccountModal.Component />
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import * as ServerInputModal from './ServerInput'
|
|||||||
import * as ReportPostModal from './ReportPost'
|
import * as ReportPostModal from './ReportPost'
|
||||||
import * as ReportAccountModal from './ReportAccount'
|
import * as ReportAccountModal from './ReportAccount'
|
||||||
import * as CreateMuteListModal from './CreateMuteList'
|
import * as CreateMuteListModal from './CreateMuteList'
|
||||||
|
import * as ListAddRemoveUserModal from './ListAddUser'
|
||||||
import * as DeleteAccountModal from './DeleteAccount'
|
import * as DeleteAccountModal from './DeleteAccount'
|
||||||
import * as RepostModal from './Repost'
|
import * as RepostModal from './Repost'
|
||||||
import * as CropImageModal from './crop-image/CropImage.web'
|
import * as CropImageModal from './crop-image/CropImage.web'
|
||||||
@@ -71,6 +72,8 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||||||
element = <ReportAccountModal.Component {...modal} />
|
element = <ReportAccountModal.Component {...modal} />
|
||||||
} else if (modal.name === 'create-mute-list') {
|
} else if (modal.name === 'create-mute-list') {
|
||||||
element = <CreateMuteListModal.Component {...modal} />
|
element = <CreateMuteListModal.Component {...modal} />
|
||||||
|
} else if (modal.name === 'list-add-user') {
|
||||||
|
element = <ListAddRemoveUserModal.Component {...modal} />
|
||||||
} else if (modal.name === 'crop-image') {
|
} else if (modal.name === 'crop-image') {
|
||||||
element = <CropImageModal.Component {...modal} />
|
element = <CropImageModal.Component {...modal} />
|
||||||
} else if (modal.name === 'delete-account') {
|
} else if (modal.name === 'delete-account') {
|
||||||
|
|||||||
@@ -146,12 +146,21 @@ const ProfileHeaderLoaded = observer(
|
|||||||
navigation.push('ProfileFollows', {name: view.handle})
|
navigation.push('ProfileFollows', {name: view.handle})
|
||||||
}, [track, navigation, view])
|
}, [track, navigation, view])
|
||||||
|
|
||||||
const onPressShare = React.useCallback(async () => {
|
const onPressShare = React.useCallback(() => {
|
||||||
track('ProfileHeader:ShareButtonClicked')
|
track('ProfileHeader:ShareButtonClicked')
|
||||||
const url = toShareUrl(`/profile/${view.handle}`)
|
const url = toShareUrl(`/profile/${view.handle}`)
|
||||||
shareUrl(url)
|
shareUrl(url)
|
||||||
}, [track, view])
|
}, [track, view])
|
||||||
|
|
||||||
|
const onPressAddToLists = React.useCallback(() => {
|
||||||
|
track('ProfileHeader:AddToListsButtonClicked')
|
||||||
|
store.shell.openModal({
|
||||||
|
name: 'list-add-user',
|
||||||
|
subject: view.did,
|
||||||
|
displayName: view.displayName || view.handle,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const onPressMuteAccount = React.useCallback(async () => {
|
const onPressMuteAccount = React.useCallback(async () => {
|
||||||
track('ProfileHeader:MuteAccountButtonClicked')
|
track('ProfileHeader:MuteAccountButtonClicked')
|
||||||
try {
|
try {
|
||||||
@@ -233,6 +242,11 @@ const ProfileHeaderLoaded = observer(
|
|||||||
label: 'Share',
|
label: 'Share',
|
||||||
onPress: onPressShare,
|
onPress: onPressShare,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
testID: 'profileHeaderDropdownListAddRemoveBtn',
|
||||||
|
label: 'Add to lists',
|
||||||
|
onPress: onPressAddToLists,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
if (!isMe) {
|
if (!isMe) {
|
||||||
items.push({sep: true})
|
items.push({sep: true})
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export const ModerationMuteListsScreen = withAuthRequired(({route}: Props) => {
|
|||||||
/>
|
/>
|
||||||
<ListsList
|
<ListsList
|
||||||
listsList={mutelists}
|
listsList={mutelists}
|
||||||
|
showAddBtns={isDesktopWeb}
|
||||||
renderEmptyState={renderEmptyState}
|
renderEmptyState={renderEmptyState}
|
||||||
onPressCreateNew={onPressNewMuteList}
|
onPressCreateNew={onPressNewMuteList}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,25 +1,71 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {StyleSheet} from 'react-native'
|
||||||
import {useFocusEffect} from '@react-navigation/native'
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
||||||
|
import {observer} from 'mobx-react-lite'
|
||||||
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
||||||
import {ViewHeader} from '../com/util/ViewHeader'
|
import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||||
|
import {CenteredView} from 'view/com/util/Views'
|
||||||
|
import {ListItems} from 'view/com/lists/ListItems'
|
||||||
|
import {EmptyState} from 'view/com/util/EmptyState'
|
||||||
|
import {ListModel} from 'state/models/content/list'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {isDesktopWeb} from 'platform/detection'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileList'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileList'>
|
||||||
export const ProfileListScreen = withAuthRequired(({route}: Props) => {
|
export const ProfileListScreen = withAuthRequired(
|
||||||
const store = useStores()
|
observer(({route}: Props) => {
|
||||||
const {name, rkey} = route.params
|
const store = useStores()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const {name, rkey} = route.params
|
||||||
|
|
||||||
useFocusEffect(
|
const list: ListModel = React.useMemo(() => {
|
||||||
React.useCallback(() => {
|
const model = new ListModel(
|
||||||
store.shell.setMinimalShellMode(false)
|
store,
|
||||||
}, [store]),
|
`at://${name}/app.bsky.graph.list/${rkey}`,
|
||||||
)
|
)
|
||||||
|
return model
|
||||||
|
}, [store, name, rkey])
|
||||||
|
|
||||||
return (
|
useFocusEffect(
|
||||||
<View>
|
React.useCallback(() => {
|
||||||
<ViewHeader title="List" />
|
store.shell.setMinimalShellMode(false)
|
||||||
</View>
|
list.loadMore(true)
|
||||||
)
|
}, [store]),
|
||||||
|
)
|
||||||
|
|
||||||
|
const renderEmptyState = React.useCallback(() => {
|
||||||
|
return <EmptyState icon="users-slash" message="This list is empty!" />
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
console.log('render')
|
||||||
|
return (
|
||||||
|
<CenteredView
|
||||||
|
style={[
|
||||||
|
styles.container,
|
||||||
|
isDesktopWeb && styles.containerDesktop,
|
||||||
|
pal.view,
|
||||||
|
pal.border,
|
||||||
|
]}
|
||||||
|
testID="moderationMutelistsScreen">
|
||||||
|
<ViewHeader
|
||||||
|
title={`List by ${list.list?.creator.handle}`}
|
||||||
|
showOnDesktop
|
||||||
|
/>
|
||||||
|
<ListItems list={list} renderEmptyState={renderEmptyState} />
|
||||||
|
</CenteredView>
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
paddingBottom: isDesktopWeb ? 0 : 100,
|
||||||
|
},
|
||||||
|
containerDesktop: {
|
||||||
|
borderLeftWidth: 1,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user