Add followers and follows list

This commit is contained in:
Paul Frazee 2022-07-26 12:02:34 -05:00
parent 1504d144d9
commit 62eb9f3c93
14 changed files with 645 additions and 26 deletions

View file

@ -0,0 +1,141 @@
import React, {useState, useEffect} from 'react'
import {observer} from 'mobx-react-lite'
import {
ActivityIndicator,
FlatList,
Image,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native'
import {OnNavigateContent} from '../../routes/types'
import {
UserFollowersViewModel,
FollowerItem,
} from '../../../state/models/user-followers-view'
import {useStores} from '../../../state'
import {s} from '../../lib/styles'
import {AVIS} from '../../lib/assets'
export const ProfileFollowers = observer(function ProfileFollowers({
name,
onNavigateContent,
}: {
name: string
onNavigateContent: OnNavigateContent
}) {
const store = useStores()
const [view, setView] = useState<UserFollowersViewModel | undefined>()
useEffect(() => {
if (view?.params.user === name) {
console.log('User followers doing nothing')
return // no change needed? or trigger refresh?
}
console.log('Fetching user followers', name)
const newView = new UserFollowersViewModel(store, {user: name})
setView(newView)
newView
.setup()
.catch(err => console.error('Failed to fetch user followers', err))
}, [name, view?.params.user, store])
// loading
// =
if (
!view ||
(view.isLoading && !view.isRefreshing) ||
view.params.user !== name
) {
return (
<View>
<ActivityIndicator />
</View>
)
}
// error
// =
if (view.hasError) {
return (
<View>
<Text>{view.error}</Text>
</View>
)
}
// loaded
// =
const renderItem = ({item}: {item: FollowerItem}) => (
<User item={item} onNavigateContent={onNavigateContent} />
)
return (
<View>
<FlatList
data={view.followers}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
/>
</View>
)
})
const User = ({
item,
onNavigateContent,
}: {
item: FollowerItem
onNavigateContent: OnNavigateContent
}) => {
const onPressOuter = () => {
onNavigateContent('Profile', {
name: item.name,
})
}
return (
<TouchableOpacity style={styles.outer} onPress={onPressOuter}>
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<Image
style={styles.avi}
source={AVIS[item.name] || AVIS['alice.com']}
/>
</View>
<View style={styles.layoutContent}>
<Text style={[s.f15, s.bold]}>{item.displayName}</Text>
<Text style={[s.f14, s.gray]}>@{item.name}</Text>
</View>
</View>
</TouchableOpacity>
)
}
const styles = StyleSheet.create({
outer: {
borderTopWidth: 1,
borderTopColor: '#e8e8e8',
backgroundColor: '#fff',
},
layout: {
flexDirection: 'row',
},
layoutAvi: {
width: 60,
paddingLeft: 10,
paddingTop: 10,
paddingBottom: 10,
},
avi: {
width: 40,
height: 40,
borderRadius: 30,
resizeMode: 'cover',
},
layoutContent: {
flex: 1,
paddingRight: 10,
paddingTop: 10,
paddingBottom: 10,
},
})

View file

@ -0,0 +1,141 @@
import React, {useState, useEffect} from 'react'
import {observer} from 'mobx-react-lite'
import {
ActivityIndicator,
FlatList,
Image,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native'
import {OnNavigateContent} from '../../routes/types'
import {
UserFollowsViewModel,
FollowItem,
} from '../../../state/models/user-follows-view'
import {useStores} from '../../../state'
import {s} from '../../lib/styles'
import {AVIS} from '../../lib/assets'
export const ProfileFollows = observer(function ProfileFollows({
name,
onNavigateContent,
}: {
name: string
onNavigateContent: OnNavigateContent
}) {
const store = useStores()
const [view, setView] = useState<UserFollowsViewModel | undefined>()
useEffect(() => {
if (view?.params.user === name) {
console.log('User follows doing nothing')
return // no change needed? or trigger refresh?
}
console.log('Fetching user follows', name)
const newView = new UserFollowsViewModel(store, {user: name})
setView(newView)
newView
.setup()
.catch(err => console.error('Failed to fetch user follows', err))
}, [name, view?.params.user, store])
// loading
// =
if (
!view ||
(view.isLoading && !view.isRefreshing) ||
view.params.user !== name
) {
return (
<View>
<ActivityIndicator />
</View>
)
}
// error
// =
if (view.hasError) {
return (
<View>
<Text>{view.error}</Text>
</View>
)
}
// loaded
// =
const renderItem = ({item}: {item: FollowItem}) => (
<User item={item} onNavigateContent={onNavigateContent} />
)
return (
<View>
<FlatList
data={view.follows}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
/>
</View>
)
})
const User = ({
item,
onNavigateContent,
}: {
item: FollowItem
onNavigateContent: OnNavigateContent
}) => {
const onPressOuter = () => {
onNavigateContent('Profile', {
name: item.name,
})
}
return (
<TouchableOpacity style={styles.outer} onPress={onPressOuter}>
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<Image
style={styles.avi}
source={AVIS[item.name] || AVIS['alice.com']}
/>
</View>
<View style={styles.layoutContent}>
<Text style={[s.f15, s.bold]}>{item.displayName}</Text>
<Text style={[s.f14, s.gray]}>@{item.name}</Text>
</View>
</View>
</TouchableOpacity>
)
}
const styles = StyleSheet.create({
outer: {
borderTopWidth: 1,
borderTopColor: '#e8e8e8',
backgroundColor: '#fff',
},
layout: {
flexDirection: 'row',
},
layoutAvi: {
width: 60,
paddingLeft: 10,
paddingTop: 10,
paddingBottom: 10,
},
avi: {
width: 40,
height: 40,
borderRadius: 30,
resizeMode: 'cover',
},
layoutContent: {
flex: 1,
paddingRight: 10,
paddingTop: 10,
paddingBottom: 10,
},
})

View file

@ -6,6 +6,7 @@ import {
Image,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native'
import {OnNavigateContent} from '../../routes/types'
@ -18,8 +19,8 @@ import Toast from '../util/Toast'
export const ProfileHeader = observer(function ProfileHeader({
user,
}: // onNavigateContent,
{
onNavigateContent,
}: {
user: string
onNavigateContent: OnNavigateContent
}) {
@ -53,6 +54,12 @@ export const ProfileHeader = observer(function ProfileHeader({
err => console.error('Failed to toggle follow', err),
)
}
const onPressFollowers = () => {
onNavigateContent('ProfileFollowers', {name: user})
}
const onPressFollows = () => {
onNavigateContent('ProfileFollows', {name: user})
}
// loading
// =
@ -91,16 +98,18 @@ export const ProfileHeader = observer(function ProfileHeader({
<Text style={[s.mb5, s.f15, s['lh15-1.3']]}>{view.description}</Text>
)}
<View style={s.flexRow}>
<View style={[s.flexRow, s.mr10]}>
<TouchableOpacity
style={[s.flexRow, s.mr10]}
onPress={onPressFollowers}>
<Text style={[s.bold, s.mr2]}>{view.followersCount}</Text>
<Text style={s.gray}>
{pluralize(view.followersCount, 'follower')}
</Text>
</View>
<View style={[s.flexRow, s.mr10]}>
</TouchableOpacity>
<TouchableOpacity style={[s.flexRow, s.mr10]} onPress={onPressFollows}>
<Text style={[s.bold, s.mr2]}>{view.followsCount}</Text>
<Text style={s.gray}>following</Text>
</View>
</TouchableOpacity>
<View style={[s.flexRow, s.mr10]}>
<Text style={[s.bold, s.mr2]}>{view.postsCount}</Text>
<Text style={s.gray}>{pluralize(view.postsCount, 'post')}</Text>

View file

@ -25,6 +25,8 @@ import {PostThread} from '../screens/stacks/PostThread'
import {PostLikedBy} from '../screens/stacks/PostLikedBy'
import {PostRepostedBy} from '../screens/stacks/PostRepostedBy'
import {Profile} from '../screens/stacks/Profile'
import {ProfileFollowers} from '../screens/stacks/ProfileFollowers'
import {ProfileFollows} from '../screens/stacks/ProfileFollows'
const linking: LinkingOptions<RootTabsParamList> = {
prefixes: [
@ -40,6 +42,8 @@ const linking: LinkingOptions<RootTabsParamList> = {
NotificationsTab: 'notifications',
MenuTab: 'menu',
Profile: 'profile/:name',
ProfileFollowers: 'profile/:name/followers',
ProfileFollows: 'profile/:name/follows',
PostThread: 'profile/:name/post/:recordKey',
PostLikedBy: 'profile/:name/post/:recordKey/liked-by',
PostRepostedBy: 'profile/:name/post/:recordKey/reposted-by',
@ -93,6 +97,11 @@ function HomeStackCom() {
<HomeTabStack.Screen name="Home" component={Home} />
<HomeTabStack.Screen name="Composer" component={Composer} />
<HomeTabStack.Screen name="Profile" component={Profile} />
<HomeTabStack.Screen
name="ProfileFollowers"
component={ProfileFollowers}
/>
<HomeTabStack.Screen name="ProfileFollows" component={ProfileFollows} />
<HomeTabStack.Screen name="PostThread" component={PostThread} />
<HomeTabStack.Screen name="PostLikedBy" component={PostLikedBy} />
<HomeTabStack.Screen name="PostRepostedBy" component={PostRepostedBy} />
@ -109,6 +118,11 @@ function SearchStackCom() {
options={HIDE_HEADER}
/>
<SearchTabStack.Screen name="Profile" component={Profile} />
<SearchTabStack.Screen
name="ProfileFollowers"
component={ProfileFollowers}
/>
<SearchTabStack.Screen name="ProfileFollows" component={ProfileFollows} />
<SearchTabStack.Screen name="PostThread" component={PostThread} />
<SearchTabStack.Screen name="PostLikedBy" component={PostLikedBy} />
<SearchTabStack.Screen name="PostRepostedBy" component={PostRepostedBy} />
@ -124,6 +138,14 @@ function NotificationsStackCom() {
component={Notifications}
/>
<NotificationsTabStack.Screen name="Profile" component={Profile} />
<NotificationsTabStack.Screen
name="ProfileFollowers"
component={ProfileFollowers}
/>
<NotificationsTabStack.Screen
name="ProfileFollows"
component={ProfileFollows}
/>
<NotificationsTabStack.Screen name="PostThread" component={PostThread} />
<NotificationsTabStack.Screen
name="PostLikedBy"

View file

@ -6,6 +6,8 @@ export type RootTabsParamList = {
NotificationsTab: undefined
MenuTab: undefined
Profile: {name: string}
ProfileFollowers: {name: string}
ProfileFollows: {name: string}
PostThread: {name: string; recordKey: string}
PostLikedBy: {name: string; recordKey: string}
PostRepostedBy: {name: string; recordKey: string}

View file

@ -0,0 +1,39 @@
import React, {useLayoutEffect} from 'react'
import {TouchableOpacity} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Shell} from '../../shell'
import type {RootTabsScreenProps} from '../../routes/types'
import {ProfileFollowers as ProfileFollowersComponent} from '../../com/profile/ProfileFollowers'
export const ProfileFollowers = ({
navigation,
route,
}: RootTabsScreenProps<'ProfileFollowers'>) => {
const {name} = route.params
useLayoutEffect(() => {
navigation.setOptions({
headerShown: true,
headerTitle: 'Followers',
headerLeft: () => (
<TouchableOpacity onPress={() => navigation.goBack()}>
<FontAwesomeIcon icon="arrow-left" />
</TouchableOpacity>
),
})
}, [navigation])
const onNavigateContent = (screen: string, props: Record<string, string>) => {
// @ts-ignore it's up to the callers to supply correct params -prf
navigation.push(screen, props)
}
return (
<Shell>
<ProfileFollowersComponent
name={name}
onNavigateContent={onNavigateContent}
/>
</Shell>
)
}

View file

@ -0,0 +1,39 @@
import React, {useLayoutEffect} from 'react'
import {TouchableOpacity} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Shell} from '../../shell'
import type {RootTabsScreenProps} from '../../routes/types'
import {ProfileFollows as ProfileFollowsComponent} from '../../com/profile/ProfileFollows'
export const ProfileFollows = ({
navigation,
route,
}: RootTabsScreenProps<'ProfileFollows'>) => {
const {name} = route.params
useLayoutEffect(() => {
navigation.setOptions({
headerShown: true,
headerTitle: 'Following',
headerLeft: () => (
<TouchableOpacity onPress={() => navigation.goBack()}>
<FontAwesomeIcon icon="arrow-left" />
</TouchableOpacity>
),
})
}, [navigation])
const onNavigateContent = (screen: string, props: Record<string, string>) => {
// @ts-ignore it's up to the callers to supply correct params -prf
navigation.push(screen, props)
}
return (
<Shell>
<ProfileFollowsComponent
name={name}
onNavigateContent={onNavigateContent}
/>
</Shell>
)
}