about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2022-09-07 16:00:25 -0500
committerPaul Frazee <pfrazee@gmail.com>2022-09-07 16:00:25 -0500
commit9010078489eae77c620a3bf4802ff6b417ea31f9 (patch)
tree11c5c03099a5cda82161d077efd3d50525dd8487
parent5ae39612d7e8484ffc5be6c7c5dc0f878985c676 (diff)
downloadvoidsky-9010078489eae77c620a3bf4802ff6b417ea31f9.tar.zst
Add EditProfile modal
-rw-r--r--src/state/lib/api.ts11
-rw-r--r--src/state/models/profile-view.ts8
-rw-r--r--src/state/models/shell.ts24
-rw-r--r--src/view/com/modals/EditProfile.tsx123
-rw-r--r--src/view/com/modals/Modal.tsx8
-rw-r--r--src/view/com/profile/ProfileHeader.tsx5
-rw-r--r--src/view/com/util/ErrorMessage.tsx1
-rw-r--r--src/view/lib/styles.ts2
-rw-r--r--todos.txt3
9 files changed, 177 insertions, 8 deletions
diff --git a/src/state/lib/api.ts b/src/state/lib/api.ts
index 64a88cdec..3324525bf 100644
--- a/src/state/lib/api.ts
+++ b/src/state/lib/api.ts
@@ -124,6 +124,17 @@ export async function unfollow(
   return numDels > 0
 }
 
+export async function updateProfile(
+  adx: AdxClient,
+  user: string,
+  profile: bsky.Profile.Record,
+) {
+  return await adx
+    .repo(user, true)
+    .collection('blueskyweb.xyz:Profiles')
+    .put('Profile', 'profile', {$type: 'blueskyweb.xyz:Profile', ...profile})
+}
+
 type WherePred = (_record: GetRecordResponseValidated) => Boolean
 async function deleteWhere(
   coll: AdxRepoCollectionClient,
diff --git a/src/state/models/profile-view.ts b/src/state/models/profile-view.ts
index 89c8a75d0..42c3737e9 100644
--- a/src/state/models/profile-view.ts
+++ b/src/state/models/profile-view.ts
@@ -89,6 +89,14 @@ export class ProfileViewModel implements bsky.ProfileView.Response {
     }
   }
 
+  async updateProfile(profile: bsky.Profile.Record) {
+    if (this.did !== this.rootStore.me.did) {
+      throw new Error('Not your profile!')
+    }
+    await apilib.updateProfile(this.rootStore.api, this.did, profile)
+    await this.refresh()
+  }
+
   // state transitions
   // =
 
diff --git a/src/state/models/shell.ts b/src/state/models/shell.ts
index c67b474b7..2dddb9a33 100644
--- a/src/state/models/shell.ts
+++ b/src/state/models/shell.ts
@@ -1,4 +1,5 @@
 import {makeAutoObservable} from 'mobx'
+import {ProfileViewModel} from './profile-view'
 
 export class LinkActionsModel {
   name = 'link-actions'
@@ -24,15 +25,34 @@ export class ComposePostModel {
   }
 }
 
