Rework profile page to include working view selector

zio/stable
Paul Frazee 2022-09-06 14:26:39 -05:00
parent 2ec09ba545
commit bb06ef4f6e
19 changed files with 569 additions and 94 deletions

View File

@ -0,0 +1,49 @@
import {makeAutoObservable} from 'mobx'
import {RootStoreModel} from './root-store'
// TODO / DEBUG
// this is a temporary fake for the model until the view actually gets implemented in the bsky api
// -prf
export class BadgesViewModel {
// state
isLoading = false
isRefreshing = false
hasLoaded = false
error = ''
constructor(public rootStore: RootStoreModel) {
makeAutoObservable(
this,
{
rootStore: false,
},
{autoBind: true},
)
}
get hasContent() {
return false
}
get hasError() {
return this.error !== ''
}
get isEmpty() {
return this.hasLoaded && !this.hasContent
}
// public api
// =
async setup() {
this.hasLoaded = true
}
async refresh() {}
async loadMore() {}
async update() {}
}

View File

@ -95,6 +95,7 @@ export class FeedViewModel implements bsky.FeedView.Response {
isLoading = false isLoading = false
isRefreshing = false isRefreshing = false
hasLoaded = false hasLoaded = false
hasReachedEnd = false
error = '' error = ''
params: bsky.FeedView.Params params: bsky.FeedView.Params
_loadPromise: Promise<void> | undefined _loadPromise: Promise<void> | undefined
@ -244,7 +245,13 @@ export class FeedViewModel implements bsky.FeedView.Response {
'blueskyweb.xyz:FeedView', 'blueskyweb.xyz:FeedView',
params, params,
)) as bsky.FeedView.Response )) as bsky.FeedView.Response
this._appendAll(res) if (res.feed.length === 0) {
runInAction(() => {
this.hasReachedEnd = true
})
} else {
this._appendAll(res)
}
this._xIdle() this._xIdle()
} catch (e: any) { } catch (e: any) {
this._xIdle(`Failed to load feed: ${e.toString()}`) this._xIdle(`Failed to load feed: ${e.toString()}`)
@ -281,6 +288,7 @@ export class FeedViewModel implements bsky.FeedView.Response {
private _replaceAll(res: bsky.FeedView.Response) { private _replaceAll(res: bsky.FeedView.Response) {
this.feed.length = 0 this.feed.length = 0
this.hasReachedEnd = false
this._appendAll(res) this._appendAll(res)
} }

View File

@ -0,0 +1,98 @@
import {makeAutoObservable} from 'mobx'
import {RootStoreModel} from './root-store'
import {ProfileViewModel} from './profile-view'
import {FeedViewModel} from './feed-view'
import {BadgesViewModel} from './badges-view'
export const SECTION_IDS = {
POSTS: 0,
BADGES: 1,
}
export interface ProfileUiParams {
user: string
}
export class ProfileUiModel {
// constants
static SELECTOR_ITEMS = ['Posts', 'Badges']
// data
profile: ProfileViewModel
feed: FeedViewModel
badges: BadgesViewModel
// ui state
selectedViewIndex = 0
constructor(
public rootStore: RootStoreModel,
public params: ProfileUiParams,
) {
makeAutoObservable(
this,
{
rootStore: false,
params: false,
},
{autoBind: true},
)
this.profile = new ProfileViewModel(rootStore, {user: params.user})
this.feed = new FeedViewModel(rootStore, {author: params.user, limit: 10})
this.badges = new BadgesViewModel(rootStore)
}
get currentView(): FeedViewModel | BadgesViewModel {
if (this.selectedViewIndex === SECTION_IDS.POSTS) {
return this.feed
}
if (this.selectedViewIndex === SECTION_IDS.BADGES) {
return this.badges
}
throw new Error(`Invalid selector value: ${this.selectedViewIndex}`)
}
get isInitialLoading() {
const view = this.currentView
return view.isLoading && !view.isRefreshing && !view.hasContent
}
get isRefreshing() {
return this.profile.isRefreshing || this.currentView.isRefreshing
}
// public api
// =
setSelectedViewIndex(index: number) {
this.selectedViewIndex = index
}
async setup() {
await Promise.all([
this.profile
.setup()
.catch(err => console.error('Failed to fetch profile', err)),
this.feed
.setup()
.catch(err => console.error('Failed to fetch feed', err)),
this.badges
.setup()
.catch(err => console.error('Failed to fetch badges', err)),
])
}
async update() {
await this.currentView.update()
}
async refresh() {
await Promise.all([this.profile.refresh(), this.currentView.refresh()])
}
async loadMore() {
if (!this.currentView.isLoading && !this.currentView.hasError) {
await this.currentView.loadMore()
}
}
}

View File

@ -65,7 +65,7 @@ export class ProfileViewModel implements bsky.ProfileView.Response {
} }
async refresh() { async refresh() {
await this._load() await this._load(true)
} }
async toggleFollowing() { async toggleFollowing() {
@ -108,8 +108,8 @@ export class ProfileViewModel implements bsky.ProfileView.Response {
// loader functions // loader functions
// = // =
private async _load() { private async _load(isRefreshing = false) {
this._xLoading() this._xLoading(isRefreshing)
await new Promise(r => setTimeout(r, 250)) // DEBUG await new Promise(r => setTimeout(r, 250)) // DEBUG
try { try {
const res = (await this.rootStore.api.mainPds.view( const res = (await this.rootStore.api.mainPds.view(
@ -119,7 +119,7 @@ export class ProfileViewModel implements bsky.ProfileView.Response {
this._replaceAll(res) this._replaceAll(res)
this._xIdle() this._xIdle()
} catch (e: any) { } catch (e: any) {
this._xIdle(`Failed to load feed: ${e.toString()}`) this._xIdle(e.toString())
} }
} }

View File

@ -1,12 +1,5 @@
import React, {useState} from 'react' import React, {useState} from 'react'
import { import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
KeyboardAvoidingView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native'
import {BottomSheetTextInput} from '@gorhom/bottom-sheet' import {BottomSheetTextInput} from '@gorhom/bottom-sheet'
import LinearGradient from 'react-native-linear-gradient' import LinearGradient from 'react-native-linear-gradient'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'

View File

@ -238,7 +238,7 @@ export const PostThreadItem = observer(function PostThreadItem({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
outer: { outer: {
backgroundColor: colors.white, backgroundColor: colors.white,
borderRadius: 10, borderRadius: 6,
margin: 2, margin: 2,
marginBottom: 0, marginBottom: 0,
}, },

View File

@ -154,7 +154,7 @@ export const Post = observer(function Post({uri}: {uri: string}) {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
outer: { outer: {
marginTop: 1, marginTop: 1,
borderRadius: 4, borderRadius: 6,
backgroundColor: colors.white, backgroundColor: colors.white,
padding: 10, padding: 10,
}, },

View File

@ -3,21 +3,17 @@ import {observer} from 'mobx-react-lite'
import {Text, View, FlatList} from 'react-native' import {Text, View, FlatList} from 'react-native'
import {FeedViewModel, FeedViewItemModel} from '../../../state/models/feed-view' import {FeedViewModel, FeedViewItemModel} from '../../../state/models/feed-view'
import {FeedItem} from './FeedItem' import {FeedItem} from './FeedItem'
import {SharePostModel} from '../../../state/models/shell'
import {useStores} from '../../../state' import {useStores} from '../../../state'
export const Feed = observer(function Feed({feed}: {feed: FeedViewModel}) { export const Feed = observer(function Feed({feed}: {feed: FeedViewModel}) {
const store = useStores() const store = useStores()
const onPressShare = (uri: string) => {
store.shell.openModal(new SharePostModel(uri))
}
// TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf // TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf
// VirtualizedList: You have a large list that is slow to update - make sure your // VirtualizedList: You have a large list that is slow to update - make sure your
// renderItem function renders components that follow React performance best practices // renderItem function renders components that follow React performance best practices
// like PureComponent, shouldComponentUpdate, etc // like PureComponent, shouldComponentUpdate, etc
const renderItem = ({item}: {item: FeedViewItemModel}) => ( const renderItem = ({item}: {item: FeedViewItemModel}) => (
<FeedItem item={item} onPressShare={onPressShare} /> <FeedItem item={item} />
) )
const onRefresh = () => { const onRefresh = () => {
feed.refresh().catch(err => console.error('Failed to refresh', err)) feed.refresh().catch(err => console.error('Failed to refresh', err))
@ -33,7 +29,7 @@ export const Feed = observer(function Feed({feed}: {feed: FeedViewModel}) {
{feed.hasError && <Text>{feed.error}</Text>} {feed.hasError && <Text>{feed.error}</Text>}
{feed.hasContent && ( {feed.hasContent && (
<FlatList <FlatList
data={feed.feed} data={feed.feed.slice()}
keyExtractor={item => item._reactKey} keyExtractor={item => item._reactKey}
renderItem={renderItem} renderItem={renderItem}
refreshing={feed.isRefreshing} refreshing={feed.isRefreshing}

View File

@ -4,7 +4,7 @@ import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
import {bsky, AdxUri} from '@adxp/mock-api' import {bsky, AdxUri} from '@adxp/mock-api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {FeedViewItemModel} from '../../../state/models/feed-view' import {FeedViewItemModel} from '../../../state/models/feed-view'
import {ComposePostModel} from '../../../state/models/shell' import {ComposePostModel, SharePostModel} from '../../../state/models/shell'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {PostDropdownBtn} from '../util/DropdownBtn' import {PostDropdownBtn} from '../util/DropdownBtn'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
@ -14,10 +14,8 @@ import {useStores} from '../../../state'
export const FeedItem = observer(function FeedItem({ export const FeedItem = observer(function FeedItem({
item, item,
onPressShare,
}: { }: {
item: FeedViewItemModel item: FeedViewItemModel
onPressShare: (_uri: string) => void
}) { }) {
const store = useStores() const store = useStores()
const record = item.record as unknown as bsky.Post.Record const record = item.record as unknown as bsky.Post.Record
@ -41,6 +39,9 @@ export const FeedItem = observer(function FeedItem({
.toggleLike() .toggleLike()
.catch(e => console.error('Failed to toggle like', record, e)) .catch(e => console.error('Failed to toggle like', record, e))
} }
const onPressShare = (uri: string) => {
store.shell.openModal(new SharePostModel(uri))
}
return ( return (
<Link style={styles.outer} href={itemHref} title={itemTitle}> <Link style={styles.outer} href={itemHref} title={itemTitle}>
@ -151,7 +152,7 @@ export const FeedItem = observer(function FeedItem({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
outer: { outer: {
borderRadius: 10, borderRadius: 6,
margin: 2, margin: 2,
marginBottom: 0, marginBottom: 0,
backgroundColor: colors.white, backgroundColor: colors.white,

View File

@ -17,31 +17,13 @@ import {s, gradients, colors} from '../../lib/styles'
import {AVIS, BANNER} from '../../lib/assets' import {AVIS, BANNER} from '../../lib/assets'
import Toast from '../util/Toast' import Toast from '../util/Toast'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {Selector, SelectorItem} from '../util/Selector'
export const ProfileHeader = observer(function ProfileHeader({ export const ProfileHeader = observer(function ProfileHeader({
user, view,
}: { }: {
user: string view: ProfileViewModel
}) { }) {
const store = useStores() const store = useStores()
const [view, setView] = useState<ProfileViewModel | undefined>()
useEffect(() => {
if (view?.params.user === user) {
console.log('Profile header doing nothing')
return // no change needed? or trigger refresh?
}
console.log('Fetching profile', user)
const newView = new ProfileViewModel(store, {user: user})
setView(newView)
newView.setup().catch(err => console.error('Failed to fetch profile', err))
}, [user, view?.params.user, store])
const selectorItems: SelectorItem[] = [
{label: 'Posts', onSelect() {}},
{label: 'Badges', onSelect() {}},
]
const onPressToggleFollow = () => { const onPressToggleFollow = () => {
view?.toggleFollowing().then( view?.toggleFollowing().then(
@ -66,19 +48,15 @@ export const ProfileHeader = observer(function ProfileHeader({
// TODO // TODO
} }
const onPressFollowers = () => { const onPressFollowers = () => {
store.nav.navigate(`/profile/${user}/followers`) store.nav.navigate(`/profile/${view.name}/followers`)
} }
const onPressFollows = () => { const onPressFollows = () => {
store.nav.navigate(`/profile/${user}/follows`) store.nav.navigate(`/profile/${view.name}/follows`)
} }
// loading // loading
// = // =
if ( if (!view || (view.isLoading && !view.isRefreshing)) {
!view ||
(view.isLoading && !view.isRefreshing) ||
view.params.user !== user
) {
return ( return (
<View> <View>
<ActivityIndicator /> <ActivityIndicator />
@ -120,13 +98,13 @@ export const ProfileHeader = observer(function ProfileHeader({
<TouchableOpacity <TouchableOpacity
onPress={onPressEditProfile} onPress={onPressEditProfile}
style={[styles.mainBtn, styles.btn]}> style={[styles.mainBtn, styles.btn]}>
<Text style={[s.fw600, s.f16]}>Edit Profile</Text> <Text style={[s.fw400, s.f14]}>Edit Profile</Text>
</TouchableOpacity> </TouchableOpacity>
) : view.myState.hasFollowed ? ( ) : view.myState.hasFollowed ? (
<TouchableOpacity <TouchableOpacity
onPress={onPressToggleFollow} onPress={onPressToggleFollow}
style={[styles.mainBtn, styles.btn]}> style={[styles.mainBtn, styles.btn]}>
<Text style={[s.fw600, s.f16]}>Following</Text> <Text style={[s.fw400, s.f14]}>Following</Text>
</TouchableOpacity> </TouchableOpacity>
) : ( ) : (
<TouchableOpacity onPress={onPressToggleFollow}> <TouchableOpacity onPress={onPressToggleFollow}>
@ -146,7 +124,7 @@ export const ProfileHeader = observer(function ProfileHeader({
<FontAwesomeIcon icon="ellipsis" style={[s.gray5]} /> <FontAwesomeIcon icon="ellipsis" style={[s.gray5]} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View style={[s.flexRow, s.mb10]}> <View style={[s.flexRow]}>
<TouchableOpacity <TouchableOpacity
style={[s.flexRow, s.mr10]} style={[s.flexRow, s.mr10]}
onPress={onPressFollowers}> onPress={onPressFollowers}>
@ -167,10 +145,9 @@ export const ProfileHeader = observer(function ProfileHeader({
</View> </View>
</View> </View>
{view.description && ( {view.description && (
<Text style={[s.mb10, s.f15, s['lh15-1.3']]}>{view.description}</Text> <Text style={[s.mt10, s.f15, s['lh15-1.3']]}>{view.description}</Text>
)} )}
</View> </View>
<Selector items={selectorItems} />
</View> </View>
) )
}) })
@ -178,8 +155,6 @@ export const ProfileHeader = observer(function ProfileHeader({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
outer: { outer: {
backgroundColor: colors.white, backgroundColor: colors.white,
borderBottomWidth: 1,
borderColor: colors.gray2,
}, },
banner: { banner: {
width: '100%', width: '100%',
@ -222,14 +197,17 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingVertical: 8, paddingVertical: 6,
paddingHorizontal: 60, paddingLeft: 55,
paddingRight: 60,
borderRadius: 30, borderRadius: 30,
borderWidth: 1,
borderColor: 'transparent',
}, },
btn: { btn: {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingVertical: 8, paddingVertical: 7,
borderRadius: 30, borderRadius: 30,
borderWidth: 1, borderWidth: 1,
borderColor: colors.gray2, borderColor: colors.gray2,

View File

@ -0,0 +1,66 @@
import React from 'react'
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {colors} from '../../lib/styles'
export function ErrorMessage({
message,
onPressTryAgain,
}: {
message: string
onPressTryAgain?: () => void
}) {
return (
<View style={styles.outer}>
<View style={styles.errorIcon}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.white}}
size={16}
/>
</View>
<Text style={styles.message}>{message}</Text>
{onPressTryAgain && (
<TouchableOpacity style={styles.btn} onPress={onPressTryAgain}>
<FontAwesomeIcon
icon="arrows-rotate"
style={{color: colors.red4}}
size={16}
/>
</TouchableOpacity>
)}
</View>
)
}
const styles = StyleSheet.create({
outer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.red1,
borderWidth: 1,
borderColor: colors.red3,
borderRadius: 6,
paddingVertical: 8,
paddingHorizontal: 8,
},
errorIcon: {
backgroundColor: colors.red4,
borderRadius: 12,
width: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center',
marginRight: 8,
},
message: {
flex: 1,
color: colors.red4,
paddingRight: 10,
},
btn: {
paddingHorizontal: 4,
paddingVertical: 4,
},
})

View File

@ -0,0 +1,111 @@
import React from 'react'
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {colors} from '../../lib/styles'
export function ErrorScreen({
title,
message,
details,
onPressTryAgain,
}: {
title: string
message: string
details?: string
onPressTryAgain?: () => void
}) {
return (
<View style={styles.outer}>
<View style={styles.errorIconContainer}>
<View style={styles.errorIcon}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.white}}
size={24}
/>
</View>
</View>
<Text style={styles.title}>{title}</Text>
<Text style={styles.message}>{message}</Text>
{details && <Text style={styles.details}>{details}</Text>}
{onPressTryAgain && (
<View style={styles.btnContainer}>
<TouchableOpacity style={styles.btn} onPress={onPressTryAgain}>
<FontAwesomeIcon
icon="arrows-rotate"
style={{color: colors.white}}
size={16}
/>
<Text style={styles.btnText}>Try again</Text>
</TouchableOpacity>
</View>
)}
</View>
)
}
const styles = StyleSheet.create({
outer: {
flex: 1,
backgroundColor: colors.red1,
borderWidth: 1,
borderColor: colors.red3,
borderRadius: 6,
paddingVertical: 30,
paddingHorizontal: 14,
margin: 10,
},
title: {
textAlign: 'center',
color: colors.red4,
fontSize: 24,
marginBottom: 10,
},
message: {
textAlign: 'center',
color: colors.red4,
marginBottom: 20,
},
details: {
textAlign: 'center',
color: colors.black,
backgroundColor: colors.white,
borderWidth: 1,
borderColor: colors.gray5,
borderRadius: 6,
paddingVertical: 10,
paddingHorizontal: 14,
overflow: 'hidden',
marginBottom: 20,
},
btnContainer: {
alignItems: 'center',
},
btn: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.red4,
borderRadius: 6,
paddingHorizontal: 16,
paddingVertical: 10,
},
btnText: {
marginLeft: 5,
color: colors.white,
fontSize: 16,
fontWeight: 'bold',
},
errorIconContainer: {
alignItems: 'center',
marginBottom: 10,
},
errorIcon: {
backgroundColor: colors.red4,
borderRadius: 30,
width: 50,
height: 50,
alignItems: 'center',
justifyContent: 'center',
marginRight: 5,
},
})

View File

@ -9,17 +9,13 @@ import {
} from 'react-native' } from 'react-native'
import {colors} from '../../lib/styles' import {colors} from '../../lib/styles'
export interface SelectorItem {
label: string
}
export function Selector({ export function Selector({
style, style,
items, items,
onSelect, onSelect,
}: { }: {
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
items: SelectorItem[] items: string[]
onSelect?: (index: number) => void onSelect?: (index: number) => void
}) { }) {
const [selectedIndex, setSelectedIndex] = useState<number>(0) const [selectedIndex, setSelectedIndex] = useState<number>(0)
@ -36,7 +32,7 @@ export function Selector({
<TouchableWithoutFeedback key={i} onPress={() => onPressItem(i)}> <TouchableWithoutFeedback key={i} onPress={() => onPressItem(i)}>
<View style={selected ? styles.itemSelected : styles.item}> <View style={selected ? styles.itemSelected : styles.item}>
<Text style={selected ? styles.labelSelected : styles.label}> <Text style={selected ? styles.labelSelected : styles.label}>
{item.label} {item}
</Text> </Text>
</View> </View>
</TouchableWithoutFeedback> </TouchableWithoutFeedback>

View File

@ -5,6 +5,7 @@ import {faAngleRight} from '@fortawesome/free-solid-svg-icons/faAngleRight'
import {faArrowLeft} from '@fortawesome/free-solid-svg-icons/faArrowLeft' import {faArrowLeft} from '@fortawesome/free-solid-svg-icons/faArrowLeft'
import {faArrowUpFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowUpFromBracket' import {faArrowUpFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowUpFromBracket'
import {faArrowUpRightFromSquare} from '@fortawesome/free-solid-svg-icons/faArrowUpRightFromSquare' import {faArrowUpRightFromSquare} from '@fortawesome/free-solid-svg-icons/faArrowUpRightFromSquare'
import {faArrowsRotate} from '@fortawesome/free-solid-svg-icons/faArrowsRotate'
import {faBars} from '@fortawesome/free-solid-svg-icons/faBars' import {faBars} from '@fortawesome/free-solid-svg-icons/faBars'
import {faBell} from '@fortawesome/free-solid-svg-icons/faBell' import {faBell} from '@fortawesome/free-solid-svg-icons/faBell'
import {faBell as farBell} from '@fortawesome/free-regular-svg-icons/faBell' import {faBell as farBell} from '@fortawesome/free-regular-svg-icons/faBell'
@ -39,6 +40,7 @@ export function setup() {
faArrowLeft, faArrowLeft,
faArrowUpFromBracket, faArrowUpFromBracket,
faArrowUpRightFromSquare, faArrowUpRightFromSquare,
faArrowsRotate,
faBars, faBars,
faBell, faBell,
farBell, farBell,

View File

@ -1,7 +1,7 @@
import React, {useState, useEffect} from 'react' import React, {useState, useEffect} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {Feed} from '../com/feed/Feed' import {Feed} from '../com/posts/Feed'
import {FAB} from '../com/util/FloatingActionButton' import {FAB} from '../com/util/FloatingActionButton'
import {useStores} from '../../state' import {useStores} from '../../state'
import {FeedViewModel} from '../../state/models/feed-view' import {FeedViewModel} from '../../state/models/feed-view'

View File

@ -1,52 +1,213 @@
import React, {useState, useEffect} from 'react' import React, {useState, useEffect} from 'react'
import {View, StyleSheet} from 'react-native' import {SectionList, StyleSheet, Text, View} from 'react-native'
import {FeedViewModel} from '../../state/models/feed-view' import {observer} from 'mobx-react-lite'
import {ProfileUiModel, SECTION_IDS} from '../../state/models/profile-ui'
import {FeedViewItemModel} from '../../state/models/feed-view'
import {useStores} from '../../state' import {useStores} from '../../state'
import {ProfileHeader} from '../com/profile/ProfileHeader' import {ProfileHeader} from '../com/profile/ProfileHeader'
import {Feed} from '../com/feed/Feed' import {FeedItem} from '../com/posts/FeedItem'
import {Selector} from '../com/util/Selector'
import {ErrorScreen} from '../com/util/ErrorScreen'
import {ErrorMessage} from '../com/util/ErrorMessage'
import {s, colors} from '../lib/styles'
import {ScreenParams} from '../routes' import {ScreenParams} from '../routes'
export const Profile = ({visible, params}: ScreenParams) => { const SECTION_HEADER_ITEM = Symbol('SectionHeaderItem')
const LOADING_ITEM = Symbol('LoadingItem')
const EMPTY_ITEM = Symbol('EmptyItem')
const END_ITEM = Symbol('EndItem')
interface RenderItemParams {
item: any
index: number
section: Section
}
interface ErrorItem {
error: string
}
interface Section {
data: any[]
keyExtractor?: (v: any) => string
renderItem: (params: RenderItemParams) => JSX.Element
}
export const Profile = observer(({visible, params}: ScreenParams) => {
const store = useStores() const store = useStores()
const [hasSetup, setHasSetup] = useState<boolean>(false) const [hasSetup, setHasSetup] = useState<boolean>(false)
const [feedView, setFeedView] = useState<FeedViewModel | undefined>() const [profileUiState, setProfileUiState] = useState<
ProfileUiModel | undefined
>()
useEffect(() => { useEffect(() => {
if (!visible) { if (!visible) {
return return
} }
const author = params.name const user = params.name
if (hasSetup) { if (hasSetup) {
console.log('Updating profile feed for', author) console.log('Updating profile for', user)
feedView?.update() profileUiState?.update()
} else { } else {
console.log('Fetching profile feed for', author) console.log('Fetching profile for', user)
const newFeedView = new FeedViewModel(store, {author}) store.nav.setTitle(user)
setFeedView(newFeedView) const newProfileUiState = new ProfileUiModel(store, {user})
newFeedView setProfileUiState(newProfileUiState)
.setup() newProfileUiState.setup().then(() => {
.catch(err => console.error('Failed to fetch feed', err)) setHasSetup(true)
.then(() => { })
setHasSetup(true)
store.nav.setTitle(author)
})
} }
}, [visible, params.name, store]) }, [visible, params.name, store])
return ( // events
<View style={styles.container}> // =
<ProfileHeader user={params.name} />
<View style={styles.feed}>{feedView && <Feed feed={feedView} />}</View> const onSelectViewSelector = (index: number) =>
profileUiState?.setSelectedViewIndex(index)
const onRefresh = () => {
profileUiState
?.refresh()
.catch((err: any) => console.error('Failed to refresh', err))
}
const onEndReached = () => {
profileUiState
?.loadMore()
.catch((err: any) => console.error('Failed to load more', err))
}
const onPressTryAgain = () => {
profileUiState?.setup()
}
// rendering
// =
const renderItem = (_params: RenderItemParams) => <View />
const renderLoadingItem = (_params: RenderItemParams) => (
<Text style={styles.loading}>Loading...</Text>
)
const renderErrorItem = ({item}: {item: ErrorItem}) => (
<View style={s.p5}>
<ErrorMessage message={item.error} onPressTryAgain={onPressTryAgain} />
</View> </View>
) )
} const renderEmptyItem = (_params: RenderItemParams) => (
<Text style={styles.loading}>No posts yet!</Text>
)
const renderProfileItem = (_params: RenderItemParams) => {
if (!profileUiState) {
return <View />
}
return <ProfileHeader view={profileUiState.profile} />
}
const renderSectionHeader = ({section}: {section: Section}) => {
if (section?.data?.[0] !== SECTION_HEADER_ITEM) {
return (
<Selector
items={ProfileUiModel.SELECTOR_ITEMS}
style={styles.selector}
onSelect={onSelectViewSelector}
/>
)
}
return <View />
}
const renderPostsItem = ({item}: {item: FeedViewItemModel | Symbol}) => {
if (item === END_ITEM || item instanceof Symbol) {
return <Text style={styles.endItem}>- end of feed -</Text>
}
return <FeedItem item={item} />
}
const renderBadgesItem = ({item}: {item: any}) => <Text>todo</Text>
const sections = [
{data: [SECTION_HEADER_ITEM], renderItem: renderProfileItem},
]
if (profileUiState) {
if (profileUiState.selectedViewIndex === SECTION_IDS.POSTS) {
if (profileUiState.isInitialLoading) {
sections.push({
data: [LOADING_ITEM],
renderItem: renderLoadingItem,
} as Section)
} else if (profileUiState.feed.hasError) {
sections.push({
data: [{error: profileUiState.feed.error}],
renderItem: renderErrorItem,
} as Section)
} else if (profileUiState.currentView.hasContent) {
const items: (FeedViewItemModel | Symbol)[] =
profileUiState.feed.feed.slice()
if (profileUiState.feed.hasReachedEnd) {
items.push(END_ITEM)
}
sections.push({
data: items,
renderItem: renderPostsItem,
keyExtractor: (item: FeedViewItemModel) => item._reactKey,
} as Section)
} else if (profileUiState.currentView.isEmpty) {
sections.push({
data: [EMPTY_ITEM],
renderItem: renderEmptyItem,
})
}
}
if (profileUiState.selectedViewIndex === SECTION_IDS.BADGES) {
sections.push({
data: [{}],
renderItem: renderBadgesItem,
} as Section)
}
}
return (
<View style={styles.container}>
<View style={styles.feed}>
{profileUiState &&
(profileUiState.profile.hasError ? (
<ErrorScreen
title="Failed to load profile"
message={`There was an issue when attempting to load ${params.name}`}
details={profileUiState.profile.error}
onPressTryAgain={onPressTryAgain}
/>
) : (
<SectionList
sections={sections}
renderSectionHeader={renderSectionHeader}
renderItem={renderItem}
refreshing={profileUiState.isRefreshing}
onRefresh={onRefresh}
onEndReached={onEndReached}
/>
))}
</View>
</View>
)
})
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flexDirection: 'column', flexDirection: 'column',
height: '100%', height: '100%',
}, },
selector: {
paddingTop: 8,
backgroundColor: colors.white,
borderBottomWidth: 1,
borderColor: colors.gray2,
},
feed: { feed: {
flex: 1, flex: 1,
}, },
loading: {
paddingVertical: 10,
paddingHorizontal: 14,
},
endItem: {
paddingTop: 20,
paddingBottom: 30,
color: colors.gray5,
textAlign: 'center',
},
}) })

View File

@ -12,9 +12,14 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {AVIS} from '../../lib/assets' import {AVIS} from '../../lib/assets'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
export function createAccountsMenu(): RootSiblings { export function createAccountsMenu({
debug_onPressItem,
}: {
debug_onPressItem: () => void
}): RootSiblings {
const onPressItem = (_index: number) => { const onPressItem = (_index: number) => {
sibling.destroy() sibling.destroy()
debug_onPressItem() // TODO
} }
const onOuterPress = () => sibling.destroy() const onOuterPress = () => sibling.destroy()
const sibling = new RootSiblings( const sibling = new RootSiblings(

View File

@ -99,7 +99,10 @@ export const MobileShell: React.FC = observer(() => {
const [isLocationMenuActive, setLocationMenuActive] = useState(false) const [isLocationMenuActive, setLocationMenuActive] = useState(false)
const screenRenderDesc = constructScreenRenderDesc(store.nav) const screenRenderDesc = constructScreenRenderDesc(store.nav)
const onPressAvi = () => createAccountsMenu() const onPressAvi = () =>
createAccountsMenu({
debug_onPressItem: () => store.nav.navigate('/profile/alice.com'),
})
const onPressLocation = () => setLocationMenuActive(true) const onPressLocation = () => setLocationMenuActive(true)
const onPressEllipsis = () => createLocationMenu() const onPressEllipsis = () => createLocationMenu()

View File

@ -1,7 +1,15 @@
Paul's todo list Paul's todo list
- General
- Update to RN 0.70
- Selector swipe gesture
- Composer - Composer
- Update the view after creating a post - Update the view after creating a post
- Profile
- Real badges
- Edit profile
- More button
- Followers & following as modal?
- Search view - Search view
- * - *
- Linking - Linking