Cache user icon to resolve flicker (#1255)

* Use FastImage to load & cache user icon
* Fetch user icon before leaving login screen
This commit is contained in:
Amanda Bullington
2024-03-15 09:49:41 +11:00
committed by GitHub
parent 9588c577aa
commit d79306ffee
13 changed files with 225 additions and 99 deletions

View File

@@ -17,6 +17,18 @@ PODS:
- hermes-engine/Pre-built (= 0.71.16)
- hermes-engine/Pre-built (0.71.16)
- libevent (2.1.12)
- libwebp (1.3.2):
- libwebp/demux (= 1.3.2)
- libwebp/mux (= 1.3.2)
- libwebp/sharpyuv (= 1.3.2)
- libwebp/webp (= 1.3.2)
- libwebp/demux (1.3.2):
- libwebp/webp
- libwebp/mux (1.3.2):
- libwebp/demux
- libwebp/sharpyuv (1.3.2)
- libwebp/webp (1.3.2):
- libwebp/sharpyuv
- RCT-Folly (2021.07.22.00):
- boost
- DoubleConversion
@@ -409,6 +421,10 @@ PODS:
- React-Core
- RNDeviceInfo (10.12.0):
- React-Core
- RNFastImage (8.6.3):
- React-Core
- SDWebImage (~> 5.11.1)
- SDWebImageWebPCoder (~> 0.8.4)
- RNFlashList (1.6.3):
- React-Core
- RNFS (2.20.0):
@@ -457,6 +473,12 @@ PODS:
- React-Core
- RNVectorIcons (9.2.0):
- React-Core
- SDWebImage (5.11.1):
- SDWebImage/Core (= 5.11.1)
- SDWebImage/Core (5.11.1)
- SDWebImageWebPCoder (0.8.5):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.10)
- VisionCamera (2.15.6):
- React
- React-callinvoker
@@ -528,6 +550,7 @@ DEPENDENCIES:
- "RNCPicker (from `../node_modules/@react-native-picker/picker`)"
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
- RNFastImage (from `../node_modules/react-native-fast-image`)
- "RNFlashList (from `../node_modules/@shopify/flash-list`)"
- RNFS (from `../node_modules/react-native-fs`)
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
@@ -547,6 +570,9 @@ SPEC REPOS:
trunk:
- fmt
- libevent
- libwebp
- SDWebImage
- SDWebImageWebPCoder
EXTERNAL SOURCES:
boost:
@@ -669,6 +695,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-community/datetimepicker"
RNDeviceInfo:
:path: "../node_modules/react-native-device-info"
RNFastImage:
:path: "../node_modules/react-native-fast-image"
RNFlashList:
:path: "../node_modules/@shopify/flash-list"
RNFS:
@@ -708,6 +736,7 @@ SPEC CHECKSUMS:
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
hermes-engine: 2382506846564caf4152c45390dc24f08fce7057
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
RCTRequired: 44a3cda52ccac0be738fbf43fef90f3546a48c52
RCTTypeSafety: da7fbf9826fc898ca8b10dc840f2685562039a64
@@ -761,6 +790,7 @@ SPEC CHECKSUMS:
RNCPicker: 32ca102146bc7d34a8b93a998d9938d9f9ec7898
RNDateTimePicker: ccd988deb223cbb2e669e157ec576c2c6217128c
RNDeviceInfo: db5c64a060e66e5db3102d041ebe3ef307a85120
RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8
RNFlashList: 4b4b6b093afc0df60ae08f9cbf6ccd4c836c667a
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNGestureHandler: 6e4dc6b7ab3a385386d4e36228bd065e5a611394
@@ -772,6 +802,8 @@ SPEC CHECKSUMS:
RNStoreReview: 31dbfd0dac2eea9675f0b84f1dd3261c2110c337
RNSVG: d00c8f91c3cbf6d476451313a18f04d220d4f396
RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
VisionCamera: 523b49054bee9dace64189ab6631cb41e8b83fe0
VisionCameraPluginInatVision: 7e09a4ca0b34dd81afd4b68aa26a27eff5bb8fd4
Yoga: e29645ec5a66fb00934fad85338742d1c247d4cb

16
package-lock.json generated
View File

