Skip to content
This repository has been archived by the owner on Oct 4, 2023. It is now read-only.

Commit

Permalink
[PAY-1430] "Leaving Audius" Warning for External Links (and createMod…
Browse files Browse the repository at this point in the history
…al helper) (#3860)
  • Loading branch information
rickyrombo authored Aug 10, 2023
1 parent bee8bd1 commit 7d0e0b3
Show file tree
Hide file tree
Showing 23 changed files with 442 additions and 38 deletions.
10 changes: 10 additions & 0 deletions packages/common/src/store/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ import deletePlaylistConfirmationReducer from './ui/delete-playlist-confirmation
import { DeletePlaylistConfirmationModalState } from './ui/delete-playlist-confirmation-modal/types'
import duplicateAddConfirmationReducer from './ui/duplicate-add-confirmation-modal/slice'
import { DuplicateAddConfirmationModalState } from './ui/duplicate-add-confirmation-modal/types'
import {
LeavingAudiusModalState,
leavingAudiusModalReducer
} from './ui/leaving-audius-modal'
import mobileOverflowModalReducer from './ui/mobile-overflow-menu/slice'
import { MobileOverflowModalState } from './ui/mobile-overflow-menu/types'
import modalsReducer from './ui/modals/slice'
Expand Down Expand Up @@ -191,6 +195,9 @@ export const reducers = () => ({
publishPlaylistConfirmationModal: publishPlaylistConfirmationReducer,
mobileOverflowModal: mobileOverflowModalReducer,
modals: modalsReducer,
modalsWithState: combineReducers({
leavingAudiusModal: leavingAudiusModalReducer
}),
musicConfetti: musicConfettiReducer,
nowPlaying: nowPlayingReducer,
reactions: reactionsReducer,
Expand Down Expand Up @@ -312,6 +319,9 @@ export type CommonState = {
publishPlaylistConfirmationModal: PublishPlaylistConfirmationModalState
mobileOverflowModal: MobileOverflowModalState
modals: ModalsState
modalsWithState: {
leavingAudiusModal: LeavingAudiusModalState
}
musicConfetti: MusicConfettiState
nowPlaying: NowPlayingState
reactions: ReactionsState
Expand Down
6 changes: 6 additions & 0 deletions packages/common/src/store/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,9 @@ export {
CreateChatModalState
} from './create-chat-modal/slice'
export * as createChatModalSelectors from './create-chat-modal/selectors'

export {
leavingAudiusModalReducer,
useLeavingAudiusModal,
LeavingAudiusModalState
} from './leaving-audius-modal'
19 changes: 19 additions & 0 deletions packages/common/src/store/ui/leaving-audius-modal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createModal } from '../modals/createModal'

export type LeavingAudiusModalState = {
link: string
}

const leavingAudiusModal = createModal<LeavingAudiusModalState>({
reducerPath: 'leavingAudiusModal',
initialState: {
isOpen: false,
link: ''
},
sliceSelector: (state) => state.ui.modalsWithState
})

export const {
hook: useLeavingAudiusModal,
reducer: leavingAudiusModalReducer
} = leavingAudiusModal
104 changes: 104 additions & 0 deletions packages/common/src/store/ui/modals/createModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useCallback } from 'react'

import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { useDispatch, useSelector } from 'react-redux'

import { CommonState } from 'store/index'

export type BaseModalState = {
isOpen: boolean | 'closing'
}

/**
* Creates the necessary actions/reducers/selectors for a modal,
* and returns the reducer to be wired to the store
* and hook to be used in the application.
*/
export const createModal = <T>({
reducerPath,
initialState,
sliceSelector
}: {
reducerPath: string
initialState: T & BaseModalState
sliceSelector?: (state: CommonState) => Record<string, any>
}) => {
const slice = createSlice({
name: `modals/${reducerPath}`,
initialState,
reducers: {
open: (_, action: PayloadAction<T & BaseModalState>) => {
return action.payload
},
close: (state) => {
state.isOpen = 'closing'
},
closed: () => {
return { ...initialState, isOpen: false }
}
}
})

const selector = (state: CommonState) => {
let baseState: (T & BaseModalState) | undefined
if (sliceSelector) {
baseState = sliceSelector(state)[reducerPath]
} else {
baseState = state[reducerPath]
}
if (!baseState) {
throw new Error(
`State for ${reducerPath} is undefined - did you forget to register the reducer in the store?`
)
}
return baseState
}

// Need to explicitly retype this because TS got confused
const open = slice.actions.open as (
state: T & BaseModalState
) => PayloadAction<T & BaseModalState>

const { close, closed } = slice.actions
/**
* A hook that returns the state of the modal,
* an open callback that opens the modal,
* a close callback that closes it,
* and a closed callback that clears the state
* @returns an object with the state and all three callbacks
*/
const useModal = () => {
const { isOpen, ...data } = useSelector(selector)
const dispatch = useDispatch()
const onOpen = useCallback(
(state?: T) => {
if (!state) {
dispatch(open({ ...initialState, isOpen: true }))
} else {
dispatch(open({ ...state, isOpen: true }))
}
},
[dispatch]
)
const onClose = useCallback(() => {
dispatch(close())
}, [dispatch])

const onClosed = useCallback(() => {
dispatch(closed())
}, [dispatch])

return {
isOpen: isOpen === true,
data,
onOpen,
onClose,
onClosed
}
}

return {
hook: useModal,
reducer: slice.reducer
}
}
2 changes: 2 additions & 0 deletions packages/mobile/src/app/Drawers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { FeedFilterDrawer } from 'app/components/feed-filter-drawer'
import { ForgotPasswordDrawer } from 'app/components/forgot-password-drawer'
import { GatedContentUploadPromptDrawer } from 'app/components/gated-content-upload-prompt-drawer/GatedContentUploadPromptDrawer'
import { InboxUnavailableDrawer } from 'app/components/inbox-unavailable-drawer/InboxUnavailableDrawer'
import { LeavingAudiusDrawer } from 'app/components/leaving-audius-drawer'
import { LockedContentDrawer } from 'app/components/locked-content-drawer'
import { OverflowMenuDrawer } from 'app/components/overflow-menu-drawer'
import { PlaybackRateDrawer } from 'app/components/playback-rate-drawer'
Expand Down Expand Up @@ -156,6 +157,7 @@ export const Drawers = () => {
drawer={Drawer}
/>
))}
<LeavingAudiusDrawer />
</>
)
}
4 changes: 2 additions & 2 deletions packages/mobile/src/assets/images/iconQuestionCircle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ export const ChallengeRewardsDrawer = ({
isFullscreen
isGestureSupported={false}
title={title}
titleIcon={titleIcon}
titleImage={titleIcon}
>
<View style={styles.content}>
<View style={styles.task}>
Expand Down
17 changes: 16 additions & 1 deletion packages/mobile/src/components/core/Hyperlink.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ComponentProps } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'

import { isAudiusUrl, useLeavingAudiusModal } from '@audius/common'
import type { Match } from 'autolinker/dist/es2015'
import type { LayoutRectangle, TextStyle } from 'react-native'
import { Text, View } from 'react-native'
Expand Down Expand Up @@ -42,6 +43,7 @@ export type HyperlinkProps = ComponentProps<typeof Autolink> & {
// Pass touches through text elements
allowPointerEventsToPassThrough?: boolean
styles?: StylesProp<{ root: TextStyle; link: TextStyle }>
warnExternal?: boolean
}

export const Hyperlink = (props: HyperlinkProps) => {
Expand All @@ -50,6 +52,7 @@ export const Hyperlink = (props: HyperlinkProps) => {
source,
styles: stylesProp,
style,
warnExternal,
...other
} = props
const styles = useStyles()
Expand Down Expand Up @@ -91,7 +94,19 @@ export const Hyperlink = (props: HyperlinkProps) => {
}
}, [links, linkRefs, linkContainerRef])

