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])