@@ -60,6 +60,7 @@
"react-native-event-listeners": "^1.0.7",
"react-native-exception-handler": "^2.10.10",
"react-native-exif-reader": "github:inaturalist/react-native-exif-reader",
"react-native-fast-image": "^8.6.3",
"react-native-fs": "^2.20.0",
"react-native-geocoder-reborn": "^0.9.0",
"react-native-gesture-handler": "^2.13.0",
@@ -22801,6 +22802,15 @@
"react-native": "*"
}
},
"node_modules/react-native-fast-image": {
"version": "8.6.3",
"resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-8.6.3.tgz",
"integrity": "sha512-Sdw4ESidXCXOmQ9EcYguNY2swyoWmx53kym2zRsvi+VeFCHEdkO+WG1DK+6W81juot40bbfLNhkc63QnWtesNg==",
"peerDependencies": {
"react": "^17 || ^18",
"react-native": ">=0.60.0"
}
},
"node_modules/react-native-fs": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.20.0.tgz",
@@ -42885,6 +42895,12 @@
"from": "react-native-exif-reader@github:inaturalist/react-native-exif-reader",
"requires": {}
},
"react-native-fast-image": {
"version": "8.6.3",
"resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-8.6.3.tgz",
"integrity": "sha512-Sdw4ESidXCXOmQ9EcYguNY2swyoWmx53kym2zRsvi+VeFCHEdkO+WG1DK+6W81juot40bbfLNhkc63QnWtesNg==",
"requires": {}
},
"react-native-fs": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.20.0.tgz",

View File

@@ -85,6 +85,7 @@
"react-native-event-listeners": "^1.0.7",
"react-native-exception-handler": "^2.10.10",
"react-native-exif-reader": "github:inaturalist/react-native-exif-reader",
"react-native-fast-image": "^8.6.3",
"react-native-fs": "^2.20.0",
"react-native-geocoder-reborn": "^0.9.0",
"react-native-gesture-handler": "^2.13.0",

View File