const handlePress = useOnOpenLink(source)
const openLink = useOnOpenLink(source)
const { onOpen: openLeavingAudiusModal } = useLeavingAudiusModal()

const handlePress = useCallback(
(url) => {
if (warnExternal && !isAudiusUrl(url)) {
openLeavingAudiusModal({ link: url })
} else {
openLink(url)
}
},
[openLink, warnExternal, openLeavingAudiusModal]
)

const renderLink = useCallback(
(text, match, index) => (
Expand Down
13 changes: 10 additions & 3 deletions packages/mobile/src/components/drawer/Drawer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ReactNode } from 'react'
import type { ComponentType, ReactNode } from 'react'
import { useMemo, useCallback, useEffect, useRef, useState } from 'react'

import type {
Expand All @@ -19,6 +19,7 @@ import type { Edge } from 'react-native-safe-area-context'
import { useSafeAreaInsets, SafeAreaView } from 'react-native-safe-area-context'

import { makeStyles } from 'app/styles'
import type { SvgProps } from 'app/types/svg'
import { attachToDy } from 'app/utils/animation'

import { DrawerHeader } from './DrawerHeader'
Expand Down Expand Up @@ -114,7 +115,11 @@ export type DrawerProps = {
/**
* Icon to display in the header next to the title (must also include title)
*/
titleIcon?: ImageSourcePropType
titleIcon?: ComponentType<SvgProps>
/**
* Icon (as image source) to display in the header next to the title (must also include title)
*/
titleImage?: ImageSourcePropType
/**
* Whether or not this is a fullscreen drawer with a dismiss button
*/
Expand Down Expand Up @@ -237,7 +242,7 @@ export const springToValue = ({
// Only allow titleIcon with title
type DrawerComponent = {
(props: DrawerProps & { title: string }): React.ReactElement
(props: Omit<DrawerProps, 'titleIcon'>): React.ReactElement
(props: Omit<DrawerProps, 'titleIcon' | 'titleImage'>): React.ReactElement
}
export const Drawer: DrawerComponent = ({
isOpen,
Expand All @@ -247,6 +252,7 @@ export const Drawer: DrawerComponent = ({
onOpen,
title,
titleIcon,
titleImage,
isFullscreen,
shouldBackgroundDim = true,
isGestureSupported = true,
Expand Down Expand Up @@ -652,6 +658,7 @@ export const Drawer: DrawerComponent = ({
onClose={onClose}
title={title}
titleIcon={titleIcon}
titleImage={titleImage}
isFullscreen={isFullscreen}
/>
{children}
Expand Down
30 changes: 22 additions & 8 deletions packages/mobile/src/components/drawer/DrawerHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import type { ImageSourcePropType, ImageStyle } from 'react-native'
import type { ComponentType } from 'react'

import type { ImageSourcePropType } from 'react-native'
import { TouchableOpacity, View, Image, Text } from 'react-native'

import IconRemove from 'app/assets/images/iconRemove.svg'
import { makeStyles } from 'app/styles'
import type { SvgProps } from 'app/types/svg'
import { useColor } from 'app/utils/theme'

type DrawerHeaderProps = {
onClose: () => void
title?: string
titleIcon?: ImageSourcePropType
titleIcon?: ComponentType<SvgProps>
titleImage?: ImageSourcePropType
isFullscreen?: boolean
}

Expand Down Expand Up @@ -47,9 +51,16 @@ export const useStyles = makeStyles(({ palette, typography, spacing }) => ({
}))

export const DrawerHeader = (props: DrawerHeaderProps) => {
const { onClose, title, titleIcon, isFullscreen } = props
const {
onClose,
title,
titleIcon: TitleIcon,
titleImage,
isFullscreen
} = props
const styles = useStyles()
const closeColor = useColor('neutralLight4')
const iconRemoveColor = useColor('neutralLight4')
const titleIconColor = useColor('neutral')

return title || isFullscreen ? (
<View style={styles.titleBarContainer}>
Expand All @@ -59,14 +70,17 @@ export const DrawerHeader = (props: DrawerHeaderProps) => {
onPress={onClose}
style={styles.dismissContainer}
>
<IconRemove width={30} height={30} fill={closeColor} />
<IconRemove width={30} height={30} fill={iconRemoveColor} />
</TouchableOpacity>
)}
{title && (
<View style={styles.titleContainer}>
{titleIcon && (
<Image style={styles.titleIcon as ImageStyle} source={titleIcon} />
)}
{TitleIcon ? (
<TitleIcon style={styles.titleIcon} fill={titleIconColor} />
) : null}
{titleImage ? (
<Image style={styles.titleIcon} source={titleImage} />
) : null}
<Text style={styles.titleLabel}>{title}</Text>
</View>
)}
Expand Down
Loading

0 comments on commit 7d0e0b3

Please sign in to comment.