Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-comment-kebab-menu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@audius/mobile': patch
---

Fix the comment kebab (three-dot) menu in mobile: tapping the kebab now reliably opens the action drawer with Edit/Delete (and other) options. The previous handler toggled `isOpen`/`isVisible` from captured state, which let the two booleans drift out of sync after a swipe-close mid-animation — from then on, taps re-rendered the drawer with `isOpen=false` and it never slid back in. Also dropped a redundant inner `CommentSectionProvider` wrap inside the Portal that could short-circuit the drawer to `null` while its track query rehydrated.
24 changes: 24 additions & 0 deletions packages/common/src/models/Analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,9 @@ export enum Name {
REMIX_CONTEST_DELETE = 'Remix Contest: Delete',
REMIX_CONTEST_PICK_WINNERS_OPEN = 'Remix Contest: Pick Winners Open',
REMIX_CONTEST_PICK_WINNERS_FINALIZE = 'Remix Contest: Finalize Winners',
REMIX_CONTEST_VIEW = 'Remix Contest: View',
REMIX_CONTEST_ENTER = 'Remix Contest: Enter',
REMIX_CONTEST_VIEW_SUBMISSIONS = 'Remix Contest: View Submissions',

// Android App Lifecycle
ANDROID_APP_RESTART_HEARTBEAT = 'Android App: Restart Due to Heartbeat',
Expand Down Expand Up @@ -2868,6 +2871,24 @@ export type RemixContestPickWinnersFinalize = {
trackId: ID
}

export type RemixContestView = {
eventName: Name.REMIX_CONTEST_VIEW
remixContestId: ID
trackId: ID
}

export type RemixContestEnter = {
eventName: Name.REMIX_CONTEST_ENTER
remixContestId: ID
trackId: ID
}

export type RemixContestViewSubmissions = {
eventName: Name.REMIX_CONTEST_VIEW_SUBMISSIONS
remixContestId: ID
trackId: ID
}

export type AndroidAppRestartHeartbeat = {
eventName: Name.ANDROID_APP_RESTART_HEARTBEAT
timeSinceLastHeartbeat: number
Expand Down Expand Up @@ -3504,6 +3525,9 @@ export type AllTrackingEvents =
| RemixContestDelete
| RemixContestPickWinnersOpen
| RemixContestPickWinnersFinalize
| RemixContestView
| RemixContestEnter
| RemixContestViewSubmissions
| AndroidAppRestartHeartbeat
| AndroidAppRestartStale
| AndroidAppRestartForceQuit
Expand Down
25 changes: 13 additions & 12 deletions packages/mobile/src/components/comments/CommentOverflowMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, { useCallback, useState } from 'react'

import { useUser } from '@audius/common/api'
import {
CommentSectionProvider,
useCurrentCommentSection,
useDeleteComment,
useUpdateCommentNotificationSetting,
Expand Down Expand Up @@ -238,16 +237,20 @@ export const CommentOverflowMenu = (props: CommentOverflowMenuProps) => {
}, [deleteComment, id, parentCommentId, toast])

const handlePress = useCallback(() => {
setIsOpen(!isOpen)
setIsVisible(!isVisible)
// Always open on press; closing is driven by the drawer's onClose/onClosed
// callbacks. Toggling these from captured values let isOpen and isVisible
// drift out of sync when the drawer was swipe-closed mid-animation, after
// which subsequent taps rendered the drawer with isOpen=false (no slide-in).
setIsOpen(true)
setIsVisible(true)

trackEvent(
make({
eventName: Name.COMMENTS_OPEN_COMMENT_OVERFLOW_MENU,
commentId: id
})
)
}, [isOpen, isVisible, id])
}, [id])

