[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:
Samuel Newman
2026-01-29 23:21:38 +02:00
committed by GitHub
parent 0babd0f475
commit 58f532a495
28 changed files with 258 additions and 29 deletions
+47
View File
@@ -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': {}
+1
View File
@@ -185,6 +185,7 @@ let PostControls = ({
openComposer({
quote: post,
onPost: onPostReply,
logContext: 'QuotePost',
})
}
+2
View File
@@ -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])
+1
View File
@@ -125,6 +125,7 @@ export function PostThread({uri}: {uri: string}) {
langs: post.record.langs,
},
onPostSuccess: optimisticOnPostReply,
logContext: 'PostReply',
})
if (anchorPostSource) {
+5 -3
View File
@@ -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}
+2 -2
View File
@@ -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}
+1
View File
@@ -777,6 +777,7 @@ function Overlay({
embed: post.embed,
langs: record?.langs,
},
logContext: 'PostReply',
})
}, [openComposer, post, record])
+9
View File
@@ -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)
+89 -4
View File
@@ -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}) {
+4 -3
View File
@@ -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')) {
+1 -1
View File
@@ -123,7 +123,7 @@ export function FeedPage({
}, [onSoftReset, isPageFocused])
const onPressCompose = useCallback(() => {
openComposer({})
openComposer({logContext: 'Fab'})
}, [openComposer])
const onPressLoadLatest = useCallback(() => {
+1
View File
@@ -138,6 +138,7 @@ function PostInner({
moderation,
langs: record.langs,
},
logContext: 'PostReply',
})
}, [openComposer, post, record, moderation])
+1
View File
@@ -189,6 +189,7 @@ let FeedItemInner = ({
moderation,
langs: record.langs,
},
logContext: 'PostReply',
})
}
+1 -1
View File
@@ -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) => {
+2 -3
View File
@@ -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`)}
+7 -4
View File
@@ -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',
}
+1 -1
View File
@@ -560,7 +560,7 @@ function ComposeBtn() {
}
const onPressCompose = async () =>
openComposer({mention: await getProfileHandle()})
openComposer({mention: await getProfileHandle(), logContext: 'Fab'})
if (leftNavMinimal) {
return null