Add client events
This commit is contained in:
committed by
Samuel Newman
parent
80bae9da21
commit
9df81cecd7
@@ -217,6 +217,48 @@ export type MetricEvents = {
|
||||
hasChanged: boolean
|
||||
}
|
||||
|
||||
// Fired when user explicitly saves a draft (via "Save draft" button or prompt)
|
||||
'draft:save': {
|
||||
isNewDraft: boolean
|
||||
hasText: boolean
|
||||
hasImages: boolean
|
||||
hasVideo: boolean
|
||||
hasGif: boolean
|
||||
hasQuote: boolean
|
||||
hasLink: boolean
|
||||
postCount: number
|
||||
textLength: number
|
||||
}
|
||||
// Fired when user selects a draft from the drafts list to load it into the composer
|
||||
'draft:load': {
|
||||
draftAgeMs: number
|
||||
hasText: boolean
|
||||
hasImages: boolean
|
||||
hasVideo: boolean
|
||||
hasGif: boolean
|
||||
postCount: number
|
||||
}
|
||||
// Fired when user explicitly deletes a draft
|
||||
'draft:delete': {
|
||||
logContext: 'DraftsList' | 'ComposerClose'
|
||||
draftAgeMs: number
|
||||
}
|
||||
// Fired when user opens the drafts list view
|
||||
'draft:listOpen': {
|
||||
draftCount: number
|
||||
}
|
||||
// Fired when a draft is successfully posted
|
||||
'draft:post': {
|
||||
draftAgeMs: number
|
||||
wasEdited: boolean
|
||||
}
|
||||
// Fired when user chooses "Don't save" and discards unsaved content
|
||||
'draft:discard': {
|
||||
logContext: 'ComposerClose' | 'BeforeDraftsList'
|
||||
hadContent: boolean
|
||||
textLength: number
|
||||
}
|
||||
|
||||
// Data events
|
||||
'account:create:begin': {}
|
||||
'account:create:success': {
|
||||
|
||||
@@ -375,6 +375,17 @@ export const ComposePost = ({
|
||||
},
|
||||
)
|
||||
|
||||
// Track the timestamp of the loaded draft (for analytics)
|
||||
const [loadedDraftTimestamp, setLoadedDraftTimestamp] = useState<
|
||||
number | null
|
||||
>(() => {
|
||||
if (initDraftId && currentAccount) {
|
||||
const draft = draftsStorage.getDraftSync(currentAccount.did, initDraftId)
|
||||
return draft?.timestamp ?? null
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
// Drafts list for the drafts dialog
|
||||
const {draftsCount} = useDraftsList()
|
||||
|
||||
@@ -400,6 +411,22 @@ export const ComposePost = ({
|
||||
return currentSnapshot !== loadedDraftSnapshot
|
||||
}, [isEditingExistingDraft, loadedDraftSnapshot, composerState])
|
||||
|
||||
// Helper to get draft metadata for analytics events
|
||||
const getDraftMetadata = useCallback((state: ComposerState) => {
|
||||
const posts = state.thread.posts
|
||||
const firstPost = posts[0]
|
||||
return {
|
||||
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: firstPost?.richtext.text.length ?? 0,
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Handler for selecting a draft from the list
|
||||
const onSelectDraft = useCallback(
|
||||
(selectedDraftId: string) => {
|
||||
@@ -437,14 +464,26 @@ export const ComposePost = ({
|
||||
setCurrentDraftId(selectedDraftId)
|
||||
setIsEditingExistingDraft(true)
|
||||
setLoadedDraftSnapshot(serializeStateForComparison(newState))
|
||||
setLoadedDraftTimestamp(draft.timestamp)
|
||||
|
||||
// Load the draft and switch back to compose mode
|
||||
composerDispatch({type: 'load_draft', draft: newState})
|
||||
setViewMode('compose')
|
||||
|
||||
// Log draft:load event
|
||||
const metadata = getDraftMetadata(newState)
|
||||
logEvent('draft:load', {
|
||||
draftAgeMs: Date.now() - draft.timestamp,
|
||||
hasText: metadata.hasText,
|
||||
hasImages: metadata.hasImages,
|
||||
hasVideo: metadata.hasVideo,
|
||||
hasGif: metadata.hasGif,
|
||||
postCount: metadata.postCount,
|
||||
})
|
||||
|
||||
logger.info('Loaded draft into composer', {draftId: selectedDraftId})
|
||||
},
|
||||
[currentAccount, preferences?.postInteractionSettings],
|
||||
[currentAccount, preferences?.postInteractionSettings, getDraftMetadata],
|
||||
)
|
||||
|
||||
// Handler for opening drafts view
|
||||
@@ -458,6 +497,9 @@ export const ComposePost = ({
|
||||
} else {
|
||||
// No content or no changes - just show drafts
|
||||
setViewMode('drafts')
|
||||
logEvent('draft:listOpen', {
|
||||
draftCount: draftsCount ?? 0,
|
||||
})
|
||||
}
|
||||
}, [
|
||||
hasContent,
|
||||
@@ -465,27 +507,106 @@ export const ComposePost = ({
|
||||
hasUnsavedChanges,
|
||||
saveBeforeDraftsPromptControl,
|
||||
updateBeforeDraftsPromptControl,
|
||||
draftsCount,
|
||||
])
|
||||
|
||||
// Handler for "Save" in the save-before-drafts prompt
|
||||
const onSaveBeforeDrafts = useCallback(() => {
|
||||
saveImmediate(composerState)
|
||||
const metadata = getDraftMetadata(composerState)
|
||||
logEvent('draft:save', {
|
||||
isNewDraft: !isEditingExistingDraft,
|
||||
...metadata,
|
||||
})
|
||||
setViewMode('drafts')
|
||||
}, [saveImmediate, composerState])
|
||||
logEvent('draft:listOpen', {
|
||||
draftCount: (draftsCount ?? 0) + (isEditingExistingDraft ? 0 : 1),
|
||||
})
|
||||
}, [
|
||||
saveImmediate,
|
||||
composerState,
|
||||
getDraftMetadata,
|
||||
isEditingExistingDraft,
|
||||
draftsCount,
|
||||
])
|
||||
|
||||
// Handler for "Don't save" in the save-before-drafts prompt
|
||||
const onDiscardBeforeDrafts = useCallback(() => {
|
||||
const metadata = getDraftMetadata(composerState)
|
||||
logEvent('draft:discard', {
|
||||
logContext: 'BeforeDraftsList',
|
||||
hadContent: hasContent,
|
||||
textLength: metadata.textLength,
|
||||
})
|
||||
clearDraft()
|
||||
setViewMode('drafts')
|
||||
}, [clearDraft])
|
||||
logEvent('draft:listOpen', {
|
||||
draftCount: draftsCount ?? 0,
|
||||
})
|
||||
}, [clearDraft, getDraftMetadata, composerState, hasContent, draftsCount])
|
||||
|
||||
// Handler for saving draft and closing with toast
|
||||
const onSaveDraftAndClose = useCallback(() => {
|
||||
saveImmediate(composerState)
|
||||
const metadata = getDraftMetadata(composerState)
|
||||
logEvent('draft:save', {
|
||||
isNewDraft: !isEditingExistingDraft,
|
||||
...metadata,
|
||||
})
|
||||
closeComposer()
|
||||
clearThumbnailCache(queryClient)
|
||||
Toast.show(_(msg`Saved to drafts`))
|
||||
}, [_, saveImmediate, composerState, closeComposer, queryClient])
|
||||
}, [
|
||||
_,
|
||||
saveImmediate,
|
||||
composerState,
|
||||
getDraftMetadata,
|
||||
isEditingExistingDraft,
|
||||
closeComposer,
|
||||
queryClient,
|
||||
])
|
||||
|
||||
// Handler for "Don't save" when closing composer with unsaved content
|
||||
const onDiscardAndClose = useCallback(() => {
|
||||
const metadata = getDraftMetadata(composerState)
|
||||
logEvent('draft:discard', {
|
||||
logContext: 'ComposerClose',
|
||||
hadContent: hasContent,
|
||||
textLength: metadata.textLength,
|
||||
})
|
||||
clearDraft()
|
||||
closeComposer()
|
||||
clearThumbnailCache(queryClient)
|
||||
}, [
|
||||
getDraftMetadata,
|
||||
composerState,
|
||||
hasContent,
|
||||
clearDraft,
|
||||
closeComposer,
|
||||
queryClient,
|
||||
])
|
||||
|
||||
// Handler for "Update draft" in the update-before-drafts prompt
|
||||
const onUpdateBeforeDrafts = useCallback(() => {
|
||||
saveImmediate(composerState)
|
||||
const metadata = getDraftMetadata(composerState)
|
||||
logEvent('draft:save', {
|
||||
isNewDraft: false,
|
||||
...metadata,
|
||||
})
|
||||
setViewMode('drafts')
|
||||
logEvent('draft:listOpen', {
|
||||
draftCount: draftsCount ?? 0,
|
||||
})
|
||||
}, [saveImmediate, composerState, getDraftMetadata, draftsCount])
|
||||
|
||||
// Handler for "Don't update" in the update-before-drafts prompt
|
||||
const onSkipUpdateBeforeDrafts = useCallback(() => {
|
||||
setViewMode('drafts')
|
||||
logEvent('draft:listOpen', {
|
||||
draftCount: draftsCount ?? 0,
|
||||
})
|
||||
}, [draftsCount])
|
||||
|
||||
// Handler for when a draft is deleted from the drafts list
|
||||
const onDraftDeleted = useCallback(
|
||||
@@ -839,6 +960,13 @@ export const ComposePost = ({
|
||||
onPost?.(postUri)
|
||||
onPostSuccess?.(postSuccessData)
|
||||
}
|
||||
// Log draft:post event if we posted from an existing draft
|
||||
if (isEditingExistingDraft && loadedDraftTimestamp) {
|
||||
logEvent('draft:post', {
|
||||
draftAgeMs: Date.now() - loadedDraftTimestamp,
|
||||
wasEdited: hasUnsavedChanges,
|
||||
})
|
||||
}
|
||||
clearDraft()
|
||||
onClose()
|
||||
setTimeout(() => {
|
||||
@@ -884,6 +1012,9 @@ export const ComposePost = ({
|
||||
setLangPrefs,
|
||||
queryClient,
|
||||
navigation,
|
||||
isEditingExistingDraft,
|
||||
loadedDraftTimestamp,
|
||||
hasUnsavedChanges,
|
||||
])
|
||||
|
||||
// Preserves the referential identity passed to each post item.
|
||||
@@ -1129,11 +1260,7 @@ export const ComposePost = ({
|
||||
<Prompt.Action
|
||||
cta={_(msg`Don't save`)}
|
||||
color="negative_subtle"
|
||||
onPress={() => {
|
||||
clearDraft()
|
||||
closeComposer()
|
||||
clearThumbnailCache(queryClient)
|
||||
}}
|
||||
onPress={onDiscardAndClose}
|
||||
/>
|
||||
<Prompt.Cancel />
|
||||
</Prompt.Actions>
|
||||
@@ -1171,17 +1298,12 @@ export const ComposePost = ({
|
||||
<Prompt.Actions>
|
||||
<Prompt.Action
|
||||
cta={_(msg`Update draft`)}
|
||||
onPress={() => {
|
||||
saveImmediate(composerState)
|
||||
setViewMode('drafts')
|
||||
}}
|
||||
onPress={onUpdateBeforeDrafts}
|
||||
/>
|
||||
<Prompt.Action
|
||||
cta={_(msg`Don't update`)}
|
||||
color="negative_subtle"
|
||||
onPress={() => {
|
||||
setViewMode('drafts')
|
||||
}}
|
||||
onPress={onSkipUpdateBeforeDrafts}
|
||||
/>
|
||||
<Prompt.Cancel />
|
||||
</Prompt.Actions>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {msg, plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
@@ -131,8 +132,12 @@ function DraftListItem({
|
||||
}, [item.id, onSelect])
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
logEvent('draft:delete', {
|
||||
logContext: 'DraftsList',
|
||||
draftAgeMs: Date.now() - item.draft.timestamp,
|
||||
})
|
||||
onDelete(item.id)
|
||||
}, [item.id, onDelete])
|
||||
}, [item.id, item.draft.timestamp, onDelete])
|
||||
|
||||
const previewText = getPreviewText(item.draft, _)
|
||||
const timeAgo = getTimeAgo(item.draft.timestamp, Date.now())
|
||||
|
||||
Reference in New Issue
Block a user