+export class EditProfileModel {
+  name = 'edit-profile'
+
+  constructor(public profileView: ProfileViewModel) {
+    makeAutoObservable(this)
+  }
+}
+
 export class ShellModel {
   isModalActive = false
-  activeModal: LinkActionsModel | SharePostModel | ComposePostModel | undefined
+  activeModal:
+    | LinkActionsModel
+    | SharePostModel
+    | ComposePostModel
+    | EditProfileModel
+    | undefined
 
   constructor() {
     makeAutoObservable(this)
   }
 
-  openModal(modal: LinkActionsModel | SharePostModel | ComposePostModel) {
+  openModal(
+    modal:
+      | LinkActionsModel
+      | SharePostModel
+      | ComposePostModel
+      | EditProfileModel,
+  ) {
     this.isModalActive = true
     this.activeModal = modal
   }
diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx
new file mode 100644
index 000000000..72cbd4119
--- /dev/null
+++ b/src/view/com/modals/EditProfile.tsx
@@ -0,0 +1,123 @@
+import React, {useState} from 'react'
+import Toast from '../util/Toast'
+import {StyleSheet, Text, TextInput, TouchableOpacity, View} from 'react-native'
+import LinearGradient from 'react-native-linear-gradient'
+import {ErrorMessage} from '../util/ErrorMessage'
+import {useStores} from '../../../state'
+import {ProfileViewModel} from '../../../state/models/profile-view'
+import {s, colors, gradients} from '../../lib/styles'
+
+export const snapPoints = ['80%']
+
+export function Component({profileView}: {profileView: ProfileViewModel}) {
+  const store = useStores()
+  const [error, setError] = useState<string>('')
+  const [displayName, setDisplayName] = useState<string>(
+    profileView.displayName || '',
+  )
+  const [description, setDescription] = useState<string>(
+    profileView.description || '',
+  )
+  const onPressSave = async () => {
+    if (error) {
+      setError('')
+    }
+    try {
+      await profileView.updateProfile({
+        displayName,
+        description,
+      })
+      Toast.show('Profile updated', {
+        position: Toast.positions.TOP,
+      })
+      store.shell.closeModal()
+    } catch (e: any) {
+      console.error(e)
+      setError(
+        'Failed to save your profile. Check your internet connection and try again.',
+      )
+    }
+  }
+
+  return (
+    <View style={s.flex1}>
+      <Text style={[s.textCenter, s.bold, s.f16]}>Edit my profile</Text>
+      <View style={styles.inner}>
+        {error !== '' && (
+          <View style={s.mb10}>
+            <ErrorMessage message={error} />
+          </View>
+        )}
+        <View style={styles.group}>
+          <Text style={styles.label}>Display Name</Text>
+          <TextInput
+            style={styles.textInput}
+            placeholder="e.g. Alice Roberts"
+            value={displayName}
+            onChangeText={setDisplayName}
+          />
+        </View>
+        <View style={styles.group}>
+          <Text style={styles.label}>Biography</Text>
+          <TextInput
+            style={[styles.textArea]}
+            placeholder="e.g. Artist, dog-lover, and memelord."
+            multiline
+            value={description}
+            onChangeText={setDescription}
+          />
+        </View>
+        <TouchableOpacity style={s.mt10} onPress={onPressSave}>
+          <LinearGradient
+            colors={[gradients.primary.start, gradients.primary.end]}
+            start={{x: 0, y: 0}}
+            end={{x: 1, y: 1}}
+            style={[styles.btn]}>
+            <Text style={[s.white, s.bold]}>Save Changes</Text>
+          </LinearGradient>
+        </TouchableOpacity>
+      </View>
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  inner: {
+    padding: 14,
+  },
+  group: {
+    marginBottom: 10,
+  },
+  label: {
+    fontWeight: 'bold',
+    paddingHorizontal: 4,
+    paddingBottom: 4,
+  },
+  textInput: {
+    borderWidth: 1,
+    borderColor: colors.gray3,
+    borderRadius: 6,
+    paddingHorizontal: 14,
+    paddingVertical: 10,
+    fontSize: 16,
+  },
+  textArea: {
+    borderWidth: 1,
+    borderColor: colors.gray3,
+    borderRadius: 6,
+    paddingHorizontal: 12,
+    paddingTop: 10,
+    fontSize: 16,
+    height: 100,
+    textAlignVertical: 'top',
+  },
+  btn: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    width: '100%',
+    borderRadius: 32,
+    padding: 10,
+    marginBottom: 10,
+  },
+})
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index 6e0846000..73ac14469 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -10,6 +10,7 @@ import * as models from '../../../state/models/shell'
 import * as LinkActionsModal from './LinkActions'
 import * as SharePostModal from './SharePost.native'
 import * as ComposePostModal from './ComposePost'
+import * as EditProfile from './EditProfile'
 
 export const Modal = observer(function Modal() {
   const store = useStores()
@@ -50,6 +51,13 @@ export const Modal = observer(function Modal() {
         {...(store.shell.activeModal as models.ComposePostModel)}
       />
     )
+  } else if (store.shell.activeModal?.name === 'edit-profile') {
+    snapPoints = EditProfile.snapPoints
+    element = (
+      <EditProfile.Component
+        {...(store.shell.activeModal as models.EditProfileModel)}
+      />
+    )
   } else {
     return <View />
   }
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index 59af6b200..5ac161265 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -1,4 +1,4 @@
-import React, {useState, useEffect} from 'react'
+import React from 'react'
 import {observer} from 'mobx-react-lite'
 import {
   ActivityIndicator,
@@ -12,6 +12,7 @@ import LinearGradient from 'react-native-linear-gradient'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {ProfileViewModel} from '../../../state/models/profile-view'
 import {useStores} from '../../../state'
+import {EditProfileModel} from '../../../state/models/shell'
 import {pluralize} from '../../lib/strings'
 import {s, gradients, colors} from '../../lib/styles'
 import {AVIS, BANNER} from '../../lib/assets'
@@ -42,7 +43,7 @@ export const ProfileHeader = observer(function ProfileHeader({
     )
   }
   const onPressEditProfile = () => {
-    // TODO
+    store.shell.openModal(new EditProfileModel(view))
   }
   const onPressMenu = () => {
     // TODO
diff --git a/src/view/com/util/ErrorMessage.tsx b/src/view/com/util/ErrorMessage.tsx
index 7c8670da3..3acea1cab 100644
--- a/src/view/com/util/ErrorMessage.tsx
+++ b/src/view/com/util/ErrorMessage.tsx
@@ -35,7 +35,6 @@ export function ErrorMessage({
 
 const styles = StyleSheet.create({
   outer: {
-    flex: 1,
     flexDirection: 'row',
     alignItems: 'center',
     backgroundColor: colors.red1,
diff --git a/src/view/lib/styles.ts b/src/view/lib/styles.ts
index 17a24707b..ba6dc0de4 100644
--- a/src/view/lib/styles.ts
+++ b/src/view/lib/styles.ts
@@ -52,7 +52,7 @@ export const gradients = {
 export const s = StyleSheet.create({
   // font weights
   fw600: {fontWeight: '600'},
-  bold: {fontWeight: '600'},
+  bold: {fontWeight: 'bold'},
   fw500: {fontWeight: '500'},
   semiBold: {fontWeight: '500'},
   fw400: {fontWeight: '400'},
diff --git a/todos.txt b/todos.txt
index 0f453350c..91ccb43f6 100644
--- a/todos.txt
+++ b/todos.txt
@@ -7,9 +7,8 @@ Paul's todo list
   - Update the view after creating a post
 - Profile
   - Real badges
-  - Edit profile
+  - Edit profile: avatar, cover photo
   - More button
-  - Followers & following as modal?
 - Search view
   - *
 - Linking