[Drafts] Storage fixes (#9790)
* delete media from existsCache when deleting * revoke media URLs * skip revoking objecturls until the composer is completely closed * [Drafts] Metrics (#9794) * metrics for drafts * Nit: format * nit: use new util for clarity * nit: use new util for clarity --------- Co-authored-by: Eric Bailey <git@esb.lol> --------- Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
@@ -233,6 +233,53 @@ export type Events = {
|
||||
persist: boolean
|
||||
hasChanged: boolean
|
||||
}
|
||||
'composer:open': {
|
||||
logContext:
|
||||
| 'Fab'
|
||||
| 'PostReply'
|
||||
| 'QuotePost'
|
||||
| 'ProfileFeed'
|
||||
| 'Deeplink'
|
||||
| 'Other'
|
||||
isReply: boolean
|
||||
hasQuote: boolean
|
||||
hasDraft: boolean
|
||||
}
|
||||
'draft:save': {
|
||||
isNewDraft: boolean
|
||||
hasText: boolean
|
||||
hasImages: boolean
|
||||
hasVideo: boolean
|
||||
hasGif: boolean
|
||||
hasQuote: boolean
|
||||
hasLink: boolean
|
||||
postCount: number
|
||||
textLength: number
|
||||
}
|
||||
'draft:load': {
|
||||
draftAgeMs: number
|
||||
hasText: boolean
|
||||
hasImages: boolean
|
||||
hasVideo: boolean
|
||||
hasGif: boolean
|
||||
postCount: number
|
||||
}
|
||||
'draft:delete': {
|
||||
logContext: 'DraftsList'
|
||||
draftAgeMs: number
|
||||
}
|
||||
'draft:listOpen': {
|
||||
draftCount: number
|
||||
}
|
||||
'draft:post': {
|
||||
draftAgeMs: number
|
||||
wasEdited: boolean
|
||||
}
|
||||
'draft:discard': {
|
||||
logContext: 'ComposerClose' | 'BeforeDraftsList'
|
||||
hadContent: boolean
|
||||
textLength: number
|
||||
}
|
||||
|
||||
// Data events
|
||||
'account:create:begin': {}
|
||||
|
||||
@@ -185,6 +185,7 @@ let PostControls = ({
|
||||
openComposer({
|
||||
quote: post,
|
||||
onPost: onPostReply,
|
||||
logContext: 'QuotePost',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,7 @@ export function useComposeIntent() {
|
||||
openComposer({
|
||||
text: text ?? undefined,
|
||||
videoUri: {uri, width: Number(width), height: Number(height)},
|
||||
logContext: 'Deeplink',
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -153,6 +154,7 @@ export function useComposeIntent() {
|
||||
openComposer({
|
||||
text: text ?? undefined,
|
||||
imageUris: IS_NATIVE ? imageUris : undefined,
|
||||
logContext: 'Deeplink',
|
||||
})
|
||||
}, 500)
|
||||
},
|
||||
|
||||
@@ -261,6 +261,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
|
||||
langs: record.langs,
|
||||
},
|
||||
onPostSuccess: onPostSuccess,
|
||||
logContext: 'PostReply',
|
||||
})
|
||||
|
||||
if (postSource) {
|
||||
|
||||
@@ -237,6 +237,7 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({
|
||||
langs: post.record.langs,
|
||||
},
|
||||
onPostSuccess: onPostSuccess,
|
||||
logContext: 'PostReply',
|
||||
})
|
||||
}, [openComposer, post, record, onPostSuccess, moderation])
|
||||
|
||||
|
||||
@@ -302,6 +302,7 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({
|
||||
langs: post.record.langs,
|
||||
},
|
||||
onPostSuccess: onPostSuccess,
|
||||
logContext: 'PostReply',
|
||||
})
|
||||
}, [openComposer, post, record, onPostSuccess, moderation])
|
||||
|
||||
|
||||
@@ -125,6 +125,7 @@ export function PostThread({uri}: {uri: string}) {
|
||||
langs: post.record.langs,
|
||||
},
|
||||
onPostSuccess: optimisticOnPostReply,
|
||||
logContext: 'PostReply',
|
||||
})
|
||||
|
||||
if (anchorPostSource) {
|
||||
|
||||
@@ -13,8 +13,10 @@ import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
||||
import {ComposeIcon2} from '#/lib/icons'
|
||||
import {type CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {
|
||||
type CommonNavigatorParams,
|
||||
type NavigationProp,
|
||||
} from '#/lib/routes/types'
|
||||
import {makeRecordUri} from '#/lib/strings/url-helpers'
|
||||
import {s} from '#/lib/styles'
|
||||
import {listenSoftReset} from '#/state/events'
|
||||
@@ -236,7 +238,7 @@ export function ProfileFeedScreenInner({
|
||||
{hasSession && (
|
||||
<FAB
|
||||
testID="composeFAB"
|
||||
onPress={() => openComposer({})}
|
||||
onPress={() => openComposer({logContext: 'Fab'})}
|
||||
icon={
|
||||
<ComposeIcon2
|
||||
strokeWidth={1.5}
|
||||
|
||||
@@ -234,7 +234,7 @@ function ProfileListScreenLoaded({
|
||||
</PagerWithHeader>
|
||||
<FAB
|
||||
testID="composeFAB"
|
||||
onPress={() => openComposer({})}
|
||||
onPress={() => openComposer({logContext: 'Fab'})}
|
||||
icon={
|
||||
<ComposeIcon2
|
||||
strokeWidth={1.5}
|
||||
@@ -272,7 +272,7 @@ function ProfileListScreenLoaded({
|
||||
/>
|
||||
<FAB
|
||||
testID="composeFAB"
|
||||
onPress={() => openComposer({})}
|
||||
onPress={() => openComposer({logContext: 'Fab'})}
|
||||
icon={
|
||||
<ComposeIcon2
|
||||
strokeWidth={1.5}
|
||||
|
||||
@@ -777,6 +777,7 @@ function Overlay({
|
||||
embed: post.embed,
|
||||
langs: record?.langs,
|
||||
},
|
||||
logContext: 'PostReply',
|
||||
})
|
||||
}, [openComposer, post, record])
|
||||
|
||||
|
||||
@@ -33,6 +33,14 @@ export type OnPostSuccessData =
|
||||
}
|
||||
| undefined
|
||||
|
||||
export type ComposerLogContext =
|
||||
| 'Fab'
|
||||
| 'PostReply'
|
||||
| 'QuotePost'
|
||||
| 'ProfileFeed'
|
||||
| 'Deeplink'
|
||||
| 'Other'
|
||||
|
||||
export interface ComposerOpts {
|
||||
replyTo?: ComposerOptsPostRef
|
||||
onPost?: (postUri: string | undefined) => void
|
||||
@@ -44,6 +52,7 @@ export interface ComposerOpts {
|
||||
imageUris?: {uri: string; width: number; height: number; altText?: string}[]
|
||||
videoUri?: {uri: string; width: number; height: number}
|
||||
openGallery?: boolean
|
||||
logContext?: ComposerLogContext
|
||||
}
|
||||
|
||||
type StateContext = ComposerOpts | undefined
|
||||
|
||||
@@ -61,7 +61,7 @@ export function useComposerKeyboardShortcut() {
|
||||
)
|
||||
return
|
||||
if (event.key === 'n' || event.key === 'N') {
|
||||
openComposer({})
|
||||
openComposer({logContext: 'Other'})
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', handler)
|
||||
|
||||
@@ -70,6 +70,7 @@ import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {mimeToExt} from '#/lib/media/video/util'
|
||||
import {useCallOnce} from '#/lib/once'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {colors} from '#/lib/styles'
|
||||
@@ -142,6 +143,7 @@ import {
|
||||
useSaveDraftMutation,
|
||||
} from './drafts/state/queries'
|
||||
import {type DraftSummary} from './drafts/state/schema'
|
||||
import {revokeAllMediaUrls} from './drafts/state/storage'
|
||||
import {PostLanguageSelect} from './select-language/PostLanguageSelect'
|
||||
import {
|
||||
type AssetType,
|
||||
@@ -184,6 +186,7 @@ export const ComposePost = ({
|
||||
imageUris: initImageUris,
|
||||
videoUri: initVideoUri,
|
||||
openGallery,
|
||||
logContext,
|
||||
cancelRef,
|
||||
}: Props & {
|
||||
cancelRef?: React.RefObject<CancelRef | null>
|
||||
@@ -213,6 +216,14 @@ export const ComposePost = ({
|
||||
const [publishingStage, setPublishingStage] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
|
||||
/**
|
||||
* Track when a draft was created so we can measure draft age in metrics.
|
||||
* Set when a draft is loaded via handleSelectDraft.
|
||||
*/
|
||||
const [loadedDraftCreatedAt, setLoadedDraftCreatedAt] = useState<
|
||||
string | null
|
||||
>(null)
|
||||
|
||||
/**
|
||||
* A temporary local reference to a language suggestion that the user has
|
||||
* accepted. This overrides the global post language preference, but is not
|
||||
@@ -321,6 +332,16 @@ export const ComposePost = ({
|
||||
onInitVideo()
|
||||
}, [onInitVideo])
|
||||
|
||||
// Fire composer:open metric on mount
|
||||
useCallOnce(() => {
|
||||
ax.metric('composer:open', {
|
||||
logContext: logContext ?? 'Other',
|
||||
isReply: !!replyTo,
|
||||
hasQuote: !!initQuote,
|
||||
hasDraft: false,
|
||||
})
|
||||
})()
|
||||
|
||||
const clearVideo = useCallback(
|
||||
(postId: string) => {
|
||||
composerDispatch({
|
||||
@@ -489,6 +510,21 @@ export const ComposePost = ({
|
||||
originalLocalRefs,
|
||||
})
|
||||
|
||||
// Track when the draft was created for metrics
|
||||
setLoadedDraftCreatedAt(draftSummary.createdAt)
|
||||
|
||||
// Fire draft:load metric
|
||||
const draftPosts = draftSummary.posts
|
||||
const draftAgeMs = Date.now() - new Date(draftSummary.createdAt).getTime()
|
||||
ax.metric('draft:load', {
|
||||
draftAgeMs,
|
||||
hasText: draftPosts.some(p => p.text.trim().length > 0),
|
||||
hasImages: draftPosts.some(p => p.images && p.images.length > 0),
|
||||
hasVideo: draftPosts.some(p => !!p.video),
|
||||
hasGif: draftPosts.some(p => !!p.gif),
|
||||
postCount: draftPosts.length,
|
||||
})
|
||||
|
||||
// Initiate video processing for any restored videos
|
||||
// This is async but we don't await - videos process in the background
|
||||
for (const [postIndex, videoInfo] of restoredVideos) {
|
||||
@@ -496,7 +532,7 @@ export const ComposePost = ({
|
||||
restoreVideo(postId, videoInfo)
|
||||
}
|
||||
},
|
||||
[composerDispatch, restoreVideo],
|
||||
[composerDispatch, restoreVideo, ax],
|
||||
)
|
||||
|
||||
const [publishOnUpload, setPublishOnUpload] = useState(false)
|
||||
@@ -504,21 +540,38 @@ export const ComposePost = ({
|
||||
const onClose = useCallback(() => {
|
||||
closeComposer()
|
||||
clearThumbnailCache(queryClient)
|
||||
revokeAllMediaUrls()
|
||||
}, [closeComposer, queryClient])
|
||||
|
||||
const handleSaveDraft = React.useCallback(async () => {
|
||||
const isNewDraft = !composerState.draftId
|
||||
try {
|
||||
const result = await saveDraft({
|
||||
composerState,
|
||||
existingDraftId: composerState.draftId,
|
||||
})
|
||||
composerDispatch({type: 'mark_saved', draftId: result.draftId})
|
||||
|
||||
// Fire draft:save metric
|
||||
const posts = composerState.thread.posts
|
||||
ax.metric('draft:save', {
|
||||
isNewDraft,
|
||||
hasText: posts.some(p => p.richtext.text.trim().length > 0),
|
||||
hasImages: posts.some(p => p.embed.media?.type === 'images'),
|
||||
hasVideo: posts.some(p => p.embed.media?.type === 'video'),
|
||||
hasGif: posts.some(p => p.embed.media?.type === 'gif'),
|
||||
hasQuote: posts.some(p => !!p.embed.quote),
|
||||
hasLink: posts.some(p => !!p.embed.link),
|
||||
postCount: posts.length,
|
||||
textLength: posts[0].richtext.text.length,
|
||||
})
|
||||
|
||||
onClose()
|
||||
} catch (e) {
|
||||
logger.error('Failed to save draft', {error: e})
|
||||
setError(_(msg`Failed to save draft`))
|
||||
}
|
||||
}, [saveDraft, composerState, composerDispatch, onClose, _])
|
||||
}, [saveDraft, composerState, composerDispatch, onClose, _, ax])
|
||||
|
||||
// Save without closing - for use by DraftsButton
|
||||
const saveCurrentDraft = React.useCallback(async () => {
|
||||
@@ -529,6 +582,23 @@ export const ComposePost = ({
|
||||
composerDispatch({type: 'mark_saved', draftId: result.draftId})
|
||||
}, [saveDraft, composerState, composerDispatch])
|
||||
|
||||
// Handle discard action - fires metric and closes composer
|
||||
const handleDiscard = React.useCallback(() => {
|
||||
const posts = thread.posts
|
||||
const hasContent = posts.some(
|
||||
post =>
|
||||
post.richtext.text.trim().length > 0 ||
|
||||
post.embed.media ||
|
||||
post.embed.link,
|
||||
)
|
||||
ax.metric('draft:discard', {
|
||||
logContext: 'ComposerClose',
|
||||
hadContent: hasContent,
|
||||
textLength: posts[0].richtext.text.length,
|
||||
})
|
||||
onClose()
|
||||
}, [thread.posts, ax, onClose])
|
||||
|
||||
// Check if composer is empty (no content to save)
|
||||
const isComposerEmpty = React.useMemo(() => {
|
||||
// Has multiple posts means it's not empty
|
||||
@@ -786,6 +856,15 @@ export const ComposePost = ({
|
||||
}
|
||||
// Clean up draft and its media after successful publish
|
||||
if (composerState.draftId && composerState.originalLocalRefs) {
|
||||
// Fire draft:post metric
|
||||
if (loadedDraftCreatedAt) {
|
||||
const draftAgeMs = Date.now() - new Date(loadedDraftCreatedAt).getTime()
|
||||
ax.metric('draft:post', {
|
||||
draftAgeMs,
|
||||
wasEdited: composerState.isDirty,
|
||||
})
|
||||
}
|
||||
|
||||
logger.debug('post published, cleaning up draft', {
|
||||
draftId: composerState.draftId,
|
||||
mediaFileCount: composerState.originalLocalRefs.size,
|
||||
@@ -860,7 +939,9 @@ export const ComposePost = ({
|
||||
navigation,
|
||||
composerState.draftId,
|
||||
composerState.originalLocalRefs,
|
||||
composerState.isDirty,
|
||||
cleanupPublishedDraft,
|
||||
loadedDraftCreatedAt,
|
||||
])
|
||||
|
||||
// Preserves the referential identity passed to each post item.
|
||||
@@ -1008,7 +1089,8 @@ export const ComposePost = ({
|
||||
onDiscard={handleClearComposer}
|
||||
isEmpty={isComposerEmpty}
|
||||
isDirty={composerState.isDirty}
|
||||
isEditingDraft={!!composerState.draftId}>
|
||||
isEditingDraft={!!composerState.draftId}
|
||||
textLength={thread.posts[0].richtext.text.length}>
|
||||
{missingAltError && <AltTextReminder error={missingAltError} />}
|
||||
<ErrorBanner
|
||||
error={error}
|
||||
@@ -1088,7 +1170,7 @@ export const ComposePost = ({
|
||||
/>
|
||||
<Prompt.Action
|
||||
cta={_(msg`Discard`)}
|
||||
onPress={onClose}
|
||||
onPress={handleDiscard}
|
||||
color="negative_subtle"
|
||||
/>
|
||||
<Prompt.Cancel />
|
||||
@@ -1318,6 +1400,7 @@ function ComposerTopBar({
|
||||
isEmpty,
|
||||
isDirty,
|
||||
isEditingDraft,
|
||||
textLength,
|
||||
topBarAnimatedStyle,
|
||||
children,
|
||||
}: {
|
||||
@@ -1335,6 +1418,7 @@ function ComposerTopBar({
|
||||
isEmpty: boolean
|
||||
isDirty: boolean
|
||||
isEditingDraft: boolean
|
||||
textLength: number
|
||||
topBarAnimatedStyle: StyleProp<ViewStyle>
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
@@ -1381,6 +1465,7 @@ function ComposerTopBar({
|
||||
isEmpty={isEmpty}
|
||||
isDirty={isDirty}
|
||||
isEditingDraft={isEditingDraft}
|
||||
textLength={textLength}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
|
||||
@@ -5,6 +5,7 @@ import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {DraftsListDialog} from './DraftsListDialog'
|
||||
import {useSaveDraftMutation} from './state/queries'
|
||||
import {type DraftSummary} from './state/schema'
|
||||
@@ -16,6 +17,7 @@ export function DraftsButton({
|
||||
isEmpty,
|
||||
isDirty,
|
||||
isEditingDraft,
|
||||
textLength,
|
||||
}: {
|
||||
onSelectDraft: (draft: DraftSummary) => void
|
||||
onSaveDraft: () => Promise<void>
|
||||
@@ -23,8 +25,10 @@ export function DraftsButton({
|
||||
isEmpty: boolean
|
||||
isDirty: boolean
|
||||
isEditingDraft: boolean
|
||||
textLength: number
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const ax = useAnalytics()
|
||||
const draftsDialogControl = Dialog.useDialogControl()
|
||||
const savePromptControl = Prompt.usePromptControl()
|
||||
const {isPending: isSaving} = useSaveDraftMutation()
|
||||
@@ -45,6 +49,12 @@ export function DraftsButton({
|
||||
}
|
||||
|
||||
const handleDiscardAndOpen = () => {
|
||||
// Fire draft:discard metric before discarding
|
||||
ax.metric('draft:discard', {
|
||||
logContext: 'BeforeDraftsList',
|
||||
hadContent: !isEmpty,
|
||||
textLength,
|
||||
})
|
||||
onDiscard()
|
||||
draftsDialogControl.open()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {useCallback, useMemo} from 'react'
|
||||
import {useCallback, useEffect, useMemo} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useCallOnce} from '#/lib/once'
|
||||
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
@@ -10,6 +11,7 @@ import * as Dialog from '#/components/Dialog'
|
||||
import {PageX_Stroke2_Corner0_Rounded_Large as PageXIcon} from '#/components/icons/PageX'
|
||||
import {ListFooter} from '#/components/Lists'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
import {DraftItem} from './DraftItem'
|
||||
import {useDeleteDraftMutation, useDraftsQuery} from './state/queries'
|
||||
@@ -24,6 +26,7 @@ export function DraftsListDialog({
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const ax = useAnalytics()
|
||||
const {data, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage} =
|
||||
useDraftsQuery()
|
||||
const {mutate: deleteDraft} = useDeleteDraftMutation()
|
||||
@@ -33,6 +36,20 @@ export function DraftsListDialog({
|
||||
[data],
|
||||
)
|
||||
|
||||
// Fire draft:listOpen metric when dialog opens and data is loaded
|
||||
const draftCount = drafts.length
|
||||
const isDataReady = !isLoading && data !== undefined
|
||||
const onDraftListOpen = useCallOnce()
|
||||
useEffect(() => {
|
||||
if (isDataReady) {
|
||||
onDraftListOpen(() => {
|
||||
ax.metric('draft:listOpen', {
|
||||
draftCount,
|
||||
})
|
||||
})
|
||||
}
|
||||
}, [onDraftListOpen, isDataReady, draftCount, ax])
|
||||
|
||||
const handleSelectDraft = useCallback(
|
||||
(summary: DraftSummary) => {
|
||||
control.close(() => {
|
||||
@@ -44,9 +61,15 @@ export function DraftsListDialog({
|
||||
|
||||
const handleDeleteDraft = useCallback(
|
||||
(draftSummary: DraftSummary) => {
|
||||
// Fire draft:delete metric
|
||||
const draftAgeMs = Date.now() - new Date(draftSummary.createdAt).getTime()
|
||||
ax.metric('draft:delete', {
|
||||
logContext: 'DraftsList',
|
||||
draftAgeMs,
|
||||
})
|
||||
deleteDraft({draftId: draftSummary.id, draft: draftSummary.draft})
|
||||
},
|
||||
[deleteDraft],
|
||||
[deleteDraft, ax],
|
||||
)
|
||||
|
||||
const backButton = useCallback(
|
||||
@@ -93,7 +116,7 @@ export function DraftsListDialog({
|
||||
|
||||
const onEndReached = useCallback(() => {
|
||||
if (hasNextPage && !isFetchingNextPage) {
|
||||
fetchNextPage()
|
||||
void fetchNextPage()
|
||||
}
|
||||
}, [hasNextPage, isFetchingNextPage, fetchNextPage])
|
||||
|
||||
@@ -132,7 +155,7 @@ export function DraftsListDialog({
|
||||
<Dialog.InnerFlatList
|
||||
data={drafts}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={item => item.id}
|
||||
keyExtractor={(item: DraftSummary) => item.id}
|
||||
ListHeaderComponent={web(header)}
|
||||
stickyHeaderIndices={web([0])}
|
||||
ListEmptyComponent={emptyComponent}
|
||||
|
||||
@@ -356,6 +356,7 @@ export function draftViewToSummary(
|
||||
hasMissingMedia,
|
||||
mediaCount,
|
||||
postCount: view.draft.posts.length,
|
||||
createdAt: view.createdAt,
|
||||
updatedAt: view.updatedAt,
|
||||
posts,
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ export type DraftSummary = {
|
||||
mediaCount: number
|
||||
/** Number of posts in thread */
|
||||
postCount: number
|
||||
/** ISO timestamp of creation */
|
||||
createdAt: string
|
||||
/** ISO timestamp of last update */
|
||||
updatedAt: string
|
||||
/** All posts in the draft for full display */
|
||||
|
||||
@@ -91,6 +91,7 @@ export async function deleteMediaFromLocal(
|
||||
if (file.exists) {
|
||||
file.delete()
|
||||
}
|
||||
mediaExistsCache.delete(localRefPath)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,3 +155,17 @@ export function clearMediaCache(): void {
|
||||
cachePopulated = false
|
||||
populateCachePromise = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke a media URL (no-op on native - only needed for web blob URLs)
|
||||
*/
|
||||
export function revokeMediaUrl(_url: string): void {
|
||||
// No-op on native - file URIs don't need revocation
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke all media URLs (no-op on native - only needed for web blob URLs)
|
||||
*/
|
||||
export function revokeAllMediaUrls(): void {
|
||||
// No-op on native - file URIs don't need revocation
|
||||
}
|
||||
|
||||
@@ -84,6 +84,11 @@ export async function saveMediaToLocal(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track blob URLs created by loadMediaFromLocal for cleanup
|
||||
*/
|
||||
const createdBlobUrls = new Set<string>()
|
||||
|
||||
/**
|
||||
* Load a media file from IndexedDB
|
||||
* @returns A blob URL for the saved media
|
||||
@@ -97,7 +102,10 @@ export async function loadMediaFromLocal(
|
||||
throw new Error(`Media file not found: ${localRefPath}`)
|
||||
}
|
||||
|
||||
return URL.createObjectURL(record.blob)
|
||||
const url = URL.createObjectURL(record.blob)
|
||||
logger.debug('Created blob URL', {url})
|
||||
createdBlobUrls.add(url)
|
||||
return url
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,6 +173,20 @@ export function clearMediaCache(): void {
|
||||
*/
|
||||
export function revokeMediaUrl(url: string): void {
|
||||
if (url.startsWith('blob:')) {
|
||||
logger.debug('Revoking blob URL', {url})
|
||||
URL.revokeObjectURL(url)
|
||||
createdBlobUrls.delete(url)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke all blob URLs created by loadMediaFromLocal.
|
||||
* Call this when closing the drafts list dialog to prevent memory leaks.
|
||||
*/
|
||||
export function revokeAllMediaUrls(): void {
|
||||
logger.debug(`Revoking ${createdBlobUrls.size} blob URLs`)
|
||||
for (const url of createdBlobUrls) {
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
createdBlobUrls.clear()
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export const RQKEY = 'video-thumbnail'
|
||||
|
||||
export function clearThumbnailCache(queryClient: QueryClient) {
|
||||
clearCache().catch(() => {})
|
||||
queryClient.resetQueries({queryKey: [RQKEY]})
|
||||
void queryClient.resetQueries({queryKey: [RQKEY]})
|
||||
}
|
||||
|
||||
export function VideoTranscodeBackdrop({uri}: {uri: string}) {
|
||||
|
||||
@@ -37,7 +37,7 @@ export function ComposerPrompt() {
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
ax.metric('composerPrompt:press', {})
|
||||
openComposer({})
|
||||
openComposer({logContext: 'Fab'})
|
||||
}, [ax, openComposer])
|
||||
|
||||
const onPressImage = useCallback(async () => {
|
||||
@@ -45,7 +45,7 @@ export function ComposerPrompt() {
|
||||
|
||||
// On web, open the composer with the gallery picker auto-opening
|
||||
if (!IS_NATIVE) {
|
||||
openComposer({openGallery: true})
|
||||
openComposer({openGallery: true, logContext: 'Fab'})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export function ComposerPrompt() {
|
||||
}))
|
||||
|
||||
if (imageUris.length > 0) {
|
||||
openComposer({imageUris})
|
||||
openComposer({imageUris, logContext: 'Fab'})
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
@@ -125,6 +125,7 @@ export function ComposerPrompt() {
|
||||
|
||||
openComposer({
|
||||
imageUris: IS_NATIVE ? imageUris : undefined,
|
||||
logContext: 'Fab',
|
||||
})
|
||||
} catch (err: any) {
|
||||
if (!String(err).toLowerCase().includes('cancel')) {
|
||||
|
||||
@@ -123,7 +123,7 @@ export function FeedPage({
|
||||
}, [onSoftReset, isPageFocused])
|
||||
|
||||
const onPressCompose = useCallback(() => {
|
||||
openComposer({})
|
||||
openComposer({logContext: 'Fab'})
|
||||
}, [openComposer])
|
||||
|
||||
const onPressLoadLatest = useCallback(() => {
|
||||
|
||||
@@ -138,6 +138,7 @@ function PostInner({
|
||||
moderation,
|
||||
langs: record.langs,
|
||||
},
|
||||
logContext: 'PostReply',
|
||||
})
|
||||
}, [openComposer, post, record, moderation])
|
||||
|
||||
|
||||
@@ -189,6 +189,7 @@ let FeedItemInner = ({
|
||||
moderation,
|
||||
langs: record.langs,
|
||||
},
|
||||
logContext: 'PostReply',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ export function FeedsScreen(_props: Props) {
|
||||
[search],
|
||||
)
|
||||
const onPressCompose = React.useCallback(() => {
|
||||
openComposer({})
|
||||
openComposer({logContext: 'Fab'})
|
||||
}, [openComposer])
|
||||
const onChangeQuery = React.useCallback(
|
||||
(text: string) => {
|
||||
|
||||
@@ -30,8 +30,7 @@ import {FAB} from '#/view/com/util/fab/FAB'
|
||||
import {type ListMethods} from '#/view/com/util/List'
|
||||
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
|
||||
import {MainScrollProvider} from '#/view/com/util/MainScrollProvider'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {web} from '#/alf'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {ButtonIcon} from '#/components/Button'
|
||||
import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2'
|
||||
@@ -161,7 +160,7 @@ export function NotificationsScreen({}: Props) {
|
||||
</Pager>
|
||||
<FAB
|
||||
testID="composeFAB"
|
||||
onPress={() => openComposer({})}
|
||||
onPress={() => openComposer({logContext: 'Fab'})}
|
||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`New post`)}
|
||||
|
||||
@@ -332,7 +332,7 @@ function ProfileScreenLoaded({
|
||||
isInvalidHandle(profile.handle)
|
||||
? undefined
|
||||
: profile.handle
|
||||
openComposer({mention})
|
||||
openComposer({mention, logContext: 'ProfileFeed'})
|
||||
}
|
||||
|
||||
const onPageSelected = (i: number) => {
|
||||
@@ -434,7 +434,8 @@ function ProfileScreenLoaded({
|
||||
? {
|
||||
label: _(msg`Write a post`),
|
||||
text: _(msg`Write a post`),
|
||||
onPress: () => openComposer({}),
|
||||
onPress: () =>
|
||||
openComposer({logContext: 'ProfileFeed'}),
|
||||
size: 'small',
|
||||
color: 'primary',
|
||||
}
|
||||
@@ -474,7 +475,8 @@ function ProfileScreenLoaded({
|
||||
? {
|
||||
label: _(msg`Post a photo`),
|
||||
text: _(msg`Post a photo`),
|
||||
onPress: () => openComposer({}),
|
||||
onPress: () =>
|
||||
openComposer({logContext: 'ProfileFeed'}),
|
||||
size: 'small',
|
||||
color: 'primary',
|
||||
}
|
||||
@@ -500,7 +502,8 @@ function ProfileScreenLoaded({
|
||||
? {
|
||||
label: _(msg`Post a video`),
|
||||
text: _(msg`Post a video`),
|
||||
onPress: () => openComposer({}),
|
||||
onPress: () =>
|
||||
openComposer({logContext: 'ProfileFeed'}),
|
||||
size: 'small',
|
||||
color: 'primary',
|
||||
}
|
||||
|
||||
@@ -560,7 +560,7 @@ function ComposeBtn() {
|
||||
}
|
||||
|
||||
const onPressCompose = async () =>
|
||||
openComposer({mention: await getProfileHandle()})
|
||||
openComposer({mention: await getProfileHandle(), logContext: 'Fab'})
|
||||
|
||||
if (leftNavMinimal) {
|
||||
return null
|
||||
|
||||
Reference in New Issue
Block a user