about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/view/com/notifications/FeedItem.tsx14
-rw-r--r--src/view/com/post-thread/PostLikedBy.tsx18
-rw-r--r--src/view/com/post-thread/PostRepostedBy.tsx8
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx20
-rw-r--r--src/view/com/post/Post.tsx15
-rw-r--r--src/view/com/posts/FeedItem.tsx20
-rw-r--r--src/view/com/profile/ProfileFollowers.tsx14
-rw-r--r--src/view/com/profile/ProfileFollows.tsx8
-rw-r--r--src/view/com/profile/ProfileHeader.tsx15
-rw-r--r--src/view/com/util/UserAvatar.tsx78
-rw-r--r--src/view/screens/Settings.tsx10
-rw-r--r--src/view/shell/mobile/MainMenu.tsx12
-rw-r--r--src/view/shell/mobile/TabsSelector.tsx1
13 files changed, 159 insertions, 74 deletions
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index c498dd607..8d53e921b 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -6,7 +6,7 @@ import {FontAwesomeIcon, Props} from '@fortawesome/react-native-fontawesome'
 import {NotificationsViewItemModel} from '../../../state/models/notifications-view'
 import {s, colors} from '../../lib/styles'
 import {ago, pluralize} from '../../lib/strings'
-import {DEF_AVATER} from '../../lib/assets'
+import {UserAvatar} from '../util/UserAvatar'
 import {PostText} from '../post/PostText'
 import {Post} from '../post/Post'
 import {Link} from '../util/Link'
