diff --git a/src/state/models/content/list.ts b/src/state/models/content/list.ts new file mode 100644 index 0000000000..eccea5dce7 --- /dev/null +++ b/src/state/models/content/list.ts @@ -0,0 +1,151 @@ +import {makeAutoObservable} from 'mobx' +import { + AppBskyGraphGetList as GetList, + AppBskyActorDefs, + AppBskyGraphDefs, + AppBskyGraphList, + AppBskyRichtextFacet, + RichText, +} from '@atproto/api' +import {Image as RNImage} from 'react-native-image-crop-picker' +import {RootStoreModel} from '../root-store' +import * as apilib from 'lib/api/index' +import {cleanError} from 'lib/strings/errors' + +export class ListModel { + // state + isLoading = false + isRefreshing = false + hasLoaded = false + error = '' + params: GetList.QueryParams + + // data + uri: string + creator: AppBskyActorDefs.ProfileView + 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( + rootStore: RootStoreModel, + { + name, + description, + avatar, + }: {name: string; description: string; avatar: RNImage | undefined}, + ) { + const record: AppBskyGraphList.Record = { + purpose: 'app.bsky.graph.defs#modlist', + name, + description, + avatar: undefined, + createdAt: new Date().toISOString(), + } + if (avatar) { + const blobRes = await apilib.uploadBlob( + rootStore, + avatar.path, + avatar.mime, + ) + record.avatar = blobRes.data.blob + } + return await rootStore.agent.app.bsky.graph.list.create( + { + repo: rootStore.me.did, + }, + record, + ) + } + + constructor(public rootStore: RootStoreModel, params: GetList.QueryParams) { + makeAutoObservable( + this, + { + rootStore: false, + params: false, + }, + {autoBind: true}, + ) + this.params = params + } + + get hasContent() { + return this.uri !== '' + } + + get hasError() { + return this.error !== '' + } + + get isEmpty() { + return this.hasLoaded && !this.hasContent + } + + // public api + // = + + async setup() { + await this._load() + } + + async refresh() { + await this._load(true) + } + + // 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 profile', err) + } + } + + // loader functions + // = + + async _load(isRefreshing = false) { + this._xLoading(isRefreshing) + try { + 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) { + this.uri = res.data.list.uri + this.creator = res.data.list.creator + this.name = res.data.list.name + this.purpose = res.data.list.purpose + this.description = res.data.list.description + 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, + }) + } +} diff --git a/src/state/models/content/profile.ts b/src/state/models/content/profile.ts index dddf488a3c..a84476575c 100644 --- a/src/state/models/content/profile.ts +++ b/src/state/models/content/profile.ts @@ -18,8 +18,6 @@ import { filterProfileLabels, } from 'lib/labeling/helpers' -export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser' - export class ProfileViewerModel { muted?: boolean following?: string diff --git a/src/view/com/lists/ListCard.tsx b/src/view/com/lists/ListCard.tsx index fe1c2238d1..46994c13ef 100644 --- a/src/view/com/lists/ListCard.tsx +++ b/src/view/com/lists/ListCard.tsx @@ -1,6 +1,6 @@ import React from 'react' import {StyleSheet, View} from 'react-native' -import {AppBskyGraphDefs, RichText} from '@atproto/api' +import {AtUri, AppBskyGraphDefs, RichText} from '@atproto/api' import {Link} from '../util/Link' import {Text} from '../util/text/Text' import {RichText as RichTextCom} from '../util/text/RichText' @@ -8,11 +8,6 @@ import {UserAvatar} from '../util/UserAvatar' import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {sanitizeDisplayName} from 'lib/strings/display-names' -import { - getProfileViewBasicLabelInfo, - getProfileModeration, -} from 'lib/labeling/helpers' -import {ModerationBehaviorCode} from 'lib/labeling/types' export const ListCard = ({ testID, @@ -29,6 +24,15 @@ export const ListCard = ({ }) => { const pal = usePalette('default') + const rkey = React.useMemo(() => { + try { + const urip = new AtUri(list.uri) + return urip.rkey + } catch { + return '' + } + }, [list]) + const descriptionRichText = React.useMemo(() => { if (list.description) { return new RichText({ @@ -48,7 +52,7 @@ export const ListCard = ({ noBorder && styles.outerNoBorder, !noBg && pal.view, ]} - href={`/profile/${'list.author'}/lists/${'list.rkey'}`} + href={`/profile/${list.creator.did}/lists/${rkey}`} title={list.name} asAnchor anchorNoUnderline> @@ -65,10 +69,10 @@ export const ListCard = ({ {sanitizeDisplayName(list.name)} - {list.purpose === 'app.bsky.graph.defs#blocklist' && 'Block list'}{' '} - by @TODO + {list.purpose === 'app.bsky.graph.defs#modlist' && 'Mute list'} by @ + {list.creator.handle} - {!!list.viewer?.blocked && ( + {!!list.viewer?.muted && ( diff --git a/src/view/com/modals/CreateMuteList.tsx b/src/view/com/modals/CreateMuteList.tsx index 97e770b918..1af0504c16 100644 --- a/src/view/com/modals/CreateMuteList.tsx +++ b/src/view/com/modals/CreateMuteList.tsx @@ -14,6 +14,7 @@ import {Image as RNImage} from 'react-native-image-crop-picker' import {Text} from '../util/text/Text' import {ErrorMessage} from '../util/error/ErrorMessage' import {useStores} from 'state/index' +import {ListModel} from 'state/models/content/list' import {s, colors, gradients} from 'lib/styles' import {enforceLen} from 'lib/strings/helpers' import {compressIfNeeded} from 'lib/media/manip' @@ -71,9 +72,13 @@ export function Component({onCreate}: {onCreate?: (uri: string) => void}) { setError('') } try { - // TODO + const res = await ListModel.createModList(store, { + name, + description, + avatar: newAvatar, + }) Toast.show('Mute-list created') - onCreate?.('todo') + onCreate?.(res.uri) store.shell.closeModal() } catch (e: any) { if (isNetworkError(e)) { diff --git a/src/view/screens/ModerationMuteLists.tsx b/src/view/screens/ModerationMuteLists.tsx index 3107883b1f..cee57af958 100644 --- a/src/view/screens/ModerationMuteLists.tsx +++ b/src/view/screens/ModerationMuteLists.tsx @@ -1,13 +1,18 @@ import React from 'react' import {StyleSheet} from 'react-native' -import {useFocusEffect} from '@react-navigation/native' +import { + useFocusEffect, + useNavigation, + StackActions, +} from '@react-navigation/native' +import {AtUri} from '@atproto/api' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {EmptyStateWithButton} from 'view/com/util/EmptyStateWithButton' -import {MutelistsEmptyState} from 'view/com/lists/MutelistsEmptyState' import {useStores} from 'state/index' import {ListsListModel} from 'state/models/lists/lists-list' import {ListsList} from 'view/com/lists/ListsList' +import {NavigationProp} from 'lib/routes/types' import {usePalette} from 'lib/hooks/usePalette' import {CenteredView} from 'view/com/util/Views' import {ViewHeader} from 'view/com/util/ViewHeader' @@ -20,6 +25,7 @@ type Props = NativeStackScreenProps< export const ModerationMuteListsScreen = withAuthRequired(({route}: Props) => { const pal = usePalette('default') const store = useStores() + const navigation = useNavigation() const mutelists: ListsListModel = React.useMemo(() => { const list = new ListsListModel(store, 'mutelists') @@ -37,7 +43,13 @@ export const ModerationMuteListsScreen = withAuthRequired(({route}: Props) => { store.shell.openModal({ name: 'create-mute-list', onCreate: (uri: string) => { - // TODO + try { + const urip = new AtUri(uri) + navigation.navigate('ProfileList', { + name: urip.hostname, + rkey: urip.rkey, + }) + } catch {} }, }) }, [store])