From d79306ffee92f6f531c8ce17dddad8d92d85d5d9 Mon Sep 17 00:00:00 2001
From: Amanda Bullington <35536439+albullington@users.noreply.github.com>
Date: Fri, 15 Mar 2024 09:49:41 +1100
Subject: [PATCH] Cache user icon to resolve flicker (#1255)
* Use FastImage to load & cache user icon
* Fetch user icon before leaving login screen
---
ios/Podfile.lock | 32 +++
package-lock.json | 16 ++
package.json | 1 +
src/components/App.js | 1 +
.../LoginSignUp/AuthenticationService.js | 19 +-
.../SharedComponents/UserIcon/UserIcon.js | 8 +-
src/components/styledComponents.js | 6 +
.../CustomTabBarContainer.js | 22 +-
.../BottomTabNavigator/NavButton.js | 4 +-
src/navigation/BottomTabNavigator/index.js | 4 +-
src/sharedHooks/useUserMe.js | 6 +-
tests/jest.setup.js | 8 +
.../__snapshots__/UserIcon.test.js.snap | 197 +++++++++++-------
13 files changed, 225 insertions(+), 99 deletions(-)
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index c1fdc276c..a6d7e6745 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -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
diff --git a/package-lock.json b/package-lock.json
index fcae4a6f4..d3c6c8973 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index d120b870b..5475feb04 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/App.js b/src/components/App.js
index c976f787f..df5ad0af8 100644
--- a/src/components/App.js
+++ b/src/components/App.js
@@ -42,6 +42,7 @@ type Props = {
const App = ( { children }: Props ): Node => {
const realm = useRealm( );
const currentUser = useCurrentUser( );
+
useIconicTaxa( { reload: true } );
useReactQueryRefetch( );
useFreshInstall( currentUser );
diff --git a/src/components/LoginSignUp/AuthenticationService.js b/src/components/LoginSignUp/AuthenticationService.js
index 25eb87e58..30243c51a 100644
--- a/src/components/LoginSignUp/AuthenticationService.js
+++ b/src/components/LoginSignUp/AuthenticationService.js
@@ -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 );
diff --git a/src/components/SharedComponents/UserIcon/UserIcon.js b/src/components/SharedComponents/UserIcon/UserIcon.js
index 1db82c68b..eb518daf4 100644
--- a/src/components/SharedComponents/UserIcon/UserIcon.js
+++ b/src/components/SharedComponents/UserIcon/UserIcon.js
@@ -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 };
diff --git a/src/components/styledComponents.js b/src/components/styledComponents.js
index f410010d3..e98516b88 100644
--- a/src/components/styledComponents.js
+++ b/src/components/styledComponents.js
@@ -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,
diff --git a/src/navigation/BottomTabNavigator/CustomTabBarContainer.js b/src/navigation/BottomTabNavigator/CustomTabBarContainer.js
index 34791401a..06cf585ce 100644
--- a/src/navigation/BottomTabNavigator/CustomTabBarContainer.js
+++ b/src/navigation/BottomTabNavigator/CustomTabBarContainer.js
@@ -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,
diff --git a/src/navigation/BottomTabNavigator/NavButton.js b/src/navigation/BottomTabNavigator/NavButton.js
index e2801fe83..d14b9e542 100644
--- a/src/navigation/BottomTabNavigator/NavButton.js
+++ b/src/navigation/BottomTabNavigator/NavButton.js
@@ -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 = {
diff --git a/src/navigation/BottomTabNavigator/index.js b/src/navigation/BottomTabNavigator/index.js
index b151b718e..ce49bde32 100644
--- a/src/navigation/BottomTabNavigator/index.js
+++ b/src/navigation/BottomTabNavigator/index.js
@@ -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 => ;
+ const renderTabBar = props => ;
const aboutTitle = () => {t( "ABOUT-INATURALIST" )};
const donateTitle = () => {t( "DONATE" )};
diff --git a/src/sharedHooks/useUserMe.js b/src/sharedHooks/useUserMe.js
index 9f914b45b..3e39286e3 100644
--- a/src/sharedHooks/useUserMe.js
+++ b/src/sharedHooks/useUserMe.js
@@ -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;
diff --git a/tests/jest.setup.js b/tests/jest.setup.js
index 2f4b0e4d1..5c278ffa1 100644
--- a/tests/jest.setup.js
+++ b/tests/jest.setup.js
@@ -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( )
+ };
+} );
diff --git a/tests/unit/components/SharedComponents/UserIcon/__snapshots__/UserIcon.test.js.snap b/tests/unit/components/SharedComponents/UserIcon/__snapshots__/UserIcon.test.js.snap
index 7d8118363..0704cf72f 100644
--- a/tests/unit/components/SharedComponents/UserIcon/__snapshots__/UserIcon.test.js.snap
+++ b/tests/unit/components/SharedComponents/UserIcon/__snapshots__/UserIcon.test.js.snap
@@ -1,98 +1,155 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UserIcon displays active user image correctly 1`] = `
-
+>
+
+
`;
exports[`UserIcon displays small user image correctly 1`] = `
-
+>
+
+
`;
exports[`UserIcon displays user image correctly 1`] = `
-
+>
+
+
`;