diff options
Diffstat (limited to 'src/view/com/lists')
-rw-r--r-- | src/view/com/lists/ListActions.tsx | 98 | ||||
-rw-r--r-- | src/view/com/lists/ListCard.tsx | 5 | ||||
-rw-r--r-- | src/view/com/lists/ListItems.tsx | 261 | ||||
-rw-r--r-- | src/view/com/lists/ListsList.tsx | 137 |
4 files changed, 110 insertions, 391 deletions
diff --git a/src/view/com/lists/ListActions.tsx b/src/view/com/lists/ListActions.tsx deleted file mode 100644 index 353338198..000000000 --- a/src/view/com/lists/ListActions.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import {Button} from '../util/forms/Button' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {usePalette} from 'lib/hooks/usePalette' - -export const ListActions = ({ - muted, - onToggleSubscribed, - onPressEditList, - isOwner, - onPressDeleteList, - onPressShareList, - onPressReportList, - reversed = false, // Default value of reversed is false -}: { - isOwner: boolean - muted?: boolean - onToggleSubscribed?: () => void - onPressEditList?: () => void - onPressDeleteList?: () => void - onPressShareList?: () => void - onPressReportList?: () => void - reversed?: boolean // New optional prop -}) => { - const pal = usePalette('default') - - let buttons = [ - <Button - key="subscribeListBtn" - testID={muted ? 'unsubscribeListBtn' : 'subscribeListBtn'} - type={muted ? 'inverted' : 'primary'} - label={muted ? 'Unsubscribe' : 'Subscribe & Mute'} - accessibilityLabel={muted ? 'Unsubscribe' : 'Subscribe and mute'} - accessibilityHint="" - onPress={onToggleSubscribed} - />, - isOwner && ( - <Button - key="editListBtn" - testID="editListBtn" - type="default" - label="Edit List" - accessibilityLabel="Edit list" - accessibilityHint="" - onPress={onPressEditList} - /> - ), - isOwner && ( - <Button - key="deleteListBtn" - testID="deleteListBtn" - type="default" - accessibilityLabel="Delete list" - accessibilityHint="" - onPress={onPressDeleteList}> - <FontAwesomeIcon icon={['far', 'trash-can']} style={[pal.text]} /> - </Button> - ), - <Button - key="shareListBtn" - testID="shareListBtn" - type="default" - accessibilityLabel="Share list" - accessibilityHint="" - onPress={onPressShareList}> - <FontAwesomeIcon icon={'share'} style={[pal.text]} /> - </Button>, - !isOwner && ( - <Button - key="reportListBtn" - testID="reportListBtn" - type="default" - accessibilityLabel="Report list" - accessibilityHint="" - onPress={onPressReportList}> - <FontAwesomeIcon icon={'circle-exclamation'} style={[pal.text]} /> - </Button> - ), - ] - - // If reversed is true, reverse the array to reverse the order of the buttons - if (reversed) { - buttons = buttons.filter(Boolean).reverse() // filterting out any falsey values and reversing the array - } else { - buttons = buttons.filter(Boolean) // filterting out any falsey values - } - - return <View style={styles.headerBtns}>{buttons}</View> -} - -const styles = StyleSheet.create({ - headerBtns: { - flexDirection: 'row', - gap: 8, - marginTop: 12, - }, -}) diff --git a/src/view/com/lists/ListCard.tsx b/src/view/com/lists/ListCard.tsx index 159d966eb..a481902d8 100644 --- a/src/view/com/lists/ListCard.tsx +++ b/src/view/com/lists/ListCard.tsx @@ -76,7 +76,10 @@ export const ListCard = ({ {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, '@')} diff --git a/src/view/com/lists/ListItems.tsx b/src/view/com/lists/ListItems.tsx index c5ea13169..855c07d14 100644 --- a/src/view/com/lists/ListItems.tsx +++ b/src/view/com/lists/ListItems.tsx @@ -3,34 +3,26 @@ import { ActivityIndicator, RefreshControl, StyleProp, - StyleSheet, View, ViewStyle, - FlatList, } from 'react-native' -import {AppBskyActorDefs, AppBskyGraphDefs, RichText} from '@atproto/api' +import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api' +import {FlatList} from '../util/Views' import {observer} from 'mobx-react-lite' 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/analytics' import {usePalette} from 'lib/hooks/usePalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useStores} from 'state/index' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {s} from 'lib/styles' -import {ListActions} from './ListActions' -import {makeProfileLink} from 'lib/routes/links' -import {sanitizeHandle} from 'lib/strings/handles' +import {OnScrollCb} from 'lib/hooks/useOnMainScroll' 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__'} @@ -39,36 +31,35 @@ export const ListItems = observer(function ListItemsImpl({ list, style, scrollElRef, + onScroll, onPressTryAgain, - onToggleSubscribed, - onPressEditList, - onPressDeleteList, - onPressShareList, - onPressReportList, + renderHeader, renderEmptyState, testID, + scrollEventThrottle, headerOffset = 0, + desktopFixedHeightOffset, }: { list: ListModel style?: StyleProp<ViewStyle> scrollElRef?: MutableRefObject<FlatList<any> | null> + onScroll?: OnScrollCb onPressTryAgain?: () => void - onToggleSubscribed: () => void - onPressEditList: () => void - onPressDeleteList: () => void - onPressShareList: () => void - onPressReportList: () => void - renderEmptyState?: () => JSX.Element + renderHeader: () => JSX.Element + renderEmptyState: () => JSX.Element testID?: string + scrollEventThrottle?: number headerOffset?: number + desktopFixedHeightOffset?: number }) { const pal = usePalette('default') const store = useStores() const {track} = useAnalytics() const [isRefreshing, setIsRefreshing] = React.useState(false) + const {isMobile} = useWebMediaQueries() const data = React.useMemo(() => { - let items: any[] = [HEADER_ITEM] + let items: any[] = [] if (list.hasLoaded) { if (list.hasError) { items = items.concat([ERROR_ITEM]) @@ -124,11 +115,18 @@ export const ListItems = observer(function ListItemsImpl({ const onPressEditMembership = React.useCallback( (profile: AppBskyActorDefs.ProfileViewBasic) => { store.shell.openModal({ - name: 'list-add-remove-user', + name: 'user-add-remove-lists', subject: profile.did, displayName: profile.displayName || profile.handle, - onUpdate() { - list.refresh() + onAdd(listUri: string) { + if (listUri === list.uri) { + list.cacheAddMember(profile) + } + }, + onRemove(listUri: string) { + if (listUri === list.uri) { + list.cacheRemoveMember(profile) + } }, }) }, @@ -145,6 +143,7 @@ export const ListItems = observer(function ListItemsImpl({ } return ( <Button + testID={`user-${profile.handle}-editBtn`} type="default" label="Edit" onPress={() => onPressEditMembership(profile)} @@ -157,22 +156,7 @@ export const ListItems = observer(function ListItemsImpl({ const renderItem = React.useCallback( ({item}: {item: any}) => { if (item === EMPTY_ITEM) { - if (renderEmptyState) { - return renderEmptyState() - } - return <View /> - } else if (item === HEADER_ITEM) { - return list.list ? ( - <ListHeader - list={list.list} - isOwner={list.isOwner} - onToggleSubscribed={onToggleSubscribed} - onPressEditList={onPressEditList} - onPressDeleteList={onPressDeleteList} - onPressShareList={onPressShareList} - onPressReportList={onPressReportList} - /> - ) : null + return renderEmptyState() } else if (item === ERROR_ITEM) { return ( <ErrorMessage @@ -197,178 +181,59 @@ export const ListItems = observer(function ListItemsImpl({ }`} profile={(item as AppBskyGraphDefs.ListItemView).subject} renderButton={renderMemberButton} + style={{paddingHorizontal: isMobile ? 8 : 14, paddingVertical: 4}} /> ) }, [ renderMemberButton, renderEmptyState, - list.list, - list.isOwner, list.error, - onToggleSubscribed, - onPressEditList, - onPressDeleteList, - onPressShareList, - onPressReportList, onPressTryAgain, onPressRetryLoadMore, + isMobile, ], ) const Footer = React.useCallback( - () => - list.isLoading ? ( - <View style={styles.feedFooter}> - <ActivityIndicator /> - </View> - ) : ( - <View /> - ), - [list], + () => ( + <View style={{paddingTop: 20, paddingBottom: 200}}> + {list.isLoading && <ActivityIndicator />} + </View> + ), + [list.isLoading], ) return ( <View testID={testID} style={style}> - {data.length > 0 && ( - <FlatList - testID={testID ? `${testID}-flatlist` : undefined} - ref={scrollElRef} - data={data} - keyExtractor={item => item._reactKey} - renderItem={renderItem} - ListFooterComponent={Footer} - refreshControl={ - <RefreshControl - refreshing={isRefreshing} - onRefresh={onRefresh} - tintColor={pal.colors.text} - titleColor={pal.colors.text} - progressViewOffset={headerOffset} - /> - } - contentContainerStyle={s.contentContainer} - style={{paddingTop: headerOffset}} - onEndReached={onEndReached} - onEndReachedThreshold={0.6} - removeClippedSubviews={true} - contentOffset={{x: 0, y: headerOffset * -1}} - // @ts-ignore our .web version only -prf - desktopFixedHeight - /> - )} + <FlatList + testID={testID ? `${testID}-flatlist` : undefined} + ref={scrollElRef} + data={data} + keyExtractor={(item: any) => item._reactKey} + renderItem={renderItem} + ListHeaderComponent={renderHeader} + ListFooterComponent={Footer} + refreshControl={ + <RefreshControl + refreshing={isRefreshing} + onRefresh={onRefresh} + tintColor={pal.colors.text} + titleColor={pal.colors.text} + progressViewOffset={headerOffset} + /> + } + contentContainerStyle={s.contentContainer} + style={{paddingTop: headerOffset}} + onScroll={onScroll} + onEndReached={onEndReached} + onEndReachedThreshold={0.6} + scrollEventThrottle={scrollEventThrottle} + removeClippedSubviews={true} + contentOffset={{x: 0, y: headerOffset * -1}} + // @ts-ignore our .web version only -prf + desktopFixedHeight={desktopFixedHeightOffset || true} + /> </View> ) }) - -const ListHeader = observer(function ListHeaderImpl({ - list, - isOwner, - onToggleSubscribed, - onPressEditList, - onPressDeleteList, - onPressShareList, - onPressReportList, -}: { - list: AppBskyGraphDefs.ListView - isOwner: boolean - onToggleSubscribed: () => void - onPressEditList: () => void - onPressDeleteList: () => void - onPressShareList: () => void - onPressReportList: () => void -}) { - const pal = usePalette('default') - const store = useStores() - const {isDesktop} = useWebMediaQueries() - const descriptionRT = React.useMemo( - () => - list?.description && - new RichText({ - text: list.description, - facets: (list.descriptionFacets || [])?.slice(), - }), - [list], - ) - return ( - <> - <View style={[styles.header, pal.border]}> - <View style={s.flex1}> - <Text testID="listName" 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={sanitizeHandle(list.creator.handle, '@')} - href={makeProfileLink(list.creator)} - style={pal.textLight} - /> - )} - </Text> - )} - {descriptionRT && ( - <RichTextCom - testID="listDescription" - style={[pal.text, styles.headerDescription]} - richText={descriptionRT} - /> - )} - {isDesktop && ( - <ListActions - isOwner={isOwner} - muted={list.viewer?.muted} - onPressDeleteList={onPressDeleteList} - onPressEditList={onPressEditList} - onToggleSubscribed={onToggleSubscribed} - onPressShareList={onPressShareList} - onPressReportList={onPressReportList} - /> - )} - </View> - <View> - <UserAvatar type="list" avatar={list.avatar} size={64} /> - </View> - </View> - <View - style={{flexDirection: 'row', paddingHorizontal: isDesktop ? 16 : 6}}> - <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: { - flex: 1, - marginTop: 8, - }, - headerBtns: { - flexDirection: 'row', - gap: 8, - marginTop: 12, - }, - fakeSelectorItem: { - paddingHorizontal: 12, - paddingBottom: 8, - borderBottomWidth: 3, - }, - feedFooter: {paddingTop: 20}, -}) diff --git a/src/view/com/lists/ListsList.tsx b/src/view/com/lists/ListsList.tsx index 4c8befa1f..efc874ef3 100644 --- a/src/view/com/lists/ListsList.tsx +++ b/src/view/com/lists/ListsList.tsx @@ -1,57 +1,44 @@ -import React, {MutableRefObject} from 'react' +import React from 'react' import { + ActivityIndicator, + FlatList as RNFlatList, RefreshControl, StyleProp, StyleSheet, View, ViewStyle, - FlatList, } from 'react-native' import {observer} from 'mobx-react-lite' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' import {AppBskyGraphDefs as GraphDefs} from '@atproto/api' import {ListCard} from './ListCard' -import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' import {ErrorMessage} from '../util/error/ErrorMessage' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' -import {Button} from '../util/forms/Button' import {Text} from '../util/text/Text' import {ListsListModel} from 'state/models/lists/lists-list' import {useAnalytics} from 'lib/analytics/analytics' import {usePalette} from 'lib/hooks/usePalette' +import {FlatList} from '../util/Views.web' import {s} from 'lib/styles' -const LOADING_ITEM = {_reactKey: '__loading__'} -const CREATENEW_ITEM = {_reactKey: '__loading__'} -const EMPTY_ITEM = {_reactKey: '__empty__'} +const LOADING = {_reactKey: '__loading__'} +const EMPTY = {_reactKey: '__empty__'} const ERROR_ITEM = {_reactKey: '__error__'} const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} export const ListsList = observer(function ListsListImpl({ listsList, - showAddBtns, + inline, style, - scrollElRef, onPressTryAgain, - onPressCreateNew, renderItem, - renderEmptyState, testID, - headerOffset = 0, }: { listsList: ListsListModel - showAddBtns?: boolean + inline?: boolean style?: StyleProp<ViewStyle> - scrollElRef?: MutableRefObject<FlatList<any> | null> - onPressCreateNew: () => void onPressTryAgain?: () => void - renderItem?: (list: GraphDefs.ListView) => JSX.Element - renderEmptyState?: () => JSX.Element + renderItem?: (list: GraphDefs.ListView, index: number) => JSX.Element testID?: string - headerOffset?: number }) { const pal = usePalette('default') const {track} = useAnalytics() @@ -59,33 +46,27 @@ export const ListsList = observer(function ListsListImpl({ const data = React.useMemo(() => { let items: any[] = [] - if (listsList.hasLoaded) { - if (listsList.hasError) { - items = items.concat([ERROR_ITEM]) - } - if (listsList.isEmpty) { - items = items.concat([EMPTY_ITEM]) - } else { - if (showAddBtns) { - items = items.concat([CREATENEW_ITEM]) - } - items = items.concat(listsList.lists) - } - if (listsList.loadMoreError) { - items = items.concat([LOAD_MORE_ERROR_ITEM]) - } - } else if (listsList.isLoading) { - items = items.concat([LOADING_ITEM]) + if (listsList.hasError) { + items = items.concat([ERROR_ITEM]) + } + if (!listsList.hasLoaded && listsList.isLoading) { + items = items.concat([LOADING]) + } else if (listsList.isEmpty) { + items = items.concat([EMPTY]) + } else { + items = items.concat(listsList.lists) + } + if (listsList.loadMoreError) { + items = items.concat([LOAD_MORE_ERROR_ITEM]) } return items }, [ listsList.hasError, listsList.hasLoaded, listsList.isLoading, - listsList.isEmpty, listsList.lists, + listsList.isEmpty, listsList.loadMoreError, - showAddBtns, ]) // events @@ -119,14 +100,15 @@ export const ListsList = observer(function ListsListImpl({ // = const renderItemInner = React.useCallback( - ({item}: {item: any}) => { - if (item === EMPTY_ITEM) { - if (renderEmptyState) { - return renderEmptyState() - } - return <View /> - } else if (item === CREATENEW_ITEM) { - return <CreateNewItem onPress={onPressCreateNew} /> + ({item, index}: {item: any; index: number}) => { + if (item === EMPTY) { + return ( + <View + testID="listsEmpty" + style={[{padding: 18, borderTopWidth: 1}, pal.border]}> + <Text style={pal.textLight}>You have no lists.</Text> + </View> + ) } else if (item === ERROR_ITEM) { return ( <ErrorMessage @@ -141,11 +123,15 @@ export const ListsList = observer(function ListsListImpl({ onPress={onPressRetryLoadMore} /> ) - } else if (item === LOADING_ITEM) { - return <ProfileCardFeedLoadingPlaceholder /> + } else if (item === LOADING) { + return ( + <View style={{padding: 20}}> + <ActivityIndicator /> + </View> + ) } return renderItem ? ( - renderItem(item) + renderItem(item, index) ) : ( <ListCard list={item} @@ -154,24 +140,17 @@ export const ListsList = observer(function ListsListImpl({ /> ) }, - [ - listsList, - onPressTryAgain, - onPressRetryLoadMore, - onPressCreateNew, - renderItem, - renderEmptyState, - ], + [listsList, onPressTryAgain, onPressRetryLoadMore, renderItem, pal], ) + const FlatListCom = inline ? RNFlatList : FlatList return ( <View testID={testID} style={style}> {data.length > 0 && ( - <FlatList + <FlatListCom testID={testID ? `${testID}-flatlist` : undefined} - ref={scrollElRef} data={data} - keyExtractor={item => item._reactKey} + keyExtractor={(item: any) => item._reactKey} renderItem={renderItemInner} refreshControl={ <RefreshControl @@ -179,15 +158,12 @@ export const ListsList = observer(function ListsListImpl({ onRefresh={onRefresh} tintColor={pal.colors.text} titleColor={pal.colors.text} - progressViewOffset={headerOffset} /> } contentContainerStyle={[s.contentContainer]} - style={{paddingTop: headerOffset}} onEndReached={onEndReached} onEndReachedThreshold={0.6} removeClippedSubviews={true} - contentOffset={{x: 0, y: headerOffset * -1}} // @ts-ignore our .web version only -prf desktopFixedHeight /> @@ -196,36 +172,9 @@ export const ListsList = observer(function ListsListImpl({ ) }) -function CreateNewItem({onPress}: {onPress: () => void}) { - const pal = usePalette('default') - - return ( - <View style={[styles.createNewContainer]}> - <Button type="default" onPress={onPress} style={styles.createNewButton}> - <FontAwesomeIcon icon="plus" style={pal.text as FontAwesomeIconStyle} /> - <Text type="button" style={pal.text}> - New Mute List - </Text> - </Button> - </View> - ) -} - const styles = StyleSheet.create({ - createNewContainer: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 18, - paddingTop: 18, - paddingBottom: 16, - }, - createNewButton: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }, - feedFooter: {paddingTop: 20}, item: { paddingHorizontal: 18, + paddingVertical: 4, }, }) |