Add update/delete/subscribe/unsubscribe to lists
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<ViewStyle>
|
||||
scrollElRef?: MutableRefObject<FlatList<any> | 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 (
|
||||
<Button
|
||||
type="default"
|
||||
label="Edit"
|
||||
onPress={() => onPressEditMembership(profile)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[list],
|
||||
)
|
||||
|
||||
const renderItem = React.useCallback(
|
||||
({item}: {item: any}) => {
|
||||
if (item === EMPTY_ITEM) {
|
||||
@@ -108,6 +155,16 @@ export const ListItems = observer(
|
||||
return renderEmptyState()
|
||||
}
|
||||
return <View />
|
||||
} else if (item === HEADER_ITEM) {
|
||||
return list.hasLoaded ? (
|
||||
<ListHeader
|
||||
list={list.list}
|
||||
isOwner={list.isOwner}
|
||||
onToggleSubscribed={onToggleSubscribed}
|
||||
onPressEditList={onPressEditList}
|
||||
onPressDeleteList={onPressDeleteList}
|
||||
/>
|
||||
) : null
|
||||
} else if (item === ERROR_ITEM) {
|
||||
return (
|
||||
<ErrorMessage
|
||||
@@ -125,9 +182,14 @@ export const ListItems = observer(
|
||||
} else if (item === LOADING_ITEM) {
|
||||
return <ProfileCardFeedLoadingPlaceholder />
|
||||
}
|
||||
return <View /> // TODO
|
||||
return (
|
||||
<ProfileCard
|
||||
profile={(item as AppBskyGraphDefs.ListItemView).subject}
|
||||
renderButton={renderMemberButton}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[list, onPressTryAgain, onPressRetryLoadMore],
|
||||
[list, onPressTryAgain, onPressRetryLoadMore, renderMemberButton],
|
||||
)
|
||||
|
||||
const Footer = React.useCallback(
|
||||
@@ -176,6 +238,130 @@ export const ListItems = observer(
|
||||
},
|
||||
)
|
||||
|
||||
const ListHeader = observer(
|
||||
({
|
||||
list,
|
||||
isOwner,
|
||||
onToggleSubscribed,
|
||||
onPressEditList,
|
||||
onPressDeleteList,
|
||||
}: {
|
||||
list: AppBskyGraphDefs.ListView
|
||||
isOwner: boolean
|
||||
onToggleSubscribed?: () => void
|
||||
onPressEditList?: () => void
|
||||
onPressDeleteList?: () => void
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const descriptionRT = React.useMemo(
|
||||
() =>
|
||||
list?.description &&
|
||||
new RichText({text: list.description, facets: list.descriptionFacets}),
|
||||
[list],
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<View style={[styles.header, pal.border]}>
|
||||
<View style={s.flex1}>
|
||||
<Text type="title-xl" style={[pal.text, s.bold]}>
|
||||
{list.name}
|
||||
</Text>
|
||||
{list && (
|
||||
<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'
|
||||
) : (
|
||||
<TextLink
|
||||
text={`@${list.creator.handle}`}
|
||||
href={`/profile/${list.creator.did}`}
|
||||
/>
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
{descriptionRT && (
|
||||
<RichTextCom
|
||||
style={[pal.text, styles.headerDescription]}
|
||||
richText={descriptionRT}
|
||||
/>
|
||||
)}
|
||||
{isDesktopWeb && (
|
||||
<View style={styles.headerBtns}>
|
||||
{list.viewer?.muted ? (
|
||||
<Button
|
||||
type="inverted"
|
||||
label="Unsubscribe"
|
||||
onPress={onToggleSubscribed}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
label="Subscribe & Mute"
|
||||
onPress={onToggleSubscribed}
|
||||
/>
|
||||
)}
|
||||
{isOwner &&
|
||||
false /*TODO disabled until we can edit records*/ && (
|
||||
<Button
|
||||
type="default"
|
||||
label="Edit List"
|
||||
onPress={onPressEditList}
|
||||
/>
|
||||
)}
|
||||
{isOwner && (
|
||||
<Button
|
||||
type="default"
|
||||
label="Delete List"
|
||||
onPress={onPressDeleteList}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View>
|
||||
<UserAvatar avatar={list.avatar} size={64} />
|
||||
</View>
|
||||
</View>
|
||||
<View style={[styles.fakeSelector, pal.border]}>
|
||||
<View
|
||||
style={[styles.fakeSelectorItem, {borderColor: pal.colors.link}]}>
|
||||
<Text type="md-medium" style={[pal.text]}>
|
||||
Muted users
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 16,
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
headerDescription: {
|
||||
marginTop: 8,
|
||||
},
|
||||
headerBtns: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
marginTop: 12,
|
||||
},
|
||||
fakeSelector: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: isDesktopWeb ? 16 : 6,
|
||||
},
|
||||
fakeSelectorItem: {
|
||||
paddingHorizontal: 12,
|
||||
paddingBottom: 8,
|
||||
borderBottomWidth: 3,
|
||||
},
|
||||
feedFooter: {paddingTop: 20},
|
||||
})
|
||||
|
||||
+33
-13
@@ -30,7 +30,13 @@ const MAX_DESCRIPTION = 300 // todo
|
||||
|
||||
export const snapPoints = ['fullscreen']
|
||||
|
||||
export function Component({onCreate}: {onCreate?: (uri: string) => void}) {
|
||||
export function Component({
|
||||
onSave,
|
||||
list,
|
||||
}: {
|
||||
onSave?: (uri: string) => void
|
||||
list?: ListModel
|
||||
}) {
|
||||
const store = useStores()
|
||||
const [error, setError] = useState<string>('')
|
||||
const pal = usePalette('default')
|
||||
@@ -38,9 +44,11 @@ export function Component({onCreate}: {onCreate?: (uri: string) => void}) {
|
||||
const {track} = useAnalytics()
|
||||
|
||||
const [isProcessing, setProcessing] = useState<boolean>(false)
|
||||
const [name, setName] = useState<string>('')
|
||||
const [description, setDescription] = useState<string>('')
|
||||
const [avatar, setAvatar] = useState<string | undefined>(undefined)
|
||||
const [name, setName] = useState<string>(list?.list.name || '')
|
||||
const [description, setDescription] = useState<string>(
|
||||
list?.list.description || '',
|
||||
)
|
||||
const [avatar, setAvatar] = useState<string | undefined>(list?.list.avatar)
|
||||
const [newAvatar, setNewAvatar] = useState<RNImage | undefined | null>()
|
||||
|
||||
const onPressCancel = useCallback(() => {
|
||||
@@ -73,13 +81,23 @@ export function Component({onCreate}: {onCreate?: (uri: string) => void}) {
|
||||
setError('')
|
||||
}
|
||||
try {
|
||||
const res = await ListModel.createModList(store, {
|
||||
name,
|
||||
description,
|
||||
avatar: newAvatar,
|
||||
})
|
||||
Toast.show('Mute list created')
|
||||
onCreate?.(res.uri)
|
||||
if (list) {
|
||||
await list.updateMetadata({
|
||||
name,
|
||||
description,
|
||||
avatar: newAvatar,
|
||||
})
|
||||
Toast.show('Mute list updated')
|
||||
onSave?.(list.uri)
|
||||
} else {
|
||||
const res = await ListModel.createModList(store, {
|
||||
name,
|
||||
description,
|
||||
avatar: newAvatar,
|
||||
})
|
||||
Toast.show('Mute list created')
|
||||
onSave?.(res.uri)
|
||||
}
|
||||
store.shell.closeModal()
|
||||
} catch (e: any) {
|
||||
if (isNetworkError(e)) {
|
||||
@@ -96,7 +114,7 @@ export function Component({onCreate}: {onCreate?: (uri: string) => void}) {
|
||||
setProcessing,
|
||||
setError,
|
||||
error,
|
||||
onCreate,
|
||||
onSave,
|
||||
store,
|
||||
name,
|
||||
description,
|
||||
@@ -108,7 +126,9 @@ export function Component({onCreate}: {onCreate?: (uri: string) => void}) {
|
||||
<ScrollView
|
||||
style={[pal.view, styles.container]}
|
||||
testID="createMuteListModal">
|
||||
<Text style={[styles.title, pal.text]}>New Mute List</Text>
|
||||
<Text style={[styles.title, pal.text]}>
|
||||
{list ? 'Edit Mute List' : 'New Mute List'}
|
||||
</Text>
|
||||
{error !== '' && (
|
||||
<View style={styles.errorContainer}>
|
||||
<ErrorMessage message={error} />
|
||||
@@ -24,9 +24,11 @@ export const snapPoints = ['fullscreen']
|
||||
export function Component({
|
||||
subject,
|
||||
displayName,
|
||||
onUpdate,
|
||||
}: {
|
||||
subject: string
|
||||
displayName: string
|
||||
onUpdate?: () => void
|
||||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
@@ -61,15 +63,17 @@ export function Component({
|
||||
const onPressSave = useCallback(async () => {
|
||||
try {
|
||||
await memberships.updateTo(selected)
|
||||
store.shell.closeModal()
|
||||
} catch (err) {
|
||||
store.log.error('Failed to update memberships', {err})
|
||||
return
|
||||
}
|
||||
onUpdate?.()
|
||||
store.shell.closeModal()
|
||||
}, [store, selected])
|
||||
|
||||
const onPressNewMuteList = useCallback(() => {
|
||||
store.shell.openModal({
|
||||
name: 'create-mute-list',
|
||||
name: 'create-or-edit-mute-list',
|
||||
onCreate: (uri: string) => {
|
||||
listsList.refresh()
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@ import * as EditProfileModal from './EditProfile'
|
||||
import * as ServerInputModal from './ServerInput'
|
||||
import * as ReportPostModal from './ReportPost'
|
||||
import * as RepostModal from './Repost'
|
||||
import * as CreateMuteListModal from './CreateMuteList'
|
||||
import * as CreateOrEditMuteListModal from './CreateOrEditMuteList'
|
||||
import * as ListAddRemoveUserModal from './ListAddUser'
|
||||
import * as AltImageModal from './AltImage'
|
||||
import * as ReportAccountModal from './ReportAccount'
|
||||
@@ -68,9 +68,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
|
||||
} else if (activeModal?.name === 'report-account') {
|
||||
snapPoints = ReportAccountModal.snapPoints
|
||||
element = <ReportAccountModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'create-mute-list') {
|
||||
snapPoints = CreateMuteListModal.snapPoints
|
||||
element = <CreateMuteListModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'create-or-edit-mute-list') {
|
||||
snapPoints = CreateOrEditMuteListModal.snapPoints
|
||||
element = <CreateOrEditMuteListModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'list-add-user') {
|
||||
snapPoints = ListAddRemoveUserModal.snapPoints
|
||||
element = <ListAddRemoveUserModal.Component {...activeModal} />
|
||||
|
||||
@@ -11,7 +11,7 @@ import * as EditProfileModal from './EditProfile'
|
||||
import * as ServerInputModal from './ServerInput'
|
||||
import * as ReportPostModal from './ReportPost'
|
||||
import * as ReportAccountModal from './ReportAccount'
|
||||
import * as CreateMuteListModal from './CreateMuteList'
|
||||
import * as CreateOrEditMuteListModal from './CreateOrEditMuteList'
|
||||
import * as ListAddRemoveUserModal from './ListAddUser'
|
||||
import * as DeleteAccountModal from './DeleteAccount'
|
||||
import * as RepostModal from './Repost'
|
||||
@@ -70,8 +70,8 @@ function Modal({modal}: {modal: ModalIface}) {
|
||||
element = <ReportPostModal.Component {...modal} />
|
||||
} else if (modal.name === 'report-account') {
|
||||
element = <ReportAccountModal.Component {...modal} />
|
||||
} else if (modal.name === 'create-mute-list') {
|
||||
element = <CreateMuteListModal.Component {...modal} />
|
||||
} else if (modal.name === 'create-or-edit-mute-list') {
|
||||
element = <CreateOrEditMuteListModal.Component {...modal} />
|
||||
} else if (modal.name === 'list-add-user') {
|
||||
element = <ListAddRemoveUserModal.Component {...modal} />
|
||||
} else if (modal.name === 'crop-image') {
|
||||
|
||||
@@ -32,7 +32,7 @@ export const ProfileCard = observer(
|
||||
noBorder?: boolean
|
||||
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||
overrideModeration?: boolean
|
||||
renderButton?: () => JSX.Element
|
||||
renderButton?: (profile: AppBskyActorDefs.ProfileViewBasic) => JSX.Element
|
||||
}) => {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
@@ -92,7 +92,7 @@ export const ProfileCard = observer(
|
||||
)}
|
||||
</View>
|
||||
{renderButton ? (
|
||||
<View style={styles.layoutButton}>{renderButton()}</View>
|
||||
<View style={styles.layoutButton}>{renderButton(profile)}</View>
|
||||
) : undefined}
|
||||
</View>
|
||||
{profile.description ? (
|
||||
|
||||
@@ -48,7 +48,7 @@ export const ViewHeader = observer(function ({
|
||||
|
||||
if (isDesktopWeb) {
|
||||
if (showOnDesktop) {
|
||||
return <DesktopWebHeader title={title} />
|
||||
return <DesktopWebHeader title={title} renderButton={renderButton} />
|
||||
}
|
||||
return null
|
||||
} else {
|
||||
@@ -91,7 +91,13 @@ export const ViewHeader = observer(function ({
|
||||
}
|
||||
})
|
||||
|
||||
function DesktopWebHeader({title}: {title: string}) {
|
||||
function DesktopWebHeader({
|
||||
title,
|
||||
renderButton,
|
||||
}: {
|
||||
title: string
|
||||
renderButton?: () => JSX.Element
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<CenteredView style={[styles.header, styles.desktopHeader, pal.border]}>
|
||||
@@ -100,6 +106,7 @@ function DesktopWebHeader({title}: {title: string}) {
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
{renderButton?.()}
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,22 +28,22 @@ export const ModerationMuteListsScreen = withAuthRequired(({route}: Props) => {
|
||||
const store = useStores()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const mutelists: ListsListModel = React.useMemo(() => {
|
||||
const list = new ListsListModel(store, 'my-modlists')
|
||||
list.loadMore()
|
||||
return list
|
||||
}, [store])
|
||||
const mutelists: ListsListModel = React.useMemo(
|
||||
() => new ListsListModel(store, 'my-modlists'),
|
||||
[store],
|
||||
)
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
store.shell.setMinimalShellMode(false)
|
||||
mutelists.refresh()
|
||||
}, [store]),
|
||||
)
|
||||
|
||||
const onPressNewMuteList = React.useCallback(() => {
|
||||
store.shell.openModal({
|
||||
name: 'create-mute-list',
|
||||
onCreate: (uri: string) => {
|
||||
name: 'create-or-edit-mute-list',
|
||||
onSave: (uri: string) => {
|
||||
try {
|
||||
const urip = new AtUri(uri)
|
||||
navigation.navigate('ProfileList', {
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet} from 'react-native'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
||||
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 {Button} from 'view/com/util/forms/Button'
|
||||
import * as Toast from 'view/com/util/Toast'
|
||||
import {ListModel} from 'state/models/content/list'
|
||||
import {useStores} from 'state/index'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {isDesktopWeb} from 'platform/detection'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileList'>
|
||||
export const ProfileListScreen = withAuthRequired(
|
||||
observer(({route}: Props) => {
|
||||
const store = useStores()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const pal = usePalette('default')
|
||||
const {name, rkey} = route.params
|
||||
|
||||
@@ -35,11 +40,88 @@ export const ProfileListScreen = withAuthRequired(
|
||||
}, [store]),
|
||||
)
|
||||
|
||||
const onToggleSubscribed = React.useCallback(async () => {
|
||||
try {
|
||||
if (list.list?.viewer?.muted) {
|
||||
await store.agent.app.bsky.graph.unsubscribeMuteList({list: list.uri})
|
||||
Toast.show('List unsubscribed')
|
||||
} else {
|
||||
await store.agent.app.bsky.graph.subscribeMuteList({list: list.uri})
|
||||
Toast.show('List subscribed')
|
||||
}
|
||||
} catch (err) {
|
||||
Toast.show(
|
||||
'There was an an issue updating your subscription, please check your internet connection and try again.',
|
||||
)
|
||||
store.log.error('Failed up update subscription', {err})
|
||||
}
|
||||
}, [store, list])
|
||||
|
||||
const onPressEditList = React.useCallback(() => {
|
||||
store.shell.openModal({
|
||||
name: 'create-or-edit-mute-list',
|
||||
list,
|
||||
onSave() {
|
||||
list.refresh()
|
||||
},
|
||||
})
|
||||
}, [store, list])
|
||||
|
||||
const onPressDeleteList = React.useCallback(() => {
|
||||
store.shell.openModal({
|
||||
name: 'confirm',
|
||||
title: 'Delete List',
|
||||
message: 'Are you sure?',
|
||||
async onPressConfirm() {
|
||||
await list.delete()
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('Home')
|
||||
}
|
||||
},
|
||||
})
|
||||
}, [store, list])
|
||||
|
||||
const renderEmptyState = React.useCallback(() => {
|
||||
return <EmptyState icon="users-slash" message="This list is empty!" />
|
||||
}, [])
|
||||
|
||||
console.log('render')
|
||||
const renderHeaderBtn = React.useCallback(() => {
|
||||
return (
|
||||
<View style={styles.headerBtns}>
|
||||
{list?.isOwner &&
|
||||
false /*TODO disabled until we can edit records*/ && (
|
||||
<Button
|
||||
type="default"
|
||||
label="Edit List"
|
||||
onPress={onPressEditList}
|
||||
/>
|
||||
)}
|
||||
{list?.isOwner && (
|
||||
<Button
|
||||
type="default"
|
||||
label="Delete List"
|
||||
onPress={onPressDeleteList}
|
||||
/>
|
||||
)}
|
||||
{list.list?.viewer?.muted ? (
|
||||
<Button
|
||||
type="inverted"
|
||||
label="Unsubscribe"
|
||||
onPress={onToggleSubscribed}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
label="Subscribe & Mute"
|
||||
onPress={onToggleSubscribed}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}, [list?.isOwner, onPressDeleteList, onPressEditList, onToggleSubscribed])
|
||||
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
@@ -49,17 +131,24 @@ export const ProfileListScreen = withAuthRequired(
|
||||
pal.border,
|
||||
]}
|
||||
testID="moderationMutelistsScreen">
|
||||
<ViewHeader
|
||||
title={`List by ${list.list?.creator.handle}`}
|
||||
showOnDesktop
|
||||
<ViewHeader title="" renderButton={renderHeaderBtn} />
|
||||
<ListItems
|
||||
list={list}
|
||||
renderEmptyState={renderEmptyState}
|
||||
onToggleSubscribed={onToggleSubscribed}
|
||||
onPressEditList={onPressEditList}
|
||||
onPressDeleteList={onPressDeleteList}
|
||||
/>
|
||||
<ListItems list={list} renderEmptyState={renderEmptyState} />
|
||||
</CenteredView>
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
headerBtns: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingBottom: isDesktopWeb ? 0 : 100,
|
||||
|
||||
Reference in New Issue
Block a user