Skip to content

Commit

Permalink
feat: implement activity dot indicator experiment (#11319)
Browse files Browse the repository at this point in the history
* chore: bump palette-mobile to get latest colors and VisualClue

* feat: define the experiment

And define a payload that allows us to force the dots to display (for QA)

* feat: add a hook for convenience

* feat: implement the experiment on the activity panel bell

* feat: update red for inbox indicator

* feat: add variants to bottom tab

Strangely, I don't see how the profile icon's dot ever gets requested
so I've added the ability to force it here

* refactor: move color calculation into hook

* feat: track the activity dot experiment, once per session

* feat: address PR feedback

- Use new var instead of mutating destructured var from props
- Avoid 'px' in positioning props
  • Loading branch information
anandaroop authored Jan 2, 2025
1 parent bb6db0e commit a04dab7
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ describe("UrgencyInfo", () => {
)

jest.advanceTimersByTime(2000)
expect(getByText("58m 59s left")).toHaveStyle({ color: "#C82400" })
expect(getByText("58m 59s left")).toHaveStyle({ color: "#D71023" })
})
it("text color is blue when time is greater than 1 hour", () => {
const start = new Date(new Date().getTime() - 10).toISOString()
Expand Down
4 changes: 3 additions & 1 deletion src/app/Navigation/utils/useBottomTabsBadges.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ jest.mock("@artsy/palette-mobile", () => ({
jest.mock("app/utils/hooks/useVisualClue", () => ({
useVisualClue: jest.fn(),
}))

jest.mock("app/utils/experiments/useActivityDotExperiment", () => ({
useActivityDotExperiment: jest.fn(() => ({ forceDots: false, color: "blue100" })),
}))
jest.mock("app/utils/useTabBarBadge", () => ({
useTabBarBadge: jest.fn(),
}))
Expand Down
26 changes: 20 additions & 6 deletions src/app/Navigation/utils/useBottomTabsBadges.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useColor, useSpace } from "@artsy/palette-mobile"
import { BottomTabType } from "app/Scenes/BottomTabs/BottomTabType"
import { bottomTabsConfig } from "app/Scenes/BottomTabs/bottomTabsConfig"
import { useActivityDotExperiment } from "app/utils/experiments/useActivityDotExperiment"
import { useVisualClue } from "app/utils/hooks/useVisualClue"
import { useTabBarBadge } from "app/utils/useTabBarBadge"
import { StyleProp, TextStyle } from "react-native"
Expand All @@ -18,13 +19,14 @@ export const useBottomTabsBadges = () => {
const space = useSpace()

const { showVisualClue } = useVisualClue()

const { unreadConversationsCount, hasUnseenNotifications } = useTabBarBadge()

const { forceDots, color: backgroundColor } = useActivityDotExperiment()

const tabsBadges: Record<string, BadgeProps> = {}

const visualClueStyles = {
backgroundColor: color("blue100"),
backgroundColor: color(backgroundColor),
top: space(1),
minWidth: VISUAL_CLUE_HEIGHT,
maxHeight: VISUAL_CLUE_HEIGHT,
Expand Down Expand Up @@ -61,7 +63,7 @@ export const useBottomTabsBadges = () => {

switch (tab) {
case "home": {
if (hasUnseenNotifications) {
if (hasUnseenNotifications || forceDots) {
tabsBadges[tab] = {
tabBarBadge: "",
tabBarBadgeStyle: {
Expand All @@ -73,11 +75,23 @@ export const useBottomTabsBadges = () => {
}

case "inbox": {
if (unreadConversationsCount) {
if (unreadConversationsCount || forceDots) {
tabsBadges[tab] = {
tabBarBadge: unreadConversationsCount || (forceDots ? 42 : 0),
tabBarBadgeStyle: {
backgroundColor: color("red50"),
},
}
}
return
}

case "profile": {
if (forceDots) {
tabsBadges[tab] = {
tabBarBadge: unreadConversationsCount,
tabBarBadge: "",
tabBarBadgeStyle: {
backgroundColor: color("red100"),
...visualClueStyles,
},
}
}
Expand Down
69 changes: 56 additions & 13 deletions src/app/Scenes/HomeView/Components/ActivityIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { BellIcon, Box, DEFAULT_HIT_SLOP, VisualClueDot } from "@artsy/palette-mobile"
import { useHomeViewTracking } from "app/Scenes/HomeView/hooks/useHomeViewTracking"
import { navigate } from "app/system/navigation/navigate"
import { useActivityDotExperiment } from "app/utils/experiments/useActivityDotExperiment"
import React from "react"
import { TouchableOpacity } from "react-native"

interface ActivityIndicatorProps {
hasUnseenNotifications: boolean
}

export const ActivityIndicator: React.FC<ActivityIndicatorProps> = ({ hasUnseenNotifications }) => {
export const ActivityIndicator: React.FC<ActivityIndicatorProps> = (props) => {
const { hasUnseenNotifications } = props
const tracking = useHomeViewTracking()

const { enabled, variant, forceDots } = useActivityDotExperiment()

let BellVariant = BellWithSmallDot
if (enabled && variant !== "control") BellVariant = BellWithLargeDot

const displayUnseenNotifications = hasUnseenNotifications || forceDots

const navigateToActivityPanel = () => {
tracking.tappedNotificationBell()

Expand All @@ -23,19 +33,52 @@ export const ActivityIndicator: React.FC<ActivityIndicatorProps> = ({ hasUnseenN
onPress={navigateToActivityPanel}
hitSlop={DEFAULT_HIT_SLOP}
>
<BellIcon height={24} width={24} />

{!!hasUnseenNotifications && (
<Box
position="absolute"
top={0}
right={0}
accessibilityLabel="Unseen Notifications Indicator"
>
<VisualClueDot diameter={4} />
</Box>
)}
<BellVariant hasUnseenNotifications={displayUnseenNotifications} />
</TouchableOpacity>
</Box>
)
}

const BellWithSmallDot: React.FC<{ hasUnseenNotifications: boolean }> = (props) => {
const { hasUnseenNotifications } = props
const { color } = useActivityDotExperiment()

return (
<>
<BellIcon height={24} width={24} />

{!!hasUnseenNotifications && (
<Box
position="absolute"
top={0}
right={0}
accessibilityLabel="Unseen Notifications Indicator"
>
<VisualClueDot diameter={4} color={color} />
</Box>
)}
</>
)
}

const BellWithLargeDot: React.FC<{ hasUnseenNotifications: boolean }> = (props) => {
const { hasUnseenNotifications } = props
const { color } = useActivityDotExperiment()

return (
<>
<BellIcon height={24} width={30} />

{!!hasUnseenNotifications && (
<Box
position="absolute"
top={-0.5}
right={0}
accessibilityLabel="Unseen Notifications Indicator"
>
<VisualClueDot diameter={8} color={color} />
</Box>
)}
</>
)
}
13 changes: 11 additions & 2 deletions src/app/Scenes/HomeView/HomeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { navigate } from "app/system/navigation/navigate"
import { getRelayEnvironment } from "app/system/relay/defaultEnvironment"
import { useBottomTabsScrollToTop } from "app/utils/bottomTabsHelper"
import { useExperimentVariant } from "app/utils/experiments/hooks"
import { useActivityDotExperiment } from "app/utils/experiments/useActivityDotExperiment"
import { extractNodes } from "app/utils/extractNodes"
import { useDevToggle } from "app/utils/hooks/useDevToggle"
import { useIsDeepLink } from "app/utils/hooks/useIsDeepLink"
Expand Down Expand Up @@ -60,14 +61,22 @@ export const HomeView: React.FC = memo(() => {

const trackedSectionTypes = HomeViewStore.useStoreState((state) => state.trackedSectionTypes)

const { trackExperiment } = useExperimentVariant("onyx_artwork-card-save-and-follow-cta-redesign")
const { trackExperiment: trackCardRedesignExperiment } = useExperimentVariant(
"onyx_artwork-card-save-and-follow-cta-redesign"
)

useEffect(() => {
if (trackedSectionTypes.includes("HomeViewSectionArtworks")) {
trackExperiment()
trackCardRedesignExperiment()
}
}, [trackedSectionTypes.includes("HomeViewSectionArtworks")])

const { trackExperiment: trackActvityDotExperiment } = useActivityDotExperiment()

useEffect(() => {
trackActvityDotExperiment()
}, [])

const { data, loadNext, hasNext } = usePaginationFragment<
HomeViewQuery,
HomeViewSectionsConnection_viewer$key
Expand Down
7 changes: 7 additions & 0 deletions src/app/utils/experiments/experiments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ export const experiments = {
payloadSuggestions: ["variant-a", "variant-b", "variant-c"],
variantSuggestions: ["variant-a", "variant-b", "variant-c"],
},
"onyx_activity-dot-experiment": {
description: "Replace current visual clue dot with a larger or red variant",
fallbackEnabled: true,
fallbackVariant: "control",
variantSuggestions: ["control", "variant-b", "variant-c"],
payloadSuggestions: ['{"forceDots": "true"}', '{"forceDots": "false"}'],
},
} satisfies { [key: string]: ExperimentDescriptor }

export type EXPERIMENT_NAME = keyof typeof experiments
23 changes: 23 additions & 0 deletions src/app/utils/experiments/useActivityDotExperiment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useExperimentVariant } from "app/utils/experiments/hooks"

type Variant = "control" | "variant-b" | "variant-c"
type Color = "red50" | "blue100"

export function useActivityDotExperiment() {
const { enabled, trackExperiment, variant, payload } = useExperimentVariant(
"onyx_activity-dot-experiment"
)

// Dev Menu helper to force visible dots for testing during QA
const forceDots = Boolean(payload && JSON.parse(payload)?.forceDots === "true")

const color: Color = enabled ? (variant === "variant-b" ? "red50" : "blue100") : "blue100"

return {
enabled,
variant: variant as Variant,
color,
trackExperiment,
forceDots,
}
}
Loading

0 comments on commit a04dab7

Please sign in to comment.