diff --git a/src/state/models/content/list.ts b/src/state/models/content/list.ts index 3d771ac928..1e74e1c073 100644 --- a/src/state/models/content/list.ts +++ b/src/state/models/content/list.ts @@ -1,5 +1,6 @@ import {makeAutoObservable} from 'mobx' import { + AtUri, AppBskyGraphGetList as GetList, AppBskyGraphDefs as GraphDefs, AppBskyGraphList, @@ -49,12 +50,14 @@ export class ListModel { ) record.avatar = blobRes.data.blob } - return await rootStore.agent.app.bsky.graph.list.create( + const res = await rootStore.agent.app.bsky.graph.list.create( { repo: rootStore.me.did, }, record, ) + await rootStore.agent.app.bsky.graph.subscribeMuteList({list: res.uri}) + return res } constructor(public rootStore: RootStoreModel, public uri: string) { @@ -79,6 +82,10 @@ export class ListModel { return this.hasLoaded && !this.hasContent } + get isOwner() { + return this.list?.creator.did === this.rootStore.me.did + } + // public api // = @@ -108,6 +115,85 @@ export class ListModel { } }) + async updateMetadata({ + name, + description, + avatar, + }: { + name: string + description: string + avatar: RNImage | null | undefined + }) { + if (!this.isOwner) { + throw new Error('Cannot edit this list') + } + + // get the current record + const {rkey} = new AtUri(this.uri) + const {value: record} = await this.rootStore.agent.app.bsky.graph.list.get({ + repo: this.rootStore.me.did, + rkey, + }) + + // update the fields + record.name = name + record.description = description + if (avatar) { + const blobRes = await apilib.uploadBlob( + this.rootStore, + avatar.path, + avatar.mime, + ) + record.avatar = blobRes.data.blob + } else if (avatar === null) { + record.avatar = undefined + } + // TODO: server doesn't accept puts for list records yet + throw new Error('TODO') + // return await this.rootStore.agent.app.bsky.graph.list.( + // { + // repo: this.rootStore.me.did, + // }, + // record, + // ) + } + + async delete() { + // fetch all the listitem records that belong to this list + 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.list === this.uri), + ) + cursor = res.cursor + if (!cursor) { + break + } + } + + // batch delete the list and listitem records + const createDel = (uri: string) => { + const urip = new AtUri(uri) + return { + $type: 'com.atproto.repo.applyWrites#delete', + collection: urip.collection, + rkey: urip.rkey, + } + } + await this.rootStore.agent.com.atproto.repo.applyWrites({ + repo: this.rootStore.me.did, + writes: [createDel(this.uri)].concat( + records.map(record => createDel(record.uri)), + ), + }) + } + /** * Attempt to load more again after a failure */ diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index a6ccb4e50b..2ffd42bd49 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -5,6 +5,7 @@ import {ProfileModel} from '../content/profile' import {isObj, hasProp} from 'lib/type-guards' import {Image as RNImage} from 'react-native-image-crop-picker' import {ImageModel} from '../media/image' +import {ListModel} from '../content/list' export interface ConfirmModal { name: 'confirm' @@ -37,15 +38,17 @@ export interface ReportAccountModal { did: string } -export interface CreateMuteListModal { - name: 'create-mute-list' - onCreate?: (uri: string) => void +export interface CreateOrEditMuteListModal { + name: 'create-or-edit-mute-list' + list?: ListModel + onSave?: (uri: string) => void } export interface ListAddUserModal { name: 'list-add-user' subject: string displayName: string + onUpdate?: () => void } export interface CropImageModal { diff --git a/src/view/com/lists/ListItems.tsx b/src/view/com/lists/ListItems.tsx index 2a0b64a1c1..9355510858 100644 --- a/src/view/com/lists/ListItems.tsx +++ b/src/view/com/lists/ListItems.tsx @@ -7,17 +7,27 @@ import { View, ViewStyle, } from 'react-native' +import {AppBskyActorDefs, AppBskyGraphDefs, RichText} from '@atproto/api' 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 {ProfileCard} from '../profile/ProfileCard' +import {Button} from '../util/forms/Button' +import {Text} from '../util/text/Text' +import {RichText as RichTextCom} from '../util/text/RichText' +import {UserAvatar} from '../util/UserAvatar' +import {TextLink} from '../util/Link' import {ListModel} from 'state/models/content/list' import {useAnalytics} from 'lib/analytics' import {usePalette} from 'lib/hooks/usePalette' +import {useStores} from 'state/index' import {s} from 'lib/styles' +import {isDesktopWeb} from 'platform/detection' const LOADING_ITEM = {_reactKey: '__loading__'} +const HEADER_ITEM = {_reactKey: '__header__'} const EMPTY_ITEM = {_reactKey: '__empty__'} const ERROR_ITEM = {_reactKey: '__error__'} const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} @@ -28,6 +38,9 @@ export const ListItems = observer( style, scrollElRef, onPressTryAgain, + onToggleSubscribed, + onPressEditList, + onPressDeleteList, renderEmptyState, testID, headerOffset = 0, @@ -36,16 +49,20 @@ export const ListItems = observer( style?: StyleProp scrollElRef?: MutableRefObject | null> onPressTryAgain?: () => void + onToggleSubscribed?: () => void + onPressEditList?: () => void + onPressDeleteList?: () => void renderEmptyState?: () => JSX.Element testID?: string headerOffset?: number }) => { const pal = usePalette('default') + const store = useStores() const {track} = useAnalytics() const [isRefreshing, setIsRefreshing] = React.useState(false) const data = React.useMemo(() => { - let items: any[] = [] + let items: any[] = [HEADER_ITEM] if (list.hasLoaded) { if (list.hasError) { items = items.concat([ERROR_ITEM]) @@ -98,9 +115,39 @@ export const ListItems = observer( list.retryLoadMore() }, [list]) + const onPressEditMembership = React.useCallback( + (profile: AppBskyActorDefs.ProfileViewBasic) => { + store.shell.openModal({ + name: 'list-add-user', + subject: profile.did, + displayName: profile.displayName || profile.handle, + onUpdate() { + list.refresh() + }, + }) + }, + [store, list], + ) + // rendering // = + const renderMemberButton = React.useCallback( + (profile: AppBskyActorDefs.ProfileViewBasic) => { + if (!list.isOwner) { + return null + } + return ( +