@@ -114,7 +114,11 @@ export const FeedItem = observer(function FeedItem({
                 key={author.href}
                 href={author.href}
                 title={`@${author.name}`}>
-                <Image style={styles.avi} source={DEF_AVATER} />
+                <UserAvatar
+                  size={30}
+                  displayName={author.displayName}
+                  name={author.name}
+                />
               </Link>
             ))}
             {authors.length > MAX_AUTHORS ? (
@@ -197,12 +201,6 @@ const styles = StyleSheet.create({
     flexDirection: 'row',
     alignItems: 'center',
   },
-  avi: {
-    width: 30,
-    height: 30,
-    borderRadius: 15,
-    resizeMode: 'cover',
-  },
   aviExtraCount: {
     fontWeight: 'bold',
     paddingLeft: 6,
diff --git a/src/view/com/post-thread/PostLikedBy.tsx b/src/view/com/post-thread/PostLikedBy.tsx
index fbeb52eea..071e69fac 100644
--- a/src/view/com/post-thread/PostLikedBy.tsx
+++ b/src/view/com/post-thread/PostLikedBy.tsx
@@ -1,22 +1,14 @@
 import React, {useState, useEffect} from 'react'
 import {observer} from 'mobx-react-lite'
-import {
-  ActivityIndicator,
-  FlatList,
-  Image,
-  StyleSheet,
-  Text,
-  TouchableOpacity,
-  View,
-} from 'react-native'
+import {ActivityIndicator, FlatList, StyleSheet, Text, View} from 'react-native'
 import {
   LikedByViewModel,
   LikedByViewItemModel,
 } from '../../../state/models/liked-by-view'
 import {Link} from '../util/Link'
+import {UserAvatar} from '../util/UserAvatar'
 import {useStores} from '../../../state'
 import {s, colors} from '../../lib/styles'
-import {DEF_AVATER} from '../../lib/assets'
 
 export const PostLikedBy = observer(function PostLikedBy({uri}: {uri: string}) {
   const store = useStores()
@@ -78,7 +70,11 @@ const LikedByItem = ({item}: {item: LikedByViewItemModel}) => {
     <Link style={styles.outer} href={`/profile/${item.name}`} title={item.name}>
       <View style={styles.layout}>
         <View style={styles.layoutAvi}>
-          <Image style={styles.avi} source={DEF_AVATER} />
+          <UserAvatar
+            size={40}
+            displayName={item.displayName}
+            name={item.name}
+          />
         </View>
         <View style={styles.layoutContent}>
           <Text style={[s.f15, s.bold]}>{item.displayName}</Text>
diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx
index 6ffe3a2c4..df462c1a1 100644
--- a/src/view/com/post-thread/PostRepostedBy.tsx
+++ b/src/view/com/post-thread/PostRepostedBy.tsx
@@ -12,10 +12,10 @@ import {
   RepostedByViewModel,
   RepostedByViewItemModel,
 } from '../../../state/models/reposted-by-view'
+import {UserAvatar} from '../util/UserAvatar'
 import {Link} from '../util/Link'
 import {useStores} from '../../../state'
 import {s, colors} from '../../lib/styles'
-import {DEF_AVATER} from '../../lib/assets'
 
 export const PostRepostedBy = observer(function PostRepostedBy({
   uri,
@@ -83,7 +83,11 @@ const RepostedByItem = ({item}: {item: RepostedByViewItemModel}) => {
     <Link style={styles.outer} href={`/profile/${item.name}`} title={item.name}>
       <View style={styles.layout}>
         <View style={styles.layoutAvi}>
-          <Image style={styles.avi} source={DEF_AVATER} />
+          <UserAvatar
+            size={40}
+            displayName={item.displayName}
+            name={item.name}
+          />
         </View>
         <View style={styles.layoutContent}>
           <Text style={[s.f15, s.bold]}>{item.displayName}</Text>
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index e8fdd91af..4f0683f09 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -10,9 +10,9 @@ import {ComposePostModel} from '../../../state/models/shell'
 import {Link} from '../util/Link'
 import {RichText} from '../util/RichText'
 import {PostDropdownBtn} from '../util/DropdownBtn'
+import {UserAvatar} from '../util/UserAvatar'
 import {s, colors} from '../../lib/styles'
 import {ago, pluralize} from '../../lib/strings'
-import {DEF_AVATER} from '../../lib/assets'
 import {useStores} from '../../../state'
 
 const PARENT_REPLY_LINE_LENGTH = 8
@@ -116,7 +116,11 @@ export const PostThreadItem = observer(function PostThreadItem({
       <View style={styles.outer}>
         <View style={styles.layout}>
           <Link style={styles.layoutAvi} href={authorHref} title={authorTitle}>
-            <Image style={styles.avi} source={DEF_AVATER} />
+            <UserAvatar
+              size={50}
+              displayName={item.author.displayName}
+              name={item.author.name}
+            />
           </Link>
           <View style={styles.layoutContent}>
             <View style={[styles.meta, s.mt5]}>
@@ -231,7 +235,11 @@ export const PostThreadItem = observer(function PostThreadItem({
         )}
         <View style={styles.layout}>
           <Link style={styles.layoutAvi} href={authorHref} title={authorTitle}>
-            <Image style={styles.avi} source={DEF_AVATER} />
+            <UserAvatar
+              size={50}
+              displayName={item.author.displayName}
+              name={item.author.name}
+            />
           </Link>
           <View style={styles.layoutContent}>
             {item.replyingToAuthor &&
@@ -321,12 +329,6 @@ const styles = StyleSheet.create({
     paddingTop: 10,
     paddingBottom: 10,
   },
-  avi: {
-    width: 50,
-    height: 50,
-    borderRadius: 25,
-    resizeMode: 'cover',
-  },
   layoutContent: {
     flex: 1,
     paddingRight: 10,
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 42611a39a..b74bbfc42 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -4,7 +4,6 @@ import {AtUri} from '../../../third-party/uri'
 import * as PostType from '../../../third-party/api/src/types/app/bsky/post'
 import {
   ActivityIndicator,
-  Image,
   StyleSheet,
   Text,
   TouchableOpacity,
@@ -16,10 +15,10 @@ import {ComposePostModel} from '../../../state/models/shell'
 import {Link} from '../util/Link'
 import {UserInfoText} from '../util/UserInfoText'
 import {RichText} from '../util/RichText'
+import {UserAvatar} from '../util/UserAvatar'
 import {useStores} from '../../../state'
 import {s, colors} from '../../lib/styles'
 import {ago} from '../../lib/strings'
-import {DEF_AVATER} from '../../lib/assets'
 
 export const Post = observer(function Post({uri}: {uri: string}) {
   const store = useStores()
@@ -91,7 +90,11 @@ export const Post = observer(function Post({uri}: {uri: string}) {
     <Link style={styles.outer} href={itemHref} title={itemTitle}>
       <View style={styles.layout}>
         <Link style={styles.layoutAvi} href={authorHref} title={authorTitle}>
-          <Image style={styles.avi} source={DEF_AVATER} />
+          <UserAvatar
+            size={50}
+            displayName={item.author.displayName}
+            name={item.author.name}
+          />
         </Link>
         <View style={styles.layoutContent}>
           <View style={styles.meta}>
@@ -185,12 +188,6 @@ const styles = StyleSheet.create({
   layoutAvi: {
     width: 60,
   },
-  avi: {
-    width: 50,
-    height: 50,
-    borderRadius: 25,
-    resizeMode: 'cover',
-  },
   layoutContent: {
     flex: 1,
   },
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index 6eb2b38fb..cfb7d7ed7 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -1,24 +1,24 @@
 import React, {useMemo} from 'react'
 import {observer} from 'mobx-react-lite'
-import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
+import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
 import {AtUri} from '../../../third-party/uri'
 import * as PostType from '../../../third-party/api/src/types/app/bsky/post'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {FeedViewItemModel} from '../../../state/models/feed-view'
+import {FeedItemModel} from '../../../state/models/feed-view'
 import {ComposePostModel, SharePostModel} from '../../../state/models/shell'
 import {Link} from '../util/Link'
 import {PostDropdownBtn} from '../util/DropdownBtn'
 import {UserInfoText} from '../util/UserInfoText'
 import {RichText} from '../util/RichText'
+import {UserAvatar} from '../util/UserAvatar'
 import {s, colors} from '../../lib/styles'
 import {ago} from '../../lib/strings'
-import {DEF_AVATER} from '../../lib/assets'
 import {useStores} from '../../../state'
 
 export const FeedItem = observer(function FeedItem({
   item,
 }: {
-  item: FeedViewItemModel
+  item: FeedItemModel
 }) {
   const store = useStores()
   const record = item.record as unknown as PostType.Record
@@ -73,7 +73,11 @@ export const FeedItem = observer(function FeedItem({
           style={styles.layoutAvi}
           href={authorHref}
           title={item.author.name}>
-          <Image style={styles.avi} source={DEF_AVATER} />
+          <UserAvatar
+            size={50}
+            displayName={item.author.displayName}
+            name={item.author.name}
+          />
         </Link>
         <View style={styles.layoutContent}>
           <View style={styles.meta}>
@@ -199,12 +203,6 @@ const styles = StyleSheet.create({
     width: 60,
     paddingTop: 5,
   },
-  avi: {
-    width: 50,
-    height: 50,
-    borderRadius: 25,
-    resizeMode: 'cover',
-  },
   layoutContent: {
     flex: 1,
   },
diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx
index 24f28f645..30145e7b5 100644
--- a/src/view/com/profile/ProfileFollowers.tsx
+++ b/src/view/com/profile/ProfileFollowers.tsx
@@ -13,9 +13,9 @@ import {
   FollowerItem,
 } from '../../../state/models/user-followers-view'
 import {Link} from '../util/Link'
+import {UserAvatar} from '../util/UserAvatar'
 import {useStores} from '../../../state'
 import {s, colors} from '../../lib/styles'
-import {DEF_AVATER} from '../../lib/assets'
 
 export const ProfileFollowers = observer(function ProfileFollowers({
   name,
@@ -81,7 +81,11 @@ const User = ({item}: {item: FollowerItem}) => {
     <Link style={styles.outer} href={`/profile/${item.name}`} title={item.name}>
       <View style={styles.layout}>
         <View style={styles.layoutAvi}>
-          <Image style={styles.avi} source={DEF_AVATER} />
+          <UserAvatar
+            size={40}
+            displayName={item.displayName}
+            name={item.name}
+          />
         </View>
         <View style={styles.layoutContent}>
           <Text style={[s.f15, s.bold]}>{item.displayName}</Text>
@@ -106,12 +110,6 @@ const styles = StyleSheet.create({
     paddingTop: 10,
     paddingBottom: 10,
   },
-  avi: {
-    width: 40,
-    height: 40,
-    borderRadius: 20,
-    resizeMode: 'cover',
-  },
   layoutContent: {
     flex: 1,
     paddingRight: 10,
diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx
index 719765edf..56a5371ba 100644
--- a/src/view/com/profile/ProfileFollows.tsx
+++ b/src/view/com/profile/ProfileFollows.tsx
@@ -14,8 +14,8 @@ import {
 } from '../../../state/models/user-follows-view'
 import {useStores} from '../../../state'
 import {Link} from '../util/Link'
+import {UserAvatar} from '../util/UserAvatar'
 import {s, colors} from '../../lib/styles'
-import {DEF_AVATER} from '../../lib/assets'
 
 export const ProfileFollows = observer(function ProfileFollows({
   name,
@@ -81,7 +81,11 @@ const User = ({item}: {item: FollowItem}) => {
     <Link style={styles.outer} href={`/profile/${item.name}`} title={item.name}>
       <View style={styles.layout}>
         <View style={styles.layoutAvi}>
-          <Image style={styles.avi} source={DEF_AVATER} />
+          <UserAvatar
+            size={40}
+            displayName={item.displayName}
+            name={item.name}
+          />
         </View>
         <View style={styles.layoutContent}>
           <Text style={[s.f15, s.bold]}>{item.displayName}</Text>
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index 9565ae4a8..6778663a3 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -15,8 +15,9 @@ import {useStores} from '../../../state'
 import {EditProfileModel} from '../../../state/models/shell'
 import {pluralize} from '../../lib/strings'
 import {s, gradients, colors} from '../../lib/styles'
-import {DEF_AVATER, BANNER} from '../../lib/assets'
+import {BANNER} from '../../lib/assets'
 import Toast from '../util/Toast'
+import {UserAvatar} from '../util/UserAvatar'
 import {Link} from '../util/Link'
 
 export const ProfileHeader = observer(function ProfileHeader({
@@ -81,7 +82,9 @@ export const ProfileHeader = observer(function ProfileHeader({
   return (
     <View style={styles.outer}>
       <Image style={styles.banner} source={BANNER} />
-      <Image style={styles.avi} source={DEF_AVATER} />
+      <View style={styles.avi}>
+        <UserAvatar size={80} displayName={view.displayName} name={view.name} />
+      </View>
       <View style={styles.content}>
         <View style={[styles.displayNameLine]}>
           <Text style={styles.displayName}>{view.displayName}</Text>
@@ -178,12 +181,12 @@ const styles = StyleSheet.create({
     position: 'absolute',
     top: 80,
     left: 10,
-    width: 80,
-    height: 80,
-    borderRadius: 40,
-    resizeMode: 'cover',
+    width: 84,
+    height: 84,
+    borderRadius: 42,
     borderWidth: 2,
     borderColor: colors.white,
+    backgroundColor: colors.white,
   },
   content: {
     paddingTop: 8,
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
new file mode 100644
index 000000000..b38f1158c
--- /dev/null
+++ b/src/view/com/util/UserAvatar.tsx
@@ -0,0 +1,78 @@
+import React from 'react'
+import Svg, {Circle, Text, Defs, LinearGradient, Stop} from 'react-native-svg'
+import {colors} from '../../lib/styles'
+
+const GRADIENTS = [
+  [colors.pink3, colors.purple3],
+  [colors.purple3, colors.blue3],
+  [colors.blue3, colors.green3],
+  [colors.red3, colors.pink3],
+]
+
+export function UserAvatar({
+  size,
+  displayName,
+  name,
+}: {
+  size: number
+  displayName: string | undefined
+  name: string
+}) {
+  const initials = getInitials(displayName || name)
+  const gi = cyrb53(name) % GRADIENTS.length
+  return (
+    <Svg width={size} height={size} viewBox="0 0 100 100">
+      <Defs>
+        <LinearGradient id="grad" x1="0" y1="0" x2="1" y2="1">
+          <Stop offset="0" stopColor={GRADIENTS[gi][0]} stopOpacity="1" />
+          <Stop offset="1" stopColor={GRADIENTS[gi][1]} stopOpacity="1" />
+        </LinearGradient>
+      </Defs>
+      <Circle cx="50" cy="50" r="50" fill="url(#grad)" />
+      <Text
+        fill="white"
+        fontSize="50"
+        fontWeight="bold"
+        x="50"
+        y="67"
+        textAnchor="middle">
+        {initials}
+      </Text>
+    </Svg>
+  )
+}
+
+function getInitials(str: string): string {
+  const tokens = str
+    .split(' ')
+    .filter(Boolean)
+    .map(v => v.trim())
+  if (tokens.length >= 2 && tokens[0][0] && tokens[0][1]) {
+    return tokens[0][0].toUpperCase() + tokens[1][0].toUpperCase()
+  }
+  if (tokens.length === 1 && tokens[0][0]) {
+    return tokens[0][0].toUpperCase()
+  }
+  return 'X'
+}
+
+// deterministic string->hash
+// https://stackoverflow.com/a/52171480
+function cyrb53(str: string, seed = 0): number {
+  let h1 = 0xdeadbeef ^ seed,
+    h2 = 0x41c6ce57 ^ seed
+  for (let i = 0, ch; i < str.length; i++) {
+    ch = str.charCodeAt(i)
+    h1 = Math.imul(h1 ^ ch, 2654435761)
+    h2 = Math.imul(h2 ^ ch, 1597334677)
+  }
+
+  h1 =
+    Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^
+    Math.imul(h2 ^ (h2 >>> 13), 3266489909)
+  h2 =
+    Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
+    Math.imul(h1 ^ (h1 >>> 13), 3266489909)
+
+  return 4294967296 * (2097151 & h2) + (h1 >>> 0)
+}
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index 0f7d6b89b..2438ea75d 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -4,8 +4,8 @@ import {observer} from 'mobx-react-lite'
 import {useStores} from '../../state'
 import {ScreenParams} from '../routes'
 import {s, colors} from '../lib/styles'
-import {DEF_AVATER} from '../lib/assets'
 import {Link} from '../com/util/Link'
+import {UserAvatar} from '../com/util/UserAvatar'
 
 export const Settings = observer(function Settings({visible}: ScreenParams) {
   const store = useStores()
@@ -33,8 +33,12 @@ export const Settings = observer(function Settings({visible}: ScreenParams) {
       </View>
       <Link href={`/profile/${store.me.name}`} title="Your profile">
         <View style={styles.profile}>
-          <Image style={styles.avi} source={DEF_AVATER} />
-          <View>
+          <UserAvatar
+            size={40}
+            displayName={store.me.displayName}
+            name={store.me.name || ''}
+          />
+          <View style={[s.ml10]}>
             <Text style={[s.f18]}>{store.me.displayName}</Text>
             <Text style={[s.gray5]}>@{store.me.name}</Text>
           </View>
diff --git a/src/view/shell/mobile/MainMenu.tsx b/src/view/shell/mobile/MainMenu.tsx
index ee09d4aa0..85cf23716 100644
--- a/src/view/shell/mobile/MainMenu.tsx
+++ b/src/view/shell/mobile/MainMenu.tsx
@@ -19,6 +19,7 @@ import Animated, {
 import {IconProp} from '@fortawesome/fontawesome-svg-core'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {HomeIcon, UserGroupIcon} from '../../lib/icons'
+import {UserAvatar} from '../../com/util/UserAvatar'
 import {useStores} from '../../../state'
 import {s, colors} from '../../lib/styles'
 import {DEF_AVATER} from '../../lib/assets'
@@ -131,7 +132,13 @@ export const MainMenu = observer(
               <TouchableOpacity
                 style={styles.profile}
                 onPress={() => onNavigate(`/profile/${store.me.name || ''}`)}>
-                <Image style={styles.profileImage} source={DEF_AVATER} />
+                <View style={styles.profileImage}>
+                  <UserAvatar
+                    size={30}
+                    displayName={store.me.displayName}
+                    name={store.me.name || ''}
+                  />
+                </View>
                 <Text style={styles.profileText} numberOfLines={1}>
                   {store.me.displayName || store.me.name || 'My profile'}
                 </Text>
@@ -231,9 +238,6 @@ const styles = StyleSheet.create({
     alignItems: 'center',
   },
   profileImage: {
-    borderRadius: 15,
-    width: 30,
-    height: 30,
     marginRight: 8,
   },
   profileText: {
diff --git a/src/view/shell/mobile/TabsSelector.tsx b/src/view/shell/mobile/TabsSelector.tsx
index 8e3fba41a..18c5f086b 100644
--- a/src/view/shell/mobile/TabsSelector.tsx
+++ b/src/view/shell/mobile/TabsSelector.tsx
@@ -21,7 +21,6 @@ import Swipeable from 'react-native-gesture-handler/Swipeable'
 import LinearGradient from 'react-native-linear-gradient'
 import {useStores} from '../../../state'
 import {s, colors, gradients} from '../../lib/styles'
-import {DEF_AVATER} from '../../lib/assets'
 import {match} from '../../routes'
 import {LinkActionsModel} from '../../../state/models/shell'