diff --git a/packages/mobile/src/app/Drawers.tsx b/packages/mobile/src/app/Drawers.tsx index 2c9c9e44e98..b36a6f738ae 100644 --- a/packages/mobile/src/app/Drawers.tsx +++ b/packages/mobile/src/app/Drawers.tsx @@ -30,6 +30,7 @@ import { HostRemixContestDrawer } from 'app/components/host-remix-contest-drawer 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 { LockedTextPostDrawer } from 'app/components/locked-text-post-drawer' import { ManagerModeDrawer } from 'app/components/manager-mode-drawer/ManagerModeDrawer' import { OverflowMenuDrawer } from 'app/components/overflow-menu-drawer' import { PlaybackRateDrawer } from 'app/components/playback-rate-drawer' @@ -163,6 +164,7 @@ const nativeDrawersMap: { [DrawerName in Drawer]?: ComponentType } = { RemoveDownloadedFavorites: RemoveDownloadedFavoritesDrawer, UnfavoriteDownloadedCollection: UnfavoriteDownloadedCollectionDrawer, LockedContent: LockedContentDrawer, + LockedTextPost: LockedTextPostDrawer, ChatActions: ChatActionsDrawer, CreateChatActions: CreateChatActionsDrawer, BlockMessages: BlockMessagesDrawer, diff --git a/packages/mobile/src/components/lineup-tile/LineupTileAccessStatus.tsx b/packages/mobile/src/components/lineup-tile/LineupTileAccessStatus.tsx index f46ddf560b8..f6e2ea32ae9 100644 --- a/packages/mobile/src/components/lineup-tile/LineupTileAccessStatus.tsx +++ b/packages/mobile/src/components/lineup-tile/LineupTileAccessStatus.tsx @@ -1,11 +1,6 @@ import { useCallback } from 'react' -import { useArtistCoin } from '@audius/common/api' -import type { - ID, - AccessConditions, - TokenGatedConditions -} from '@audius/common/models' +import type { ID, AccessConditions } from '@audius/common/models' import { ModalSource, isContentTokenGated, @@ -33,7 +28,6 @@ import { } from '@audius/harmony-native' import LoadingSpinner from 'app/components/loading-spinner' import { useIsUSDCEnabled } from 'app/hooks/useIsUSDCEnabled' -import { useNavigation } from 'app/hooks/useNavigation' import { make, track } from 'app/services/analytics' import { setVisibility } from 'app/store/drawers/slice' import { makeStyles } from 'app/styles' @@ -84,12 +78,6 @@ export const LineupTileAccessStatus = ({ isUSDCEnabled && isContentUSDCPurchaseGated(streamConditions) const isTokenGated = isContentTokenGated(streamConditions) const isUnlocking = gatedTrackStatus === 'UNLOCKING' - const navigation = useNavigation() - - const { data: token } = useArtistCoin( - (streamConditions as TokenGatedConditions)?.token_gate?.token_mint, - { enabled: isTokenGated } - ) const handlePress = useCallback(() => { if (hasStreamAccess) { @@ -120,13 +108,6 @@ export const LineupTileAccessStatus = ({ source: determineModalSource() } ) - } else if (isTokenGated) { - if (token?.ticker) { - navigation.push('BuySell', { - initialTab: 'buy', - coinTicker: token.ticker - }) - } } else if (contentId) { dispatch(setLockedContentId({ id: contentId })) dispatch(setVisibility({ drawer: 'LockedContent', visible: true })) @@ -134,13 +115,10 @@ export const LineupTileAccessStatus = ({ }, [ hasStreamAccess, isUSDCPurchase, - isTokenGated, contentId, contentType, openPremiumContentPurchaseModal, tileSource, - token?.ticker, - navigation, dispatch ]) diff --git a/packages/mobile/src/components/locked-text-post-drawer/LockedTextPostDrawer.tsx b/packages/mobile/src/components/locked-text-post-drawer/LockedTextPostDrawer.tsx new file mode 100644 index 00000000000..2cb8a706c0b --- /dev/null +++ b/packages/mobile/src/components/locked-text-post-drawer/LockedTextPostDrawer.tsx @@ -0,0 +1,92 @@ +import { useCallback } from 'react' + +import { useArtistCoin } from '@audius/common/api' +import { View } from 'react-native' + +import { Button, Flex, IconLock, Text } from '@audius/harmony-native' +import { NativeDrawer } from 'app/components/drawer' +import { useDrawer } from 'app/hooks/useDrawer' +import { useNavigation } from 'app/hooks/useNavigation' +import { makeStyles, flexRowCentered } from 'app/styles' +import { spacing } from 'app/styles/spacing' +import { useColor } from 'app/utils/theme' + +const DRAWER_NAME = 'LockedTextPost' + +const messages = { + howToUnlock: 'HOW TO UNLOCK', + description: 'To unlock this post, you need to hold', + buyCoins: 'Buy Coins' +} + +const useStyles = makeStyles(({ spacing, palette }) => ({ + drawer: { + paddingVertical: spacing(6), + alignItems: 'center', + backgroundColor: palette.white, + paddingHorizontal: spacing(4), + gap: spacing(6) + }, + titleContainer: { + ...flexRowCentered(), + justifyContent: 'center', + paddingBottom: spacing(4), + gap: spacing(2), + borderBottomColor: palette.neutralLight8, + borderBottomWidth: 1, + width: '100%' + } +})) + +export const LockedTextPostDrawer = () => { + const styles = useStyles() + const neutralLight2 = useColor('neutralLight2') + const navigation = useNavigation() + const { data, onClose } = useDrawer('LockedTextPost') + const mint = (data as { mint: string } | undefined)?.mint + const { data: coin } = useArtistCoin(mint) + + const handleBuyCoins = useCallback(() => { + if (coin?.ticker) { + onClose() + navigation.navigate('BuySell', { + initialTab: 'buy', + coinTicker: coin.ticker + }) + } + }, [coin?.ticker, navigation, onClose]) + + return ( + + + + + + {messages.howToUnlock} + + + + + {messages.description}{' '} + + {coin?.ticker ? `$${coin.ticker}` : "the artist's coins"} + + . + + + + + + ) +} diff --git a/packages/mobile/src/components/locked-text-post-drawer/index.ts b/packages/mobile/src/components/locked-text-post-drawer/index.ts new file mode 100644 index 00000000000..961211e50eb --- /dev/null +++ b/packages/mobile/src/components/locked-text-post-drawer/index.ts @@ -0,0 +1 @@ +export { LockedTextPostDrawer } from './LockedTextPostDrawer' diff --git a/packages/mobile/src/screens/coin-details-screen/components/FanClubTab.tsx b/packages/mobile/src/screens/coin-details-screen/components/FanClubTab.tsx index 244f687b36b..cd7ed8681a5 100644 --- a/packages/mobile/src/screens/coin-details-screen/components/FanClubTab.tsx +++ b/packages/mobile/src/screens/coin-details-screen/components/FanClubTab.tsx @@ -277,7 +277,11 @@ const FanClubFeed = ({ mint }: { mint: string }) => { {hasTextPosts ? textPosts.map((item) => ( - + )) : null} @@ -377,9 +381,7 @@ export const FanClubTab = ({ mint, onSwitchToCoinTab }: FanClubTabProps) => { ) : null} - {membershipKnown ? ( - - ) : null} + {membershipKnown ? : null} ) } diff --git a/packages/mobile/src/screens/coin-details-screen/components/TextPostCard.tsx b/packages/mobile/src/screens/coin-details-screen/components/TextPostCard.tsx index d8037376ae5..dbcfe5b5a3b 100644 --- a/packages/mobile/src/screens/coin-details-screen/components/TextPostCard.tsx +++ b/packages/mobile/src/screens/coin-details-screen/components/TextPostCard.tsx @@ -1,8 +1,8 @@ +import { useCallback } from 'react' + import { useComment } from '@audius/common/api' import type { ID } from '@audius/common/models' import { getLargestTimeUnitText } from '@audius/common/utils' -import { View } from 'react-native' -import LinearGradient from 'react-native-linear-gradient' import { Button, @@ -10,44 +10,29 @@ import { IconLock, Paper, Skeleton, - Text, - useTheme + Text } from '@audius/harmony-native' import { ProfilePicture } from 'app/components/core' import { UserLink } from 'app/components/user-link' +import { useDrawer } from 'app/hooks/useDrawer' const messages = { unlock: 'Unlock', membersOnly: 'Members Only' } -/** - * Generate a deterministic pseudo-random placeholder string for locked posts - * so each post looks visually distinct behind the blur. - */ -const generatePlaceholder = (commentId: ID) => { - const seed = Number(commentId) - const wordCount = 5 + (seed % 21) // 5–25 words - const words: string[] = [] - const pool = 'abcdefghijklmnopqrstuvwxyz' - for (let i = 0; i < wordCount; i++) { - const len = 3 + ((seed * (i + 1) * 7) % 8) // 3–10 chars per word - let word = '' - for (let j = 0; j < len; j++) { - word += pool[(seed * (i + 1) * (j + 1) * 13) % pool.length] - } - words.push(word) - } - return words.join(' ') -} - type TextPostCardProps = { commentId: ID + mint: string } -export const TextPostCard = ({ commentId }: TextPostCardProps) => { +export const TextPostCard = ({ commentId, mint }: TextPostCardProps) => { const { data: comment, isPending } = useComment(commentId) - const { color } = useTheme() + const { onOpen: openLockedTextPostDrawer } = useDrawer('LockedTextPost') + + const handleUnlock = useCallback(() => { + openLockedTextPostDrawer({ mint }) + }, [openLockedTextPostDrawer, mint]) if (isPending) { return ( @@ -100,34 +85,21 @@ export const TextPostCard = ({ commentId }: TextPostCardProps) => { {isLocked ? ( - - - - {generatePlaceholder(commentId)} + + + + + + {messages.membersOnly} - - - - - - - - {messages.membersOnly} - - ) : ( diff --git a/packages/mobile/src/store/drawers/slice.ts b/packages/mobile/src/store/drawers/slice.ts index 6eded2d68dd..cbafaccc266 100644 --- a/packages/mobile/src/store/drawers/slice.ts +++ b/packages/mobile/src/store/drawers/slice.ts @@ -35,6 +35,7 @@ export type Drawer = | 'PickWinners' | 'CoinInsightsOverflowMenu' | 'WalletRowOverflowMenu' + | 'LockedTextPost' export type DrawerData = { EnablePushNotifications: undefined @@ -77,6 +78,7 @@ export type DrawerData = { Comment: { userId: ID; entityId: ID; isEntityOwner: boolean; artistId: ID } EditTrackFormOverflowMenu: undefined PickWinners: undefined + LockedTextPost: { mint: string } CoinInsightsOverflowMenu: { mint: string } WalletRowOverflowMenu: { address: string @@ -117,6 +119,7 @@ const initialState: DrawersState = { Comment: false, EditTrackFormOverflowMenu: false, PickWinners: false, + LockedTextPost: false, CoinInsightsOverflowMenu: false, WalletRowOverflowMenu: false, data: {} diff --git a/packages/web/src/pages/fan-club-detail-page/components/LockedTextPostModal.tsx b/packages/web/src/pages/fan-club-detail-page/components/LockedTextPostModal.tsx new file mode 100644 index 00000000000..2560a00d532 --- /dev/null +++ b/packages/web/src/pages/fan-club-detail-page/components/LockedTextPostModal.tsx @@ -0,0 +1,74 @@ +import { useCallback } from 'react' + +import { useArtistCoin } from '@audius/common/api' +import { useBuySellModal } from '@audius/common/store' +import { + Button, + Flex, + IconArtistCoin, + IconLock, + Modal, + ModalContent, + ModalHeader, + ModalTitle, + Text +} from '@audius/harmony' + +const messages = { + howToUnlock: 'HOW TO UNLOCK', + description: 'To unlock this post, you need to hold', + buyCoins: 'Buy Coins' +} + +type LockedTextPostModalProps = { + isOpen: boolean + onClose: () => void + mint: string +} + +export const LockedTextPostModal = ({ + isOpen, + onClose, + mint +}: LockedTextPostModalProps) => { + const { data: coin } = useArtistCoin(mint) + const { onOpen: openBuySellModal } = useBuySellModal() + + const handleBuyCoins = useCallback(() => { + if (!coin?.ticker) return + openBuySellModal({ isOpen: true, ticker: coin.ticker }) + onClose() + }, [coin?.ticker, openBuySellModal, onClose]) + + return ( + + + } /> + + + + + {messages.description}{' '} + {coin?.ticker ? ( + + ${coin.ticker} + + ) : ( + "the artist's coins" + )} + . + + + + + + ) +} diff --git a/packages/web/src/pages/fan-club-detail-page/components/TextPostCard.tsx b/packages/web/src/pages/fan-club-detail-page/components/TextPostCard.tsx index d90cad695e2..e84dc0bfad4 100644 --- a/packages/web/src/pages/fan-club-detail-page/components/TextPostCard.tsx +++ b/packages/web/src/pages/fan-club-detail-page/components/TextPostCard.tsx @@ -28,6 +28,8 @@ import { ComposerInput } from 'components/composer-input/ComposerInput' import { ConfirmationModal } from 'components/confirmation-modal' import { UserLink } from 'components/link' +import { LockedTextPostModal } from './LockedTextPostModal' + const messages = { unlock: 'Unlock', membersOnly: 'Members Only', @@ -74,6 +76,7 @@ export const TextPostCard = ({ commentId, mint }: TextPostCardProps) => { const [isEditing, setIsEditing] = useState(false) const [editMessageId, setEditMessageId] = useState(0) const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) + const [showUnlockModal, setShowUnlockModal] = useState(false) const isOwner = currentUserId != null && comment?.userId === currentUserId const isCoinOwner = currentUserId != null && coin?.ownerId === currentUserId @@ -196,10 +199,17 @@ export const TextPostCard = ({ commentId, mint }: TextPostCardProps) => { size='small' rounded css={{ height: '24px' }} + onClick={() => setShowUnlockModal(true)} > {messages.unlock} - + setShowUnlockModal(true)} + css={{ cursor: 'pointer' }} + > {messages.membersOnly} @@ -284,6 +294,12 @@ export const TextPostCard = ({ commentId, mint }: TextPostCardProps) => { onConfirm={handleDelete} onClose={() => setShowDeleteConfirm(false)} /> + + setShowUnlockModal(false)} + mint={mint} + /> ) }