|
|
@@ -1,4 +1,5 @@
|
|
|
-import { Row, View } from 'app/design/view'
|
|
|
+import { Row, View, Pressable } from 'app/design/view'
|
|
|
+import { Text } from 'app/design/typography'
|
|
|
import { NeoButton } from 'app/design/controls'
|
|
|
import { useState, useEffect, useMemo } from 'react'
|
|
|
import { useWindowDimensions } from 'react-native'
|
|
|
@@ -9,6 +10,36 @@ import { CardList } from 'app/ui/molecules/page/card'
|
|
|
import { useTranslation } from 'react-i18next'
|
|
|
import FormModal, { handleFormModal, getFormModal } from 'app/ui/molecules/dialogs/form-modal';
|
|
|
import { BlockWrapper } from 'app/components/block-wrapper'
|
|
|
+import { useBreakpointName } from 'app/context/measure'
|
|
|
+import { pickLibraryMedia } from 'app/lib/media/pick-media'
|
|
|
+import { queueFieldAssets } from 'app/lib/form/pending-field-assets'
|
|
|
+
|
|
|
+// The first form's media field, as its own "Add Photos or Videos" button in the
|
|
|
+// composer toolbar would open it (forms/feed.js getAttachmentMenuItems).
|
|
|
+function getMediaField(formPage) {
|
|
|
+ const findInputs = (node) => {
|
|
|
+ if (!node || typeof node !== 'object') return null;
|
|
|
+ if (node.inputs && typeof node.inputs === 'object') return node.inputs;
|
|
|
+ for (const key of Object.keys(node)) {
|
|
|
+ const inputs = findInputs(node[key]);
|
|
|
+ if (inputs) return inputs;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ };
|
|
|
+ const inputs = findInputs(formPage?.elements);
|
|
|
+ if (inputs?.video?.type === 'files')
|
|
|
+ return { name: 'video', mediaTypes: ['images', 'videos'], icon: 'ImagePlay', title: 'Add Photos or Videos' };
|
|
|
+ if (inputs?.photo?.type === 'files')
|
|
|
+ return { name: 'photo', mediaTypes: ['images'], icon: 'Image', title: 'Add Photos' };
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
+// The "Create new …" trigger is not a form input (nothing is typed here), so it
|
|
|
+// is a plain Pressable rather than a NeoButton or a TextInput look-alike: text
|
|
|
+// only on phones, fading while pressed; a capsule on tablet and desktop.
|
|
|
+const TRIGGER_CLASS = 'flex-1 min-w-0 h-11 justify-center rounded-full active:opacity-50 '
|
|
|
+ + 'sm:px-4 sm:bg-button sm:hover:bg-button-hover web:transition-[background-color,opacity] web:duration-150';
|
|
|
+const TRIGGER_TEXT_CLASS = 'text-base font-medium tracking-tight text-muted-foreground';
|
|
|
|
|
|
export default function MultiPostForm({ data, blockWrapperProps }) {
|
|
|
const { currentUser } = useCurrentUser();
|
|
|
@@ -19,6 +50,8 @@ export default function MultiPostForm({ data, blockWrapperProps }) {
|
|
|
// `display: none` subtree of native (Expo UI) buttons trips Fabric's
|
|
|
// layout ownership assert in debug builds (facebook/react-native#52349).
|
|
|
const showOtherTypes = isWeb || !!getBreakpoint(windowWidth);
|
|
|
+ // Phones get a borderless media button (hydration-safe: bordered until hydrated on web).
|
|
|
+ const mediaButtonStyle = useBreakpointName() === '' ? 'borderless' : 'bordered';
|
|
|
const [pageData, setPageData] = useState(false);
|
|
|
const [pageDataDef, setPageDataDef] = useState(false);
|
|
|
const menu_add_items = menuItemsByNameNew('', data.menu, currentUser).filter(item => item.name != 'more-auto');
|
|
|
@@ -43,6 +76,8 @@ export default function MultiPostForm({ data, blockWrapperProps }) {
|
|
|
// Keep showing until user opens the post form
|
|
|
|
|
|
|
|
|
+ const mediaField = useMemo(() => getMediaField(pageDataDef), [pageDataDef]);
|
|
|
+
|
|
|
const getFirstForm = async () => {
|
|
|
if (pageDataDef) {
|
|
|
setPageData({ ...cloneObject(pageDataDef), ts: Date.now() });
|
|
|
@@ -58,10 +93,24 @@ export default function MultiPostForm({ data, blockWrapperProps }) {
|
|
|
|
|
|
|
|
|
|
|
|
+ const triggerLabel = t('Create new ') + firstForm.title.toLowerCase();
|
|
|
+
|
|
|
const handleTriggerPress = () => {
|
|
|
// User clicked to create post - permanently dismiss tooltip
|
|
|
|
|
|
- getFirstForm();
|
|
|
+ void getFirstForm();
|
|
|
+ };
|
|
|
+
|
|
|
+ // Fast track to "Add Photos or Videos": pick first (on web the file dialog
|
|
|
+ // needs the click's user gesture), then open the form with the files queued
|
|
|
+ // for its media field, which uploads them on mount. Unsupported picks still
|
|
|
+ // open the form, where the field says so, like a pick inside the form.
|
|
|
+ const handleMediaPress = async () => {
|
|
|
+ if (!mediaField) return;
|
|
|
+ const picked = await pickLibraryMedia(mediaField.mediaTypes);
|
|
|
+ if (!picked) return;
|
|
|
+ queueFieldAssets(mediaField.name, picked.assets, picked.unsupported);
|
|
|
+ await getFirstForm();
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
@@ -70,15 +119,25 @@ export default function MultiPostForm({ data, blockWrapperProps }) {
|
|
|
<View className="flex-none">
|
|
|
<Profile {...profileData} displaySize="base" displayType="unit_wo_info" />
|
|
|
</View>
|
|
|
- <NeoButton
|
|
|
- style="bordered"
|
|
|
- borderShape="capsule"
|
|
|
- controlSize="regular"
|
|
|
- align="start"
|
|
|
- width="fill"
|
|
|
- label={t('Create new ') + firstForm.title.toLowerCase()}
|
|
|
+ <Pressable
|
|
|
+ className={TRIGGER_CLASS}
|
|
|
+ accessibilityRole="button"
|
|
|
+ accessibilityLabel={triggerLabel}
|
|
|
onPress={handleTriggerPress}
|
|
|
- />
|
|
|
+ >
|
|
|
+ <Text className={TRIGGER_TEXT_CLASS} numberOfLines={1}>{triggerLabel}</Text>
|
|
|
+ </Pressable>
|
|
|
+ {mediaField && (
|
|
|
+ <NeoButton
|
|
|
+ style={mediaButtonStyle}
|
|
|
+ borderShape="circle"
|
|
|
+ controlSize="regular"
|
|
|
+ image={mediaField.icon}
|
|
|
+ tooltip={t(mediaField.title)}
|
|
|
+ accessibilityLabel={t(mediaField.title)}
|
|
|
+ onPress={handleMediaPress}
|
|
|
+ />
|
|
|
+ )}
|
|
|
|
|
|
<FormModal key={pageData?.ts} pageData={pageData} setPageData={setPageData} />
|
|
|
{menu_add_items.length > 0 && showOtherTypes && <Row className="hidden sm:flex gap-3 flex-none">
|