Skip to content
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"appName": "eigen",
"version": "9.19.0",
"version": "9.20.0",
"slug": "eigen",
"expo": {
"buildCacheProvider": {
Expand All @@ -18,7 +18,7 @@
"ios",
"android"
],
"runtimeVersion": "9.19.0",
"runtimeVersion": "9.20.0",
"owner": "artsy_org",
"extra": {
"eas": {
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@
},
"dependencies": {
"@artsy/changelog": "0.1.0",
"@artsy/cohesion": "4.411.0",
"@artsy/cohesion": "4.412.0",
"@artsy/icons": "3.82.0",
"@artsy/palette-mobile": "24.15.0",
"@artsy/palette-mobile": "24.17.0",
"@artsy/to-title-case": "1.2.0",
"@braze/react-native-sdk": "16.1.0",
"@d11/react-native-fast-image": "patch:@d11/react-native-fast-image@npm%3A8.13.0#~/.yarn/patches/@d11-react-native-fast-image-npm-8.13.0-1ba5757ff5.patch",
Expand Down Expand Up @@ -207,7 +207,6 @@
"react-native-pager-view": "8.0.2",
"react-native-permissions": "5.4.4",
"react-native-reanimated": "4.5.1",
"react-native-reanimated-zoom": "0.3.3",
"react-native-render-html": "6.3.4",
"react-native-safe-area-context": "patch:react-native-safe-area-context@npm%3A5.7.0#~/.yarn/patches/react-native-safe-area-context-npm-5.7.0-fcff6bf187.patch",
"react-native-screens": "4.26.0",
Expand All @@ -221,6 +220,7 @@
"react-native-webview": "13.16.1",
"react-native-worklets": "0.10.1",
"react-native-youtube-iframe": "2.4.1",
"react-native-zoom-reanimated": "1.6.0",
"react-relay": "18.2.0",
"react-tracking": "9.3.2",
"react-use": "^17.6.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Flex, Popover, Text } from "@artsy/palette-mobile"
import { useIsFocused } from "@react-navigation/native"
import { useProgressiveOnboardingTracking } from "app/Components/ProgressiveOnboarding/useProgressiveOnboardingTracking"
import { useSetActivePopover } from "app/Components/ProgressiveOnboarding/useSetActivePopover"
import { GlobalStore } from "app/store/GlobalStore"
import { getCurrentEmissionState, GlobalStore } from "app/store/GlobalStore"
import { PROGRESSIVE_ONBOARDING_ARTSY_LENS } from "app/store/ProgressiveOnboardingModel"
import { Sentinel } from "app/utils/Sentinel"
import { useDebouncedValue } from "app/utils/hooks/useDebouncedValue"
Expand All @@ -17,6 +17,7 @@ export const ProgressiveOnboardingArtsyLens: React.FC<
> = ({ children, isSearchOverlayVisible, ownerType }) => {
const [isInView, setIsInView] = useState(false)
const isFocused = useIsFocused()
const { launchCount } = getCurrentEmissionState()
const {
isDismissed,
sessionState: { isReady, deferHomeTooltipsThisSession },
Expand All @@ -29,6 +30,7 @@ export const ProgressiveOnboardingArtsyLens: React.FC<
})

const isDisplayable =
launchCount >= 3 &&
isReady &&
isFocused &&
isInView &&
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { OwnerType } from "@artsy/cohesion"
import { fireEvent, screen } from "@testing-library/react-native"
import { ProgressiveOnboardingArtsyLens } from "app/Components/ProgressiveOnboarding/ProgressiveOnboardingArtsyLens"
import { ArtsyNativeModule } from "app/NativeModules/ArtsyNativeModule"
import { __globalStoreTestUtils__ } from "app/store/GlobalStore"
import { mockTrackEvent } from "app/utils/tests/globallyMockedStuff"
import { renderWithWrappers } from "app/utils/tests/renderWithWrappers"
Expand Down Expand Up @@ -28,6 +29,7 @@ jest.mock("@react-navigation/native", () => ({
}))

describe("ProgressiveOnboardingArtsyLens", () => {
const originalLaunchCount = ArtsyNativeModule.launchCount
const renderTooltip = (
isSearchOverlayVisible = false,
ownerType: OwnerType.home | OwnerType.search = OwnerType.home
Expand All @@ -43,6 +45,7 @@ describe("ProgressiveOnboardingArtsyLens", () => {

beforeEach(() => {
jest.clearAllMocks()
ArtsyNativeModule.launchCount = 3
mockUseIsFocused.mockReturnValue(true)
__globalStoreTestUtils__?.injectState({
progressiveOnboarding: {
Expand All @@ -56,6 +59,36 @@ describe("ProgressiveOnboardingArtsyLens", () => {
})
})

afterEach(() => {
ArtsyNativeModule.launchCount = originalLaunchCount
})

it.each([OwnerType.home, OwnerType.search] as const)(
"stays hidden on %s during the first two launches",
(ownerType) => {
for (const launchCount of [1, 2]) {
ArtsyNativeModule.launchCount = launchCount
const { unmount } = renderTooltip(false, ownerType)

expect(screen.queryByText("Artsy Lens")).not.toBeOnTheScreen()
expect(mockTrackEvent).not.toHaveBeenCalled()
expect(
__globalStoreTestUtils__?.getCurrentState().progressiveOnboarding.sessionState
.activePopover
).toBeUndefined()

unmount()
}
}
)

it.each([3, 4])("can appear from launch %s onwards", async (launchCount) => {
ArtsyNativeModule.launchCount = launchCount
renderTooltip()

expect(await screen.findByText("Artsy Lens")).toBeOnTheScreen()
})

it("shows the Artsy Lens copy and tracks the view", async () => {
renderTooltip()

Expand Down Expand Up @@ -106,6 +139,19 @@ describe("ProgressiveOnboardingArtsyLens", () => {
expect(screen.queryByText("Artsy Lens")).not.toBeOnTheScreen()
})

it("waits while another progressive onboarding popover is active", () => {
__globalStoreTestUtils__?.injectState({
progressiveOnboarding: { sessionState: { activePopover: "another-popover" } },
})

renderTooltip()

expect(screen.queryByText("Artsy Lens")).not.toBeOnTheScreen()
expect(
__globalStoreTestUtils__?.getCurrentState().progressiveOnboarding.sessionState.activePopover
).toBe("another-popover")
})

it("waits when Home tooltips are deferred for this session", () => {
__globalStoreTestUtils__?.injectState({
progressiveOnboarding: { sessionState: { deferHomeTooltipsThisSession: true } },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ import {
import { ImageCarouselVimeoVideo } from "app/Scenes/Artwork/Components/ImageCarousel/ImageCarouselVimeoVideo"
import { GlobalStore } from "app/store/GlobalStore"
import { useScreenDimensions } from "app/utils/hooks/useScreenDimensions"
import { useCallback, useContext, useEffect, useMemo, useState } from "react"
import { FlatList, Modal, NativeScrollEvent, NativeSyntheticEvent } from "react-native"
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"
import {
FlatList,
LayoutChangeEvent,
Modal,
NativeScrollEvent,
NativeSyntheticEvent,
} from "react-native"
import { GestureHandlerRootView } from "react-native-gesture-handler"
import { createZoomListComponent } from "react-native-reanimated-zoom"
import { ImageCarouselCloseButton } from "./ImageCarouselCloseButton"
import { ImageZoomViewAndroid } from "./ImageZoomViewAndroid"
import { IndexIndicator } from "./IndexIndicator"

const ZoomFlatList = createZoomListComponent(FlatList)

export const ImageCarouselFullScreenAndroid = () => {
const screenDimensions = useScreenDimensions()
const color = useColor()
Expand All @@ -24,6 +27,17 @@ export const ImageCarouselFullScreenAndroid = () => {
const initialScrollIndex = useMemo(() => imageIndex.current, [])
const { setIsDeepZoomModalVisible } = GlobalStore.actions.devicePrefs
const [showBackButton, setShowBackButton] = useState(false)
const listRef = useRef<FlatList<ImageCarouselMedia>>(null)
// The modal is edge-to-edge on Android 15+, so it is taller than Dimensions' "window".
// Size the pages from the modal's own layout instead.
const [measuredPageSize, setMeasuredPageSize] = useState<{ width: number; height: number }>()
const pageWidth = measuredPageSize?.width ?? screenDimensions.width
const pageHeight = measuredPageSize?.height ?? screenDimensions.height

const onLayout = useCallback((e: LayoutChangeEvent) => {
const { width, height } = e.nativeEvent.layout
setMeasuredPageSize({ width, height })
}, [])

const onClose = useCallback(() => {
dispatch({ type: "FULL_SCREEN_DISMISSED" })
Expand All @@ -41,29 +55,31 @@ export const ImageCarouselFullScreenAndroid = () => {
const renderItem = useCallback(
({ item, index }: { item: ImageCarouselMedia; index: number }) => {
if (item.__typename === "Video") {
return (
<ImageCarouselVimeoVideo
width={screenDimensions.width}
height={screenDimensions.height}
vimeoUrl={item.url}
/>
)
return <ImageCarouselVimeoVideo width={pageWidth} height={pageHeight} vimeoUrl={item.url} />
}

return <ImageZoomViewAndroid image={item} index={index} />
return (
<ImageZoomViewAndroid
image={item}
index={index}
width={pageWidth}
height={pageHeight}
parentScrollRef={listRef}
/>
)
},
[screenDimensions.orientation]
[pageWidth, pageHeight]
)

// Update the imageIndex on scroll
const onScroll = useCallback(
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
const nextImageIndex = Math.round(e.nativeEvent.contentOffset.x / screenDimensions.width)
const nextImageIndex = Math.round(e.nativeEvent.contentOffset.x / pageWidth)
if (fullScreenState.current === "entered" && nextImageIndex !== imageIndex.current) {
dispatch({ type: "IMAGE_INDEX_CHANGED", nextImageIndex })
}
},
[screenDimensions.orientation]
[pageWidth]
)

return (
Expand All @@ -81,8 +97,12 @@ export const ImageCarouselFullScreenAndroid = () => {
>
{!!showBackButton && <ImageCarouselCloseButton onClose={onClose} />}

<GestureHandlerRootView style={{ flex: 1, backgroundColor: color("mono0") }}>
<ZoomFlatList<ImageCarouselMedia>
<GestureHandlerRootView
style={{ flex: 1, backgroundColor: color("mono0") }}
onLayout={onLayout}
>
<FlatList<ImageCarouselMedia>
ref={listRef}
data={media}
pagingEnabled
showsHorizontalScrollIndicator={false}
Expand All @@ -93,8 +113,8 @@ export const ImageCarouselFullScreenAndroid = () => {
initialScrollIndex={initialScrollIndex}
getItemLayout={(_, index) => ({
index,
offset: index * screenDimensions.width,
length: screenDimensions.width,
offset: index * pageWidth,
length: pageWidth,
})}
onScroll={onScroll}
initialNumToRender={2}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,26 @@ import {
ImageCarouselContext,
ImageDescriptor,
} from "app/Scenes/Artwork/Components/ImageCarousel/ImageCarouselContext"
import { useScreenDimensions } from "app/utils/hooks/useScreenDimensions"
import { useContext, useEffect, useState } from "react"
import { RefObject, useContext, useEffect, useState } from "react"
import { LayoutAnimation } from "react-native"
import { Zoom } from "react-native-reanimated-zoom"
import Zoom, { ScrollableRef } from "react-native-zoom-reanimated"
import usePrevious from "react-use/lib/usePrevious"

export interface ImageZoomViewAndroidProps {
image: ImageDescriptor
index: number
width: number
height: number
parentScrollRef: RefObject<ScrollableRef | null>
}

export const ImageZoomViewAndroid: React.FC<ImageZoomViewAndroidProps> = ({ image, index }) => {
export const ImageZoomViewAndroid: React.FC<ImageZoomViewAndroidProps> = ({
image,
index,
width: screenWidth,
height: screenHeight,
parentScrollRef,
}) => {
const [isLoading, setIsLoading] = useState(false)
const [opacity, setOpacity] = useState(0)
const color = useColor()
Expand Down Expand Up @@ -44,31 +52,40 @@ export const ImageZoomViewAndroid: React.FC<ImageZoomViewAndroidProps> = ({ imag
// Only preload the next image if the user is swiping right
previousImageIndex < imageIndex.current
) {
FastImage.preload([
{
uri: images[imageIndex.current + 1].largeImageURL!,
},
])
const nextImageURL = images[imageIndex.current + 1].largeImageURL
if (nextImageURL) {
FastImage.preload([{ uri: nextImageURL }])
}
}
}, [imageIndex.current, previousImageIndex])

const { width: screenWidth, height: screenHeight } = useScreenDimensions()
// Fall back to filling the page when the image has no dimensions
const aspectRatio =
image.width && image.height ? image.width / image.height : screenWidth / screenHeight

let imageHeight = (image.height! * screenWidth) / image.width!
let imageHeight = screenWidth / aspectRatio
let imageWidth = screenWidth

// Make sure image doesn't get out of bounds
if (imageHeight > screenHeight) {
imageWidth = (image.width! * screenHeight) / image.height!
imageWidth = screenHeight * aspectRatio
imageHeight = screenHeight
}

return (
<Flex width={screenWidth} height={screenHeight} alignItems="center" justifyContent="center">
<Zoom>
<Zoom
// fill the page so pan bounds and gallery-swipe edges are measured against the screen
style={{ width: screenWidth, height: screenHeight }}
maxScale={8}
enableGallerySwipe
parentScrollRef={parentScrollRef}
currentIndex={index}
itemWidth={screenWidth}
>
<FastImage
source={{
uri: image.largeImageURL!,
uri: image.largeImageURL ?? undefined,
}}
style={{
width: imageWidth,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ const Sheet: React.FC<Props> = ({
{itineraries.map((itinerary) => (
<AddToItineraryRow
key={`${itinerary.internalID}`}
itineraryID={itinerary.internalID}
title={itinerary.title}
stopsCount={itineraryStopsCount(itinerary)}
imageUrl={itinerary.heroImage?.url}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CheckmarkFillIcon, EmptyCheckCircleIcon, NoArtIcon } from "@artsy/icons/native"
import { Flex, Text } from "@artsy/palette-mobile"
import { useItineraryFallbackImage } from "app/Scenes/CityGuide/hooks/useItineraryFallbackImage"
import { pluralize } from "app/utils/pluralize"
// TODO: Replace with Image from @artsy/palette-mobile once the itinerary hero is a real image.
import { Image as RNImage, TouchableOpacity } from "react-native"
Expand All @@ -11,6 +12,7 @@ const NO_ICON_SIZE = 20
const ROW_RADIUS = 10

interface Props {
itineraryID?: string
title: string
/** Omitted when nothing knows the count — see `itineraryStopsCount`. */
stopsCount?: number
Expand All @@ -21,12 +23,20 @@ interface Props {

/** One itinerary in the Add to Itinerary sheet, ticked when it holds this stop. */
export const AddToItineraryRow: React.FC<Props> = ({
itineraryID,
title,
stopsCount,
imageUrl,
imageUrl: serverImageUrl,
selected,
onPress,
}) => {
const fallbackImageUrl = useItineraryFallbackImage({
itineraryID,
stopsCount,
skip: !!serverImageUrl,
})
const imageUrl = serverImageUrl ?? fallbackImageUrl

return (
<TouchableOpacity
testID="add-to-itinerary-row"
Expand Down
Loading
Loading