From 00a1f404b64c6cf015741c012eead47624e2338e Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Tue, 27 Oct 2026 12:04:19 -0700 Subject: [PATCH] feat(web): waveform with comment avatars on desktop track tiles Large desktop track tiles get a bar waveform between the title and the stats row, behind the track_tile_waveform flag (default on in dev, off in prod). Clicking the waveform seeks the playing track or starts the tile at that point. Comments with a track timestamp sit on the baseline as avatars; hover shows the comment, click opens it on the track page. No node serves audio peaks yet, so the shape is synthesized from the track id. Comments are fetched once per tile, only for tiles with comments that have scrolled near the viewport. The timestamped-comments hook moves from mobile into common so both the Now Playing scrubber and the web tile share it. Co-Authored-By: Claude Fable 5.1 --- .../src/api/tan-query/comments/index.ts | 1 + .../comments}/useTimestampedComments.ts | 5 +- .../services/remote-config/feature-flags.ts | 10 +- .../now-playing-drawer/CommentBubble.tsx | 3 +- .../now-playing-drawer/NowPlayingDrawer.tsx | 3 +- .../components/track/desktop/TrackTile.tsx | 50 ++- .../track/desktop/TrackTileWaveform.tsx | 324 ++++++++++++++++++ 7 files changed, 380 insertions(+), 16 deletions(-) rename packages/{mobile/src/components/now-playing-drawer => common/src/api/tan-query/comments}/useTimestampedComments.ts (90%) create mode 100644 packages/web/src/components/track/desktop/TrackTileWaveform.tsx diff --git a/packages/common/src/api/tan-query/comments/index.ts b/packages/common/src/api/tan-query/comments/index.ts index e0cca9a48c3..12278377c7a 100644 --- a/packages/common/src/api/tan-query/comments/index.ts +++ b/packages/common/src/api/tan-query/comments/index.ts @@ -2,6 +2,7 @@ export * from './types' export * from './utils' export * from './useUserComments' export * from './useTrackComments' +export * from './useTimestampedComments' export * from './useComment' export * from './useComments' export * from './useTrackCommentCount' diff --git a/packages/mobile/src/components/now-playing-drawer/useTimestampedComments.ts b/packages/common/src/api/tan-query/comments/useTimestampedComments.ts similarity index 90% rename from packages/mobile/src/components/now-playing-drawer/useTimestampedComments.ts rename to packages/common/src/api/tan-query/comments/useTimestampedComments.ts index ea6863c240b..ad1ee72adde 100644 --- a/packages/mobile/src/components/now-playing-drawer/useTimestampedComments.ts +++ b/packages/common/src/api/tan-query/comments/useTimestampedComments.ts @@ -1,7 +1,8 @@ import { useMemo } from 'react' -import { useTrackComments } from '@audius/common/api' -import type { Comment, ID } from '@audius/common/models' +import type { Comment, ID } from '~/models' + +import { useTrackComments } from './useTrackComments' const PAGE_SIZE = 50 // GetTrackCommentsSortMethodEnum.Timestamp diff --git a/packages/common/src/services/remote-config/feature-flags.ts b/packages/common/src/services/remote-config/feature-flags.ts index fab3d5a4901..33bb76af67c 100644 --- a/packages/common/src/services/remote-config/feature-flags.ts +++ b/packages/common/src/services/remote-config/feature-flags.ts @@ -18,7 +18,8 @@ export enum FeatureFlags { QUEUE_NEW_FEATURE_BADGE = 'queue_new_feature_badge', WEEKLY_ROTATION = 'weekly_rotation', TRACK_ROWS = 'track_rows', - NOW_PLAYING_COMMENTS = 'now_playing_comments' + NOW_PLAYING_COMMENTS = 'now_playing_comments', + TRACK_TILE_WAVEFORM = 'track_tile_waveform' } type FlagDefaults = Record @@ -28,7 +29,8 @@ export const environmentFlagDefaults: Record< Partial > = { development: { - [FeatureFlags.FAN_CLUB_TEXT_POST_POSTING]: true + [FeatureFlags.FAN_CLUB_TEXT_POST_POSTING]: true, + [FeatureFlags.TRACK_TILE_WAVEFORM]: true }, production: {} } @@ -59,5 +61,7 @@ export const flagDefaults: FlagDefaults = { [FeatureFlags.TRACK_ROWS]: false, // Timestamped comments as markers and bubbles on the Now Playing scrubber. // Default on; the remote flag is the kill switch. - [FeatureFlags.NOW_PLAYING_COMMENTS]: true + [FeatureFlags.NOW_PLAYING_COMMENTS]: true, + // Waveform with timestamped comment avatars on desktop web track tiles. + [FeatureFlags.TRACK_TILE_WAVEFORM]: false } diff --git a/packages/mobile/src/components/now-playing-drawer/CommentBubble.tsx b/packages/mobile/src/components/now-playing-drawer/CommentBubble.tsx index f9547c4aa14..765055fd0ec 100644 --- a/packages/mobile/src/components/now-playing-drawer/CommentBubble.tsx +++ b/packages/mobile/src/components/now-playing-drawer/CommentBubble.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { useUser } from '@audius/common/api' +import type { TimestampedComment } from '@audius/common/api' import type { ID } from '@audius/common/models' import { playbackSelectors } from '@audius/common/store' import { Dimensions, Pressable, View } from 'react-native' @@ -13,8 +14,6 @@ import { useCommentDrawer } from 'app/components/comments/CommentDrawerContext' import { ProfilePicture } from 'app/components/core' import { useNavigation } from 'app/hooks/useNavigation' -import type { TimestampedComment } from './useTimestampedComments' - const { getSeek, getSeekCounter } = playbackSelectors const messages = { diff --git a/packages/mobile/src/components/now-playing-drawer/NowPlayingDrawer.tsx b/packages/mobile/src/components/now-playing-drawer/NowPlayingDrawer.tsx index a217ed0b090..8681e30f751 100644 --- a/packages/mobile/src/components/now-playing-drawer/NowPlayingDrawer.tsx +++ b/packages/mobile/src/components/now-playing-drawer/NowPlayingDrawer.tsx @@ -8,7 +8,7 @@ import { useState } from 'react' -import { useUser } from '@audius/common/api' +import { useUser, useTimestampedComments } from '@audius/common/api' import { useCurrentTrack, useFeatureFlag } from '@audius/common/hooks' import { FeatureFlags } from '@audius/common/services' import { playbackActions, playbackSelectors } from '@audius/common/store' @@ -60,7 +60,6 @@ import { TitleBar } from './TitleBar' import { TrackInfo } from './TrackInfo' import { PLAY_BAR_HEIGHT } from './constants' import { useCurrentTrackDuration } from './useCurrentTrackDuration' -import { useTimestampedComments } from './useTimestampedComments' const { seekTo: seek, reset, next, previous } = playbackActions const { getPlaying, getTrackId, getCounter, getBuffering } = playbackSelectors diff --git a/packages/web/src/components/track/desktop/TrackTile.tsx b/packages/web/src/components/track/desktop/TrackTile.tsx index 04908f19b44..0091b81fd57 100644 --- a/packages/web/src/components/track/desktop/TrackTile.tsx +++ b/packages/web/src/components/track/desktop/TrackTile.tsx @@ -1,13 +1,14 @@ import { useCallback, useEffect, MouseEvent } from 'react' import { useCurrentUserId, useTrack, useUser } from '@audius/common/api' -import { useGatedContentAccess } from '@audius/common/hooks' +import { useFeatureFlag, useGatedContentAccess } from '@audius/common/hooks' import { ShareSource, RepostSource, FavoriteSource, ID } from '@audius/common/models' +import { FeatureFlags } from '@audius/common/services' import { tracksSocialActions, shareModalUIActions, @@ -51,6 +52,7 @@ import { TrackTileSize } from '../types' import styles from './TrackTile.module.css' import { TrackTileDuration } from './TrackTileDuration' +import { TrackTileWaveform } from './TrackTileWaveform' const { requestOpen: requestOpenShareModal } = shareModalUIActions const { repostTrack, undoRepostTrack, saveTrack, unsaveTrack } = @@ -62,6 +64,8 @@ const { getPlaying } = playbackSelectors +const WAVEFORM_ARTWORK_SIZE = 176 + // Props from ConnectedTrackTile export type TrackTileProps = { id: ID @@ -139,7 +143,9 @@ export const TrackTile = ({ permalink, _co_sign: coSign, has_current_user_reposted: isReposted, - has_current_user_saved: isFavorited + has_current_user_saved: isFavorited, + duration, + comment_count: commentCount } = trackWithFallback const isArtistPick = partialUser?.artist_pick_track_id === trackId @@ -148,6 +154,21 @@ export const TrackTile = ({ useGatedContentAccess(trackWithFallback) const loading = isLoading || isFetchingNFTAccess || isPending + const { isEnabled: isWaveformEnabled } = useFeatureFlag( + FeatureFlags.TRACK_TILE_WAVEFORM + ) + const showWaveform = + isWaveformEnabled && + size === TrackTileSize.LARGE && + !loading && + duration > 0 + // With the waveform the right column is ~176px tall; the artwork matches it. + const artworkSize = showWaveform + ? WAVEFORM_ARTWORK_SIZE + : size === TrackTileSize.LARGE + ? 128 + : 108 + const [, setLockedContentVisibility] = useModalState('LockedContent') useEffect(() => { @@ -319,7 +340,12 @@ export const TrackTile = ({ disableActions && { opacity: 0.5, pointerEvents: 'none' }, { cursor: canClickTile ? 'pointer' : 'default', - height: size === TrackTileSize.LARGE ? 144 : 128, + // The waveform tile is taller; its height comes from the content. + height: showWaveform + ? undefined + : size === TrackTileSize.LARGE + ? 144 + : 128, containerType: 'inline-size', '&:hover .artworkIcon': { opacity: 0.75 }, '&:hover': { transform: 'scale(1.004)' }, @@ -343,10 +369,7 @@ export const TrackTile = ({ )} {/* Track tile image */} - +