Skip to content

Commit 6432667

Browse files
authored
ALF lists screen (#8941)
* alf list screens * relocate to `#/screens`, balkanize * use useBreakpoints * showCancel on subscribe menu * fix typo
1 parent 4a1b1f1 commit 6432667

File tree

10 files changed

+1226
-1079
lines changed

10 files changed

+1226
-1079
lines changed

src/Navigation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ import {PostThreadScreen} from '#/view/screens/PostThread'
6464
import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy'
6565
import {ProfileScreen} from '#/view/screens/Profile'
6666
import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy'
67-
import {ProfileListScreen} from '#/view/screens/ProfileList'
6867
import {SavedFeeds} from '#/view/screens/SavedFeeds'
6968
import {Storybook} from '#/view/screens/Storybook'
7069
import {SupportScreen} from '#/view/screens/Support'
@@ -92,6 +91,7 @@ import {ProfileFollowersScreen} from '#/screens/Profile/ProfileFollowers'
9291
import {ProfileFollowsScreen} from '#/screens/Profile/ProfileFollows'
9392
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
9493
import {ProfileSearchScreen} from '#/screens/Profile/ProfileSearch'
94+
import {ProfileListScreen} from '#/screens/ProfileList'
9595
import {SearchScreen} from '#/screens/Search'
9696
import {AboutSettingsScreen} from '#/screens/Settings/AboutSettings'
9797
import {AccessibilitySettingsScreen} from '#/screens/Settings/AccessibilitySettings'
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import {useCallback, useImperativeHandle, useState} from 'react'
2+
import {View} from 'react-native'
3+
import {type AppBskyGraphDefs} from '@atproto/api'
4+
import {msg, Trans} from '@lingui/macro'
5+
import {useLingui} from '@lingui/react'
6+
7+
import {isNative} from '#/platform/detection'
8+
import {useSession} from '#/state/session'
9+
import {ListMembers} from '#/view/com/lists/ListMembers'
10+
import {EmptyState} from '#/view/com/util/EmptyState'
11+
import {type ListRef} from '#/view/com/util/List'
12+
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
13+
import {atoms as a, useBreakpoints} from '#/alf'
14+
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
15+
import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person'
16+
17+
interface SectionRef {
18+
scrollToTop: () => void
19+
}
20+
21+
interface AboutSectionProps {
22+
ref?: React.Ref<SectionRef>
23+
list: AppBskyGraphDefs.ListView
24+
onPressAddUser: () => void
25+
headerHeight: number
26+
scrollElRef: ListRef
27+
}
28+
29+
export function AboutSection({
30+
ref,
31+
list,
32+
onPressAddUser,
33+
headerHeight,
34+
scrollElRef,
35+
}: AboutSectionProps) {
36+
const {_} = useLingui()
37+
const {currentAccount} = useSession()
38+
const {gtMobile} = useBreakpoints()
39+
const [isScrolledDown, setIsScrolledDown] = useState(false)
40+
const isOwner = list.creator.did === currentAccount?.did
41+
42+
const onScrollToTop = useCallback(() => {
43+
scrollElRef.current?.scrollToOffset({
44+
animated: isNative,
45+
offset: -headerHeight,
46+
})
47+
}, [scrollElRef, headerHeight])
48+
49+
useImperativeHandle(ref, () => ({
50+
scrollToTop: onScrollToTop,
51+
}))
52+
53+
const renderHeader = useCallback(() => {
54+
if (!isOwner) {
55+
return <View />
56+
}
57+
if (!gtMobile) {
58+
return (
59+
<View style={[a.px_sm, a.py_sm]}>
60+
<Button
61+
testID="addUserBtn"
62+
label={_(msg`Add a user to this list`)}
63+
onPress={onPressAddUser}
64+
color="primary"
65+
size="small"
66+
variant="outline"
67+
style={[a.py_md]}>
68+
<ButtonIcon icon={PersonPlusIcon} />
69+
<ButtonText>
70+
<Trans>Add people</Trans>
71+
</ButtonText>
72+
</Button>
73+
</View>
74+
)
75+
}
76+
return (
77+
<View style={[a.px_lg, a.py_md, a.flex_row_reverse]}>
78+
<Button
79+
testID="addUserBtn"
80+
label={_(msg`Add a user to this list`)}
81+
onPress={onPressAddUser}
82+
color="primary"
83+
size="small"
84+
variant="ghost"
85+
style={[a.py_sm]}>
86+
<ButtonIcon icon={PersonPlusIcon} />
87+
<ButtonText>
88+
<Trans>Add people</Trans>
89+
</ButtonText>
90+
</Button>
91+
</View>
92+
)
93+
}, [isOwner, _, onPressAddUser, gtMobile])
94+
95+
const renderEmptyState = useCallback(() => {
96+
return (
97+
<View style={[a.gap_xl, a.align_center]}>
98+
<EmptyState icon="users-slash" message={_(msg`This list is empty.`)} />
99+
{isOwner && (
100+
<Button
101+
testID="emptyStateAddUserBtn"
102+
label={_(msg`Start adding people`)}
103+
onPress={onPressAddUser}
104+
color="primary"
105+
size="small">
106+
<ButtonIcon icon={PersonPlusIcon} />
107+
<ButtonText>
108+
<Trans>Start adding people!</Trans>
109+
</ButtonText>
110+
</Button>
111+
)}
112+
</View>
113+
)
114+
}, [_, onPressAddUser, isOwner])
115+
116+
return (
117+
<View>
118+
<ListMembers
119+
testID="listItems"
120+
list={list.uri}
121+
scrollElRef={scrollElRef}
122+
renderHeader={renderHeader}
123+
renderEmptyState={renderEmptyState}
124+
headerOffset={headerHeight}
125+
onScrolledDownChange={setIsScrolledDown}
126+
/>
127+
{isScrolledDown && (
128+
<LoadLatestBtn
129+
onPress={onScrollToTop}
130+
label={_(msg`Scroll to top`)}
131+
showIndicator={false}
132+
/>
133+
)}
134+
</View>
135+
)
136+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import {useCallback, useEffect, useImperativeHandle, useState} from 'react'
2+
import {View} from 'react-native'
3+
import {msg, Trans} from '@lingui/macro'
4+
import {useLingui} from '@lingui/react'
5+
import {useIsFocused} from '@react-navigation/native'
6+
import {useQueryClient} from '@tanstack/react-query'
7+
8+
import {isNative} from '#/platform/detection'
9+
import {listenSoftReset} from '#/state/events'
10+
import {type FeedDescriptor} from '#/state/queries/post-feed'
11+
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
12+
import {PostFeed} from '#/view/com/posts/PostFeed'
13+
import {EmptyState} from '#/view/com/util/EmptyState'
14+
import {type ListRef} from '#/view/com/util/List'
15+
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
16+
import {atoms as a} from '#/alf'
17+
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
18+
import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person'
19+
20+
interface SectionRef {
21+
scrollToTop: () => void
22+
}
23+
24+
interface FeedSectionProps {
25+
ref?: React.Ref<SectionRef>
26+
feed: FeedDescriptor
27+
headerHeight: number
28+
scrollElRef: ListRef
29+
isFocused: boolean
30+
isOwner: boolean
31+
onPressAddUser: () => void
32+
}
33+
34+
export function FeedSection({
35+
ref,
36+
feed,
37+
scrollElRef,
38+
headerHeight,
39+
isFocused,
40+
isOwner,
41+
onPressAddUser,
42+
}: FeedSectionProps) {
43+
const queryClient = useQueryClient()
44+
const [hasNew, setHasNew] = useState(false)
45+
const [isScrolledDown, setIsScrolledDown] = useState(false)
46+
const isScreenFocused = useIsFocused()
47+
const {_} = useLingui()
48+
49+
const onScrollToTop = useCallback(() => {
50+
scrollElRef.current?.scrollToOffset({
51+
animated: isNative,
52+
offset: -headerHeight,
53+
})
54+
queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
55+
setHasNew(false)
56+
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
57+
useImperativeHandle(ref, () => ({
58+
scrollToTop: onScrollToTop,
59+
}))
60+
61+
useEffect(() => {
62+
if (!isScreenFocused) {
63+
return
64+
}
65+
return listenSoftReset(onScrollToTop)
66+
}, [onScrollToTop, isScreenFocused])
67+
68+
const renderPostsEmpty = useCallback(() => {
69+
return (
70+
<View style={[a.gap_xl, a.align_center]}>
71+
<EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
72+
{isOwner && (
73+
<Button
74+
label={_(msg`Start adding people`)}
75+
onPress={onPressAddUser}
76+
color="primary"
77+
size="small">
78+
<ButtonIcon icon={PersonPlusIcon} />
79+
<ButtonText>
80+
<Trans>Start adding people!</Trans>
81+
</ButtonText>
82+
</Button>
83+
)}
84+
</View>
85+
)
86+
}, [_, onPressAddUser, isOwner])
87+
88+
return (
89+
<View>
90+
<PostFeed
91+
testID="listFeed"
92+
enabled={isFocused}
93+
feed={feed}
94+
pollInterval={60e3}
95+
disablePoll={hasNew}
96+
scrollElRef={scrollElRef}
97+
onHasNew={setHasNew}
98+
onScrolledDownChange={setIsScrolledDown}
99+
renderEmptyState={renderPostsEmpty}
100+
headerOffset={headerHeight}
101+
/>
102+
{(isScrolledDown || hasNew) && (
103+
<LoadLatestBtn
104+
onPress={onScrollToTop}
105+
label={_(msg`Load new posts`)}
106+
showIndicator={hasNew}
107+
/>
108+
)}
109+
</View>
110+
)
111+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {View} from 'react-native'
2+
import {msg, Trans} from '@lingui/macro'
3+
import {useLingui} from '@lingui/react'
4+
import {useNavigation} from '@react-navigation/native'
5+
6+
import {type NavigationProp} from '#/lib/routes/types'
7+
import {atoms as a, useTheme} from '#/alf'
8+
import {Button, ButtonText} from '#/components/Button'
9+
import {Text} from '#/components/Typography'
10+
11+
export function ErrorScreen({error}: {error: React.ReactNode}) {
12+
const t = useTheme()
13+
const navigation = useNavigation<NavigationProp>()
14+
const {_} = useLingui()
15+
const onPressBack = () => {
16+
if (navigation.canGoBack()) {
17+
navigation.goBack()
18+
} else {
19+
navigation.navigate('Home')
20+
}
21+
}
22+
23+
return (
24+
<View style={[a.px_xl, a.py_md, a.gap_md]}>
25+
<Text style={[a.text_4xl, a.font_heavy]}>
26+
<Trans>Could not load list</Trans>
27+
</Text>
28+
<Text style={[a.text_md, t.atoms.text_contrast_high, a.leading_snug]}>
29+
{error}
30+
</Text>
31+
32+
<View style={[a.flex_row, a.mt_lg]}>
33+
<Button
34+
label={_(msg`Go back`)}
35+
accessibilityHint={_(msg`Returns to previous page`)}
36+
onPress={onPressBack}
37+
size="small"
38+
color="secondary">
39+
<ButtonText>
40+
<Trans>Go back</Trans>
41+
</ButtonText>
42+
</Button>
43+
</View>
44+
</View>
45+
)
46+
}

0 commit comments

Comments
 (0)