Add update/delete/subscribe/unsubscribe to lists
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import {makeAutoObservable} from 'mobx'
|
import {makeAutoObservable} from 'mobx'
|
||||||
import {
|
import {
|
||||||
|
AtUri,
|
||||||
AppBskyGraphGetList as GetList,
|
AppBskyGraphGetList as GetList,
|
||||||
AppBskyGraphDefs as GraphDefs,
|
AppBskyGraphDefs as GraphDefs,
|
||||||
AppBskyGraphList,
|
AppBskyGraphList,
|
||||||
@@ -49,12 +50,14 @@ export class ListModel {
|
|||||||
)
|
)
|
||||||
record.avatar = blobRes.data.blob
|
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,
|
repo: rootStore.me.did,
|
||||||
},
|
},
|
||||||
record,
|
record,
|
||||||
)
|
)
|
||||||
|
await rootStore.agent.app.bsky.graph.subscribeMuteList({list: res.uri})
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(public rootStore: RootStoreModel, public uri: string) {
|
constructor(public rootStore: RootStoreModel, public uri: string) {
|
||||||
@@ -79,6 +82,10 @@ export class ListModel {
|
|||||||
return this.hasLoaded && !this.hasContent
|
return this.hasLoaded && !this.hasContent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isOwner() {
|
||||||
|
return this.list?.creator.did === this.rootStore.me.did
|
||||||
|
}
|
||||||
|
|
||||||
// public api
|
// 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
|
* 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 {isObj, hasProp} from 'lib/type-guards'
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||||
import {ImageModel} from '../media/image'
|
import {ImageModel} from '../media/image'
|
||||||
|
import {ListModel} from '../content/list'
|
||||||
|
|
||||||
export interface ConfirmModal {
|
export interface ConfirmModal {
|
||||||
name: 'confirm'
|
name: 'confirm'
|
||||||
@@ -37,15 +38,17 @@ export interface ReportAccountModal {
|
|||||||
did: string
|
did: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateMuteListModal {
|
export interface CreateOrEditMuteListModal {
|
||||||
name: 'create-mute-list'
|
name: 'create-or-edit-mute-list'
|
||||||
onCreate?: (uri: string) => void
|
list?: ListModel
|
||||||
|
onSave?: (uri: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListAddUserModal {
|
export interface ListAddUserModal {
|
||||||
name: 'list-add-user'
|
name: 'list-add-user'
|
||||||
subject: string
|
subject: string
|
||||||
displayName: string
|
displayName: string
|
||||||
|
onUpdate?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CropImageModal {
|
export interface CropImageModal {
|
||||||
|
|||||||
@@ -7,17 +7,27 @@ import {
|
|||||||
View,
|
View,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
|
import {AppBskyActorDefs, AppBskyGraphDefs, RichText} from '@atproto/api'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {FlatList} from '../util/Views'
|
import {FlatList} from '../util/Views'
|
||||||
import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
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 {ListModel} from 'state/models/content/list'
|
||||||
import {useAnalytics} from 'lib/analytics'
|
import {useAnalytics} from 'lib/analytics'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {useStores} from 'state/index'
|
||||||
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 HEADER_ITEM = {_reactKey: '__header__'}
|
||||||
const EMPTY_ITEM = {_reactKey: '__empty__'}
|
const EMPTY_ITEM = {_reactKey: '__empty__'}
|
||||||
const ERROR_ITEM = {_reactKey: '__error__'}
|
const ERROR_ITEM = {_reactKey: '__error__'}
|
||||||
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
|
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
|
||||||
@@ -28,6 +38,9 @@ export const ListItems = observer(
|
|||||||
style,
|
style,
|
||||||
scrollElRef,
|
scrollElRef,
|
||||||
onPressTryAgain,
|
onPressTryAgain,
|
||||||
|
onToggleSubscribed,
|
||||||
|
onPressEditList,
|
||||||
|
onPressDeleteList,
|
||||||
renderEmptyState,
|
renderEmptyState,
|
||||||
testID,
|
testID,
|
||||||
headerOffset = 0,
|
headerOffset = 0,
|
||||||
@@ -36,16 +49,20 @@ export const ListItems = observer(
|
|||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
scrollElRef?: MutableRefObject<FlatList<any> | null>
|
scrollElRef?: MutableRefObject<FlatList<any> | null>
|
||||||
onPressTryAgain?: () => void
|
onPressTryAgain?: () => void
|
||||||
|
onToggleSubscribed?: () => void
|
||||||
|
onPressEditList?: () => void
|
||||||
|
onPressDeleteList?: () => void
|
||||||
renderEmptyState?: () => JSX.Element
|
renderEmptyState?: () => JSX.Element
|
||||||
testID?: string
|
testID?: string
|
||||||
headerOffset?: number
|
headerOffset?: number
|
||||||
}) => {
|
}) => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const store = useStores()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const [isRefreshing, setIsRefreshing] = React.useState(false)
|
const [isRefreshing, setIsRefreshing] = React.useState(false)
|
||||||
|
|
||||||
const data = React.useMemo(() => {
|
const data = React.useMemo(() => {
|
||||||
let items: any[] = []
|
let items: any[] = [HEADER_ITEM]
|
||||||
if (list.hasLoaded) {
|
if (list.hasLoaded) {
|
||||||
if (list.hasError) {
|
if (list.hasError) {
|
||||||
items = items.concat([ERROR_ITEM])
|
items = items.concat([ERROR_ITEM])
|
||||||
@@ -98,9 +115,39 @@ export const ListItems = observer(
|
|||||||
list.retryLoadMore()
|
list.retryLoadMore()
|
||||||
}, [list])
|
}, [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
|
// 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(
|
const renderItem = React.useCallback(
|
||||||
({item}: {item: any}) => {
|
({item}: {item: any}) => {
|
||||||
if (item === EMPTY_ITEM) {
|
if (item === EMPTY_ITEM) {
|
||||||
@@ -108,6 +155,16 @@ export const ListItems = observer(
|
|||||||
return renderEmptyState()
|
return renderEmptyState()
|
||||||
}
|
}
|
||||||
return <View />
|
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) {
|
} else if (item === ERROR_ITEM) {
|
||||||
return (
|
return (
|
||||||
<ErrorMessage
|
<ErrorMessage
|
||||||
@@ -125,9 +182,14 @@ export const ListItems = observer(
|
|||||||
} else if (item === LOADING_ITEM) {
|
} else if (item === LOADING_ITEM) {
|
||||||
return <ProfileCardFeedLoadingPlaceholder />
|
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(
|
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({
|
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},
|
feedFooter: {paddingTop: 20},
|
||||||
})
|
})
|
||||||
|
|||||||
+33
-13
@@ -30,7 +30,13 @@ const MAX_DESCRIPTION = 300 // todo
|
|||||||
|
|
||||||
export const snapPoints = ['fullscreen']
|
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 store = useStores()
|
||||||
const [error, setError] = useState<string>('')
|
const [error, setError] = useState<string>('')
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
@@ -38,9 +44,11 @@ export function Component({onCreate}: {onCreate?: (uri: string) => void}) {
|
|||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
|
|
||||||
const [isProcessing, setProcessing] = useState<boolean>(false)
|
const [isProcessing, setProcessing] = useState<boolean>(false)
|
||||||
const [name, setName] = useState<string>('')
|
const [name, setName] = useState<string>(list?.list.name || '')
|
||||||
const [description, setDescription] = useState<string>('')
|
const [description, setDescription] = useState<string>(
|
||||||
const [avatar, setAvatar] = useState<string | undefined>(undefined)
|
list?.list.description || '',
|
||||||
|
)
|
||||||
|
const [avatar, setAvatar] = useState<string | undefined>(list?.list.avatar)
|
||||||
const [newAvatar, setNewAvatar] = useState<RNImage | undefined | null>()
|
const [newAvatar, setNewAvatar] = useState<RNImage | undefined | null>()
|
||||||
|
|
||||||
const onPressCancel = useCallback(() => {
|
const onPressCancel = useCallback(() => {
|
||||||
@@ -73,13 +81,23 @@ export function Component({onCreate}: {onCreate?: (uri: string) => void}) {
|
|||||||
setError('')
|
setError('')
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await ListModel.createModList(store, {
|
if (list) {
|
||||||
name,
|
await list.updateMetadata({
|
||||||
description,
|
name,
|
||||||
avatar: newAvatar,
|
description,
|
||||||
})
|
avatar: newAvatar,
|
||||||
Toast.show('Mute list created')
|
})
|
||||||
onCreate?.(res.uri)
|
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()
|
store.shell.closeModal()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (isNetworkError(e)) {
|
if (isNetworkError(e)) {
|
||||||
@@ -96,7 +114,7 @@ export function Component({onCreate}: {onCreate?: (uri: string) => void}) {
|
|||||||
setProcessing,
|
setProcessing,
|
||||||
setError,
|
setError,
|
||||||
error,
|
error,
|
||||||
onCreate,
|
onSave,
|
||||||
store,
|
store,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
@@ -108,7 +126,9 @@ export function Component({onCreate}: {onCreate?: (uri: string) => void}) {
|
|||||||
<ScrollView
|
<ScrollView
|
||||||
style={[pal.view, styles.container]}
|
style={[pal.view, styles.container]}
|
||||||
testID="createMuteListModal">
|
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 !== '' && (
|
{error !== '' && (
|
||||||
<View style={styles.errorContainer}>
|
<View style={styles.errorContainer}>
|
||||||
<ErrorMessage message={error} />
|
<ErrorMessage message={error} />
|
||||||
@@ -24,9 +24,11 @@ export const snapPoints = ['fullscreen']
|
|||||||
export function Component({
|
export function Component({
|
||||||
subject,
|
subject,
|
||||||
displayName,
|
displayName,
|
||||||
|
onUpdate,
|
||||||
}: {
|
}: {
|
||||||
subject: string
|
subject: string
|
||||||
displayName: string
|
displayName: string
|
||||||
|
onUpdate?: () => void
|
||||||
}) {
|
}) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
@@ -61,15 +63,17 @@ export function Component({
|
|||||||
const onPressSave = useCallback(async () => {
|
const onPressSave = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await memberships.updateTo(selected)
|
await memberships.updateTo(selected)
|
||||||
store.shell.closeModal()
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
store.log.error('Failed to update memberships', {err})
|
store.log.error('Failed to update memberships', {err})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
onUpdate?.()
|
||||||
|
store.shell.closeModal()
|
||||||
}, [store, selected])
|
}, [store, selected])
|
||||||
|
|
||||||
const onPressNewMuteList = useCallback(() => {
|
const onPressNewMuteList = useCallback(() => {
|
||||||
store.shell.openModal({
|
store.shell.openModal({
|
||||||
name: 'create-mute-list',
|
name: 'create-or-edit-mute-list',
|
||||||
onCreate: (uri: string) => {
|
onCreate: (uri: string) => {
|
||||||
listsList.refresh()
|
listsList.refresh()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import * as EditProfileModal from './EditProfile'
|
|||||||
import * as ServerInputModal from './ServerInput'
|
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 CreateOrEditMuteListModal from './CreateOrEditMuteList'
|
||||||
import * as ListAddRemoveUserModal from './ListAddUser'
|
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'
|
||||||
@@ -68,9 +68,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
|
|||||||
} else if (activeModal?.name === 'report-account') {
|
} else if (activeModal?.name === 'report-account') {
|
||||||
snapPoints = ReportAccountModal.snapPoints
|
snapPoints = ReportAccountModal.snapPoints
|
||||||
element = <ReportAccountModal.Component {...activeModal} />
|
element = <ReportAccountModal.Component {...activeModal} />
|
||||||
} else if (activeModal?.name === 'create-mute-list') {
|
} else if (activeModal?.name === 'create-or-edit-mute-list') {
|
||||||
snapPoints = CreateMuteListModal.snapPoints
|
snapPoints = CreateOrEditMuteListModal.snapPoints
|
||||||
element = <CreateMuteListModal.Component {...activeModal} />
|
element = <CreateOrEditMuteListModal.Component {...activeModal} />
|
||||||
} else if (activeModal?.name === 'list-add-user') {
|
} else if (activeModal?.name === 'list-add-user') {
|
||||||
snapPoints = ListAddRemoveUserModal.snapPoints
|
snapPoints = ListAddRemoveUserModal.snapPoints
|
||||||
element = <ListAddRemoveUserModal.Component {...activeModal} />
|
element = <ListAddRemoveUserModal.Component {...activeModal} />
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import * as EditProfileModal from './EditProfile'
|
|||||||
import * as ServerInputModal from './ServerInput'
|
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 CreateOrEditMuteListModal from './CreateOrEditMuteList'
|
||||||
import * as ListAddRemoveUserModal from './ListAddUser'
|
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'
|
||||||
@@ -70,8 +70,8 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||||||
element = <ReportPostModal.Component {...modal} />
|
element = <ReportPostModal.Component {...modal} />
|
||||||
} else if (modal.name === 'report-account') {
|
} else if (modal.name === 'report-account') {
|
||||||
element = <ReportAccountModal.Component {...modal} />
|
element = <ReportAccountModal.Component {...modal} />
|
||||||
} else if (modal.name === 'create-mute-list') {
|
} else if (modal.name === 'create-or-edit-mute-list') {
|
||||||
element = <CreateMuteListModal.Component {...modal} />
|
element = <CreateOrEditMuteListModal.Component {...modal} />
|
||||||
} else if (modal.name === 'list-add-user') {
|
} else if (modal.name === 'list-add-user') {
|
||||||
element = <ListAddRemoveUserModal.Component {...modal} />
|
element = <ListAddRemoveUserModal.Component {...modal} />
|
||||||
} else if (modal.name === 'crop-image') {
|
} else if (modal.name === 'crop-image') {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const ProfileCard = observer(
|
|||||||
noBorder?: boolean
|
noBorder?: boolean
|
||||||
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||||
overrideModeration?: boolean
|
overrideModeration?: boolean
|
||||||
renderButton?: () => JSX.Element
|
renderButton?: (profile: AppBskyActorDefs.ProfileViewBasic) => JSX.Element
|
||||||
}) => {
|
}) => {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
@@ -92,7 +92,7 @@ export const ProfileCard = observer(
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
{renderButton ? (
|
{renderButton ? (
|
||||||
<View style={styles.layoutButton}>{renderButton()}</View>
|
<View style={styles.layoutButton}>{renderButton(profile)}</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</View>
|
</View>
|
||||||
{profile.description ? (
|
{profile.description ? (
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const ViewHeader = observer(function ({
|
|||||||
|
|
||||||
if (isDesktopWeb) {
|
if (isDesktopWeb) {
|
||||||
if (showOnDesktop) {
|
if (showOnDesktop) {
|
||||||
return <DesktopWebHeader title={title} />
|
return <DesktopWebHeader title={title} renderButton={renderButton} />
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
} else {
|
} 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')
|
const pal = usePalette('default')
|
||||||
return (
|
return (
|
||||||
<CenteredView style={[styles.header, styles.desktopHeader, pal.border]}>
|
<CenteredView style={[styles.header, styles.desktopHeader, pal.border]}>
|
||||||
@@ -100,6 +106,7 @@ function DesktopWebHeader({title}: {title: string}) {
|
|||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
{renderButton?.()}
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,22 +28,22 @@ export const ModerationMuteListsScreen = withAuthRequired(({route}: Props) => {
|
|||||||
const store = useStores()
|
const store = useStores()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
|
||||||
const mutelists: ListsListModel = React.useMemo(() => {
|
const mutelists: ListsListModel = React.useMemo(
|
||||||
const list = new ListsListModel(store, 'my-modlists')
|
() => new ListsListModel(store, 'my-modlists'),
|
||||||
list.loadMore()
|
[store],
|
||||||
return list
|
)
|
||||||
}, [store])
|
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
store.shell.setMinimalShellMode(false)
|
store.shell.setMinimalShellMode(false)
|
||||||
|
mutelists.refresh()
|
||||||
}, [store]),
|
}, [store]),
|
||||||
)
|
)
|
||||||
|
|
||||||
const onPressNewMuteList = React.useCallback(() => {
|
const onPressNewMuteList = React.useCallback(() => {
|
||||||
store.shell.openModal({
|
store.shell.openModal({
|
||||||
name: 'create-mute-list',
|
name: 'create-or-edit-mute-list',
|
||||||
onCreate: (uri: string) => {
|
onSave: (uri: string) => {
|
||||||
try {
|
try {
|
||||||
const urip = new AtUri(uri)
|
const urip = new AtUri(uri)
|
||||||
navigation.navigate('ProfileList', {
|
navigation.navigate('ProfileList', {
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleSheet} from 'react-native'
|
import {StyleSheet, View} 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 {useNavigation} from '@react-navigation/native'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
||||||
import {ViewHeader} from 'view/com/util/ViewHeader'
|
import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||||
import {CenteredView} from 'view/com/util/Views'
|
import {CenteredView} from 'view/com/util/Views'
|
||||||
import {ListItems} from 'view/com/lists/ListItems'
|
import {ListItems} from 'view/com/lists/ListItems'
|
||||||
import {EmptyState} from 'view/com/util/EmptyState'
|
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 {ListModel} from 'state/models/content/list'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
import {isDesktopWeb} from 'platform/detection'
|
import {isDesktopWeb} from 'platform/detection'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileList'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileList'>
|
||||||
export const ProfileListScreen = withAuthRequired(
|
export const ProfileListScreen = withAuthRequired(
|
||||||
observer(({route}: Props) => {
|
observer(({route}: Props) => {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {name, rkey} = route.params
|
const {name, rkey} = route.params
|
||||||
|
|
||||||
@@ -35,11 +40,88 @@ export const ProfileListScreen = withAuthRequired(
|
|||||||
}, [store]),
|
}, [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(() => {
|
const renderEmptyState = React.useCallback(() => {
|
||||||
return <EmptyState icon="users-slash" message="This list is empty!" />
|
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 (
|
return (
|
||||||
<CenteredView
|
<CenteredView
|
||||||
style={[
|
style={[
|
||||||
@@ -49,17 +131,24 @@ export const ProfileListScreen = withAuthRequired(
|
|||||||
pal.border,
|
pal.border,
|
||||||
]}
|
]}
|
||||||
testID="moderationMutelistsScreen">
|
testID="moderationMutelistsScreen">
|
||||||
<ViewHeader
|
<ViewHeader title="" renderButton={renderHeaderBtn} />
|
||||||
title={`List by ${list.list?.creator.handle}`}
|
<ListItems
|
||||||
showOnDesktop
|
list={list}
|
||||||
|
renderEmptyState={renderEmptyState}
|
||||||
|
onToggleSubscribed={onToggleSubscribed}
|
||||||
|
onPressEditList={onPressEditList}
|
||||||
|
onPressDeleteList={onPressDeleteList}
|
||||||
/>
|
/>
|
||||||
<ListItems list={list} renderEmptyState={renderEmptyState} />
|
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
headerBtns: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingBottom: isDesktopWeb ? 0 : 100,
|
paddingBottom: isDesktopWeb ? 0 : 100,
|
||||||
|
|||||||
Reference in New Issue
Block a user