Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Android nav #4362

Merged
merged 42 commits into from
Feb 2, 2021
Merged

feat: Android nav #4362

merged 42 commits into from
Feb 2, 2021

Conversation

ds300
Copy link
Contributor

@ds300 ds300 commented Jan 26, 2021

This PR resolves CX-873 CX-872 CX-874

Description

This PR introduces navigation stacks for Android (and eventually iOS) using react-navigation + react-native-screens. This means we are using platform native navigation primitives under the hood, for speed 🚗

This PR also puts Artsy's iOS-only native modules (e.g. ARTemporaryAPIModule) behind a LegacyNativeModules object to emphasise that they are old, and many of them are ripe for removal as we get Android up and running.

nav-stacks.mp4
admin-menu.mp4

Follow up work

PR Checklist (tick all before merging)

  • I have included screenshots or videos to illustrate my changes, or I have not changed anything that impacts the UI.
  • I have added tests for my changes, or my changes don't require testing, or I have included a link to a separate Jira ticket covering the tests.
  • I have documented any follow-up work that this PR will require, or it does not require any.
  • I have added an app state migration, or my changes do not require one. (What are migrations?)
  • I have added a CHANGELOG.yml entry or my changes do not require one.

@ds300 ds300 changed the title Android nav Android 'quit early if possible' ci Jan 26, 2021
@ds300 ds300 changed the title Android 'quit early if possible' ci [wip] android nav Jan 26, 2021
@@ -84,7 +84,9 @@
"@react-native-community/masked-view": "^0.1.10",
"@react-native-community/netinfo": "4.6.1",
"@react-native-community/picker": "1.3.0",
"@react-native-community/viewpager": "^4.2.2",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was a transitive dependency of react-native-scrollable-tab-view that we had to make explicit, otherwise the android autolinking doesn't work.

Comment on lines -88 to +106
<OpaqueImageView
aspectRatio={artwork.image?.aspectRatio ?? 1}
imageURL={artwork.image?.url}
style={styles.artworkImage}
>
<View>
<OpaqueImageView aspectRatio={artwork.image?.aspectRatio ?? 1} imageURL={artwork.image?.url} />
{Boolean(!hideUrgencyTags && urgencyTag && artwork?.sale?.isAuction && !artwork?.sale?.isClosed) && (
<Flex backgroundColor="white" px="5px" py="3px" borderRadius={2} alignSelf="flex-start">
<Flex
position="absolute"
bottom="5px"
left="5px"
backgroundColor="white"
px="5px"
py="3px"
borderRadius={2}
alignSelf="flex-start"
>
<Sans size="2" color="black100" numberOfLines={1}>
{urgencyTag}
</Sans>
</Flex>
)}
</OpaqueImageView>
</View>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lots of screens were causing the app to crash because a few commonly-used components were putting children inside an OpaqueImageView. On android OpaqueImageView currently uses the normal react-native Image component under the hood, which doesn't support children like our iOS native image component, so we decided to just refactor the few places where this was being done.

Comment on lines +79 to +82
<View
key={index}
pointerEvents={index === activeIndex ? undefined : "none"}
style={{ position: "absolute", width: "100%", height: "100%" }}
>
<Animated.View style={{ opacity: opacities[index], flex: 1, backgroundColor: "white" }}>
{!!(hasLoaded[index] || index === activeIndex) && v}
</Animated.View>
</View>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We refactored this component because there seemed to be an Animated bug on android during fade transitions. When animating the next view from opacity 0 to 1 the first frame would have opacity 1 already for some reason. e.g. if there were 10 frames in the animation, the opacity at each frame should be like [0, 0.1, 0.2, ..., 1] but we were seeing [1, 0.1, 0.2, ..., 1].

Most confusing.

Anyway we made the problem disappear by moving things around.

Comment on lines +22 to +24
<TouchableOpacity onPress={onPress}>
<ChevronIcon direction="left" />
</TouchableOpacity>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 At first this was using our custom Touchable but when debugging on android the onPress event was not being fired, no clue why. Most bizarre.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, we could take a look at a KS.

