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 }