Add actor types to the profiles and clean up the UI
parent
3f730f1173
commit
60b1c53d85
|
@ -3,19 +3,21 @@ import {RootStoreModel} from './root-store'
|
|||
import {ProfileViewModel} from './profile-view'
|
||||
import {FeedModel} from './feed-view'
|
||||
|
||||
export const SECTION_IDS = {
|
||||
POSTS: 0,
|
||||
BADGES: 1,
|
||||
export enum Sections {
|
||||
Posts = 'Posts',
|
||||
Scenes = 'Scenes',
|
||||
Trending = 'Trending',
|
||||
Members = 'Members',
|
||||
}
|
||||
|
||||
const USER_SELECTOR_ITEMS = [Sections.Posts, Sections.Scenes]
|
||||
const SCENE_SELECTOR_ITEMS = [Sections.Trending, Sections.Members]
|
||||
|
||||
export interface ProfileUiParams {
|
||||
user: string
|
||||
}
|
||||
|
||||
export class ProfileUiModel {
|
||||
// constants
|
||||
static SELECTOR_ITEMS = ['Posts', 'Scenes']
|
||||
|
||||
// data
|
||||
profile: ProfileViewModel
|
||||
feed: FeedModel
|
||||
|
@ -43,7 +45,10 @@ export class ProfileUiModel {
|
|||
}
|
||||
|
||||
get currentView(): FeedModel {
|
||||
if (this.selectedViewIndex === SECTION_IDS.POSTS) {
|
||||
if (
|
||||
this.selectedView === Sections.Posts ||
|
||||
this.selectedView === Sections.Trending
|
||||
) {
|
||||
return this.feed
|
||||
}
|
||||
throw new Error(`Invalid selector value: ${this.selectedViewIndex}`)
|
||||
|
@ -58,6 +63,28 @@ export class ProfileUiModel {
|
|||
return this.profile.isRefreshing || this.currentView.isRefreshing
|
||||
}
|
||||
|
||||
get isUser() {
|
||||
return this.profile.isUser
|
||||
}
|
||||
|
||||
get isScene() {
|
||||
return this.profile.isScene
|
||||
}
|
||||
|
||||
get selectorItems() {
|
||||
if (this.isUser) {
|
||||
return USER_SELECTOR_ITEMS
|
||||
} else if (this.isScene) {
|
||||
return SCENE_SELECTOR_ITEMS
|
||||
} else {
|
||||
return USER_SELECTOR_ITEMS
|
||||
}
|
||||
}
|
||||
|
||||
get selectedView() {
|
||||
return this.selectorItems[this.selectedViewIndex]
|
||||
}
|
||||
|
||||
// public api
|
||||
// =
|
||||
|
||||
|
|
|
@ -4,6 +4,9 @@ import * as Profile from '../../third-party/api/src/client/types/app/bsky/actor/
|
|||
import {RootStoreModel} from './root-store'
|
||||
import * as apilib from '../lib/api'
|
||||
|
||||
export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser'
|
||||
export const ACTOR_TYPE_SCENE = 'app.bsky.system.actorScene'
|
||||
|
||||
export class ProfileViewMyStateModel {
|
||||
follow?: string
|
||||
|
||||
|
@ -23,6 +26,7 @@ export class ProfileViewModel {
|
|||
// data
|
||||
did: string = ''
|
||||
handle: string = ''
|
||||
actorType = ACTOR_TYPE_USER
|
||||
displayName?: string
|
||||
description?: string
|
||||
followersCount: number = 0
|
||||
|
@ -57,6 +61,14 @@ export class ProfileViewModel {
|
|||
return this.hasLoaded && !this.hasContent
|
||||
}
|
||||
|
||||
get isUser() {
|
||||
return this.actorType === ACTOR_TYPE_USER
|
||||
}
|
||||
|
||||
get isScene() {
|
||||
return this.actorType === ACTOR_TYPE_SCENE
|
||||
}
|
||||
|
||||
// public api
|
||||
// =
|
||||
|
||||
|
|
|
@ -102,9 +102,88 @@ export const ProfileHeader = observer(function ProfileHeader({
|
|||
/>
|
||||
</View>
|
||||
<View style={styles.content}>
|
||||
<View style={[styles.displayNameLine]}>
|
||||
<View style={[styles.buttonsLine]}>
|
||||
{isMe ? (
|
||||
<TouchableOpacity
|
||||
onPress={onPressEditProfile}
|
||||
style={[styles.btn, styles.mainBtn]}>
|
||||
<Text style={[s.fw400, s.f14]}>Edit Profile</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<>
|
||||
{view.myState.follow ? (
|
||||
<TouchableOpacity
|
||||
onPress={onPressToggleFollow}
|
||||
style={[styles.btn, styles.mainBtn]}>
|
||||
<FontAwesomeIcon icon="check" style={[s.mr5]} size={14} />
|
||||
<Text style={[s.fw400, s.f14]}>Following</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity onPress={onPressToggleFollow}>
|
||||
<LinearGradient
|
||||
colors={[gradients.primary.start, gradients.primary.end]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={[styles.btn, styles.gradientBtn]}>
|
||||
<FontAwesomeIcon icon="plus" style={[s.white, s.mr5]} />
|
||||
<Text style={[s.white, s.fw600, s.f16]}>Follow</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
onPress={onPressMenu}
|
||||
style={[styles.btn, styles.secondaryBtn]}>
|
||||
<FontAwesomeIcon icon="ellipsis" style={[s.gray5]} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.displayNameLine}>
|
||||
<Text style={styles.displayName}>{view.displayName}</Text>
|
||||
</View>
|
||||
<View style={styles.handleLine}>
|
||||
{view.isScene ? (
|
||||
<View style={styles.typeLabelWrapper}>
|
||||
<Text style={styles.typeLabel}>Scene</Text>
|
||||
</View>
|
||||
) : undefined}
|
||||
<Text style={styles.handle}>@{view.handle}</Text>
|
||||
</View>
|
||||
<View style={styles.metricsLine}>
|
||||
<TouchableOpacity
|
||||
style={[s.flexRow, s.mr10]}
|
||||
onPress={onPressFollowers}>
|
||||
<Text style={[s.bold, s.mr2]}>{view.followersCount}</Text>
|
||||
<Text style={s.gray5}>
|
||||
{pluralize(view.followersCount, 'follower')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{view.isUser ? (
|
||||
<TouchableOpacity
|
||||
style={[s.flexRow, s.mr10]}
|
||||
onPress={onPressFollows}>
|
||||
<Text style={[s.bold, s.mr2]}>{view.followsCount}</Text>
|
||||
<Text style={s.gray5}>following</Text>
|
||||
</TouchableOpacity>
|
||||
) : undefined}
|
||||
{view.isScene ? (
|
||||
<TouchableOpacity
|
||||
style={[s.flexRow, s.mr10]}
|
||||
onPress={onPressFollows}>
|
||||
<Text style={[s.bold, s.mr2]}>{view.followsCount}</Text>
|
||||
<Text style={s.gray5}>
|
||||
{pluralize(view.followsCount, 'member')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : undefined}
|
||||
<View style={[s.flexRow, s.mr10]}>
|
||||
<Text style={[s.bold, s.mr2]}>{view.postsCount}</Text>
|
||||
<Text style={s.gray5}>{pluralize(view.postsCount, 'post')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
{view.description && (
|
||||
<Text style={[s.mb5, s.f15, s['lh15-1.3']]}>{view.description}</Text>
|
||||
)}
|
||||
{
|
||||
undefined /*<View style={styles.badgesLine}>
|
||||
<FontAwesomeIcon icon="shield" style={s.mr5} size={12} />
|
||||
|
@ -115,71 +194,6 @@ export const ProfileHeader = observer(function ProfileHeader({
|
|||
</Link>
|
||||
</View>*/
|
||||
}
|
||||
<View style={[styles.buttonsLine]}>
|
||||
{isMe ? (
|
||||
<TouchableOpacity
|
||||
onPress={onPressEditProfile}
|
||||
style={[styles.mainBtn, styles.btn]}>
|
||||
<Text style={[s.fw400, s.f14]}>Edit Profile</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<>
|
||||
{view.myState.follow ? (
|
||||
<TouchableOpacity
|
||||
onPress={onPressToggleFollow}
|
||||
style={[styles.mainBtn, styles.btn]}>
|
||||
<FontAwesomeIcon icon="check" style={[s.mr5]} size={14} />
|
||||
<Text style={[s.fw400, s.f14]}>Following</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
onPress={onPressToggleFollow}
|
||||
style={[styles.mainBtn, styles.btn]}>
|
||||
<FontAwesomeIcon icon="rss" style={[s.mr5]} size={13} />
|
||||
<Text style={[s.fw400, s.f14]}>Follow</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
onPress={onPressMenu}
|
||||
style={[styles.btn, styles.secondaryBtn, s.mr5]}>
|
||||
<FontAwesomeIcon icon="user-plus" style={[s.gray5]} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={onPressMenu}
|
||||
style={[styles.btn, styles.secondaryBtn, s.mr5]}>
|
||||
<FontAwesomeIcon icon="note-sticky" style={[s.gray5]} />
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
onPress={onPressMenu}
|
||||
style={[styles.btn, styles.secondaryBtn]}>
|
||||
<FontAwesomeIcon icon="ellipsis" style={[s.gray5]} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={[s.flexRow]}>
|
||||
<TouchableOpacity
|
||||
style={[s.flexRow, s.mr10]}
|
||||
onPress={onPressFollowers}>
|
||||
<Text style={[s.bold, s.mr2]}>{view.followersCount}</Text>
|
||||
<Text style={s.gray5}>
|
||||
{pluralize(view.followersCount, 'follower')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[s.flexRow, s.mr10]}
|
||||
onPress={onPressFollows}>
|
||||
<Text style={[s.bold, s.mr2]}>{view.followsCount}</Text>
|
||||
<Text style={s.gray5}>following</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={[s.flexRow, s.mr10]}>
|
||||
<Text style={[s.bold, s.mr2]}>{view.postsCount}</Text>
|
||||
<Text style={s.gray5}>{pluralize(view.postsCount, 'post')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
{view.description && (
|
||||
<Text style={[s.mt10, s.f15, s['lh15-1.3']]}>{view.description}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
@ -222,46 +236,70 @@ const styles = StyleSheet.create({
|
|||
paddingHorizontal: 14,
|
||||
paddingBottom: 4,
|
||||
},
|
||||
|
||||
buttonsLine: {
|
||||
flexDirection: 'row',
|
||||
marginLeft: 'auto',
|
||||
marginBottom: 12,
|
||||
},
|
||||
gradientBtn: {
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 6,
|
||||
},
|
||||
mainBtn: {
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
secondaryBtn: {
|
||||
paddingHorizontal: 14,
|
||||
},
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 7,
|
||||
borderRadius: 50,
|
||||
backgroundColor: colors.gray1,
|
||||
marginLeft: 6,
|
||||
},
|
||||
|
||||
displayNameLine: {
|
||||
paddingLeft: 86,
|
||||
marginBottom: 14,
|
||||
// paddingLeft: 86,
|
||||
// marginBottom: 14,
|
||||
},
|
||||
displayName: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
|
||||
handleLine: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 8,
|
||||
},
|
||||
handle: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: colors.gray5,
|
||||
},
|
||||
typeLabelWrapper: {
|
||||
backgroundColor: colors.gray1,
|
||||
paddingHorizontal: 4,
|
||||
borderRadius: 4,
|
||||
marginRight: 5,
|
||||
},
|
||||
typeLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: colors.gray5,
|
||||
},
|
||||
|
||||
metricsLine: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 8,
|
||||
},
|
||||
|
||||
badgesLine: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 10,
|
||||
},
|
||||
buttonsLine: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 12,
|
||||
},
|
||||
followBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 6,
|
||||
borderRadius: 6,
|
||||
marginRight: 6,
|
||||
},
|
||||
btn: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 7,
|
||||
borderRadius: 4,
|
||||
backgroundColor: colors.gray1,
|
||||
marginRight: 6,
|
||||
},
|
||||
mainBtn: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
secondaryBtn: {
|
||||
flex: 0,
|
||||
paddingHorizontal: 14,
|
||||
marginRight: 0,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React, {useEffect, useState} from 'react'
|
||||
import React, {useEffect, useState, useMemo} from 'react'
|
||||
import {StyleSheet, Text, View} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {ViewSelector} from '../com/util/ViewSelector'
|
||||
import {ScreenParams} from '../routes'
|
||||
import {ProfileUiModel, SECTION_IDS} from '../../state/models/profile-ui'
|
||||
import {ProfileUiModel, Sections} from '../../state/models/profile-ui'
|
||||
import {useStores} from '../../state'
|
||||
import {ProfileHeader} from '../com/profile/ProfileHeader'
|
||||
import {FeedItem} from '../com/posts/FeedItem'
|
||||
|
@ -18,25 +18,23 @@ const EMPTY_ITEM = {_reactKey: '__empty__'}
|
|||
export const Profile = observer(({visible, params}: ScreenParams) => {
|
||||
const store = useStores()
|
||||
const [hasSetup, setHasSetup] = useState<boolean>(false)
|
||||
const [profileUiState, setProfileUiState] = useState<
|
||||
ProfileUiModel | undefined
|
||||
>()
|
||||
const uiState = useMemo(
|
||||
() => new ProfileUiModel(store, {user: params.name}),
|
||||
[params.user],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let aborted = false
|
||||
if (!visible) {
|
||||
return
|
||||
}
|
||||
const user = params.name
|
||||
if (hasSetup) {
|
||||
console.log('Updating profile for', user)
|
||||
profileUiState?.update()
|
||||
console.log('Updating profile for', params.name)
|
||||
uiState.update()
|
||||
} else {
|
||||
console.log('Fetching profile for', user)
|
||||
store.nav.setTitle(user)
|
||||
const newProfileUiState = new ProfileUiModel(store, {user})
|
||||
setProfileUiState(newProfileUiState)
|
||||
newProfileUiState.setup().then(() => {
|
||||
console.log('Fetching profile for', params.name)
|
||||
store.nav.setTitle(params.name)
|
||||
uiState.setup().then(() => {
|
||||
if (aborted) return
|
||||
setHasSetup(true)
|
||||
})
|
||||
|
@ -50,42 +48,45 @@ export const Profile = observer(({visible, params}: ScreenParams) => {
|
|||
// =
|
||||
|
||||
const onSelectView = (index: number) => {
|
||||
profileUiState?.setSelectedViewIndex(index)
|
||||
uiState.setSelectedViewIndex(index)
|
||||
}
|
||||
const onRefresh = () => {
|
||||
profileUiState
|
||||
?.refresh()
|
||||
uiState
|
||||
.refresh()
|
||||
.catch((err: any) => console.error('Failed to refresh', err))
|
||||
}
|
||||
const onEndReached = () => {
|
||||
profileUiState
|
||||
?.loadMore()
|
||||
uiState
|
||||
.loadMore()
|
||||
.catch((err: any) => console.error('Failed to load more', err))
|
||||
}
|
||||
const onPressTryAgain = () => {
|
||||
profileUiState?.setup()
|
||||
uiState.setup()
|
||||
}
|
||||
|
||||
// rendering
|
||||
// =
|
||||
|
||||
const renderHeader = () => {
|
||||
if (!profileUiState) {
|
||||
if (!uiState) {
|
||||
return <View />
|
||||
}
|
||||
return <ProfileHeader view={profileUiState.profile} />
|
||||
return <ProfileHeader view={uiState.profile} />
|
||||
}
|
||||
let renderItem
|
||||
let items: any[] = []
|
||||
if (profileUiState) {
|
||||
if (profileUiState.selectedViewIndex === SECTION_IDS.POSTS) {
|
||||
if (profileUiState.isInitialLoading) {
|
||||
if (uiState) {
|
||||
if (
|
||||
uiState.selectedView === Sections.Posts ||
|
||||
uiState.selectedView === Sections.Trending
|
||||
) {
|
||||
if (uiState.isInitialLoading) {
|
||||
items.push(LOADING_ITEM)
|
||||
renderItem = () => <Text style={styles.loading}>Loading...</Text>
|
||||
} else if (profileUiState.feed.hasError) {
|
||||
} else if (uiState.feed.hasError) {
|
||||
items.push({
|
||||
_reactKey: '__error__',
|
||||
error: profileUiState.feed.error,
|
||||
error: uiState.feed.error,
|
||||
})
|
||||
renderItem = (item: any) => (
|
||||
<View style={s.p5}>
|
||||
|
@ -95,9 +96,9 @@ export const Profile = observer(({visible, params}: ScreenParams) => {
|
|||
/>
|
||||
</View>
|
||||
)
|
||||
} else if (profileUiState.currentView.hasContent) {
|
||||
items = profileUiState.feed.feed.slice()
|
||||
if (profileUiState.feed.hasReachedEnd) {
|
||||
} else if (uiState.currentView.hasContent) {
|
||||
items = uiState.feed.feed.slice()
|
||||
if (uiState.feed.hasReachedEnd) {
|
||||
items.push(END_ITEM)
|
||||
}
|
||||
renderItem = (item: any) => {
|
||||
|
@ -106,12 +107,11 @@ export const Profile = observer(({visible, params}: ScreenParams) => {
|
|||
}
|
||||
return <FeedItem item={item} />
|
||||
}
|
||||
} else if (profileUiState.currentView.isEmpty) {
|
||||
} else if (uiState.currentView.isEmpty) {
|
||||
items.push(EMPTY_ITEM)
|
||||
renderItem = () => <Text style={styles.loading}>No posts yet!</Text>
|
||||
}
|
||||
}
|
||||
if (profileUiState.selectedViewIndex === SECTION_IDS.BADGES) {
|
||||
} else {
|
||||
items.push(EMPTY_ITEM)
|
||||
renderItem = () => <Text>TODO</Text>
|
||||
}
|
||||
|
@ -122,20 +122,20 @@ export const Profile = observer(({visible, params}: ScreenParams) => {
|
|||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{profileUiState?.profile.hasError ? (
|
||||
{uiState.profile.hasError ? (
|
||||
<ErrorScreen
|
||||
title="Failed to load profile"
|
||||
message={`There was an issue when attempting to load ${params.name}`}
|
||||
details={profileUiState.profile.error}
|
||||
details={uiState.profile.error}
|
||||
onPressTryAgain={onPressTryAgain}
|
||||
/>
|
||||
) : (
|
||||
<ViewSelector
|
||||
sections={ProfileUiModel.SELECTOR_ITEMS}
|
||||
sections={uiState.selectorItems}
|
||||
items={items}
|
||||
renderHeader={renderHeader}
|
||||
renderItem={renderItem}
|
||||
refreshing={profileUiState?.isRefreshing || false}
|
||||
refreshing={uiState.isRefreshing || false}
|
||||
onSelectView={onSelectView}
|
||||
onRefresh={onRefresh}
|
||||
onEndReached={onEndReached}
|
||||
|
|
Loading…
Reference in New Issue