Comment on lines 18 to 22
public static void didLaunch(SharedPreferences prefs) {
launchCount = prefs.getInt(LAUNCH_COUNT, 0) + 1;
prefs.edit().putInt(LAUNCH_COUNT, launchCount).commit();
System.out.println("LaunchCount:" + launchCount);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the launchCount at dev time for maintaining navigation state across reloads.

We want the behaviour to be:

  • If you relaunch the app, the navigation state should reset
  • If you reload the js bundle, the navigation state should kept

We couldn't find a built-in way to tell the difference between relaunch and reload, so we added this launchCount property that is incremented only when the app launches. We already had it available on iOS.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Smort! Nice one.

<NavigationContainer>
<NavigationContainer independent>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now have a NavigationContainer wrapping the main part of the app (after onboarding). So any places where we use react-navigation in modals, e.g. here, we need to set this independent prop to true, which just silences a console.error that is intended for people who are using react-navigation in the normal way.

Comment on lines +14 to +31
onStartShouldSetResponderCapture={() => {
const now = Date.now()
const state = gestureState.current

if (now - state.lastTapTimestamp < 500) {
state.numTaps += 1
} else {
state.numTaps = 1
}

state.lastTapTimestamp = now

if (state.numTaps >= 5) {
state.numTaps = 0
setIsShowingAdminMenu(true)
}
return false
}}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a placeholder admin menu already, to allow us to clear AsyncStorage easily during development. For now I just made it trigger on five taps anywhere, with less than 0.5 seconds between each tap. We can iterate on this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be good to have like a three finger hold or something, but yea, that's for the admin jira ticket 👍.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only problem with a three finger hold is, how to do that on a simulator?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm.. true.. ok we will figure it out in the other ticket. I only don't like the spam tapping because it's easy to keep tapping and then tap something on the admin menu 😬.

@ds300 ds300 changed the title [wip] android nav Android nav Feb 1, 2021
@ds300 ds300 changed the title Android nav feat: Android nav Feb 1, 2021
@ds300 ds300 requested a review from MounirDhahri February 1, 2021 13:19
ds300 and others added 13 commits February 1, 2021 13:34
Co-authored-by: Mounir Dhahri <mounir.dhahri@ensi-uma.tn>
Co-authored-by: Mounir Dhahri <mounir.dhahri@ensi-uma.tn>
Co-authored-by: Mounir Dhahri <mounir.dhahri@ensi-uma.tn>
Co-authored-by: Mounir Dhahri <mounir.dhahri@ensi-uma.tn>
Co-authored-by: Mounir Dhahri <mounir.dhahri@ensi-uma.tn>
Co-authored-by: Mounir Dhahri <mounir.dhahri@ensi-uma.tn>
Co-authored-by: Mounir Dhahri <mounir.dhahri@ensi-uma.tn>
Co-authored-by: Mounir Dhahri <mounir.dhahri@ensi-uma.tn>
Co-authored-by: Mounir Dhahri <mounir.dhahri@ensi-uma.t>
Co-authored-by: Mounir Dhahri <mounir.dhahri@ensi-uma.tn>
Co-authored-by: Mounir Dhahri <mounir.dhahri@ensi-uma.tn>
Co-authored-by: Mounir Dhahri <mounir.dhahri@ensi-uma.tn>
Co-authored-by: Mounir Dhahri <mounir.dhahri@ensi-uma.tn>
@ds300
Copy link
Contributor Author

ds300 commented Feb 1, 2021

hmm QAing this and it seems to have affected touch handling on the FilterModal on iOS, I am investigating, don't merge yet.

@ds300
Copy link
Contributor Author

ds300 commented Feb 1, 2021

OK @pvinis this is good to go

return null
}
return (
<Animated.View
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need this to be an Animated.View

@@ -1,67 +0,0 @@
import type { ViewDescriptor } from "lib/navigation/navigate"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👋

@@ -180,6 +180,8 @@ export const FilterModalNavigator: React.FC<FilterModalProps> = (props) => {
<FancyModal visible={props.isFilterArtworksModalVisible} onBackgroundPressed={handleClosingModal} maxHeight={550}>
<View style={{ flex: 1 }}>
<Stack.Navigator
// force it to not use react-native-screens, which is broken inside a react-native Modal for some reason
detachInactiveScreens={false}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an interesting prop!

@MounirDhahri
Copy link
Member

🔥🔥🔥

Copy link
Contributor

@pvinis pvinis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very very cool work from both of you!

Comment on lines 18 to 22
public static void didLaunch(SharedPreferences prefs) {
launchCount = prefs.getInt(LAUNCH_COUNT, 0) + 1;
prefs.edit().putInt(LAUNCH_COUNT, launchCount).commit();
System.out.println("LaunchCount:" + launchCount);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Smort! Nice one.

import { NativeModules } from "react-native"

/**
* Here we maintain references to all the navigators in the main app navigation hierarchy, which are:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

Comment on lines +22 to +24
<TouchableOpacity onPress={onPress}>
<ChevronIcon direction="left" />
</TouchableOpacity>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, we could take a look at a KS.

Comment on lines +11 to +16
/**
* ModalStack is the root navigation stack in the app. The root screen in this stack is
* the main app (with bottom tabs, etc), and then whenever we present a modal it gets
* pushed on the top of this stack. We use react-native-screens to get native modal presetation
* transitions etc.
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These comments are very nice and helpful! I'm sure they will help make everything clear to every dev that comes in here.

export const AdminMenuWrapper: React.FC = ({ children }) => {
const [isShowingAdminMenu, setIsShowingAdminMenu] = useState(false)
const gestureState = useRef({ lastTapTimestamp: 0, numTaps: 0 })
// TODO: if public release, just return <>{children}</>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say we keep it in release too, still useful as it is now.

Comment on lines +14 to +31
onStartShouldSetResponderCapture={() => {
const now = Date.now()
const state = gestureState.current

if (now - state.lastTapTimestamp < 500) {
state.numTaps += 1
} else {
state.numTaps = 1
}

state.lastTapTimestamp = now

if (state.numTaps >= 5) {
state.numTaps = 0
setIsShowingAdminMenu(true)
}
return false
}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be good to have like a three finger hold or something, but yea, that's for the admin jira ticket 👍.

@pvinis pvinis added the Squash On Green A label to indicate that Peril should squash-merge this PR when all statuses are green label Feb 2, 2021
@artsy-peril artsy-peril bot merged commit 7710872 into master Feb 2, 2021
@artsy-peril artsy-peril bot deleted the android-nav branch February 2, 2021 09:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Jira Synced Squash On Green A label to indicate that Peril should squash-merge this PR when all statuses are green
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants