Add client events, auto-open gallery when button is pressed
This commit is contained in:
@@ -203,6 +203,9 @@ export type MetricEvents = {
|
|||||||
'composer:gif:open': {}
|
'composer:gif:open': {}
|
||||||
'composer:gif:select': {}
|
'composer:gif:select': {}
|
||||||
'postComposer:click': {}
|
'postComposer:click': {}
|
||||||
|
'composerPrompt:press': {}
|
||||||
|
'composerPrompt:camera:press': {}
|
||||||
|
'composerPrompt:gallery:press': {}
|
||||||
|
|
||||||
'composer:threadgate:open': {
|
'composer:threadgate:open': {
|
||||||
nudged: boolean
|
nudged: boolean
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export interface ComposerOpts {
|
|||||||
text?: string
|
text?: string
|
||||||
imageUris?: {uri: string; width: number; height: number; altText?: string}[]
|
imageUris?: {uri: string; width: number; height: number; altText?: string}[]
|
||||||
videoUri?: {uri: string; width: number; height: number}
|
videoUri?: {uri: string; width: number; height: number}
|
||||||
|
openGallery?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type StateContext = ComposerOpts | undefined
|
type StateContext = ComposerOpts | undefined
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ export const ComposePost = ({
|
|||||||
text: initText,
|
text: initText,
|
||||||
imageUris: initImageUris,
|
imageUris: initImageUris,
|
||||||
videoUri: initVideoUri,
|
videoUri: initVideoUri,
|
||||||
|
openGallery,
|
||||||
cancelRef,
|
cancelRef,
|
||||||
}: Props & {
|
}: Props & {
|
||||||
cancelRef?: React.RefObject<CancelRef | null>
|
cancelRef?: React.RefObject<CancelRef | null>
|
||||||
@@ -721,6 +722,7 @@ export const ComposePost = ({
|
|||||||
}}
|
}}
|
||||||
currentLanguages={currentLanguages}
|
currentLanguages={currentLanguages}
|
||||||
onSelectLanguage={onSelectLanguage}
|
onSelectLanguage={onSelectLanguage}
|
||||||
|
openGallery={openGallery}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@@ -1334,6 +1336,7 @@ function ComposerFooter({
|
|||||||
onAddPost,
|
onAddPost,
|
||||||
currentLanguages,
|
currentLanguages,
|
||||||
onSelectLanguage,
|
onSelectLanguage,
|
||||||
|
openGallery,
|
||||||
}: {
|
}: {
|
||||||
post: PostDraft
|
post: PostDraft
|
||||||
dispatch: (action: PostAction) => void
|
dispatch: (action: PostAction) => void
|
||||||
@@ -1344,6 +1347,7 @@ function ComposerFooter({
|
|||||||
onAddPost: () => void
|
onAddPost: () => void
|
||||||
currentLanguages: string[]
|
currentLanguages: string[]
|
||||||
onSelectLanguage?: (language: string) => void
|
onSelectLanguage?: (language: string) => void
|
||||||
|
openGallery?: boolean
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
@@ -1463,6 +1467,7 @@ function ComposerFooter({
|
|||||||
allowedAssetTypes={selectedAssetsType}
|
allowedAssetTypes={selectedAssetsType}
|
||||||
selectedAssetsCount={selectedAssetsCount}
|
selectedAssetsCount={selectedAssetsCount}
|
||||||
onSelectAssets={onSelectAssets}
|
onSelectAssets={onSelectAssets}
|
||||||
|
autoOpen={openGallery}
|
||||||
/>
|
/>
|
||||||
<OpenCameraBtn
|
<OpenCameraBtn
|
||||||
disabled={media?.type === 'images' ? isMaxImages : !!media}
|
disabled={media?.type === 'images' ? isMaxImages : !!media}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {useCallback} from 'react'
|
import {useCallback, useEffect, useRef} from 'react'
|
||||||
import {Keyboard} from 'react-native'
|
import {Keyboard} from 'react-native'
|
||||||
import {type ImagePickerAsset} from 'expo-image-picker'
|
import {type ImagePickerAsset} from 'expo-image-picker'
|
||||||
import {msg, plural} from '@lingui/macro'
|
import {msg, plural} from '@lingui/macro'
|
||||||
@@ -31,6 +31,10 @@ export type SelectMediaButtonProps = {
|
|||||||
assets: ImagePickerAsset[]
|
assets: ImagePickerAsset[]
|
||||||
errors: string[]
|
errors: string[]
|
||||||
}) => void
|
}) => void
|
||||||
|
/**
|
||||||
|
* If true, automatically open the media picker when the component mounts.
|
||||||
|
*/
|
||||||
|
autoOpen?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -358,12 +362,14 @@ export function SelectMediaButton({
|
|||||||
allowedAssetTypes,
|
allowedAssetTypes,
|
||||||
selectedAssetsCount,
|
selectedAssetsCount,
|
||||||
onSelectAssets,
|
onSelectAssets,
|
||||||
|
autoOpen,
|
||||||
}: SelectMediaButtonProps) {
|
}: SelectMediaButtonProps) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||||
const {requestVideoAccessIfNeeded} = useVideoLibraryPermission()
|
const {requestVideoAccessIfNeeded} = useVideoLibraryPermission()
|
||||||
const sheetWrapper = useSheetWrapper()
|
const sheetWrapper = useSheetWrapper()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const hasAutoOpened = useRef(false)
|
||||||
|
|
||||||
const selectionCountRemaining = MAX_IMAGES - selectedAssetsCount
|
const selectionCountRemaining = MAX_IMAGES - selectedAssetsCount
|
||||||
|
|
||||||
@@ -460,6 +466,13 @@ export function SelectMediaButton({
|
|||||||
selectionCountRemaining,
|
selectionCountRemaining,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoOpen && !hasAutoOpened.current && !disabled) {
|
||||||
|
hasAutoOpened.current = true
|
||||||
|
onPressSelectMedia()
|
||||||
|
}
|
||||||
|
}, [autoOpen, disabled, onPressSelectMedia])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
testID="openMediaBtn"
|
testID="openMediaBtn"
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ import {
|
|||||||
import {openCamera, openUnifiedPicker} from '#/lib/media/picker'
|
import {openCamera, openUnifiedPicker} from '#/lib/media/picker'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {createComposerImage} from '#/state/gallery'
|
|
||||||
import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile'
|
import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile'
|
||||||
import {useComposerControls} from '#/state/shell/composer'
|
|
||||||
import {MAX_IMAGES} from '#/view/com/composer/state/composer'
|
import {MAX_IMAGES} from '#/view/com/composer/state/composer'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, native, useTheme, web} from '#/alf'
|
import {atoms as a, native, useTheme, web} from '#/alf'
|
||||||
@@ -25,11 +23,10 @@ import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Ima
|
|||||||
import {SubtleHover} from '#/components/SubtleHover'
|
import {SubtleHover} from '#/components/SubtleHover'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
export function FeedComposerPrompt() {
|
export function ComposerPrompt() {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {openComposer} = useOpenComposer()
|
const {openComposer} = useOpenComposer()
|
||||||
const {closeComposer} = useComposerControls()
|
|
||||||
const profile = useCurrentAccountProfile()
|
const profile = useCurrentAccountProfile()
|
||||||
const [hover, setHover] = useState(false)
|
const [hover, setHover] = useState(false)
|
||||||
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
||||||
@@ -38,20 +35,20 @@ export function FeedComposerPrompt() {
|
|||||||
const sheetWrapper = useSheetWrapper()
|
const sheetWrapper = useSheetWrapper()
|
||||||
|
|
||||||
const onPress = React.useCallback(() => {
|
const onPress = React.useCallback(() => {
|
||||||
logger.metric('postComposer:click', {})
|
logger.metric('composerPrompt:press', {})
|
||||||
openComposer({})
|
openComposer({})
|
||||||
}, [openComposer])
|
}, [openComposer])
|
||||||
|
|
||||||
const onPressImage = useCallback(async () => {
|
const onPressImage = useCallback(async () => {
|
||||||
logger.metric('postComposer:click', {})
|
logger.metric('composerPrompt:gallery:press', {})
|
||||||
|
|
||||||
// Open the composer first so it's ready
|
// On web, open the composer with the gallery picker auto-opening
|
||||||
openComposer({})
|
if (!isNative) {
|
||||||
|
openComposer({openGallery: true})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Use a small delay to ensure composer starts rendering before opening picker
|
|
||||||
setTimeout(async () => {
|
|
||||||
try {
|
try {
|
||||||
if (isNative) {
|
|
||||||
const [photoAccess, videoAccess] = await Promise.all([
|
const [photoAccess, videoAccess] = await Promise.all([
|
||||||
requestPhotoAccessIfNeeded(),
|
requestPhotoAccessIfNeeded(),
|
||||||
requestVideoAccessIfNeeded(),
|
requestVideoAccessIfNeeded(),
|
||||||
@@ -60,9 +57,8 @@ export function FeedComposerPrompt() {
|
|||||||
if (!photoAccess && !videoAccess) {
|
if (!photoAccess && !videoAccess) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (isNative && Keyboard.isVisible()) {
|
if (Keyboard.isVisible()) {
|
||||||
Keyboard.dismiss()
|
Keyboard.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,58 +67,22 @@ export function FeedComposerPrompt() {
|
|||||||
openUnifiedPicker({selectionCountRemaining}),
|
openUnifiedPicker({selectionCountRemaining}),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (canceled) return
|
if (canceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (assets.length > 0) {
|
if (assets.length > 0) {
|
||||||
// Process images and add them to the composer
|
const imageUris = assets
|
||||||
const images = await Promise.all(
|
|
||||||
assets
|
|
||||||
.filter(asset => asset.mimeType?.startsWith('image/'))
|
.filter(asset => asset.mimeType?.startsWith('image/'))
|
||||||
.slice(0, MAX_IMAGES)
|
.slice(0, MAX_IMAGES)
|
||||||
.map(async image => {
|
.map(asset => ({
|
||||||
try {
|
uri: asset.uri,
|
||||||
return await createComposerImage({
|
width: asset.width,
|
||||||
path: image.uri,
|
height: asset.height,
|
||||||
width: image.width,
|
}))
|
||||||
height: image.height,
|
|
||||||
mime: image.mimeType!,
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(`createComposerImage failed`, {
|
|
||||||
safeMessage: e instanceof Error ? e.message : String(e),
|
|
||||||
})
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const validImages = images.filter(
|
if (imageUris.length > 0) {
|
||||||
(img): img is NonNullable<typeof img> => img !== null,
|
openComposer({imageUris})
|
||||||
)
|
|
||||||
|
|
||||||
if (validImages.length > 0) {
|
|
||||||
// Convert to imageUris format for the composer
|
|
||||||
// createComposerImage returns ComposerImageWithoutTransformation which always has source
|
|
||||||
const imageUris = validImages.map(img => {
|
|
||||||
const source = img.source
|
|
||||||
return {
|
|
||||||
uri: source.path,
|
|
||||||
width: source.width,
|
|
||||||
height: source.height,
|
|
||||||
altText: img.alt,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Close the composer first (if it was opened), then reopen with images
|
|
||||||
// This ensures the composer state is reset and can accept the new imageUris
|
|
||||||
closeComposer()
|
|
||||||
// Wait a bit for the picker to fully close and composer to close, then reopen with images
|
|
||||||
// This follows the same pattern as useIntentHandler
|
|
||||||
setTimeout(() => {
|
|
||||||
openComposer({
|
|
||||||
imageUris: isNative ? imageUris : undefined,
|
|
||||||
})
|
|
||||||
}, 100)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -130,23 +90,16 @@ export function FeedComposerPrompt() {
|
|||||||
logger.warn('Error opening image picker', {error: err})
|
logger.warn('Error opening image picker', {error: err})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 100)
|
|
||||||
}, [
|
}, [
|
||||||
openComposer,
|
openComposer,
|
||||||
closeComposer,
|
|
||||||
requestPhotoAccessIfNeeded,
|
requestPhotoAccessIfNeeded,
|
||||||
requestVideoAccessIfNeeded,
|
requestVideoAccessIfNeeded,
|
||||||
sheetWrapper,
|
sheetWrapper,
|
||||||
])
|
])
|
||||||
|
|
||||||
const onPressCamera = useCallback(async () => {
|
const onPressCamera = useCallback(async () => {
|
||||||
logger.metric('postComposer:click', {})
|
logger.metric('composerPrompt:camera:press', {})
|
||||||
|
|
||||||
// Open the composer first so it's ready
|
|
||||||
openComposer({})
|
|
||||||
|
|
||||||
// Use a small delay to ensure composer starts rendering before opening camera
|
|
||||||
setTimeout(async () => {
|
|
||||||
try {
|
try {
|
||||||
if (!(await requestCameraAccessIfNeeded())) {
|
if (!(await requestCameraAccessIfNeeded())) {
|
||||||
return
|
return
|
||||||
@@ -160,45 +113,23 @@ export function FeedComposerPrompt() {
|
|||||||
mediaTypes: 'images',
|
mediaTypes: 'images',
|
||||||
})
|
})
|
||||||
|
|
||||||
// Process the image
|
|
||||||
try {
|
|
||||||
const composerImage = await createComposerImage({
|
|
||||||
path: image.path,
|
|
||||||
width: image.width,
|
|
||||||
height: image.height,
|
|
||||||
mime: image.mime,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Convert to imageUris format for the composer
|
|
||||||
const source = composerImage.source
|
|
||||||
const imageUris = [
|
const imageUris = [
|
||||||
{
|
{
|
||||||
uri: source.path,
|
uri: image.path,
|
||||||
width: source.width,
|
width: image.width,
|
||||||
height: source.height,
|
height: image.height,
|
||||||
altText: composerImage.alt,
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// Close the composer first (if it was opened), then reopen with image
|
|
||||||
closeComposer()
|
|
||||||
setTimeout(() => {
|
|
||||||
openComposer({
|
openComposer({
|
||||||
imageUris: isNative ? imageUris : undefined,
|
imageUris: isNative ? imageUris : undefined,
|
||||||
})
|
})
|
||||||
}, 100)
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(`createComposerImage failed`, {
|
|
||||||
safeMessage: e instanceof Error ? e.message : String(e),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (!String(err).toLowerCase().includes('cancel')) {
|
if (!String(err).toLowerCase().includes('cancel')) {
|
||||||
logger.warn('Error opening camera', {error: err})
|
logger.warn('Error opening camera', {error: err})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 100)
|
}, [openComposer, requestCameraAccessIfNeeded])
|
||||||
}, [openComposer, closeComposer, requestCameraAccessIfNeeded])
|
|
||||||
|
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
return null
|
return null
|
||||||
@@ -264,8 +195,8 @@ export function FeedComposerPrompt() {
|
|||||||
{
|
{
|
||||||
lineHeight: a.text_md.fontSize,
|
lineHeight: a.text_md.fontSize,
|
||||||
includeFontPadding: false,
|
includeFontPadding: false,
|
||||||
top: 1,
|
|
||||||
},
|
},
|
||||||
|
native({top: 1}),
|
||||||
]}>
|
]}>
|
||||||
{_(msg`What's up?`)}
|
{_(msg`What's up?`)}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -69,7 +69,7 @@ import {
|
|||||||
} from '#/components/feeds/PostFeedVideoGridRow'
|
} from '#/components/feeds/PostFeedVideoGridRow'
|
||||||
import {TrendingInterstitial} from '#/components/interstitials/Trending'
|
import {TrendingInterstitial} from '#/components/interstitials/Trending'
|
||||||
import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos'
|
import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos'
|
||||||
import {FeedComposerPrompt} from '../feeds/FeedComposerPrompt'
|
import {ComposerPrompt} from '../feeds/ComposerPrompt'
|
||||||
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
|
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
|
||||||
import {FeedShutdownMsg} from './FeedShutdownMsg'
|
import {FeedShutdownMsg} from './FeedShutdownMsg'
|
||||||
import {PostFeedErrorMessage} from './PostFeedErrorMessage'
|
import {PostFeedErrorMessage} from './PostFeedErrorMessage'
|
||||||
@@ -755,7 +755,7 @@ let PostFeed = ({
|
|||||||
} else if (row.type === 'interstitialTrending') {
|
} else if (row.type === 'interstitialTrending') {
|
||||||
return <TrendingInterstitial />
|
return <TrendingInterstitial />
|
||||||
} else if (row.type === 'composerPrompt') {
|
} else if (row.type === 'composerPrompt') {
|
||||||
return <FeedComposerPrompt />
|
return <ComposerPrompt />
|
||||||
} else if (row.type === 'interstitialTrendingVideos') {
|
} else if (row.type === 'interstitialTrendingVideos') {
|
||||||
return <TrendingVideosInterstitial />
|
return <TrendingVideosInterstitial />
|
||||||
} else if (row.type === 'fallbackMarker') {
|
} else if (row.type === 'fallbackMarker') {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export function Composer({}: {winHeight: number}) {
|
|||||||
text={state?.text}
|
text={state?.text}
|
||||||
imageUris={state?.imageUris}
|
imageUris={state?.imageUris}
|
||||||
videoUri={state?.videoUri}
|
videoUri={state?.videoUri}
|
||||||
|
openGallery={state?.openGallery}
|
||||||
/>
|
/>
|
||||||
</TooltipSheetCompatProvider>
|
</TooltipSheetCompatProvider>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export function Composer({winHeight}: {winHeight: number}) {
|
|||||||
text={state.text}
|
text={state.text}
|
||||||
imageUris={state.imageUris}
|
imageUris={state.imageUris}
|
||||||
videoUri={state.videoUri}
|
videoUri={state.videoUri}
|
||||||
|
openGallery={state.openGallery}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ function Inner({state}: {state: ComposerOpts}) {
|
|||||||
openEmojiPicker={onOpenPicker}
|
openEmojiPicker={onOpenPicker}
|
||||||
text={state.text}
|
text={state.text}
|
||||||
imageUris={state.imageUris}
|
imageUris={state.imageUris}
|
||||||
|
openGallery={state.openGallery}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<EmojiPicker state={pickerState} close={onClosePicker} />
|
<EmojiPicker state={pickerState} close={onClosePicker} />
|
||||||
|
|||||||
Reference in New Issue
Block a user