From 5691cc65abbfc903f43ad429fd07959e60cff63b Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Fri, 8 Sep 2023 17:50:24 -0700 Subject: [PATCH] [C-3060] Fix mobile outbound link experience (#4055) --- packages/common/src/utils/index.ts | 1 + packages/common/src/utils/linking.ts | 19 +++++++ packages/common/src/utils/urlUtils.ts | 17 ++++++ .../components/action-drawer/ActionDrawer.tsx | 39 +++++-------- .../mobile/src/components/core/Hyperlink.tsx | 24 ++++++-- packages/mobile/src/components/core/Text.tsx | 8 ++- .../src/components/drawer/AppDrawer.tsx | 2 +- .../mobile/src/components/drawer/Drawer.tsx | 2 +- .../src/components/drawer/DrawerHeader.tsx | 41 ++++++-------- .../feed-filter-drawer/FeedFilterDrawer.tsx | 21 ++----- .../LeavingAudiusDrawer.tsx | 15 ++--- .../PlaybackRateDrawer.tsx | 34 +---------- .../components/share-drawer/ShareDrawer.tsx | 56 ++++--------------- .../CollapsibleTopTabNavigator.tsx | 9 ++- .../src/screens/chat-screen/LinkPreview.tsx | 3 +- .../screens/profile-screen/ProfileScreen.tsx | 2 +- .../{ => ProfileTabs}/AlbumsTab.tsx | 4 +- .../{ => ProfileTabs}/CollectiblesTab.tsx | 4 +- .../{ => ProfileTabs}/PlaylistsTab.tsx | 5 +- .../{ => ProfileTabs}/ProfileTabNavigator.tsx | 7 ++- .../{ => ProfileTabs}/RepostsTab.tsx | 6 +- .../{ => ProfileTabs}/TracksTab.tsx | 5 +- .../src/screens/profile-screen/selectors.ts | 17 ++++-- .../web/src/components/link/ExternalLink.tsx | 22 ++------ packages/web/src/utils/route.ts | 8 +++ 25 files changed, 173 insertions(+), 198 deletions(-) create mode 100644 packages/common/src/utils/linking.ts rename packages/mobile/src/screens/profile-screen/{ => ProfileTabs}/AlbumsTab.tsx (93%) rename packages/mobile/src/screens/profile-screen/{ => ProfileTabs}/CollectiblesTab.tsx (97%) rename packages/mobile/src/screens/profile-screen/{ => ProfileTabs}/PlaylistsTab.tsx (93%) rename packages/mobile/src/screens/profile-screen/{ => ProfileTabs}/ProfileTabNavigator.tsx (95%) rename packages/mobile/src/screens/profile-screen/{ => ProfileTabs}/RepostsTab.tsx (89%) rename packages/mobile/src/screens/profile-screen/{ => ProfileTabs}/TracksTab.tsx (92%) diff --git a/packages/common/src/utils/index.ts b/packages/common/src/utils/index.ts index 13dd0b7b3d..a9c7d4c52a 100644 --- a/packages/common/src/utils/index.ts +++ b/packages/common/src/utils/index.ts @@ -30,3 +30,4 @@ export * from './streaming' export * from './dogEarUtils' export * from './uploadConstants' export * from './updatePlaylistArtwork' +export * from './linking' diff --git a/packages/common/src/utils/linking.ts b/packages/common/src/utils/linking.ts new file mode 100644 index 0000000000..7e7b775831 --- /dev/null +++ b/packages/common/src/utils/linking.ts @@ -0,0 +1,19 @@ +export const externalLinkAllowList = new Set([ + 'facebook.com', + 'instagram.com', + 'tiktok.com', + 'twitter.com', + 'x.com', + 'audius.co', + 'discord.gg' +]) + +export const isAllowedExternalLink = (link: string) => { + try { + let hostname = new URL(link).hostname + hostname = hostname.replace(/^www\./, '') + return externalLinkAllowList.has(hostname) + } catch (e) { + return false + } +} diff --git a/packages/common/src/utils/urlUtils.ts b/packages/common/src/utils/urlUtils.ts index fea7df9361..28a48908fb 100644 --- a/packages/common/src/utils/urlUtils.ts +++ b/packages/common/src/utils/urlUtils.ts @@ -1,8 +1,25 @@ +const AUDIUS_PRESS_LINK = 'https://brand.audius.co' +const AUDIUS_MERCH_LINK = 'https://merch.audius.co/' +const AUDIUS_REMIX_CONTESTS_LINK = 'https://remix.audius.co/' +const AUDIUS_BLOG_LINK = 'https://blog.audius.co/' + +export const externalAudiusLinks = [ + AUDIUS_PRESS_LINK, + AUDIUS_MERCH_LINK, + AUDIUS_REMIX_CONTESTS_LINK, + AUDIUS_BLOG_LINK, + 'https://help.audius.co' +] const audiusUrlRegex = // eslint-disable-next-line no-useless-escape /^(?:https?:\/\/)?(?:[^@\/\n]+@)?(?:www\.)?(staging\.)?(audius\.co)(\/.+)?/gim export const isAudiusUrl = (url: string) => new RegExp(audiusUrlRegex).test(url) +export const isInteralAudiusUrl = (url: string) => + isAudiusUrl(url) && + !externalAudiusLinks.some((externalLink) => externalLink.includes(url)) +export const isExternalAudiusUrl = (url: string) => + new RegExp(audiusUrlRegex).test(url) export const getPathFromAudiusUrl = (url: string) => new RegExp(audiusUrlRegex).exec(url)?.[3] ?? null diff --git a/packages/mobile/src/components/action-drawer/ActionDrawer.tsx b/packages/mobile/src/components/action-drawer/ActionDrawer.tsx index 44acda6e48..7302a59340 100644 --- a/packages/mobile/src/components/action-drawer/ActionDrawer.tsx +++ b/packages/mobile/src/components/action-drawer/ActionDrawer.tsx @@ -4,11 +4,13 @@ import { useCallback } from 'react' import type { Modals } from '@audius/common' import type { TextStyle, ViewStyle } from 'react-native' import { TouchableHighlight, View } from 'react-native' +import type { SetOptional } from 'type-fest' -import Text from 'app/components/text' +import { Text } from 'app/components/core' import { makeStyles } from 'app/styles' import { useThemeColors } from 'app/utils/theme' +import type { AppDrawerProps } from '../drawer/AppDrawer' import { AppDrawer, useDrawerState } from '../drawer/AppDrawer' export type ActionDrawerRow = { @@ -19,19 +21,14 @@ export type ActionDrawerRow = { isDestructive?: boolean } -type ActionSheetModalProps = { +type ActionDrawerProps = { modalName: Modals rows: ActionDrawerRow[] - title?: string - renderTitle?: () => React.ReactNode styles?: { row?: ViewStyle } disableAutoClose?: boolean -} +} & SetOptional const useStyles = makeStyles(({ palette, typography, spacing }) => ({ - container: { - paddingVertical: spacing(4) - }, row: { height: 56, alignItems: 'center', @@ -57,14 +54,14 @@ const useStyles = makeStyles(({ palette, typography, spacing }) => ({ })) // `ActionDrawer` is a drawer that presents a list of clickable rows with text -const ActionDrawer = (props: ActionSheetModalProps) => { +const ActionDrawer = (props: ActionDrawerProps) => { const { modalName, rows, - title, - renderTitle, styles: stylesProp, - disableAutoClose + disableAutoClose, + children, + ...other } = props const styles = useStyles() const { onClose } = useDrawerState(modalName) @@ -85,13 +82,9 @@ const ActionDrawer = (props: ActionSheetModalProps) => { const { neutralLight9 } = useThemeColors() return ( - - - {renderTitle ? ( - renderTitle() - ) : title ? ( - {title} - ) : null} + + + {children} {rows.map(({ text, isDestructive = false, icon, style }, index) => ( { {icon ? {icon} : null} {text} diff --git a/packages/mobile/src/components/core/Hyperlink.tsx b/packages/mobile/src/components/core/Hyperlink.tsx index 88ff28b73a..6c2f00b341 100644 --- a/packages/mobile/src/components/core/Hyperlink.tsx +++ b/packages/mobile/src/components/core/Hyperlink.tsx @@ -1,7 +1,13 @@ import type { ComponentProps } from 'react' import { useCallback, useEffect, useRef, useState } from 'react' -import { isAudiusUrl, useLeavingAudiusModal } from '@audius/common' +import { + getPathFromAudiusUrl, + isAllowedExternalLink, + isInteralAudiusUrl, + useLeavingAudiusModal +} from '@audius/common' +import { useLinkTo } from '@react-navigation/native' import type { Match } from 'autolinker/dist/es2015' import type { LayoutRectangle, TextStyle } from 'react-native' import { Text, View } from 'react-native' @@ -55,6 +61,7 @@ export const Hyperlink = (props: HyperlinkProps) => { ...other } = props const styles = useStyles() + const linkTo = useLinkTo() const linkContainerRef = useRef(null) const [linkRefs, setLinkRefs] = useState>({}) @@ -97,14 +104,19 @@ export const Hyperlink = (props: HyperlinkProps) => { const { onOpen: openLeavingAudiusModal } = useLeavingAudiusModal() const handlePress = useCallback( - (url) => { - if (!isAudiusUrl(url)) { - openLeavingAudiusModal({ link: url }) - } else { + (url: string) => { + if (isInteralAudiusUrl(url)) { + const path = getPathFromAudiusUrl(url) + if (path) { + linkTo(path) + } + } else if (isAllowedExternalLink(url)) { openLink(url) + } else { + openLeavingAudiusModal({ link: url }) } }, - [openLink, openLeavingAudiusModal] + [linkTo, openLink, openLeavingAudiusModal] ) const renderLink = useCallback( diff --git a/packages/mobile/src/components/core/Text.tsx b/packages/mobile/src/components/core/Text.tsx index 6823ceb914..056a7bf0e0 100644 --- a/packages/mobile/src/components/core/Text.tsx +++ b/packages/mobile/src/components/core/Text.tsx @@ -68,7 +68,13 @@ export const Text = (props: TextProps) => { // Fix for demibold's weird positioning marginTop: weight === 'demiBold' && Platform.OS === 'ios' - ? spacing(fontSize === 'large' ? 1 : fontSize === 'small' ? 0.5 : 0) + ? spacing( + fontSize && ['xl', 'large'].includes(fontSize) + ? 1 + : fontSize === 'small' + ? 0.5 + : 0 + ) : undefined }, fontSize && diff --git a/packages/mobile/src/components/drawer/AppDrawer.tsx b/packages/mobile/src/components/drawer/AppDrawer.tsx index 9fc7e050ca..4c6fd302a8 100644 --- a/packages/mobile/src/components/drawer/AppDrawer.tsx +++ b/packages/mobile/src/components/drawer/AppDrawer.tsx @@ -33,7 +33,7 @@ export const useDrawerState = (modalName: Modals) => { } } -type AppDrawerProps = SetOptional & { +export type AppDrawerProps = SetOptional & { modalName: Modals } diff --git a/packages/mobile/src/components/drawer/Drawer.tsx b/packages/mobile/src/components/drawer/Drawer.tsx index 304f95b8dc..7305b5f4c1 100644 --- a/packages/mobile/src/components/drawer/Drawer.tsx +++ b/packages/mobile/src/components/drawer/Drawer.tsx @@ -111,7 +111,7 @@ export type DrawerProps = { /** * Header title if this drawer has a header */ - title?: string + title?: ReactNode /** * Icon to display in the header next to the title (must also include title) */ diff --git a/packages/mobile/src/components/drawer/DrawerHeader.tsx b/packages/mobile/src/components/drawer/DrawerHeader.tsx index 85e2786dda..c495d7c5e2 100644 --- a/packages/mobile/src/components/drawer/DrawerHeader.tsx +++ b/packages/mobile/src/components/drawer/DrawerHeader.tsx @@ -1,27 +1,28 @@ -import type { ComponentType } from 'react' +import type { ComponentType, ReactNode } from 'react' import type { ImageSourcePropType } from 'react-native' -import { TouchableOpacity, View, Image, Text } from 'react-native' +import { TouchableOpacity, View, Image } from 'react-native' import IconRemove from 'app/assets/images/iconRemove.svg' +import { Text } from 'app/components/core' import { makeStyles } from 'app/styles' import type { SvgProps } from 'app/types/svg' import { useColor } from 'app/utils/theme' type DrawerHeaderProps = { onClose: () => void - title?: string + title?: ReactNode titleIcon?: ComponentType titleImage?: ImageSourcePropType isFullscreen?: boolean } -export const useStyles = makeStyles(({ palette, typography, spacing }) => ({ +export const useStyles = makeStyles(({ spacing }) => ({ titleBarContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', - padding: spacing(6) + padding: spacing(8) }, dismissContainer: { @@ -32,21 +33,11 @@ export const useStyles = makeStyles(({ palette, typography, spacing }) => ({ titleContainer: { flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingTop: spacing(5) + columnGap: spacing(2) }, - - titleIcon: { - marginRight: spacing(3), + titleImage: { height: spacing(6), width: spacing(6) - }, - - titleLabel: { - fontFamily: typography.fontByWeight.bold, - fontSize: typography.fontSize.large, - color: palette.neutral } })) @@ -64,7 +55,7 @@ export const DrawerHeader = (props: DrawerHeaderProps) => { return title || isFullscreen ? ( - {isFullscreen && ( + {isFullscreen ? ( { > - )} - {title && ( + ) : null} + {title ? ( {TitleIcon ? ( - + ) : null} {titleImage ? ( - + ) : null} - {title} + + {title} + - )} + ) : null} ) : ( diff --git a/packages/mobile/src/components/feed-filter-drawer/FeedFilterDrawer.tsx b/packages/mobile/src/components/feed-filter-drawer/FeedFilterDrawer.tsx index 580960e9a4..1485b29bb2 100644 --- a/packages/mobile/src/components/feed-filter-drawer/FeedFilterDrawer.tsx +++ b/packages/mobile/src/components/feed-filter-drawer/FeedFilterDrawer.tsx @@ -6,12 +6,11 @@ import { feedPageLineupActions as feedActions, feedPageActions } from '@audius/common' -import { Text } from 'react-native' import { useDispatch } from 'react-redux' import ActionDrawer from 'app/components/action-drawer' +import { Text } from 'app/components/core' import { make, track } from 'app/services/analytics' -import { makeStyles } from 'app/styles' const { setFeedFilter } = feedPageActions @@ -24,21 +23,9 @@ export const messages = { filterReposts: 'Reposts' } -const useStyles = makeStyles(({ palette, spacing, typography }) => ({ - title: { - ...typography.body, - color: palette.neutral, - textAlign: 'center', - marginTop: spacing(2), - marginBottom: spacing(4) - } -})) - export const FeedFilterDrawer = () => { const dispatch = useDispatch() - const styles = useStyles() - const handleSelectFilter = useCallback( (filter: FeedFilter) => { dispatch(setFeedFilter(filter)) @@ -74,7 +61,11 @@ export const FeedFilterDrawer = () => { return ( {messages.title}} + title={ + + {messages.title} + + } rows={rows} /> ) diff --git a/packages/mobile/src/components/leaving-audius-drawer/LeavingAudiusDrawer.tsx b/packages/mobile/src/components/leaving-audius-drawer/LeavingAudiusDrawer.tsx index 1a2e122290..a13d8e2a37 100644 --- a/packages/mobile/src/components/leaving-audius-drawer/LeavingAudiusDrawer.tsx +++ b/packages/mobile/src/components/leaving-audius-drawer/LeavingAudiusDrawer.tsx @@ -5,10 +5,10 @@ import { View } from 'react-native' import IconExternalLink from 'app/assets/images/iconExternalLink.svg' import IconInfo from 'app/assets/images/iconInfo.svg' +import { Button, Text, useLink } from 'app/components/core' import Drawer from 'app/components/drawer/Drawer' import { makeStyles } from 'app/styles' -import { Button, Text, useLink } from '../core' import { HelpCallout } from '../help-callout/HelpCallout' const messages = { @@ -23,9 +23,6 @@ const useStyles = makeStyles(({ spacing }) => ({ gap: spacing(4), paddingBottom: spacing(6), paddingHorizontal: spacing(4) - }, - button: { - width: '100%' } })) @@ -49,16 +46,12 @@ export const LeavingAudiusDrawer = () => { content={link} icon={IconExternalLink} /> +