Lists updates: curate lists and blocklists (#1689)
* Add lists screen * Update Lists screen and List create/edit modal to support curate lists * Rework the ProfileList screen and add curatelist support * More ProfileList progress * Update list modals * Rename mutelists to modlists * Layout updates/fixes * More layout fixes * Modal fixes * List list screen updates * Update feed page to give more info * Layout fixes to ListAddUser modal * Layout fixes to FlatList and Feed on desktop * Layout fix to LoadLatestBtn on Web * Handle did resolution before showing the ProfileList screen * Rename the CustomFeed routes to ProfileFeed for consistency * Fix layout issues with the pager and feeds * Factor out some common code * Fix UIs for mobile * Fix user list rendering * Fix: dont bubble custom feed errors in the merge feed * Refactor feed models to reduce usage of the SavedFeeds model * Replace CustomFeedModel with FeedSourceModel which abstracts feed-generators and lists * Add the ability to pin lists * Add pinned lists to mobile * Remove dead code * Rework the ProfileScreenHeader to create more real-estate for action buttons * Improve layout behavior on web mobile breakpoints * Refactor feed & list pages to use new Tabs layout component * Refactor to ProfileSubpageHeader * Implement modlist block and mute * Switch to new api and just modify state on modlist actions * Fix some UI overflows * Fix: dont show edit buttons on lists you dont own * Fix alignment issue on long titles * Improve loading and error states for feeds & lists * Update list dropdown icons for ios * Fetch feed display names in the mergefeed * Improve rendering off offline feeds in the feed-listing page * Update Feeds listing UI to react to changes in saved/pinned state * Refresh list and feed on posts tab press * Fix pinned feed ordering UI * Fixes to list pinning * Remove view=simple qp * Add list to feed tuners * Render richtext * Add list href * Add 'view avatar' * Remove unused import * Fix missing import * Correctly reflect block by list state * Replace the <Tabs> component with the more effective <PagerWithHeader> component * Improve the responsiveness of the PagerWithHeader * Fix visual jank in the feed loading state * Improve performance of the PagerWithHeader * Fix a case that would cause the header to animate too aggressively * Add the ability to scroll to top by tapping the selected tab * Fix unit test runner * Update modlists test * Add curatelist tests * Fix: remove link behavior in ListAddUser modal * Fix some layout jank in the PagerWithHeader on iOS * Simplify ListItems header rendering * Wait for the appview to recognize the list before proceeding with list creation * Fix glitch in the onPageSelecting index of the Pager * Fix until() * Copy fix Co-authored-by: Eric Bailey <git@esb.lol> --------- Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
parent
f9944b55e2
commit
f57a8cf8ba
87 changed files with 4090 additions and 1988 deletions
|
@ -1,4 +1,4 @@
|
|||
import React, {useState, useCallback} from 'react'
|
||||
import React, {useState, useCallback, useMemo} from 'react'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
|
@ -31,9 +31,11 @@ const MAX_DESCRIPTION = 300 // todo
|
|||
export const snapPoints = ['fullscreen']
|
||||
|
||||
export function Component({
|
||||
purpose,
|
||||
onSave,
|
||||
list,
|
||||
}: {
|
||||
purpose?: string
|
||||
onSave?: (uri: string) => void
|
||||
list?: ListModel
|
||||
}) {
|
||||
|
@ -44,12 +46,24 @@ export function Component({
|
|||
const theme = useTheme()
|
||||
const {track} = useAnalytics()
|
||||
|
||||
const activePurpose = useMemo(() => {
|
||||
if (list?.data?.purpose) {
|
||||
return list.data.purpose
|
||||
}
|
||||
if (purpose) {
|
||||
return purpose
|
||||
}
|
||||
return 'app.bsky.graph.defs#curatelist'
|
||||
}, [list, purpose])
|
||||
const isCurateList = activePurpose === 'app.bsky.graph.defs#curatelist'
|
||||
const purposeLabel = isCurateList ? 'User' : 'Moderation'
|
||||
|
||||
const [isProcessing, setProcessing] = useState<boolean>(false)
|
||||
const [name, setName] = useState<string>(list?.list?.name || '')
|
||||
const [name, setName] = useState<string>(list?.data?.name || '')
|
||||
const [description, setDescription] = useState<string>(
|
||||
list?.list?.description || '',
|
||||
list?.data?.description || '',
|
||||
)
|
||||
const [avatar, setAvatar] = useState<string | undefined>(list?.list?.avatar)
|
||||
const [avatar, setAvatar] = useState<string | undefined>(list?.data?.avatar)
|
||||
const [newAvatar, setNewAvatar] = useState<RNImage | undefined | null>()
|
||||
|
||||
const onPressCancel = useCallback(() => {
|
||||
|
@ -63,7 +77,7 @@ export function Component({
|
|||
setAvatar(undefined)
|
||||
return
|
||||
}
|
||||
track('CreateMuteList:AvatarSelected')
|
||||
track('CreateList:AvatarSelected')
|
||||
try {
|
||||
const finalImg = await compressIfNeeded(img, 1000000)
|
||||
setNewAvatar(finalImg)
|
||||
|
@ -76,7 +90,11 @@ export function Component({
|
|||
)
|
||||
|
||||
const onPressSave = useCallback(async () => {
|
||||
track('CreateMuteList:Save')
|
||||
if (isCurateList) {
|
||||
track('CreateList:SaveCurateList')
|
||||
} else {
|
||||
track('CreateList:SaveModList')
|
||||
}
|
||||
const nameTrimmed = name.trim()
|
||||
if (!nameTrimmed) {
|
||||
setError('Name is required')
|
||||
|
@ -93,22 +111,23 @@ export function Component({
|
|||
description: description.trim(),
|
||||
avatar: newAvatar,
|
||||
})
|
||||
Toast.show('Mute list updated')
|
||||
Toast.show(`${purposeLabel} list updated`)
|
||||
onSave?.(list.uri)
|
||||
} else {
|
||||
const res = await ListModel.createModList(store, {
|
||||
const res = await ListModel.createList(store, {
|
||||
purpose: activePurpose,
|
||||
name,
|
||||
description,
|
||||
avatar: newAvatar,
|
||||
})
|
||||
Toast.show('Mute list created')
|
||||
Toast.show(`${purposeLabel} list created`)
|
||||
onSave?.(res.uri)
|
||||
}
|
||||
store.shell.closeModal()
|
||||
} catch (e: any) {
|
||||
if (isNetworkError(e)) {
|
||||
setError(
|
||||
'Failed to create the mute list. Check your internet connection and try again.',
|
||||
'Failed to create the list. Check your internet connection and try again.',
|
||||
)
|
||||
} else {
|
||||
setError(cleanError(e))
|
||||
|
@ -122,6 +141,9 @@ export function Component({
|
|||
error,
|
||||
onSave,
|
||||
store,
|
||||
activePurpose,
|
||||
isCurateList,
|
||||
purposeLabel,
|
||||
name,
|
||||
description,
|
||||
newAvatar,
|
||||
|
@ -137,9 +159,9 @@ export function Component({
|
|||
paddingHorizontal: isMobile ? 16 : 0,
|
||||
},
|
||||
]}
|
||||
testID="createOrEditMuteListModal">
|
||||
testID="createOrEditListModal">
|
||||
<Text style={[styles.title, pal.text]}>
|
||||
{list ? 'Edit Mute List' : 'New Mute List'}
|
||||
{list ? 'Edit' : 'New'} {purposeLabel} List
|
||||
</Text>
|
||||
{error !== '' && (
|
||||
<View style={styles.errorContainer}>
|
||||
|
@ -163,7 +185,9 @@ export function Component({
|
|||
<TextInput
|
||||
testID="editNameInput"
|
||||
style={[styles.textInput, pal.border, pal.text]}
|
||||
placeholder="e.g. spammers"
|
||||
placeholder={
|
||||
isCurateList ? 'e.g. Great Posters' : 'e.g. Spammers'
|
||||
}
|
||||
placeholderTextColor={colors.gray4}
|
||||
value={name}
|
||||
onChangeText={v => setName(enforceLen(v, MAX_NAME))}
|
||||
|
@ -180,7 +204,11 @@ export function Component({
|
|||
<TextInput
|
||||
testID="editDescriptionInput"
|
||||
style={[styles.textArea, pal.border, pal.text]}
|
||||
placeholder="e.g. users that repeatedly reply with ads."
|
||||
placeholder={
|
||||
isCurateList
|
||||
? 'e.g. The posters who never miss.'
|
||||
: 'e.g. Users that repeatedly reply with ads.'
|
||||
}
|
||||
placeholderTextColor={colors.gray4}
|
||||
keyboardAppearance={theme.colorScheme}
|
||||
multiline
|
||||
|
@ -203,7 +231,7 @@ export function Component({
|
|||
onPress={onPressSave}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Save"
|
||||
accessibilityHint="Creates the mute list">
|
||||
accessibilityHint="">
|
||||
<LinearGradient
|
||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||
start={{x: 0, y: 0}}
|
281
src/view/com/modals/ListAddUser.tsx
Normal file
281
src/view/com/modals/ListAddUser.tsx
Normal file
|
@ -0,0 +1,281 @@
|
|||
import React, {useEffect, useCallback, useState, useMemo} from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Pressable,
|
||||
SafeAreaView,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {ScrollView, TextInput} from './util'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {Button} from '../util/forms/Button'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {useStores} from 'state/index'
|
||||
import {ListModel} from 'state/models/content/list'
|
||||
import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
|
||||
export const snapPoints = ['90%']
|
||||
|
||||
export const Component = observer(function Component({
|
||||
list,
|
||||
onAdd,
|
||||
}: {
|
||||
list: ListModel
|
||||
onAdd?: (profile: AppBskyActorDefs.ProfileViewBasic) => void
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const [query, setQuery] = useState('')
|
||||
const autocompleteView = useMemo<UserAutocompleteModel>(
|
||||
() => new UserAutocompleteModel(store),
|
||||
[store],
|
||||
)
|
||||
|
||||
// initial setup
|
||||
useEffect(() => {
|
||||
autocompleteView.setup().then(() => {
|
||||
autocompleteView.setPrefix('')
|
||||
})
|
||||
autocompleteView.setActive(true)
|
||||
list.loadAll()
|
||||
}, [autocompleteView, list])
|
||||
|
||||
const onChangeQuery = useCallback(
|
||||
(text: string) => {
|
||||
setQuery(text)
|
||||
autocompleteView.setPrefix(text)
|
||||
},
|
||||
[setQuery, autocompleteView],
|
||||
)
|
||||
|
||||
const onPressCancelSearch = useCallback(
|
||||
() => onChangeQuery(''),
|
||||
[onChangeQuery],
|
||||
)
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
testID="listAddUserModal"
|
||||
style={[pal.view, isWeb ? styles.fixedHeight : s.flex1]}>
|
||||
<View
|
||||
style={[
|
||||
s.flex1,
|
||||
isMobile && {paddingHorizontal: 18, paddingBottom: 40},
|
||||
]}>
|
||||
<View style={styles.titleSection}>
|
||||
<Text type="title-lg" style={[pal.text, styles.title]}>
|
||||
Add User to List
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[styles.searchContainer, pal.border]}>
|
||||
<FontAwesomeIcon icon="search" size={16} />
|
||||
<TextInput
|
||||
testID="searchInput"
|
||||
style={[styles.searchInput, pal.border, pal.text]}
|
||||
placeholder="Search for users"
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
value={query}
|
||||
onChangeText={onChangeQuery}
|
||||
accessible={true}
|
||||
accessibilityLabel="Search"
|
||||
accessibilityHint=""
|
||||
autoCapitalize="none"
|
||||
autoComplete="off"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
{query ? (
|
||||
<Pressable
|
||||
onPress={onPressCancelSearch}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Cancel search"
|
||||
accessibilityHint="Exits inputting search query"
|
||||
onAccessibilityEscape={onPressCancelSearch}>
|
||||
<FontAwesomeIcon
|
||||
icon="xmark"
|
||||
size={16}
|
||||
color={pal.colors.textLight}
|
||||
/>
|
||||
</Pressable>
|
||||
) : undefined}
|
||||
</View>
|
||||
<ScrollView style={[s.flex1]}>
|
||||
{autocompleteView.suggestions.length ? (
|
||||
<>
|
||||
{autocompleteView.suggestions.slice(0, 40).map((item, i) => (
|
||||
<UserResult
|
||||
key={item.did}
|
||||
list={list}
|
||||
profile={item}
|
||||
noBorder={i === 0}
|
||||
onAdd={onAdd}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<Text
|
||||
type="xl"
|
||||
style={[
|
||||
pal.textLight,
|
||||
{paddingHorizontal: 12, paddingVertical: 16},
|
||||
]}>
|
||||
No results found for {autocompleteView.prefix}
|
||||
</Text>
|
||||
)}
|
||||
</ScrollView>
|
||||
<View style={[styles.btnContainer]}>
|
||||
<Button
|
||||
testID="doneBtn"
|
||||
type="primary"
|
||||
onPress={() => store.shell.closeModal()}
|
||||
accessibilityLabel="Done"
|
||||
accessibilityHint=""
|
||||
label="Done"
|
||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||
labelStyle={[s.f18]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
)
|
||||
})
|
||||
|
||||
function UserResult({
|
||||
profile,
|
||||
list,
|
||||
noBorder,
|
||||
onAdd,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
list: ListModel
|
||||
noBorder: boolean
|
||||
onAdd?: (profile: AppBskyActorDefs.ProfileViewBasic) => void | undefined
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
const [isAdded, setIsAdded] = useState(list.isMember(profile.did))
|
||||
|
||||
const onPressAdd = useCallback(async () => {
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
await list.addMember(profile)
|
||||
Toast.show('Added to list')
|
||||
setIsAdded(true)
|
||||
onAdd?.(profile)
|
||||
} catch (e) {
|
||||
Toast.show(cleanError(e))
|
||||
} finally {
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}, [list, profile, setIsProcessing, setIsAdded, onAdd])
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
pal.border,
|
||||
{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderTopWidth: noBorder ? 0 : 1,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
]}>
|
||||
<View
|
||||
style={{
|
||||
alignSelf: 'baseline',
|
||||
width: 54,
|
||||
paddingLeft: 4,
|
||||
paddingTop: 10,
|
||||
}}>
|
||||
<UserAvatar size={40} avatar={profile.avatar} />
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingRight: 10,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
}}>
|
||||
<Text
|
||||
type="lg"
|
||||
style={[s.bold, pal.text]}
|
||||
numberOfLines={1}
|
||||
lineHeight={1.2}>
|
||||
{sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
)}
|
||||
</Text>
|
||||
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
|
||||
{sanitizeHandle(profile.handle, '@')}
|
||||
</Text>
|
||||
{!!profile.viewer?.followedBy && <View style={s.flexRow} />}
|
||||
</View>
|
||||
<View>
|
||||
{isAdded ? (
|
||||
<FontAwesomeIcon icon="check" />
|
||||
) : isProcessing ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<Button
|
||||
testID={`user-${profile.handle}-addBtn`}
|
||||
type="default"
|
||||
label="Add"
|
||||
onPress={onPressAdd}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
fixedHeight: {
|
||||
// @ts-ignore web only -prf
|
||||
height: '80vh',
|
||||
},
|
||||
titleSection: {
|
||||
paddingTop: isWeb ? 0 : 4,
|
||||
paddingBottom: isWeb ? 14 : 10,
|
||||
},
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
fontWeight: '600',
|
||||
marginBottom: 5,
|
||||
},
|
||||
searchContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
borderWidth: 1,
|
||||
borderRadius: 24,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
searchInput: {
|
||||
fontSize: 16,
|
||||
flex: 1,
|
||||
},
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 32,
|
||||
padding: 14,
|
||||
backgroundColor: colors.blue3,
|
||||
},
|
||||
btnContainer: {
|
||||
paddingTop: 20,
|
||||
},
|
||||
})
|
|
@ -16,8 +16,9 @@ import * as ProfilePreviewModal from './ProfilePreview'
|
|||
import * as ServerInputModal from './ServerInput'
|
||||
import * as RepostModal from './Repost'
|
||||
import * as SelfLabelModal from './SelfLabel'
|
||||
import * as CreateOrEditMuteListModal from './CreateOrEditMuteList'
|
||||
import * as ListAddRemoveUserModal from './ListAddRemoveUser'
|
||||
import * as CreateOrEditListModal from './CreateOrEditList'
|
||||
import * as UserAddRemoveListsModal from './UserAddRemoveLists'
|
||||
import * as ListAddUserModal from './ListAddUser'
|
||||
import * as AltImageModal from './AltImage'
|
||||
import * as EditImageModal from './AltImage'
|
||||
import * as ReportModal from './report/Modal'
|
||||
|
@ -101,12 +102,15 @@ export const ModalsContainer = observer(function ModalsContainer() {
|
|||
} else if (activeModal?.name === 'report') {
|
||||
snapPoints = ReportModal.snapPoints
|
||||
element = <ReportModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'create-or-edit-mute-list') {
|
||||
snapPoints = CreateOrEditMuteListModal.snapPoints
|
||||
element = <CreateOrEditMuteListModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'list-add-remove-user') {
|
||||
snapPoints = ListAddRemoveUserModal.snapPoints
|
||||
element = <ListAddRemoveUserModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'create-or-edit-list') {
|
||||
snapPoints = CreateOrEditListModal.snapPoints
|
||||
element = <CreateOrEditListModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'user-add-remove-lists') {
|
||||
snapPoints = UserAddRemoveListsModal.snapPoints
|
||||
element = <UserAddRemoveListsModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'list-add-user') {
|
||||
snapPoints = ListAddUserModal.snapPoints
|
||||
element = <ListAddUserModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'delete-account') {
|
||||
snapPoints = DeleteAccountModal.snapPoints
|
||||
element = <DeleteAccountModal.Component />
|
||||
|
|
|
@ -11,8 +11,9 @@ import * as EditProfileModal from './EditProfile'
|
|||
import * as ProfilePreviewModal from './ProfilePreview'
|
||||
import * as ServerInputModal from './ServerInput'
|
||||
import * as ReportModal from './report/Modal'
|
||||
import * as CreateOrEditMuteListModal from './CreateOrEditMuteList'
|
||||
import * as ListAddRemoveUserModal from './ListAddRemoveUser'
|
||||
import * as CreateOrEditListModal from './CreateOrEditList'
|
||||
import * as UserAddRemoveLists from './UserAddRemoveLists'
|
||||
import * as ListAddUserModal from './ListAddUser'
|
||||
import * as DeleteAccountModal from './DeleteAccount'
|
||||
import * as RepostModal from './Repost'
|
||||
import * as SelfLabelModal from './SelfLabel'
|
||||
|
@ -79,10 +80,12 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||
element = <ServerInputModal.Component {...modal} />
|
||||
} else if (modal.name === 'report') {
|
||||
element = <ReportModal.Component {...modal} />
|
||||
} else if (modal.name === 'create-or-edit-mute-list') {
|
||||
element = <CreateOrEditMuteListModal.Component {...modal} />
|
||||
} else if (modal.name === 'list-add-remove-user') {
|
||||
element = <ListAddRemoveUserModal.Component {...modal} />
|
||||
} else if (modal.name === 'create-or-edit-list') {
|
||||
element = <CreateOrEditListModal.Component {...modal} />
|
||||
} else if (modal.name === 'user-add-remove-lists') {
|
||||
element = <UserAddRemoveLists.Component {...modal} />
|
||||
} else if (modal.name === 'list-add-user') {
|
||||
element = <ListAddUserModal.Component {...modal} />
|
||||
} else if (modal.name === 'crop-image') {
|
||||
element = <CropImageModal.Component {...modal} />
|
||||
} else if (modal.name === 'delete-account') {
|
||||
|
|
|
@ -31,8 +31,25 @@ export function Component({
|
|||
description =
|
||||
'Moderator has chosen to set a general warning on the content.'
|
||||
} else if (moderation.cause.type === 'blocking') {
|
||||
name = 'User Blocked'
|
||||
description = 'You have blocked this user. You cannot view their content.'
|
||||
if (moderation.cause.source.type === 'list') {
|
||||
const list = moderation.cause.source.list
|
||||
name = 'User Blocked by List'
|
||||
description = (
|
||||
<>
|
||||
This user is included in the{' '}
|
||||
<TextLink
|
||||
type="2xl"
|
||||
href={listUriToHref(list.uri)}
|
||||
text={list.name}
|
||||
style={pal.link}
|
||||
/>{' '}
|
||||
list which you have blocked.
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
name = 'User Blocked'
|
||||
description = 'You have blocked this user. You cannot view their content.'
|
||||
}
|
||||
} else if (moderation.cause.type === 'blocked-by') {
|
||||
name = 'User Blocks You'
|
||||
description = 'This user has blocked you. You cannot view their content.'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, {useCallback} from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {Pressable, StyleSheet, View, ActivityIndicator} from 'react-native'
|
||||
import {ActivityIndicator, Pressable, StyleSheet, View} from 'react-native'
|
||||
import {AppBskyGraphDefs as GraphDefs} from '@atproto/api'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
|
@ -11,7 +11,6 @@ import {UserAvatar} from '../util/UserAvatar'
|
|||
import {ListsList} from '../lists/ListsList'
|
||||
import {ListsListModel} from 'state/models/lists/lists-list'
|
||||
import {ListMembershipModel} from 'state/models/content/list-membership'
|
||||
import {EmptyStateWithButton} from '../util/EmptyStateWithButton'
|
||||
import {Button} from '../util/forms/Button'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {useStores} from 'state/index'
|
||||
|
@ -24,14 +23,16 @@ import isEqual from 'lodash.isequal'
|
|||
|
||||
export const snapPoints = ['fullscreen']
|
||||
|
||||
export const Component = observer(function ListAddRemoveUserImpl({
|
||||
export const Component = observer(function UserAddRemoveListsImpl({
|
||||
subject,
|
||||
displayName,
|
||||
onUpdate,
|
||||
onAdd,
|
||||
onRemove,
|
||||
}: {
|
||||
subject: string
|
||||
displayName: string
|
||||
onUpdate?: () => void
|
||||
onAdd?: (listUri: string) => void
|
||||
onRemove?: (listUri: string) => void
|
||||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
|
@ -71,25 +72,22 @@ export const Component = observer(function ListAddRemoveUserImpl({
|
|||
}, [store])
|
||||
|
||||
const onPressSave = useCallback(async () => {
|
||||
let changes
|
||||
try {
|
||||
await memberships.updateTo(selected)
|
||||
changes = await memberships.updateTo(selected)
|
||||
} catch (err) {
|
||||
store.log.error('Failed to update memberships', {err})
|
||||
return
|
||||
}
|
||||
Toast.show('Lists updated')
|
||||
onUpdate?.()
|
||||
for (const uri of changes.added) {
|
||||
onAdd?.(uri)
|
||||
}
|
||||
for (const uri of changes.removed) {
|
||||
onRemove?.(uri)
|
||||
}
|
||||
store.shell.closeModal()
|
||||
}, [store, selected, memberships, onUpdate])
|
||||
|
||||
const onPressNewMuteList = useCallback(() => {
|
||||
store.shell.openModal({
|
||||
name: 'create-or-edit-mute-list',
|
||||
onSave: (_uri: string) => {
|
||||
listsList.refresh()
|
||||
},
|
||||
})
|
||||
}, [store, listsList])
|
||||
}, [store, selected, memberships, onAdd, onRemove])
|
||||
|
||||
const onToggleSelected = useCallback(
|
||||
(uri: string) => {
|
||||
|
@ -103,7 +101,7 @@ export const Component = observer(function ListAddRemoveUserImpl({
|
|||
)
|
||||
|
||||
const renderItem = useCallback(
|
||||
(list: GraphDefs.ListView) => {
|
||||
(list: GraphDefs.ListView, index: number) => {
|
||||
const isSelected = selected.includes(list.uri)
|
||||
return (
|
||||
<Pressable
|
||||
|
@ -111,7 +109,10 @@ export const Component = observer(function ListAddRemoveUserImpl({
|
|||
style={[
|
||||
styles.listItem,
|
||||
pal.border,
|
||||
{opacity: membershipsLoaded ? 1 : 0.5},
|
||||
{
|
||||
opacity: membershipsLoaded ? 1 : 0.5,
|
||||
borderTopWidth: index === 0 ? 0 : 1,
|
||||
},
|
||||
]}
|
||||
accessibilityLabel={`${isSelected ? 'Remove from' : 'Add to'} ${
|
||||
list.name
|
||||
|
@ -131,7 +132,11 @@ export const Component = observer(function ListAddRemoveUserImpl({
|
|||
{sanitizeDisplayName(list.name)}
|
||||
</Text>
|
||||
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
|
||||
{list.purpose === 'app.bsky.graph.defs#modlist' && 'Mute list'} by{' '}
|
||||
{list.purpose === 'app.bsky.graph.defs#curatelist' &&
|
||||
'User list '}
|
||||
{list.purpose === 'app.bsky.graph.defs#modlist' &&
|
||||
'Moderation list '}
|
||||
by{' '}
|
||||
{list.creator.did === store.me.did
|
||||
? 'you'
|
||||
: sanitizeHandle(list.creator.handle, '@')}
|
||||
|
@ -166,30 +171,19 @@ export const Component = observer(function ListAddRemoveUserImpl({
|
|||
],
|
||||
)
|
||||
|
||||
const renderEmptyState = React.useCallback(() => {
|
||||
return (
|
||||
<EmptyStateWithButton
|
||||
icon="users-slash"
|
||||
message="You can subscribe to mute lists to automatically mute all of the users they include. Mute lists are public but your subscription to a mute list is private."
|
||||
buttonLabel="New Mute List"
|
||||
onPress={onPressNewMuteList}
|
||||
/>
|
||||
)
|
||||
}, [onPressNewMuteList])
|
||||
|
||||
// Only show changes button if there are some items on the list to choose from AND user has made changes in selection
|
||||
const canSaveChanges =
|
||||
!listsList.isEmpty && !isEqual(selected, originalSelections)
|
||||
|
||||
return (
|
||||
<View testID="listAddRemoveUserModal" style={s.hContentRegion}>
|
||||
<Text style={[styles.title, pal.text]}>Add {displayName} to Lists</Text>
|
||||
<View testID="userAddRemoveListsModal" style={s.hContentRegion}>
|
||||
<Text style={[styles.title, pal.text]}>
|
||||
Update {displayName} in Lists
|
||||
</Text>
|
||||
<ListsList
|
||||
listsList={listsList}
|
||||
showAddBtns
|
||||
onPressCreateNew={onPressNewMuteList}
|
||||
inline
|
||||
renderItem={renderItem}
|
||||
renderEmptyState={renderEmptyState}
|
||||
style={[styles.list, pal.border]}
|
||||
/>
|
||||
<View style={[styles.btns, pal.border]}>
|
||||
|
@ -258,7 +252,6 @@ const styles = StyleSheet.create({
|
|||
listItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderTopWidth: 1,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
},
|
Loading…
Add table
Add a link
Reference in a new issue