@@ -42,6 +42,7 @@ type Props = {
const App = ( { children }: Props ): Node => {
const realm = useRealm( );
const currentUser = useCurrentUser( );
useIconicTaxa( { reload: true } );
useReactQueryRefetch( );
useFreshInstall( currentUser );

View File

@@ -1,5 +1,6 @@
// @flow
import { getUserAgent } from "api/userAgent";
import { fetchUserMe } from "api/users";
import { create } from "apisauce";
import axios from "axios";
import i18next from "i18next";
@@ -348,9 +349,23 @@ const authenticateUser = async (
// Save userId to local, encrypted storage
const currentUser = { id: userId, login: remoteUsername, signedIn: true };
logger.debug( "writing current user to realm: ", currentUser );
// try to fetch user data (especially for loading user icon) from userMe
const apiToken = await getJWT( );
const options = {
api_token: apiToken
};
const remoteUser = await fetchUserMe( { }, options );
const localUser = remoteUser
? {
...remoteUser,
signedIn: true
}
: currentUser;
logger.debug( "writing current user to realm: ", localUser );
safeRealmWrite( realm, ( ) => {
realm.create( "User", currentUser, "modified" );
realm.create( "User", localUser, "modified" );
}, "saving current user in AuthenticationService" );
const currentRealmUser = User.currentUser( realm );
logger.debug( "Signed in", currentRealmUser.login, currentRealmUser.id, currentRealmUser );

View File

@@ -1,8 +1,9 @@
// @flow
import classNames from "classnames";
import { Image } from "components/styledComponents";
import * as React from "react";
import { FastImage as Image } from "components/styledComponents";
import type { Node } from "react";
import React from "react";
import colors from "styles/tailwindColors";
type Props = {
@@ -15,7 +16,7 @@ type Props = {
const UserIcon = ( {
uri, small, active, large, medium
}: Props ): React.Node => {
}: Props ): Node => {
const getSize = ( ) => {
if ( small ) {
return "w-[22px] h-[22px]";
@@ -34,6 +35,7 @@ const UserIcon = ( {
"rounded-full",
size
);
// For unknown reasons, the green border doesn't show up on Android using nativewind classNames
// but it works with style, might warrant further investigation or an issue in nativewind
const style = { borderColor: colors.inatGreen, borderWidth: 3 };

View File

@@ -17,6 +17,8 @@ import {
TextInput as UntyledTextInput,
View as UnstyledView
} from "react-native";
// eslint-disable-next-line import/no-extraneous-dependencies
import UnstyledFastImage from "react-native-fast-image";
import UnstyledLinearGradient from "react-native-linear-gradient";
// $FlowIgnore
@@ -68,8 +70,12 @@ const fontMonoClass: string = ( Platform.OS === "ios"
// $FlowIgnore
const LinearGradient = styled( UnstyledLinearGradient );
// $FlowIgnore
const FastImage = styled( UnstyledFastImage );
export {
BottomSheetTextInput,
FastImage,
fontMonoClass,
Image,
ImageBackground,

View File

@@ -13,11 +13,10 @@ const OBS_LIST_SCREEN_ID = "ObservationsStackNavigator";
const NOTIFICATIONS_SCREEN_ID = "Notifications";
type Props = {
navigation: Object,
isOnline: boolean
navigation: Object
};
const CustomTabBarContainer = ( { navigation, isOnline }: Props ): Node => {
const CustomTabBarContainer = ( { navigation }: Props ): Node => {
const { t } = useTranslation( );
const currentUser = useCurrentUser( );
const [activeTab, setActiveTab] = useState( OBS_LIST_SCREEN_ID );
@@ -29,8 +28,6 @@ const CustomTabBarContainer = ( { navigation, isOnline }: Props ): Node => {
testID: DRAWER_ID,
accessibilityLabel: t( "Open-drawer" ),
accessibilityHint: t( "Opens-the-side-drawer-menu" ),
width: 44,
height: 44,
size: 32,
onPress: ( ) => {
navigation.openDrawer( );
@@ -43,8 +40,6 @@ const CustomTabBarContainer = ( { navigation, isOnline }: Props ): Node => {
testID: EXPLORE_SCREEN_ID,
accessibilityLabel: t( "Explore" ),
accessibilityHint: t( "Navigates-to-explore" ),
width: 44,
height: 44,
size: 40,
onPress: ( ) => {
navigation.navigate( "ObservationsStackNavigator", {
@@ -56,16 +51,10 @@ const CustomTabBarContainer = ( { navigation, isOnline }: Props ): Node => {
},
{
icon: "person",
userIconUri: isOnline
? User.uri( currentUser )
: null,
testID: User.uri( currentUser ) && isOnline
? "NavButton.avatar"
: "NavButton.personIcon",
userIconUri: User.uri( currentUser ),
testID: "NavButton.personIcon",
accessibilityLabel: t( "Observations" ),
accessibilityHint: t( "Navigates-to-observations" ),
width: 44,
height: 44,
size: 40,
onPress: ( ) => {
navigation.navigate( "ObservationsStackNavigator", {
@@ -80,8 +69,6 @@ const CustomTabBarContainer = ( { navigation, isOnline }: Props ): Node => {
testID: NOTIFICATIONS_SCREEN_ID,
accessibilityLabel: t( "Notifications" ),
accessibilityHint: t( "Navigates-to-notifications" ),
width: 44,
height: 44,
size: 32,
onPress: ( ) => {
navigation.reset( {
@@ -94,7 +81,6 @@ const CustomTabBarContainer = ( { navigation, isOnline }: Props ): Node => {
}
] ), [
activeTab,
isOnline,
currentUser,
isDrawerOpen,
navigation,

View File

@@ -29,8 +29,8 @@ const NavButton = ( {
accessibilityLabel,
accessibilityHint,
accessibilityRole = "tab",
width,
height
width = 44,
height = 44
}: Props ): React.Node => {
/* eslint-disable react/jsx-props-no-spreading */
const sharedProps = {

View File

@@ -17,7 +17,6 @@ import DeveloperStackNavigator from "navigation/StackNavigators/DeveloperStackNa
import ObservationsStackNavigator from "navigation/StackNavigators/ObservationsStackNavigator";
import ProjectsStackNavigator from "navigation/StackNavigators/ProjectsStackNavigator";
import React from "react";
import { useIsConnected } from "sharedHooks";
import CustomTabBarContainer from "./CustomTabBarContainer";
@@ -28,8 +27,7 @@ const OBS_LIST_SCREEN_ID = "ObservationsStackNavigator";
/* eslint-disable react/jsx-props-no-spreading */
const BottomTabs = ( ) => {
const isOnline = useIsConnected( );
const renderTabBar = props => <CustomTabBarContainer {...props} isOnline={isOnline} />;
const renderTabBar = props => <CustomTabBarContainer {...props} />;
const aboutTitle = () => <Heading4>{t( "ABOUT-INATURALIST" )}</Heading4>;
const donateTitle = () => <Heading4>{t( "DONATE" )}</Heading4>;

View File

@@ -3,7 +3,11 @@ import { fetchUserMe } from "api/users";
import { RealmContext } from "providers/contexts";
import { useCallback, useEffect } from "react";
import safeRealmWrite from "sharedHelpers/safeRealmWrite";
import { useAuthenticatedQuery, useCurrentUser, useIsConnected } from "sharedHooks";
import {
useAuthenticatedQuery,
useCurrentUser,
useIsConnected
} from "sharedHooks";
const { useRealm } = RealmContext;

View File

@@ -378,3 +378,11 @@ inatjs.announcements.search.mockResolvedValue( makeResponse( ) );
inatjs.observations.updates.mockResolvedValue( makeResponse( ) );
jest.mock( "react-native-audio-recorder-player", ( ) => MockAudioRecorderPlayer );
jest.mock( "react-native-fast-image", ( ) => {
const actualNav = jest.requireActual( "react-native-fast-image" );
return {
...actualNav,
preload: jest.fn( )
};
} );

View File

@@ -1,98 +1,155 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UserIcon displays active user image correctly 1`] = `
<Image
accessibilityIgnoresInvertColors={true}
accessibilityRole="image"
source={
{
"uri": "some_uri",
}
}
<View
style={
[
{
"overflow": "hidden",
},
[
{
"borderBottomLeftRadius": 9999,
"borderBottomRightRadius": 9999,
"borderTopLeftRadius": 9999,
"borderTopRightRadius": 9999,
},
{
"width": 40,
},
{
"height": 40,
},
{
"borderColor": "#74AC00",
"borderWidth": 3,
},
[
{
"borderBottomLeftRadius": 9999,
"borderBottomRightRadius": 9999,
"borderTopLeftRadius": 9999,
"borderTopRightRadius": 9999,
},
{
"width": 40,
},
{
"height": 40,
},
{
"borderColor": "#74AC00",
"borderWidth": 3,
},
],
],
]
}
testID="UserIcon.photo"
/>
>
<FastImageView
accessibilityIgnoresInvertColors={true}
accessibilityRole="image"
defaultSource={null}
resizeMode="cover"
source={
{
"uri": "some_uri",
}
}
style={
{
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
testID="UserIcon.photo"
/>
</View>
`;
exports[`UserIcon displays small user image correctly 1`] = `
<Image
accessibilityIgnoresInvertColors={true}
accessibilityRole="image"
source={
{
"uri": "some_uri",
}
}
<View
style={
[
{
"overflow": "hidden",
},
[
{
"borderBottomLeftRadius": 9999,
"borderBottomRightRadius": 9999,
"borderTopLeftRadius": 9999,
"borderTopRightRadius": 9999,
},
{
"width": 22,
},
{
"height": 22,
},
[
{
"borderBottomLeftRadius": 9999,
"borderBottomRightRadius": 9999,
"borderTopLeftRadius": 9999,
"borderTopRightRadius": 9999,
},
{
"width": 22,
},
{
"height": 22,
},
],
],
]
}
testID="UserIcon.photo"
/>
>
<FastImageView
accessibilityIgnoresInvertColors={true}
accessibilityRole="image"
defaultSource={null}
resizeMode="cover"
source={
{
"uri": "some_uri",
}
}
style={
{
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
testID="UserIcon.photo"
/>
</View>
`;
exports[`UserIcon displays user image correctly 1`] = `
<Image
accessibilityIgnoresInvertColors={true}
accessibilityRole="image"
source={
{
"uri": "some_uri",
}
}
<View
style={
[
{
"overflow": "hidden",
},
[
{
"borderBottomLeftRadius": 9999,
"borderBottomRightRadius": 9999,
"borderTopLeftRadius": 9999,
"borderTopRightRadius": 9999,
},
{
"width": 40,
},
{
"height": 40,
},
[
{
"borderBottomLeftRadius": 9999,
"borderBottomRightRadius": 9999,
"borderTopLeftRadius": 9999,
"borderTopRightRadius": 9999,
},
{
"width": 40,
},
{
"height": 40,
},
],
],
]
}
testID="UserIcon.photo"
/>
>
<FastImageView
accessibilityIgnoresInvertColors={true}
accessibilityRole="image"
defaultSource={null}
resizeMode="cover"
source={
{
"uri": "some_uri",
}
}
style={
{
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
testID="UserIcon.photo"
/>
</View>
`;