about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2022-11-05 11:58:48 -0500
committerPaul Frazee <pfrazee@gmail.com>2022-11-05 11:58:48 -0500
commit60b1c53d8571dfcb0b60e530e67ca311da82370a (patch)
tree853293eb23e2433572bbfbaba1783b46b3fc1084
parent3f730f1173a3c27a6bd54b91f48ecb9220a42730 (diff)
downloadvoidsky-60b1c53d8571dfcb0b60e530e67ca311da82370a.tar.zst
Add actor types to the profiles and clean up the UI
-rw-r--r--src/state/models/profile-ui.ts41
-rw-r--r--src/state/models/profile-view.ts12
-rw-r--r--src/view/com/profile/ProfileHeader.tsx168
-rw-r--r--src/view/screens/Profile.tsx72
4 files changed, 185 insertions, 108 deletions
diff --git a/src/state/models/profile-ui.ts b/src/state/models/profile-ui.ts
index 830dc22b1..0ad893dd1 100644
--- a/src/state/models/profile-ui.ts
+++ b/src/state/models/profile-ui.ts
@@ -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
   // =
 
diff --git a/src/state/models/profile-view.ts b/src/state/models/profile-view.ts
index ebb75bdb6..09f1991e1 100644
--- a/src/state/models/profile-view.ts
+++ b/src/state/models/profile-view.ts
@@ -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
   // =
 
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index e1b46f4c9..d492aa1f3 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -102,24 +102,11 @@ export const ProfileHeader = observer(function ProfileHeader({
         />
       </View>
       <View style={styles.content}>
-        <View style={[styles.displayNameLine]}>
-          <Text style={styles.displayName}>{view.displayName}</Text>
-        </View>
-        {
-          undefined /*<View style={styles.badgesLine}>
-          <FontAwesomeIcon icon="shield" style={s.mr5} size={12} />
-          <Link href="/" title="Badge TODO">
-            <Text style={[s.f12, s.bold]}>
-              Employee <Text style={[s.blue3]}>@blueskyweb.xyz</Text>
-            </Text>
-          </Link>
-        </View>*/
-        }
         <View style={[styles.buttonsLine]}>
           {isMe ? (
             <TouchableOpacity
               onPress={onPressEditProfile}
-              style={[styles.mainBtn, styles.btn]}>
+              style={[styles.btn, styles.mainBtn]}>
               <Text style={[s.fw400, s.f14]}>Edit Profile</Text>
             </TouchableOpacity>
           ) : (
@@ -127,28 +114,22 @@ export const ProfileHeader = observer(function ProfileHeader({
               {view.myState.follow ? (
                 <TouchableOpacity
                   onPress={onPressToggleFollow}
-                  style={[styles.mainBtn, styles.btn]}>
+                  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}
-                  style={[styles.mainBtn, styles.btn]}>
-                  <FontAwesomeIcon icon="rss" style={[s.mr5]} size={13} />
-                  <Text style={[s.fw400, s.f14]}>Follow</Text>
+                <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, 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
@@ -157,7 +138,18 @@ export const ProfileHeader = observer(function ProfileHeader({
             <FontAwesomeIcon icon="ellipsis" style={[s.gray5]} />
           </TouchableOpacity>
         </View>
-        <View style={[s.flexRow]}>
+        <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}>
@@ -166,20 +158,42 @@ export const ProfileHeader = observer(function ProfileHeader({
               {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.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.mt10, s.f15, s['lh15-1.3']]}>{view.description}</Text>
+          <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} />
+          <Link href="/" title="Badge TODO">
+            <Text style={[s.f12, s.bold]}>
+              Employee <Text style={[s.blue3]}>@blueskyweb.xyz</Text>
+            </Text>
+          </Link>
+        </View>*/
+        }
       </View>
     </View>
   )
@@ -222,46 +236,70 @@ const styles = StyleSheet.create({
     paddingHorizontal: 14,
     paddingBottom: 4,
   },
-  displayNameLine: {
-    paddingLeft: 86,
-    marginBottom: 14,
-  },
-  displayName: {
-    fontSize: 24,
-    fontWeight: 'bold',
-  },
-  badgesLine: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    marginBottom: 10,
-  },
+
   buttonsLine: {
     flexDirection: 'row',
+    marginLeft: 'auto',
     marginBottom: 12,
   },
-  followBtn: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
+  gradientBtn: {
+    paddingHorizontal: 24,
     paddingVertical: 6,
-    borderRadius: 6,
-    marginRight: 6,
+  },
+  mainBtn: {
+    paddingHorizontal: 24,
+  },
+  secondaryBtn: {
+    paddingHorizontal: 14,
   },
   btn: {
-    flex: 1,
+    flexDirection: 'row',
     alignItems: 'center',
     justifyContent: 'center',
     paddingVertical: 7,
-    borderRadius: 4,
+    borderRadius: 50,
     backgroundColor: colors.gray1,
-    marginRight: 6,
+    marginLeft: 6,
   },
-  mainBtn: {
+
+  displayNameLine: {
+    // paddingLeft: 86,
+    // marginBottom: 14,
+  },
+  displayName: {
+    fontSize: 24,
+    fontWeight: 'bold',
+  },
+
+  handleLine: {
     flexDirection: 'row',
+    marginBottom: 8,
   },
-  secondaryBtn: {
-    flex: 0,
-    paddingHorizontal: 14,
-    marginRight: 0,
+  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,
   },
 })
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index f5f4f553e..6f7281bd9 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -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}