From 4ae6fbd3c8e8be9d47d0bd959aeac380f7bf67ce Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Tue, 15 Nov 2022 12:07:41 -0600
Subject: [PATCH] Better loading screens

---
 src/state/models/post.ts                 |  5 +-
 src/view/com/post/PostText.tsx           | 10 +++-
 src/view/com/util/LoadingPlaceholder.tsx | 73 ++++++++++++++++++++++++
 src/view/com/util/UserInfoText.tsx       | 30 ++++++----
 4 files changed, 101 insertions(+), 17 deletions(-)
 create mode 100644 src/view/com/util/LoadingPlaceholder.tsx

diff --git a/src/state/models/post.ts b/src/state/models/post.ts
index 7ecd6228..767182a9 100644
--- a/src/state/models/post.ts
+++ b/src/state/models/post.ts
@@ -2,6 +2,7 @@ import {makeAutoObservable} from 'mobx'
 import * as Post from '../../third-party/api/src/client/types/app/bsky/feed/post'
 import {AtUri} from '../../third-party/uri'
 import {RootStoreModel} from './root-store'
+import {cleanError} from '../../view/lib/strings'
 
 export type PostEntities = Post.Record['entities']
 export type PostReply = Post.Record['reply']
@@ -67,7 +68,7 @@ export class PostModel implements RemoveIndex<Post.Record> {
   private _xIdle(err: string = '') {
     this.isLoading = false
     this.hasLoaded = true
-    this.error = err
+    this.error = cleanError(err)
   }
 
   // loader functions
@@ -88,7 +89,7 @@ export class PostModel implements RemoveIndex<Post.Record> {
       this._replaceAll(res.value)
       this._xIdle()
     } catch (e: any) {
-      this._xIdle(`Failed to load post: ${e.toString()}`)
+      this._xIdle(e.toString())
     }
   }
 
diff --git a/src/view/com/post/PostText.tsx b/src/view/com/post/PostText.tsx
index 541f2fc1..5d6c4511 100644
--- a/src/view/com/post/PostText.tsx
+++ b/src/view/com/post/PostText.tsx
@@ -1,6 +1,8 @@
 import React, {useState, useEffect} from 'react'
 import {observer} from 'mobx-react-lite'
-import {ActivityIndicator, Text, View} from 'react-native'
+import {Text, View} from 'react-native'
+import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
+import {ErrorMessage} from '../util/ErrorMessage'
 import {PostModel} from '../../../state/models/post'
 import {useStores} from '../../../state'
 
@@ -28,7 +30,9 @@ export const PostText = observer(function PostText({
   if (!model || model.isLoading || model.uri !== uri) {
     return (
       <View>
-        <ActivityIndicator />
+        <LoadingPlaceholder width="100%" height={8} style={{marginTop: 6}} />
+        <LoadingPlaceholder width="100%" height={8} style={{marginTop: 6}} />
+        <LoadingPlaceholder width={100} height={8} style={{marginTop: 6}} />
       </View>
     )
   }
@@ -38,7 +42,7 @@ export const PostText = observer(function PostText({
   if (model.hasError) {
     return (
       <View>
-        <Text style={style}>{model.error}</Text>
+        <ErrorMessage style={style} message={model.error} />
       </View>
     )
   }
diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx
new file mode 100644
index 00000000..55b6ad1b
--- /dev/null
+++ b/src/view/com/util/LoadingPlaceholder.tsx
@@ -0,0 +1,73 @@
+import React, {useEffect, useMemo} from 'react'
+import {
+  Animated,
+  StyleProp,
+  useWindowDimensions,
+  View,
+  ViewStyle,
+} from 'react-native'
+import LinearGradient from 'react-native-linear-gradient'
+import {colors} from '../../lib/styles'
+
+export function LoadingPlaceholder({
+  width,
+  height,
+  style,
+}: {
+  width: string | number
+  height: string | number
+  style?: StyleProp<ViewStyle>
+}) {
+  const dim = useWindowDimensions()
+  const elWidth = typeof width === 'string' ? dim.width : width
+  const offset = useMemo(() => new Animated.Value(elWidth * -1), [])
+  useEffect(() => {
+    const anim = Animated.loop(
+      Animated.sequence([
+        Animated.timing(offset, {
+          toValue: elWidth,
+          duration: 1e3,
+          useNativeDriver: true,
+          isInteraction: false,
+        }),
+        Animated.timing(offset, {
+          toValue: elWidth * -1,
+          duration: 0,
+          delay: 500,
+          useNativeDriver: true,
+          isInteraction: false,
+        }),
+      ]),
+    )
+    anim.start()
+    return () => anim.stop()
+  }, [])
+
+  return (
+    <View
+      style={[
+        {
+          width,
+          height,
+          backgroundColor: colors.gray2,
+          borderRadius: 6,
+          overflow: 'hidden',
+        },
+        style,
+      ]}>
+      <Animated.View
+        style={{
+          width,
+          height,
+          transform: [{translateX: offset}],
+        }}>
+        <LinearGradient
+          colors={[colors.gray2, '#d4d2d2', colors.gray2]}
+          start={{x: 0, y: 0}}
+          end={{x: 1, y: 0}}
+          style={{width: '100%', height: '100%'}}
+        />
+      </Animated.View>
+    </View>
+  )
+}
diff --git a/src/view/com/util/UserInfoText.tsx b/src/view/com/util/UserInfoText.tsx
index f4dbd1fa..d1292cc7 100644
--- a/src/view/com/util/UserInfoText.tsx
+++ b/src/view/com/util/UserInfoText.tsx
@@ -2,6 +2,7 @@ import React, {useState, useEffect} from 'react'
 import * as GetProfile from '../../../third-party/api/src/client/types/app/bsky/actor/getProfile'
 import {StyleProp, Text, TextStyle} from 'react-native'
 import {Link} from './Link'
+import {LoadingPlaceholder} from './LoadingPlaceholder'
 import {useStores} from '../../../state'
 
 export function UserInfoText({
@@ -48,26 +49,31 @@ export function UserInfoText({
     }
   }, [did, store.api.app.bsky])
 
+  let inner
+  if (didFail) {
+    inner = <Text style={style}>{failed}</Text>
+  } else if (profile) {
+    inner = <Text style={style}>{`${prefix || ''}${profile[attr]}`}</Text>
+  } else {
+    inner = (
+      <LoadingPlaceholder
+        width={80}
+        height={8}
+        style={{position: 'relative', top: 1, left: 2}}
+      />
+    )
+  }
+
   if (asLink) {
     const title = profile?.displayName || profile?.handle || 'User'
     return (
       <Link
         href={`/profile/${profile?.handle ? profile.handle : did}`}
         title={title}>
-        <Text style={style}>
-          {didFail
-            ? failed
-            : profile
-            ? `${prefix || ''}${profile[attr]}`
-            : loading}
-        </Text>
+        {inner}
       </Link>
     )
   }
 
-  return (
-    <Text style={style}>
-      {didFail ? failed : profile ? `${prefix || ''}${profile[attr]}` : loading}
-    </Text>
-  )
+  return inner
 }