return (
<>
Expand All @@ -262,14 +265,12 @@ export const CommentOverflowMenu = (props: CommentOverflowMenuProps) => {

<Portal hostName='DrawerPortal'>
{isVisible ? (
<CommentSectionProvider entityId={entityId}>
<ActionDrawerWithoutRedux
rows={rows}
isOpen={isOpen}
onClose={() => setIsOpen(false)}
onClosed={() => setIsVisible(false)}
/>
</CommentSectionProvider>
<ActionDrawerWithoutRedux
rows={rows}
isOpen={isOpen}
onClose={() => setIsOpen(false)}
onClosed={() => setIsVisible(false)}
/>
) : null}

{isFlagAndHideConfirmationVisible ? (
Expand Down
37 changes: 35 additions & 2 deletions packages/mobile/src/screens/contest-screen/ContestScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState
} from 'react'

Expand All @@ -21,7 +22,7 @@ import {
useUser
} from '@audius/common/api'
import { useFeatureFlag } from '@audius/common/hooks'
import { ShareSource } from '@audius/common/models'
import { Name, ShareSource } from '@audius/common/models'
import { FeatureFlags } from '@audius/common/services'
import { shareModalUIActions } from '@audius/common/store'
import { dayjs, getLocalTimezone } from '@audius/common/utils'
Expand All @@ -36,6 +37,7 @@ import { useDispatch } from 'react-redux'
import { Button, Divider, Flex, Text } from '@audius/harmony-native'
import { Screen, ScreenContent } from 'app/components/core'
import { ProfilePicture } from 'app/components/core/ProfilePicture'
import { make, track as trackEvent } from 'app/services/analytics'
import {
CollapsibleTabNavigator,
collapsibleTabScreen
Expand Down Expand Up @@ -295,7 +297,38 @@ export const ContestScreen = () => {
dispatch(setVisibility({ drawer: 'PickWinners', visible: true }))
}, [trackId, dispatch])

const handleEnterContest = useEnterContest(trackId)
const enterContest = useEnterContest(trackId)
const handleEnterContest = useCallback(async () => {
if (trackId != null && eventId != null) {
trackEvent(
make({
eventName: Name.REMIX_CONTEST_ENTER,
remixContestId: eventId,
trackId
})
)
}
await enterContest()
}, [enterContest, trackId, eventId])

// Fire a Remix Contest: View event the first time the screen resolves
// both a trackId and an eventId. The screen is mounted once per
// navigation push, so a ref guard makes the event idempotent across
// unrelated re-renders (followers count update, scroll-y reaction,
// etc.) while still firing on each fresh push.
const hasFiredViewRef = useRef(false)
useEffect(() => {
if (hasFiredViewRef.current) return
if (trackId == null || eventId == null) return
hasFiredViewRef.current = true
trackEvent(
make({
eventName: Name.REMIX_CONTEST_VIEW,
remixContestId: eventId,
trackId
})
)
}, [trackId, eventId])

// Hide the stack navigator header — the in-hero back button is the
// only back affordance in the Figma (2888-131647). Leaving the
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { useCallback, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'

import {
getRemixesQueryKey,
useRemixContest,
useRemixesCount,
useRemixesLineup
} from '@audius/common/api'
import { Name } from '@audius/common/models'
import type { ID } from '@audius/common/models'
import { useFocusedTab } from 'react-native-collapsible-tab-view'

import { Divider, FilterButton, Flex, Text } from '@audius/harmony-native'
import { TrackLineup } from 'app/components/lineup/TrackLineup'
import { make, track as trackEvent } from 'app/services/analytics'

import { useContestPage } from '../ContestPageContext'

Expand Down Expand Up @@ -56,10 +59,33 @@ const SORT_OPTIONS = [
* gap by hydrating Redux from the tan-query cache immediately.
*/
export const ContestSubmissionsTab = () => {
const { trackId } = useContestPage()
const { trackId, eventId } = useContestPage()
const { data: contest } = useRemixContest(trackId)
const winnerCount = contest?.eventData?.winners?.length ?? 0

// Fire a Remix Contest: View Submissions event the first time the
// user actually focuses this tab. Tabs are mounted eagerly
// (`lazy: false` in `CollapsibleTabNavigator`), so a plain mount
// effect would fire even for users who only ever look at the Details
// tab. `useFocusedTab` from react-native-collapsible-tab-view is the
// primitive the tab navigator already uses — it returns the
// currently-focused tab name and re-runs effects when that changes.
const focusedTab = useFocusedTab()
const hasFiredSubmissionsViewRef = useRef(false)
useEffect(() => {
if (hasFiredSubmissionsViewRef.current) return
if (focusedTab !== 'Submissions') return
if (trackId == null || eventId == null) return
hasFiredSubmissionsViewRef.current = true
trackEvent(
make({
eventName: Name.REMIX_CONTEST_VIEW_SUBMISSIONS,
remixContestId: eventId,
trackId
})
)
}, [focusedTab, trackId, eventId])

const [sortMethod, setSortMethod] = useState<'recent' | 'plays' | 'likes'>(
'recent'
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { useExploreRoute } from './hooks'
const SearchExploreContent = () => {
const { params } = useExploreRoute<'SearchExplore'>()
const scrollRef = useRef<any>(null)
const [, setCategory] = useSearchCategory()
const [category, setCategory] = useSearchCategory()
const [filters, setFilters] = useSearchFilters()
const [query, setQuery] = useSearchQuery()
const [, setAutoFocus] = useSearchAutoFocus()
Expand Down Expand Up @@ -50,7 +50,7 @@ const SearchExploreContent = () => {
}
})

const showSearch = Boolean(query || hasAnyFilter)
const showSearch = Boolean(query || hasAnyFilter || category !== 'all')

return (
<ScreenContent>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'

import {
getRemixesQueryKey,
Expand All @@ -16,7 +16,7 @@ import {
} from '@audius/common/api'
import { useFeatureFlag } from '@audius/common/hooks'
import type { ID } from '@audius/common/models'
import { SquareSizes, ShareSource } from '@audius/common/models'
import { Name, SquareSizes, ShareSource } from '@audius/common/models'
import { FeatureFlags } from '@audius/common/services'
import {
remixesPageActions,
Expand Down Expand Up @@ -53,6 +53,7 @@ import { UserGeneratedText } from 'components/user-generated-text'
import { VideoEmbed } from 'components/video-embed/VideoEmbed'
import { useRequiresAccountCallback } from 'hooks/useRequiresAccount'
import { useTrackCoverArt } from 'hooks/useTrackCoverArt'
import { make, track as trackEvent } from 'services/analytics'
import { useRemixPageParams } from 'pages/remixes-page/hooks'
import { useUpdateSearchParams } from 'pages/search-page/hooks'
import {
Expand Down Expand Up @@ -318,6 +319,24 @@ const ContestPage = ({ containerRef: _containerRef }: ContestPageProps) => {
}
}, [dispatch])

// Fire a Remix Contest: View event the first time the page resolves a
// trackId + eventId for the contest. Guard with a ref so navigating
// between contest tabs (which doesn't unmount the page) doesn't
// re-fire the event.
const hasFiredViewRef = useRef(false)
useEffect(() => {
if (hasFiredViewRef.current) return
if (trackId == null || eventId == null) return
hasFiredViewRef.current = true
trackEvent(
make({
eventName: Name.REMIX_CONTEST_VIEW,
remixContestId: eventId,
trackId
})
)
}, [trackId, eventId])

const isEnded = useMemo(() => {
if (!contest?.endDate) return true
return dayjs(contest.endDate).isBefore(dayjs())
Expand Down Expand Up @@ -368,7 +387,19 @@ const ContestPage = ({ containerRef: _containerRef }: ContestPageProps) => {
// shape; reused across the desktop + mobile contest pages and the
// mobile track-page contest details tab so submitters get the same
// pre-filled form regardless of entry point.
const handleEnterContest = useEnterContest(trackId)
const enterContest = useEnterContest(trackId)
const handleEnterContest = useCallback(async () => {
if (trackId != null && eventId != null) {
trackEvent(
make({
eventName: Name.REMIX_CONTEST_ENTER,
remixContestId: eventId,
trackId
})
)
}
await enterContest()
}, [enterContest, trackId, eventId])

const handleShareContest = useCallback(() => {
if (!trackId) return
Expand Down Expand Up @@ -711,7 +742,18 @@ const ContestPage = ({ containerRef: _containerRef }: ContestPageProps) => {
size='large'
isSelected={activeTab === 'submissions'}
label={messages.submissionsTab(submissionsCount)}
onClick={() => setActiveTab('submissions')}
onClick={() => {
if (activeTab !== 'submissions' && trackId && eventId) {
trackEvent(
make({
eventName: Name.REMIX_CONTEST_VIEW_SUBMISSIONS,
remixContestId: eventId,
trackId
})
)
}
setActiveTab('submissions')
}}
/>
</Flex>
) : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ const SearchExplorePage = ({
minWidth: MIN_DESKTOP_CONTENT_WIDTH_PX,
overflowX: 'clip',
overflowY: 'visible',
display: showSearchResults ? 'none' : undefined
display: inputValue || showSearchResults ? 'none' : undefined
}}
>
{sectionConfigs.map(({ key, shouldRender, element }) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ const SearchExplorePage = ({
direction='column'
mt='l'
gap='2xl'
css={{ display: showSearchResults ? 'none' : undefined }}
css={{ display: inputValue || showSearchResults ? 'none' : undefined }}
>
{showTrackContent && showUserContextualContent ? (
<RecommendedTracksSection />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export const AlbumResultsPage = () => {
const { data, isFetching, hasNextPage, loadNextPage, isPending } = queryData

const isResultsEmpty = data?.length === 0
const showNoResultsTile = !isFetching && isResultsEmpty
const showNoResultsTile = !isFetching && !isPending && isResultsEmpty

return (
<InfiniteScroll
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export const PlaylistResultsPage = () => {
} = queryData

const isResultsEmpty = playlists?.length === 0
const showNoResultsTile = !isFetching && isResultsEmpty
const showNoResultsTile = !isFetching && !isPending && isResultsEmpty

return (
<InfiniteScroll
Expand Down