From f57a8cf8ba0cd10a54abf35d960d8fb90266fa6b Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 1 Nov 2023 16:15:40 -0700 Subject: 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 component with the more effective 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 --------- Co-authored-by: Eric Bailey --- src/view/com/modals/UserAddRemoveLists.tsx | 287 +++++++++++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 src/view/com/modals/UserAddRemoveLists.tsx (limited to 'src/view/com/modals/UserAddRemoveLists.tsx') diff --git a/src/view/com/modals/UserAddRemoveLists.tsx b/src/view/com/modals/UserAddRemoveLists.tsx new file mode 100644 index 000000000..ff048ca29 --- /dev/null +++ b/src/view/com/modals/UserAddRemoveLists.tsx @@ -0,0 +1,287 @@ +import React, {useCallback} from 'react' +import {observer} from 'mobx-react-lite' +import {ActivityIndicator, Pressable, StyleSheet, View} from 'react-native' +import {AppBskyGraphDefs as GraphDefs} from '@atproto/api' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {Text} from '../util/text/Text' +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 {Button} from '../util/forms/Button' +import * as Toast from '../util/Toast' +import {useStores} from 'state/index' +import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeHandle} from 'lib/strings/handles' +import {s} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {isWeb, isAndroid} from 'platform/detection' +import isEqual from 'lodash.isequal' + +export const snapPoints = ['fullscreen'] + +export const Component = observer(function UserAddRemoveListsImpl({ + subject, + displayName, + onAdd, + onRemove, +}: { + subject: string + displayName: string + onAdd?: (listUri: string) => void + onRemove?: (listUri: string) => void +}) { + const store = useStores() + const pal = usePalette('default') + const palPrimary = usePalette('primary') + const palInverted = usePalette('inverted') + const [originalSelections, setOriginalSelections] = React.useState( + [], + ) + const [selected, setSelected] = React.useState([]) + const [membershipsLoaded, setMembershipsLoaded] = React.useState(false) + + const listsList: ListsListModel = React.useMemo( + () => new ListsListModel(store, store.me.did), + [store], + ) + const memberships: ListMembershipModel = React.useMemo( + () => new ListMembershipModel(store, subject), + [store, subject], + ) + React.useEffect(() => { + listsList.refresh() + memberships.fetch().then( + () => { + const ids = memberships.memberships.map(m => m.value.list) + setOriginalSelections(ids) + setSelected(ids) + setMembershipsLoaded(true) + }, + err => { + store.log.error('Failed to fetch memberships', {err}) + }, + ) + }, [memberships, listsList, store, setSelected, setMembershipsLoaded]) + + const onPressCancel = useCallback(() => { + store.shell.closeModal() + }, [store]) + + const onPressSave = useCallback(async () => { + let changes + try { + changes = await memberships.updateTo(selected) + } catch (err) { + store.log.error('Failed to update memberships', {err}) + return + } + Toast.show('Lists updated') + for (const uri of changes.added) { + onAdd?.(uri) + } + for (const uri of changes.removed) { + onRemove?.(uri) + } + store.shell.closeModal() + }, [store, selected, memberships, onAdd, onRemove]) + + const onToggleSelected = useCallback( + (uri: string) => { + if (selected.includes(uri)) { + setSelected(selected.filter(uri2 => uri2 !== uri)) + } else { + setSelected([...selected, uri]) + } + }, + [selected, setSelected], + ) + + const renderItem = useCallback( + (list: GraphDefs.ListView, index: number) => { + const isSelected = selected.includes(list.uri) + return ( + onToggleSelected(list.uri)}> + + + + + + {sanitizeDisplayName(list.name)} + + + {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, '@')} + + + {membershipsLoaded && ( + + {isSelected && ( + + )} + + )} + + ) + }, + [ + pal, + palPrimary, + palInverted, + onToggleSelected, + selected, + store.me.did, + membershipsLoaded, + ], + ) + + // 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 ( + + + Update {displayName} in Lists + + + +