From e6af9d70f2133eaae9595ef3a8cc30b44d62a0bf Mon Sep 17 00:00:00 2001 From: Saliou Diallo Date: Wed, 7 Jun 2023 00:40:37 -0400 Subject: [PATCH] [PAY-919][PAY-1312] Chat track and playlist tiles on mobile (#3455) Co-authored-by: Saliou Diallo --- packages/common/src/api/collection.ts | 11 +- packages/common/src/api/track.ts | 12 +- packages/common/src/hooks/chats/index.ts | 2 + packages/common/src/hooks/chats/types.ts | 3 + .../common/src/hooks/chats/useTrackPlayer.ts | 134 +++++++++++++++ packages/common/src/store/pages/chat/types.ts | 8 + packages/common/src/utils/stringUtils.ts | 13 +- .../components/lineup-tile/CollectionTile.tsx | 16 +- .../src/components/lineup-tile/LineupTile.tsx | 35 ++-- .../components/lineup-tile/LineupTileRoot.tsx | 2 +- .../lineup-tile/LineupTileStats.tsx | 9 +- .../src/components/lineup-tile/TrackTile.tsx | 2 + .../src/components/lineup-tile/types.ts | 20 +++ .../screens/chat-screen/ChatListScreen.tsx | 3 +- .../chat-screen/ChatMessageListItem.tsx | 76 +++++++-- .../chat-screen/ChatMessagePlaylist.tsx | 161 ++++++++++++++++++ .../screens/chat-screen/ChatMessageTrack.tsx | 104 +++++++++++ .../src/screens/chat-screen/LinkPreview.tsx | 57 ++++--- .../track/mobile/ConnectedPlaylistTile.tsx | 10 +- .../track/mobile/ConnectedTrackTile.tsx | 6 +- .../track/mobile/PlaylistTile.module.css | 10 +- .../components/track/mobile/PlaylistTile.tsx | 11 +- .../track/mobile/TrackTile.module.css | 10 +- .../src/components/track/mobile/TrackTile.tsx | 23 ++- packages/web/src/components/track/types.ts | 3 +- .../components/ChatMessageListItem.module.css | 11 +- .../components/ChatMessageListItem.tsx | 116 +++++++------ .../components/ChatMessagePlaylist.module.css | 7 - .../components/ChatMessagePlaylist.tsx | 122 ++++++------- .../components/ChatMessageTrack.module.css | 7 - .../chat-page/components/ChatMessageTrack.tsx | 100 ++++------- .../chat-page/components/LinkPreview.tsx | 24 ++- 32 files changed, 830 insertions(+), 298 deletions(-) create mode 100644 packages/common/src/hooks/chats/types.ts create mode 100644 packages/common/src/hooks/chats/useTrackPlayer.ts create mode 100644 packages/mobile/src/screens/chat-screen/ChatMessagePlaylist.tsx create mode 100644 packages/mobile/src/screens/chat-screen/ChatMessageTrack.tsx delete mode 100644 packages/web/src/pages/chat-page/components/ChatMessagePlaylist.module.css delete mode 100644 packages/web/src/pages/chat-page/components/ChatMessageTrack.module.css diff --git a/packages/common/src/api/collection.ts b/packages/common/src/api/collection.ts index 95f17d1e13b..6ee7beb4424 100644 --- a/packages/common/src/api/collection.ts +++ b/packages/common/src/api/collection.ts @@ -1,12 +1,16 @@ import { ID, Kind } from 'models' import { createApi } from 'src/audius-query/createApi' +import { Nullable } from 'utils' const collectionApi = createApi({ reducerPath: 'collectionApi', endpoints: { getPlaylistById: { fetch: async ( - { playlistId, currentUserId }: { playlistId: ID; currentUserId: ID }, + { + playlistId, + currentUserId + }: { playlistId: ID; currentUserId: Nullable }, { apiClient } ) => { return ( @@ -25,7 +29,10 @@ const collectionApi = createApi({ // Note: Please do not use this endpoint yet as it depends on further changes on the DN side. getPlaylistByPermalink: { fetch: async ( - { permalink, currentUserId }: { permalink: string; currentUserId: ID }, + { + permalink, + currentUserId + }: { permalink: string; currentUserId: Nullable }, { apiClient } ) => { return ( diff --git a/packages/common/src/api/track.ts b/packages/common/src/api/track.ts index 88a1dd6116a..f47682e9fb2 100644 --- a/packages/common/src/api/track.ts +++ b/packages/common/src/api/track.ts @@ -1,6 +1,7 @@ import { ID, Kind } from 'models' import { createApi } from 'src/audius-query/createApi' import { parseTrackRouteFromPermalink } from 'utils/stringUtils' +import { Nullable } from 'utils/typeUtils' const trackApi = createApi({ reducerPath: 'trackApi', @@ -17,9 +18,16 @@ const trackApi = createApi({ }, getTrackByPermalink: { fetch: async ( - { permalink, currentUserId }: { permalink: string; currentUserId: ID }, + { + permalink, + currentUserId + }: { permalink: Nullable; currentUserId: Nullable }, { apiClient } ) => { + if (!permalink) { + console.error('Attempting to get track but permalink is null...') + return + } const { handle, slug } = parseTrackRouteFromPermalink(permalink) return await apiClient.getTrackByHandleAndSlug({ handle, @@ -35,7 +43,7 @@ const trackApi = createApi({ }, getTracksByIds: { fetch: async ( - { ids, currentUserId }: { ids: ID[]; currentUserId: ID }, + { ids, currentUserId }: { ids: ID[]; currentUserId: Nullable }, { apiClient } ) => { return await apiClient.getTracks({ ids, currentUserId }) diff --git a/packages/common/src/hooks/chats/index.ts b/packages/common/src/hooks/chats/index.ts index 231951284ed..e774b24fd8a 100644 --- a/packages/common/src/hooks/chats/index.ts +++ b/packages/common/src/hooks/chats/index.ts @@ -1,2 +1,4 @@ +export * from './types' export * from './useCanSendMessage' export * from './useSetInboxPermissions' +export * from './useTrackPlayer' diff --git a/packages/common/src/hooks/chats/types.ts b/packages/common/src/hooks/chats/types.ts new file mode 100644 index 00000000000..1f7a9975dde --- /dev/null +++ b/packages/common/src/hooks/chats/types.ts @@ -0,0 +1,3 @@ +import { Name } from 'models' + +export type TrackPlayback = Name.PLAYBACK_PLAY | Name.PLAYBACK_PAUSE diff --git a/packages/common/src/hooks/chats/useTrackPlayer.ts b/packages/common/src/hooks/chats/useTrackPlayer.ts new file mode 100644 index 00000000000..0db698ce085 --- /dev/null +++ b/packages/common/src/hooks/chats/useTrackPlayer.ts @@ -0,0 +1,134 @@ +import { useCallback } from 'react' + +import { useDispatch, useSelector } from 'react-redux' + +import { ID, Name } from 'models' +import { getPlaying, getUid } from 'store/player/selectors' +import { QueueSource, Queueable, queueActions } from 'store/queue' +import { makeGetCurrent } from 'store/queue/selectors' +import { Nullable } from 'utils' + +import { TrackPlayback } from './types' + +const { clear, add, play, pause } = queueActions + +type RecordAnalytics = ({ name, id }: { name: TrackPlayback; id: ID }) => void + +type UseToggleTrack = { + uid: Nullable + source: QueueSource + recordAnalytics?: RecordAnalytics + id?: Nullable +} + +/** + * Hook that exposes a function to play a track. + * Optionally records a track play analytics event. + * + * @param {Function} recordAnalytics Function that tracks play event + * + * @returns {Function} the function that plays the track + */ +export const usePlayTrack = (recordAnalytics?: RecordAnalytics) => { + const dispatch = useDispatch() + const playingUid = useSelector(getUid) + + const playTrack = useCallback( + ({ id, uid, entries }: { id?: ID; uid: string; entries: Queueable[] }) => { + if (playingUid !== uid) { + dispatch(clear({})) + dispatch(add({ entries })) + dispatch(play({ uid })) + } else { + dispatch(play({})) + } + if (recordAnalytics && id) { + recordAnalytics({ name: Name.PLAYBACK_PLAY, id }) + } + }, + [dispatch, recordAnalytics, playingUid] + ) + + return playTrack +} + +/** + * Hook that exposes a function to pause a track. + * Optionally records a track pause analytics event. + * + * @param {Function} recordAnalytics Function that tracks pause event + * + * @returns {Function} the function that pauses the track + */ +export const usePauseTrack = (recordAnalytics?: RecordAnalytics) => { + const dispatch = useDispatch() + + const pauseTrack = useCallback( + (id?: ID) => { + dispatch(pause({})) + if (recordAnalytics && id) { + recordAnalytics({ name: Name.PLAYBACK_PAUSE, id }) + } + }, + [dispatch, recordAnalytics] + ) + + return pauseTrack +} + +/** + * Represents that props passed into the useToggleTrack hook. + * + * @typedef {Object} UseToggleTrackProps + * @property {string} uid the uid of the track (nullable) + * @property {string} source the queue source + * @property {Function} recordAnalytics the function that tracks the event + * @property {number} id the id of the track (nullable and optional) + */ + +/** + * Represents that props passed into the useToggleTrack hook. + * + * @typedef {Object} UseToggleTrackResult + * @property {Function} togglePlay the function that toggles the track i.e. play/pause + * @property {boolean} isTrackPlaying whether the track is playing or paused + */ + +/** + * Hook that exposes a togglePlay function and isTrackPlaying boolean + * to facilitate the playing / pausing of a track. + * Also records the play / pause action as an analytics event. + * Leverages the useTrackPlay and useTrackPause hooks. + * + * @param {UseToggleTrackProps} param Object passed into function + * + * @returns {UseToggleTrackResult} the object that contains togglePlay and isTrackPlaying + */ +export const useToggleTrack = ({ + uid, + source, + recordAnalytics, + id +}: UseToggleTrack) => { + const currentQueueItem = useSelector(makeGetCurrent()) + const playing = useSelector(getPlaying) + const isTrackPlaying = !!( + playing && + currentQueueItem.track && + currentQueueItem.uid === uid + ) + + const playTrack = usePlayTrack(recordAnalytics) + const pauseTrack = usePauseTrack(recordAnalytics) + + const togglePlay = useCallback(() => { + if (!id || !uid) return + if (isTrackPlaying) { + pauseTrack(id) + } else { + playTrack({ id, uid, entries: [{ id, uid, source }] }) + } + }, [playTrack, pauseTrack, isTrackPlaying, id, uid, source]) + + return { togglePlay, isTrackPlaying } +} diff --git a/packages/common/src/store/pages/chat/types.ts b/packages/common/src/store/pages/chat/types.ts index 950ef7bc286..22872e94473 100644 --- a/packages/common/src/store/pages/chat/types.ts +++ b/packages/common/src/store/pages/chat/types.ts @@ -13,3 +13,11 @@ export enum ChatPermissionAction { /** User is signed out and needs to sign in */ SIGN_UP } + +export type ChatMessageTileProps = { + link: string + styles?: any + onEmpty?: () => void + onSuccess?: () => void + className?: string +} diff --git a/packages/common/src/utils/stringUtils.ts b/packages/common/src/utils/stringUtils.ts index 6a26fe5de84..5dbfcfb9b8d 100644 --- a/packages/common/src/utils/stringUtils.ts +++ b/packages/common/src/utils/stringUtils.ts @@ -19,9 +19,20 @@ export const paramsToQueryString = (params: { } /** - * Permalinks have the following format: '//' + * Track permalinks have the following format: '//' */ export const parseTrackRouteFromPermalink = (permalink: string) => { const [, handle, slug] = permalink.split('/') return { slug, trackId: null, handle } } + +/** + * Playlist permalinks have the following format: '//playlist/' + * + * @param permalink + * @returns playlist id + */ +export const parsePlaylistIdFromPermalink = (permalink: string) => { + const playlistNameWithId = permalink?.split('/').slice(-1)[0] ?? '' + return parseInt(playlistNameWithId.split('-').slice(-1)[0]) +} diff --git a/packages/mobile/src/components/lineup-tile/CollectionTile.tsx b/packages/mobile/src/components/lineup-tile/CollectionTile.tsx index b13e66f4383..6f9024af446 100644 --- a/packages/mobile/src/components/lineup-tile/CollectionTile.tsx +++ b/packages/mobile/src/components/lineup-tile/CollectionTile.tsx @@ -53,16 +53,20 @@ const { getCollection, getTracksFromCollection } = cacheCollectionsSelectors const getUserId = accountSelectors.getUserId export const CollectionTile = (props: LineupItemProps) => { - const { uid } = props + const { uid, collection: collectionOverride, tracks: tracksOverride } = props const collection = useProxySelector( - (state) => getCollection(state, { uid }), - [uid] + (state) => { + return collectionOverride ?? getCollection(state, { uid }) + }, + [collectionOverride, uid] ) const tracks = useProxySelector( - (state) => getTracksFromCollection(state, { uid }), - [uid] + (state) => { + return tracksOverride ?? getTracksFromCollection(state, { uid }) + }, + [tracksOverride, uid] ) const user = useProxySelector( @@ -102,6 +106,7 @@ const CollectionTileComponent = ({ togglePlay, tracks, user, + variant, ...lineupTileProps }: CollectionTileProps) => { const dispatch = useDispatch() @@ -253,6 +258,7 @@ const CollectionTileComponent = ({ title={playlist_name} item={collection} user={user} + variant={variant} > diff --git a/packages/mobile/src/components/lineup-tile/LineupTile.tsx b/packages/mobile/src/components/lineup-tile/LineupTile.tsx index 9f6554288d0..7515156a1a1 100644 --- a/packages/mobile/src/components/lineup-tile/LineupTile.tsx +++ b/packages/mobile/src/components/lineup-tile/LineupTile.tsx @@ -53,6 +53,8 @@ export const LineupTile = ({ item, user, isPlayingUid, + variant, + styles, TileProps }: LineupTileProps) => { const isGatedContentEnabled = useIsGatedContentEnabled() @@ -99,8 +101,10 @@ export const LineupTile = ({ isTrack && (item.genre === Genre.PODCASTS || item.genre === Genre.AUDIOBOOKS) + const isReadonly = variant === 'readonly' + return ( - + {showPremiumCornerTag && cornerTagIconType ? ( {children} - + {!isReadonly ? ( + + ) : null} ) } diff --git a/packages/mobile/src/components/lineup-tile/LineupTileRoot.tsx b/packages/mobile/src/components/lineup-tile/LineupTileRoot.tsx index 8725484cc55..4edcebc20c0 100644 --- a/packages/mobile/src/components/lineup-tile/LineupTileRoot.tsx +++ b/packages/mobile/src/components/lineup-tile/LineupTileRoot.tsx @@ -12,5 +12,5 @@ const styles = StyleSheet.create({ type LineupTileRootProps = TileProps export const LineupTileRoot = (props: LineupTileRootProps) => { - return + return } diff --git a/packages/mobile/src/components/lineup-tile/LineupTileStats.tsx b/packages/mobile/src/components/lineup-tile/LineupTileStats.tsx index 865c5db094b..09b9331a8e7 100644 --- a/packages/mobile/src/components/lineup-tile/LineupTileStats.tsx +++ b/packages/mobile/src/components/lineup-tile/LineupTileStats.tsx @@ -20,6 +20,7 @@ import { useThemeColors } from 'app/utils/theme' import { LineupTileRankIcon } from './LineupTileRankIcon' import { useStyles as useTrackTileStyles } from './styles' +import type { LineupItemVariant } from './types' const { setFavorite } = favoritesUserListActions const { setRepost } = repostsUserListActions @@ -72,6 +73,7 @@ type Props = { index: number isCollection?: boolean isTrending?: boolean + variant?: LineupItemVariant isUnlisted?: boolean playCount?: number repostCount: number @@ -87,6 +89,7 @@ export const LineupTileStats = ({ index, isCollection, isTrending, + variant, isUnlisted, playCount, repostCount, @@ -117,6 +120,8 @@ export const LineupTileStats = ({ ) + const isReadonly = variant === 'readonly' + return ( {isTrending ? ( @@ -129,7 +134,7 @@ export const LineupTileStats = ({ trackTileStyles.statItem, !repostCount ? styles.disabledStatItem : null ]} - disabled={!repostCount} + disabled={!repostCount || isReadonly} onPress={handlePressReposts} > @@ -147,7 +152,7 @@ export const LineupTileStats = ({ trackTileStyles.statItem, !saveCount ? styles.disabledStatItem : null ]} - disabled={!saveCount} + disabled={!saveCount || isReadonly} onPress={handlePressFavorites} > diff --git a/packages/mobile/src/components/lineup-tile/TrackTile.tsx b/packages/mobile/src/components/lineup-tile/TrackTile.tsx index 816514524cf..6f1065bb1dc 100644 --- a/packages/mobile/src/components/lineup-tile/TrackTile.tsx +++ b/packages/mobile/src/components/lineup-tile/TrackTile.tsx @@ -76,6 +76,7 @@ export const TrackTileComponent = ({ togglePlay, track, user, + variant, ...lineupTileProps }: TrackTileProps) => { const isGatedContentEnabled = useIsGatedContentEnabled() @@ -235,6 +236,7 @@ export const TrackTileComponent = ({ title={title} item={track} user={user} + variant={variant} /> ) } diff --git a/packages/mobile/src/components/lineup-tile/types.ts b/packages/mobile/src/components/lineup-tile/types.ts index 9c5c2b0e77f..aad7c0c2174 100644 --- a/packages/mobile/src/components/lineup-tile/types.ts +++ b/packages/mobile/src/components/lineup-tile/types.ts @@ -7,15 +7,23 @@ import type { Collection, FavoriteType, Track, + LineupTrack, User, RepostType } from '@audius/common' +import type { StyleProp, ViewStyle } from 'react-native' import type { GestureResponderHandler } from 'app/types/gesture' import type { TileProps } from '../core' import type { ImageProps } from '../image/FastImage' +/** + * Optional variant to modify the lineup item features and styles + * The 'readonly' variant will remove the action buttons on the tile + */ +export type LineupItemVariant = 'readonly' + export type LineupItemProps = { /** Index of tile in lineup */ index: number @@ -37,6 +45,18 @@ export type LineupItemProps = { /** Uid of the item */ uid: UID + + /** Optionally passed in variant */ + variant?: LineupItemVariant + + /** Optionally passed in collection to override */ + collection?: Collection + + /** Optionally passed in tracks to override */ + tracks?: LineupTrack[] + + /** Passed in styles */ + styles?: StyleProp } export type LineupTileProps = Omit & { diff --git a/packages/mobile/src/screens/chat-screen/ChatListScreen.tsx b/packages/mobile/src/screens/chat-screen/ChatListScreen.tsx index 6ea7d17879d..c6236e27605 100644 --- a/packages/mobile/src/screens/chat-screen/ChatListScreen.tsx +++ b/packages/mobile/src/screens/chat-screen/ChatListScreen.tsx @@ -104,6 +104,7 @@ export const ChatListScreen = () => { const dispatch = useDispatch() const navigation = useNavigation() const chats = useSelector(getChats) + const nonEmptyChats = chats.filter((chat) => !!chat.last_message) const chatsStatus = useSelector(getChatsStatus) // If this is the first fetch, we want to show the fade-out loading skeleton @@ -159,7 +160,7 @@ export const ChatListScreen = () => { )) ) : ( } keyExtractor={(chat) => chat.chat_id} diff --git a/packages/mobile/src/screens/chat-screen/ChatMessageListItem.tsx b/packages/mobile/src/screens/chat-screen/ChatMessageListItem.tsx index 01339286b85..e976e312381 100644 --- a/packages/mobile/src/screens/chat-screen/ChatMessageListItem.tsx +++ b/packages/mobile/src/screens/chat-screen/ChatMessageListItem.tsx @@ -6,12 +6,14 @@ import { accountSelectors, chatSelectors, decodeHashId, - formatMessageDate + formatMessageDate, + isPlaylistUrl, + isTrackUrl } from '@audius/common' import type { ChatMessageReaction } from '@audius/sdk' import { find } from 'linkifyjs' import type { ViewStyle, StyleProp } from 'react-native' -import { View } from 'react-native' +import { Dimensions, View } from 'react-native' import { useSelector } from 'react-redux' import ChatTail from 'app/assets/images/ChatTail.svg' @@ -20,6 +22,8 @@ import { makeStyles } from 'app/styles' import { reactionMap } from '../notifications-screen/Reaction' +import { ChatMessagePlaylist } from './ChatMessagePlaylist' +import { ChatMessageTrack } from './ChatMessageTrack' import { LinkPreview } from './LinkPreview' import { ResendMessageButton } from './ResendMessageButton' import { REACTION_LONGPRESS_DELAY } from './constants' @@ -121,20 +125,32 @@ const useStyles = makeStyles(({ spacing, palette, typography }) => ({ }, reactionMarginBottom: { marginBottom: spacing(2) + }, + unfurl: { + width: Dimensions.get('window').width - 48, + minHeight: 72, + borderBottomLeftRadius: 0, + borderBottomRightRadius: 0 + }, + unfurlAuthor: { + borderBottomColor: palette.secondaryDark1 + }, + unfurlOtherUser: { + borderBottomColor: palette.neutralLight7 } })) const useGetTailColor = ( isAuthor: boolean, isPressed: boolean, - isLinkPreviewOnly + hideMessage: boolean ) => { const styles = useStyles() return isPressed - ? isAuthor && !isLinkPreviewOnly + ? isAuthor && !hideMessage ? styles.pressedIsAuthor.backgroundColor : styles.pressed.backgroundColor - : isAuthor && !isLinkPreviewOnly + : isAuthor && !hideMessage ? styles.isAuthor.backgroundColor : styles.bubble.backgroundColor } @@ -178,10 +194,13 @@ export const ChatMessageListItem = memo(function ChatMessageListItem( const senderUserId = decodeHashId(message.sender_user_id) const isAuthor = senderUserId === userId const [isPressed, setIsPressed] = useState(false) + const [emptyLinkPreview, setEmptyLinkPreview] = useState(false) const links = find(message.message) const link = links.filter((link) => link.type === 'url' && link.isLink)[0] - const isLinkPreviewOnly = link && link.value === message.message - const tailColor = useGetTailColor(isAuthor, isPressed, isLinkPreviewOnly) + const linkValue = link?.value + const isLinkPreviewOnly = linkValue === message.message + const hideMessage = isLinkPreviewOnly && !emptyLinkPreview + const tailColor = useGetTailColor(isAuthor, isPressed, hideMessage) const isUnderneathPopup = useSelector((state) => isIdEqualToReactionsPopupMessageId(state, message.message_id) @@ -201,6 +220,24 @@ export const ChatMessageListItem = memo(function ChatMessageListItem( } }, [message.message_id, message.status, onLongPress]) + const onLinkPreviewEmpty = useCallback(() => { + if (linkValue) { + setEmptyLinkPreview(true) + } + }, [linkValue]) + + const onLinkPreviewSuccess = useCallback(() => { + if (linkValue) { + setEmptyLinkPreview(false) + } + }, [linkValue]) + + const chatStyles = !hideMessage + ? isAuthor + ? { ...styles.unfurl, ...styles.unfurlAuthor } + : { ...styles.unfurl, ...styles.unfurlOtherUser } + : styles.unfurl + return ( <> - {link ? ( + {isPlaylistUrl(linkValue) ? ( + + ) : isTrackUrl(linkValue) ? ( + + ) : link ? ( ) : null} - {!isLinkPreviewOnly ? ( + {!hideMessage ? ( { + const currentUserId = useSelector(getUserId) + const isPlaying = useSelector(getPlaying) + const playingTrackId = useSelector(getTrackId) + const playingUid = useSelector(getUid) + + const playlistId = parsePlaylistIdFromPermalink( + getPathFromPlaylistUrl(link) ?? '' + ) + const { data: playlist } = useGetPlaylistById( + { + playlistId, + currentUserId + }, + { disabled: !playlistId } + ) + const collection = useMemo(() => { + return playlist + ? { + ...playlist, + // todo: make sure good value is passed in here + _cover_art_sizes: {} + } + : null + }, [playlist]) + + const uid = playlist ? makeUid(Kind.COLLECTIONS, playlist.playlist_id) : null + const trackIds = + playlist?.playlist_contents?.track_ids?.map((t) => t.track) ?? [] + const { data: tracks } = useGetTracksByIds( + { + ids: trackIds, + currentUserId + }, + { disabled: !trackIds.length } + ) + + const uidMap = useMemo(() => { + return trackIds.reduce((result: { [id: ID]: string }, id) => { + result[id] = makeUid(Kind.TRACKS, id) + return result + }, {}) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [playlist?.playlist_id]) + const tracksWithUids = useMemo(() => { + return (tracks || []).map((track) => ({ + ...track, + // todo: make sure good value is passed in here + _cover_art_sizes: {}, + user: { + ...track.user, + _profile_picture_sizes: {}, + _cover_photo_sizes: {} + }, + id: track.track_id, + uid: uidMap[track.track_id] + })) + }, [tracks, uidMap]) + const entries = useMemo(() => { + return (tracks || []).map((track) => ({ + id: track.track_id, + uid: uidMap[track.track_id], + source: QueueSource.CHAT_PLAYLIST_TRACKS + })) + }, [tracks, uidMap]) + + const isActive = playingUid !== null && playingUid === uid + + const recordAnalytics = useCallback( + ({ name, id }: { name: TrackPlayback; id: ID }) => { + trackEvent( + make({ + eventName: name, + id: `${id}`, + source: PlaybackSource.CHAT_PLAYLIST_TRACK + }) + ) + }, + [] + ) + + const playTrack = usePlayTrack(recordAnalytics) + const pauseTrack = usePauseTrack(recordAnalytics) + + const togglePlay = useCallback(() => { + if (!isPlaying || !isActive) { + if (isActive) { + playTrack({ id: playingTrackId!, uid: playingUid!, entries }) + } else { + const trackUid = tracksWithUids[0] ? tracksWithUids[0].uid : null + const trackId = tracksWithUids[0] ? tracksWithUids[0].track_id : null + if (!trackUid || !trackId) return + playTrack({ id: trackId, uid: trackUid, entries }) + } + } else { + pauseTrack(playingTrackId!) + } + }, [ + isPlaying, + isActive, + playingUid, + playingTrackId, + entries, + tracksWithUids, + playTrack, + pauseTrack + ]) + + useEffect(() => { + if (collection && uid) { + onSuccess?.() + } else { + onEmpty?.() + } + }, [collection, uid, onSuccess, onEmpty]) + + return collection && uid ? ( + + ) : null +} diff --git a/packages/mobile/src/screens/chat-screen/ChatMessageTrack.tsx b/packages/mobile/src/screens/chat-screen/ChatMessageTrack.tsx new file mode 100644 index 00000000000..b19cc1b068d --- /dev/null +++ b/packages/mobile/src/screens/chat-screen/ChatMessageTrack.tsx @@ -0,0 +1,104 @@ +import { useCallback, useEffect, useMemo } from 'react' + +import type { ChatMessageTileProps, ID, TrackPlayback } from '@audius/common' +import { + Kind, + PlaybackSource, + QueueSource, + accountSelectors, + getPathFromTrackUrl, + makeUid, + useGetTrackByPermalink, + useToggleTrack +} from '@audius/common' +import { useSelector } from 'react-redux' + +import { TrackTile } from 'app/components/lineup-tile' +import { make, track as trackEvent } from 'app/services/analytics' + +const { getUserId } = accountSelectors + +export const ChatMessageTrack = ({ + link, + onEmpty, + onSuccess, + styles +}: ChatMessageTileProps) => { + const currentUserId = useSelector(getUserId) + + const permalink = getPathFromTrackUrl(link) + const { data: track } = useGetTrackByPermalink( + { + permalink, + currentUserId + }, + { disabled: !permalink } + ) + const item = useMemo(() => { + return track + ? { + ...track, + // todo: make sure good value is passed in here + _cover_art_sizes: {} + } + : null + }, [track]) + + const user = useMemo(() => { + return track + ? { + ...track.user, + // todo: make sure good values are passed in here + _profile_picture_sizes: {}, + _cover_photo_sizes: {} + } + : null + }, [track]) + + const trackId = track?.track_id + const uid = useMemo(() => { + return trackId ? makeUid(Kind.TRACKS, trackId) : null + }, [trackId]) + + const recordAnalytics = useCallback( + ({ name, id }: { name: TrackPlayback; id: ID }) => { + if (!track) return + trackEvent( + make({ + eventName: name, + id: `${id}`, + source: PlaybackSource.CHAT_TRACK + }) + ) + }, + [track] + ) + + const { togglePlay } = useToggleTrack({ + id: track?.track_id, + uid, + source: QueueSource.CHAT_TRACKS, + recordAnalytics + }) + + useEffect(() => { + if (item && user && uid) { + onSuccess?.() + } else { + onEmpty?.() + } + }, [item, user, uid, onSuccess, onEmpty]) + + return item && user && uid ? ( + + ) : null +} diff --git a/packages/mobile/src/screens/chat-screen/LinkPreview.tsx b/packages/mobile/src/screens/chat-screen/LinkPreview.tsx index 386f70de8c1..e0548387924 100644 --- a/packages/mobile/src/screens/chat-screen/LinkPreview.tsx +++ b/packages/mobile/src/screens/chat-screen/LinkPreview.tsx @@ -1,5 +1,7 @@ +import { useEffect } from 'react' + import { useLinkUnfurlMetadata } from '@audius/common' -import type { GestureResponderEvent } from 'react-native' +import type { GestureResponderEvent, ViewStyle } from 'react-native' import { View, Image } from 'react-native' import { Text, Link } from 'app/components/core' @@ -70,32 +72,44 @@ type LinkPreviewProps = { chatId: string messageId: string href: string - isLinkPreviewOnly: boolean + hideMessage: boolean isPressed: boolean onLongPress: (event: GestureResponderEvent) => void onPressIn: (event: GestureResponderEvent) => void onPressOut: (event: GestureResponderEvent) => void + onEmpty?: () => void + onSuccess?: () => void + style?: ViewStyle } export const LinkPreview = ({ chatId, messageId, href, - isLinkPreviewOnly, + hideMessage, isPressed = false, onLongPress, onPressIn, - onPressOut + onPressOut, + onEmpty, + onSuccess, + style }: LinkPreviewProps) => { const styles = useStyles() const metadata = useLinkUnfurlMetadata(chatId, messageId, href) + const { description, title, site_name: siteName, image } = metadata || {} + const willRender = !!(description || title || image) const domain = metadata?.url ? new URL(metadata.url).hostname : '' - if (!metadata) { - return null - } + useEffect(() => { + if (willRender) { + onSuccess?.() + } else { + onEmpty?.() + } + }, [willRender, onSuccess, onEmpty]) - return ( + return willRender ? ( - {metadata.description || metadata.title ? ( + {description || title ? ( <> - {metadata.image ? ( + {image ? ( {metadata.site_name} ) : null} @@ -132,28 +147,26 @@ export const LinkPreview = ({ - {metadata.title ? ( - {metadata.title} - ) : null} - {metadata.description ? ( + {title ? {title} : null} + {description ? ( - {metadata.description} + {description} ) : ( )} - ) : metadata.image ? ( + ) : image ? ( {metadata.site_name} ) : null} - ) + ) : null } diff --git a/packages/web/src/components/track/mobile/ConnectedPlaylistTile.tsx b/packages/web/src/components/track/mobile/ConnectedPlaylistTile.tsx index 2a00a5a0a6c..025ef6c8254 100644 --- a/packages/web/src/components/track/mobile/ConnectedPlaylistTile.tsx +++ b/packages/web/src/components/track/mobile/ConnectedPlaylistTile.tsx @@ -120,8 +120,8 @@ const ConnectedPlaylistTile = ({ darkMode, showRankIcon, isTrending, - isFeed = false, - isChat = false + variant, + isFeed = false }: ConnectedPlaylistTileProps) => { const collection = getCollectionWithFallback(nullableCollection) const user = getUserWithFallback(nullableUser) @@ -205,7 +205,7 @@ const ConnectedPlaylistTile = ({ const togglePlay = useCallback(() => { if (uploading) return - const source = isChat + const source = variant ? PlaybackSource.CHAT_PLAYLIST_TRACK : PlaybackSource.PLAYLIST_TILE_TRACK @@ -240,7 +240,7 @@ const ConnectedPlaylistTile = ({ ) } }, [ - isChat, + variant, isPlaying, tracks, playTrack, @@ -322,7 +322,7 @@ const ConnectedPlaylistTile = ({ isMatrix={isMatrix()} isTrending={isTrending} showRankIcon={showRankIcon} - isChat={isChat} + variant={variant} /> ) } diff --git a/packages/web/src/components/track/mobile/ConnectedTrackTile.tsx b/packages/web/src/components/track/mobile/ConnectedTrackTile.tsx index f3c3c3d2c3a..69e9e719741 100644 --- a/packages/web/src/components/track/mobile/ConnectedTrackTile.tsx +++ b/packages/web/src/components/track/mobile/ConnectedTrackTile.tsx @@ -106,8 +106,8 @@ const ConnectedTrackTile = ({ isTrending, showRankIcon, isActive, - isFeed = false, - isChat = false + variant, + isFeed = false }: ConnectedTrackTileProps) => { const trackWithFallback = getTrackWithFallback(track) const { @@ -287,7 +287,7 @@ const ConnectedTrackTile = ({ premiumConditions={premiumConditions} doesUserHaveAccess={doesUserHaveAccess} showRankIcon={showRankIcon} - isChat={isChat} + variant={variant} /> ) } diff --git a/packages/web/src/components/track/mobile/PlaylistTile.module.css b/packages/web/src/components/track/mobile/PlaylistTile.module.css index e8b43f88b36..f573b170e39 100644 --- a/packages/web/src/components/track/mobile/PlaylistTile.module.css +++ b/packages/web/src/components/track/mobile/PlaylistTile.module.css @@ -15,7 +15,7 @@ margin: 0px auto 12px; } -.container.chat { +.container.readonly { margin: 0; max-width: none; min-width: 448px; @@ -25,7 +25,7 @@ box-shadow: none; } -.container.chat:hover { +.container.readonly:hover { transform: scale3d(1.005, 1.005, 1.005); } @@ -95,7 +95,7 @@ fill: var(--primary); } -.chat .statItem:active { +.readonly .statItem:active { color: unset; transform: scale(1); } @@ -289,7 +289,7 @@ color: var(--neutral-light-4); } -.chat .favoriteButtonWrapper:hover { +.readonly .favoriteButtonWrapper:hover { transform: scale3d(1, 1, 1); } @@ -299,7 +299,7 @@ margin-left: 4px; } -.chat .repostButtonWrapper:hover { +.readonly .repostButtonWrapper:hover { transform: scale3d(1, 1, 1); } diff --git a/packages/web/src/components/track/mobile/PlaylistTile.tsx b/packages/web/src/components/track/mobile/PlaylistTile.tsx index c61c27bb5c3..847d2d055d3 100644 --- a/packages/web/src/components/track/mobile/PlaylistTile.tsx +++ b/packages/web/src/components/track/mobile/PlaylistTile.tsx @@ -135,7 +135,7 @@ const PlaylistTile = (props: PlaylistTileProps & ExtraProps) => { isTrending, showRankIcon, trackCount, - isChat + variant } = props const [artworkLoaded, setArtworkLoaded] = useState(false) useEffect(() => { @@ -144,6 +144,7 @@ const PlaylistTile = (props: PlaylistTileProps & ExtraProps) => { } }, [artworkLoaded, hasLoaded, index, showSkeleton]) + const isReadonly = variant === 'readonly' const shouldShow = artworkLoaded && !showSkeleton const fadeIn = { [styles.show]: shouldShow, @@ -151,7 +152,7 @@ const PlaylistTile = (props: PlaylistTileProps & ExtraProps) => { } return ( -
+
{formatLineupTileDuration(props.duration)} @@ -217,7 +218,7 @@ const PlaylistTile = (props: PlaylistTileProps & ExtraProps) => { [styles.disabledStatItem]: !props.saveCount })} onClick={ - props.saveCount && !isChat + props.saveCount && !isReadonly ? props.makeGoToFavoritesPage(props.id) : undefined } @@ -236,7 +237,7 @@ const PlaylistTile = (props: PlaylistTileProps & ExtraProps) => { [styles.disabledStatItem]: !props.repostCount })} onClick={ - props.repostCount && !isChat + props.repostCount && !isReadonly ? props.makeGoToRepostsPage(props.id) : undefined } @@ -261,7 +262,7 @@ const PlaylistTile = (props: PlaylistTileProps & ExtraProps) => { numLoadingSkeletonRows={numLoadingSkeletonRows} trackCount={trackCount} /> - {!isChat ? ( + {!isReadonly ? (
{ genre, isPlaying, isBuffering, - isChat + variant } = props const { isEnabled: isGatedContentEnabled } = useFlag( FeatureFlags.GATED_CONTENT_ENABLED @@ -211,8 +212,10 @@ const TrackTile = (props: TrackTileProps & ExtraProps) => { setModalVisibility ]) + const isReadonly = variant === 'readonly' + return ( -
+
{showPremiumCornerTag && cornerTagIconType ? ( { href={profilePage(artistHandle)} onClick={props.goToArtistPage} > - - {props.artistName} - +
+ + {props.artistName} + +
{
{messages.coSign}
)}
- {coSign && !isChat ? ( + {coSign ? (
{coSign.user.name} @@ -356,7 +361,7 @@ const TrackTile = (props: TrackTileProps & ExtraProps) => { [styles.isHidden]: props.isUnlisted })} onClick={ - props.repostCount && !isChat + props.repostCount && !isReadonly ? props.makeGoToRepostsPage(id) : undefined } @@ -376,7 +381,7 @@ const TrackTile = (props: TrackTileProps & ExtraProps) => { [styles.isHidden]: props.isUnlisted })} onClick={ - props.saveCount && !isChat + props.saveCount && !isReadonly ? props.makeGoToFavoritesPage(id) : undefined } @@ -400,7 +405,7 @@ const TrackTile = (props: TrackTileProps & ExtraProps) => { {formatListenCount(props.listenCount)}
- {!isChat ? ( + {!isReadonly ? ( void isTrending: boolean showRankIcon: boolean + variant?: 'readonly' } export type TrackTileProps = TileProps & { @@ -63,7 +64,6 @@ export type TrackTileProps = TileProps & { artistHandle: string artistIsVerified: boolean isFeed?: boolean - isChat?: boolean isPlaying?: boolean isBuffering?: boolean ordered?: boolean @@ -99,7 +99,6 @@ export type PlaylistTileProps = TileProps & { disableActions?: boolean ordered?: boolean isFeed?: boolean - isChat?: boolean uploading?: boolean uploadPercent?: number ownerId: ID diff --git a/packages/web/src/pages/chat-page/components/ChatMessageListItem.module.css b/packages/web/src/pages/chat-page/components/ChatMessageListItem.module.css index fc517e3fa8c..aef24464cc0 100644 --- a/packages/web/src/pages/chat-page/components/ChatMessageListItem.module.css +++ b/packages/web/src/pages/chat-page/components/ChatMessageListItem.module.css @@ -54,11 +54,11 @@ user-select: text; } -:not(.isAuthor) .linkPreview { +:not(.isAuthor) :not(.hideMessage) .unfurl { border-bottom: 1px solid var(--neutral-light-7); } -.isAuthor .linkPreview { +.isAuthor :not(.hideMessage) .unfurl { border-bottom: 1px solid var(--secondary-dark-1); } @@ -152,11 +152,16 @@ bottom: 0; } -.tail svg * { +:not(.hideMessage) .tail svg * { fill: var(--bubble-color); transition: fill var(--quick); } +.hideMessage .tail svg * { + fill: var(--white); + transition: fill var(--quick); +} + .root:not(.isAuthor) .tail { transform: scaleX(-1); left: -7px; diff --git a/packages/web/src/pages/chat-page/components/ChatMessageListItem.tsx b/packages/web/src/pages/chat-page/components/ChatMessageListItem.tsx index 9de53e71d06..34774d9d13f 100644 --- a/packages/web/src/pages/chat-page/components/ChatMessageListItem.tsx +++ b/packages/web/src/pages/chat-page/components/ChatMessageListItem.tsx @@ -57,6 +57,7 @@ export const ChatMessageListItem = (props: ChatMessageListItemProps) => { // State const [isReactionPopupVisible, setReactionPopupVisible] = useState(false) + const [emptyLinkPreview, setEmptyLinkPreview] = useState(false) // Selectors const userId = useSelector(getUserId) @@ -72,6 +73,10 @@ export const ChatMessageListItem = (props: ChatMessageListItemProps) => { const senderUserId = decodeHashId(message.sender_user_id) const isAuthor = userId === senderUserId const links = find(message.message) + const link = links.filter((link) => link.type === 'url' && link.isLink)[0] + const linkValue = link?.value + const isLinkPreviewOnly = linkValue === message.message + const hideMessage = isLinkPreviewOnly && !emptyLinkPreview // Callbacks const handleOpenReactionPopupButtonClicked = useCallback( @@ -120,6 +125,18 @@ export const ChatMessageListItem = (props: ChatMessageListItemProps) => { ) }, [dispatch, chatId, message.message, message.message_id]) + const onLinkPreviewEmpty = useCallback(() => { + if (linkValue) { + setEmptyLinkPreview(true) + } + }, [linkValue]) + + const onLinkPreviewSuccess = useCallback(() => { + if (linkValue) { + setEmptyLinkPreview(false) + } + }, [linkValue]) + // Only render reactions if user has message permissions const { canSendMessage } = useCanSendMessage(chatId) const renderReactions = () => { @@ -167,63 +184,58 @@ export const ChatMessageListItem = (props: ChatMessageListItemProps) => { >
- {links - .filter((link) => link.type === 'url' && link.isLink) - .slice(0, 1) - .map((link) => { - if (isPlaylistUrl(link.value)) { - return ( - - ) - } - if (isTrackUrl(link.value)) { - return ( - - ) - } - return ( - - ) - })} -
- ) => { - const url = event.currentTarget.href + {isPlaylistUrl(linkValue) ? ( + + ) : isTrackUrl(linkValue) ? ( + + ) : link ? ( + + ) : null} + {!hideMessage ? ( +
+ ) => { + const url = event.currentTarget.href - if (isAudiusUrl(url)) { - const path = getPathFromAudiusUrl(url) - event.nativeEvent.preventDefault() - onClickInternalLink(path ?? '/') + if (isAudiusUrl(url)) { + const path = getPathFromAudiusUrl(url) + event.nativeEvent.preventDefault() + onClickInternalLink(path ?? '/') + } } + }, + target: (href) => { + return isAudiusUrl(href) ? '' : '_blank' } - }, - target: (href) => { - return isAudiusUrl(href) ? '' : '_blank' - } - }} - > - {message.message} - -
+ }} + > + {message.message} +
+
+ ) : null} {renderReactions()} {hasTail ? (
diff --git a/packages/web/src/pages/chat-page/components/ChatMessagePlaylist.module.css b/packages/web/src/pages/chat-page/components/ChatMessagePlaylist.module.css deleted file mode 100644 index 17c03e041ad..00000000000 --- a/packages/web/src/pages/chat-page/components/ChatMessagePlaylist.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.container.isAuthor { - border-bottom: 1px solid var(--secondary-dark-1); -} - -.container:not(.isAuthor) { - border-bottom: 1px solid var(--neutral-light-7); -} diff --git a/packages/web/src/pages/chat-page/components/ChatMessagePlaylist.tsx b/packages/web/src/pages/chat-page/components/ChatMessagePlaylist.tsx index 27804c31f50..91d7aa74544 100644 --- a/packages/web/src/pages/chat-page/components/ChatMessagePlaylist.tsx +++ b/packages/web/src/pages/chat-page/components/ChatMessagePlaylist.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from 'react' +import { useCallback, useMemo, useEffect } from 'react' import { Kind, @@ -7,40 +7,34 @@ import { ID, QueueSource, playerSelectors, - queueActions, getPathFromPlaylistUrl, useGetPlaylistById, accountSelectors, - useGetTracksByIds + useGetTracksByIds, + usePlayTrack, + usePauseTrack, + ChatMessageTileProps, + parsePlaylistIdFromPermalink } from '@audius/common' -import cn from 'classnames' -import { useDispatch, useSelector } from 'react-redux' +import { useSelector } from 'react-redux' import MobilePlaylistTile from 'components/track/mobile/ConnectedPlaylistTile' -import styles from './ChatMessagePlaylist.module.css' - const { getUserId } = accountSelectors -const { getUid, getTrackId } = playerSelectors -const { clear, add, play, pause } = queueActions - -type ChatMessagePlaylistProps = { - link: string - isAuthor: boolean -} +const { getTrackId } = playerSelectors export const ChatMessagePlaylist = ({ link, - isAuthor -}: ChatMessagePlaylistProps) => { - const dispatch = useDispatch() + onEmpty, + onSuccess, + className +}: ChatMessageTileProps) => { const currentUserId = useSelector(getUserId) - const playingUid = useSelector(getUid) const playingTrackId = useSelector(getTrackId) - const permalink = getPathFromPlaylistUrl(link) - const playlistNameWithId = permalink?.split('/').slice(-1)[0] ?? '' - const playlistId = parseInt(playlistNameWithId.split('-').slice(-1)[0]) + const playlistId = parsePlaylistIdFromPermalink( + getPathFromPlaylistUrl(link) ?? '' + ) const { data: playlist, status } = useGetPlaylistById( { playlistId, @@ -48,15 +42,17 @@ export const ChatMessagePlaylist = ({ }, { disabled: !playlistId || !currentUserId } ) - const collection = playlist - ? { - ...playlist, - // todo: make sure good value is passed in here - _cover_art_sizes: {} - } - : null + const collection = useMemo(() => { + return playlist + ? { + ...playlist, + // todo: make sure good value is passed in here + _cover_art_sizes: {} + } + : null + }, [playlist]) - const uid = playlist ? makeUid(Kind.COLLECTIONS, playlist.playlist_id) : '' + const uid = playlist ? makeUid(Kind.COLLECTIONS, playlist.playlist_id) : null const trackIds = playlist?.playlist_contents?.track_ids?.map((t) => t.track) ?? [] const { data: tracks } = useGetTracksByIds( @@ -66,7 +62,6 @@ export const ChatMessagePlaylist = ({ }, { disabled: !trackIds.length || !currentUserId } ) - const playlistTracks = tracks ?? [] const uidMap = useMemo(() => { return trackIds.reduce((result: { [id: ID]: string }, id) => { @@ -75,43 +70,48 @@ export const ChatMessagePlaylist = ({ }, {}) // eslint-disable-next-line react-hooks/exhaustive-deps }, [playlist?.playlist_id]) - const tracksWithUids = playlistTracks.map((track) => ({ - ...track, - // todo: make sure good value is passed in here - _cover_art_sizes: {}, - user: { - ...track.user, - _profile_picture_sizes: {}, - _cover_photo_sizes: {} - }, - id: track.track_id, - uid: uidMap[track.track_id] - })) - const entries = playlistTracks.map((track) => ({ - id: track.track_id, - uid: uidMap[track.track_id], - source: QueueSource.CHAT_PLAYLIST_TRACKS - })) + const tracksWithUids = useMemo(() => { + return (tracks || []).map((track) => ({ + ...track, + // todo: make sure good value is passed in here + _cover_art_sizes: {}, + user: { + ...track.user, + _profile_picture_sizes: {}, + _cover_photo_sizes: {} + }, + id: track.track_id, + uid: uidMap[track.track_id] + })) + }, [tracks, uidMap]) + const entries = useMemo(() => { + return (tracks || []).map((track) => ({ + id: track.track_id, + uid: uidMap[track.track_id], + source: QueueSource.CHAT_PLAYLIST_TRACKS + })) + }, [tracks, uidMap]) + const play = usePlayTrack() const playTrack = useCallback( (uid: string) => { - if (playingUid !== uid) { - dispatch(clear({})) - dispatch(add({ entries })) - dispatch(play({ uid })) - } else { - dispatch(play({})) - } + play({ uid, entries }) }, - [dispatch, playingUid, entries] + [play, entries] ) - const pauseTrack = useCallback(() => { - dispatch(pause({})) - }, [dispatch]) + const pauseTrack = usePauseTrack() + + useEffect(() => { + if (collection && uid) { + onSuccess?.() + } else { + onEmpty?.() + } + }, [collection, uid, onSuccess, onEmpty]) - return playlist ? ( -
+ return collection && uid ? ( +
{/* You may wonder why we use the mobile web playlist tile here. It's simply because the chat playlist tile uses the same design as mobile web. */} {}} playingTrackId={playingTrackId} - isChat + variant='readonly' />
) : null diff --git a/packages/web/src/pages/chat-page/components/ChatMessageTrack.module.css b/packages/web/src/pages/chat-page/components/ChatMessageTrack.module.css deleted file mode 100644 index 17c03e041ad..00000000000 --- a/packages/web/src/pages/chat-page/components/ChatMessageTrack.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.container.isAuthor { - border-bottom: 1px solid var(--secondary-dark-1); -} - -.container:not(.isAuthor) { - border-bottom: 1px solid var(--neutral-light-7); -} diff --git a/packages/web/src/pages/chat-page/components/ChatMessageTrack.tsx b/packages/web/src/pages/chat-page/components/ChatMessageTrack.tsx index 53274134bb0..3ad70f27ed5 100644 --- a/packages/web/src/pages/chat-page/components/ChatMessageTrack.tsx +++ b/packages/web/src/pages/chat-page/components/ChatMessageTrack.tsx @@ -1,42 +1,34 @@ -import { useCallback, useMemo } from 'react' +import { useCallback, useEffect, useMemo } from 'react' import { Kind, Status, makeUid, - queueSelectors, - playerSelectors, - Name, PlaybackSource, - queueActions, QueueSource, accountSelectors, useGetTrackByPermalink, - getPathFromTrackUrl + getPathFromTrackUrl, + useToggleTrack, + ID, + TrackPlayback, + ChatMessageTileProps } from '@audius/common' -import cn from 'classnames' import { useDispatch, useSelector } from 'react-redux' import { make } from 'common/store/analytics/actions' import MobileTrackTile from 'components/track/mobile/ConnectedTrackTile' -import styles from './ChatMessageTrack.module.css' - const { getUserId } = accountSelectors -const { makeGetCurrent } = queueSelectors -const { getPlaying } = playerSelectors -const { clear, add, play, pause } = queueActions - -type ChatMessageTrackProps = { - link: string - isAuthor: boolean -} -export const ChatMessageTrack = ({ link, isAuthor }: ChatMessageTrackProps) => { +export const ChatMessageTrack = ({ + link, + onEmpty, + onSuccess, + className +}: ChatMessageTileProps) => { const dispatch = useDispatch() const currentUserId = useSelector(getUserId) - const currentQueueItem = useSelector(makeGetCurrent()) - const playing = useSelector(getPlaying) const permalink = getPathFromTrackUrl(link) const { data: track, status } = useGetTrackByPermalink( @@ -47,70 +39,46 @@ export const ChatMessageTrack = ({ link, isAuthor }: ChatMessageTrackProps) => { { disabled: !permalink || !currentUserId } ) + const trackId = track?.track_id const uid = useMemo(() => { - return track ? makeUid(Kind.TRACKS, track.track_id) : '' - }, [track]) - const isTrackPlaying = - playing && - !!track && - !!currentQueueItem.track && - currentQueueItem.uid === uid + return trackId ? makeUid(Kind.TRACKS, trackId) : null + }, [trackId]) const recordAnalytics = useCallback( - ({ name, source }: { name: Name; source: PlaybackSource }) => { + ({ name, id }: { name: TrackPlayback; id: ID }) => { if (!track) return dispatch( make(name, { - id: `${track.track_id}`, - source + id: `${id}`, + source: PlaybackSource.CHAT_TRACK }) ) }, [dispatch, track] ) - const onTogglePlay = useCallback(() => { - if (!track) return - if (isTrackPlaying) { - dispatch(pause({})) - recordAnalytics({ - name: Name.PLAYBACK_PAUSE, - source: PlaybackSource.CHAT_TRACK - }) - } else if ( - currentQueueItem.uid !== uid && - currentQueueItem.track && - currentQueueItem.uid === uid - ) { - dispatch(play({})) - recordAnalytics({ - name: Name.PLAYBACK_PLAY, - source: PlaybackSource.CHAT_TRACK - }) + const { togglePlay, isTrackPlaying } = useToggleTrack({ + id: track?.track_id, + uid, + source: QueueSource.CHAT_TRACKS, + recordAnalytics + }) + + useEffect(() => { + if (track && uid) { + onSuccess?.() } else { - dispatch(clear({})) - dispatch( - add({ - entries: [ - { id: track.track_id, uid, source: QueueSource.CHAT_TRACKS } - ] - }) - ) - dispatch(play({ uid })) - recordAnalytics({ - name: Name.PLAYBACK_PLAY, - source: PlaybackSource.CHAT_TRACK - }) + onEmpty?.() } - }, [dispatch, recordAnalytics, track, isTrackPlaying, currentQueueItem, uid]) + }, [track, uid, onSuccess, onEmpty]) - return track ? ( -
+ return track && uid ? ( +
{/* You may wonder why we use the mobile web track tile here. It's simply because the chat track tile uses the same design as mobile web. */} {}} @@ -118,7 +86,7 @@ export const ChatMessageTrack = ({ link, isAuthor }: ChatMessageTrackProps) => { showRankIcon={false} showArtistPick={false} isActive={isTrackPlaying} - isChat + variant='readonly' />
) : null diff --git a/packages/web/src/pages/chat-page/components/LinkPreview.tsx b/packages/web/src/pages/chat-page/components/LinkPreview.tsx index 25b7a870c64..3db34c5846b 100644 --- a/packages/web/src/pages/chat-page/components/LinkPreview.tsx +++ b/packages/web/src/pages/chat-page/components/LinkPreview.tsx @@ -1,3 +1,5 @@ +import { useEffect } from 'react' + import { useLinkUnfurlMetadata } from '@audius/common' import cn from 'classnames' @@ -7,20 +9,26 @@ type LinkPreviewProps = { href: string chatId: string messageId: string + onEmpty?: () => void + onSuccess?: () => void className?: string } export const LinkPreview = (props: LinkPreviewProps) => { - const { href, chatId, messageId } = props + const { href, chatId, messageId, onEmpty, onSuccess } = props const metadata = useLinkUnfurlMetadata(chatId, messageId, href) ?? {} + const { description, title, site_name: siteName, image } = metadata + const willRender = !!(description || title || image) const domain = metadata?.url ? new URL(metadata?.url).hostname : '' - const { description, title, image, site_name: siteName } = metadata - const hasMetadata = !!(description || title || image) - if (!hasMetadata) { - return null - } + useEffect(() => { + if (willRender) { + onSuccess?.() + } else { + onEmpty?.() + } + }, [willRender, onSuccess, onEmpty]) - return ( + return willRender ? ( { ) : null} - ) + ) : null }