Rework profile page to include working view selector
parent
2ec09ba545
commit
bb06ef4f6e
|
@ -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() {}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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}
|
|
@ -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,
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
})
|
|
@ -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,
|
||||||
|
},
|
||||||
|
})
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue