multi-post-form.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import { Row, View, Pressable } from 'app/design/view'
  2. import { Text } from 'app/design/typography'
  3. import { NeoButton } from 'app/design/controls'
  4. import { useState, useEffect, useMemo } from 'react'
  5. import { useWindowDimensions } from 'react-native'
  6. import { menuItemsByNameNew, cloneObject, getBreakpoint, isWeb } from 'app/lib/util'
  7. import { useCurrentUser } from 'app/context/user'
  8. import Profile from 'app/ui/molecules/profile/profile'
  9. import { CardList } from 'app/ui/molecules/page/card'
  10. import { useTranslation } from 'react-i18next'
  11. import FormModal, { handleFormModal, getFormModal } from 'app/ui/molecules/dialogs/form-modal';
  12. import { BlockWrapper } from 'app/components/block-wrapper'
  13. import { useBreakpointName } from 'app/context/measure'
  14. import { pickLibraryMedia } from 'app/lib/media/pick-media'
  15. import { queueFieldAssets } from 'app/lib/form/pending-field-assets'
  16. // The first form's media field, as its own "Add Photos or Videos" button in the
  17. // composer toolbar would open it (forms/feed.js getAttachmentMenuItems).
  18. function getMediaField(formPage) {
  19. const findInputs = (node) => {
  20. if (!node || typeof node !== 'object') return null;
  21. if (node.inputs && typeof node.inputs === 'object') return node.inputs;
  22. for (const key of Object.keys(node)) {
  23. const inputs = findInputs(node[key]);
  24. if (inputs) return inputs;
  25. }
  26. return null;
  27. };
  28. const inputs = findInputs(formPage?.elements);
  29. if (inputs?.video?.type === 'files')
  30. return { name: 'video', mediaTypes: ['images', 'videos'], icon: 'ImagePlay', title: 'Add Photos or Videos' };
  31. if (inputs?.photo?.type === 'files')
  32. return { name: 'photo', mediaTypes: ['images'], icon: 'Image', title: 'Add Photos' };
  33. return null;
  34. }
  35. // The "Create new …" trigger is not a form input (nothing is typed here), so it
  36. // is a plain Pressable rather than a NeoButton or a TextInput look-alike: text
  37. // only on phones, fading while pressed; a capsule on tablet and desktop.
  38. const TRIGGER_CLASS = 'flex-1 min-w-0 h-11 justify-center rounded-full active:opacity-50 '
  39. + 'sm:px-4 sm:bg-button sm:hover:bg-button-hover web:transition-[background-color,opacity] web:duration-150';
  40. const TRIGGER_TEXT_CLASS = 'text-base font-medium tracking-tight text-muted-foreground';
  41. export default function MultiPostForm({ data, blockWrapperProps }) {
  42. const { currentUser } = useCurrentUser();
  43. const { t } = useTranslation()
  44. const { width: windowWidth } = useWindowDimensions();
  45. // Other content types: tablet and desktop only. Web hides them with
  46. // classes (SSR-safe); native does not render them on phones, since a
  47. // `display: none` subtree of native (Expo UI) buttons trips Fabric's
  48. // layout ownership assert in debug builds (facebook/react-native#52349).
  49. const showOtherTypes = isWeb || !!getBreakpoint(windowWidth);
  50. // Phones get a borderless media button (hydration-safe: bordered until hydrated on web).
  51. const mediaButtonStyle = useBreakpointName() === '' ? 'borderless' : 'bordered';
  52. const [pageData, setPageData] = useState(false);
  53. const [pageDataDef, setPageDataDef] = useState(false);
  54. const menu_add_items = menuItemsByNameNew('', data.menu, currentUser).filter(item => item.name != 'more-auto');
  55. const firstForm = menu_add_items.shift();
  56. const profileData = useMemo(() => ({
  57. ...currentUser,
  58. url_avatar: currentUser.avatar,
  59. url: null,
  60. }));
  61. useEffect(() => {
  62. const fetchData = async () => {
  63. const sResponse = await getFormModal(firstForm, data.params);
  64. setPageDataDef(sResponse.data);
  65. };
  66. fetchData();
  67. }, []);
  68. // Show tooltip after configured delay on first feed visit
  69. // Keep showing until user opens the post form
  70. const mediaField = useMemo(() => getMediaField(pageDataDef), [pageDataDef]);
  71. const getFirstForm = async () => {
  72. if (pageDataDef) {
  73. setPageData({ ...cloneObject(pageDataDef), ts: Date.now() });
  74. }
  75. else {
  76. const a = await getFormModal(firstForm, data.params);
  77. setPageData({ ...a.data, ts: Date.now() });
  78. }
  79. }
  80. if (menu_add_items.length == 0 && !firstForm)
  81. return;
  82. const triggerLabel = t('Create new ') + firstForm.title.toLowerCase();
  83. const handleTriggerPress = () => {
  84. // User clicked to create post - permanently dismiss tooltip
  85. void getFirstForm();
  86. };
  87. // Fast track to "Add Photos or Videos": pick first (on web the file dialog
  88. // needs the click's user gesture), then open the form with the files queued
  89. // for its media field, which uploads them on mount. Unsupported picks still
  90. // open the form, where the field says so, like a pick inside the form.
  91. const handleMediaPress = async () => {
  92. if (!mediaField) return;
  93. const picked = await pickLibraryMedia(mediaField.mediaTypes);
  94. if (!picked) return;
  95. queueFieldAssets(mediaField.name, picked.assets, picked.unsupported);
  96. await getFirstForm();
  97. };
  98. return (
  99. <BlockWrapper {...blockWrapperProps}>
  100. <CardList className="flex-row items-center gap-3 min-w-0">
  101. <View className="flex-none">
  102. <Profile {...profileData} displaySize="base" displayType="unit_wo_info" />
  103. </View>
  104. <Pressable
  105. className={TRIGGER_CLASS}
  106. accessibilityRole="button"
  107. accessibilityLabel={triggerLabel}
  108. onPress={handleTriggerPress}
  109. >
  110. <Text className={TRIGGER_TEXT_CLASS} numberOfLines={1}>{triggerLabel}</Text>
  111. </Pressable>
  112. {mediaField && (
  113. <NeoButton
  114. style={mediaButtonStyle}
  115. borderShape="circle"
  116. controlSize="regular"
  117. image={mediaField.icon}
  118. tooltip={t(mediaField.title)}
  119. accessibilityLabel={t(mediaField.title)}
  120. onPress={handleMediaPress}
  121. />
  122. )}
  123. <FormModal key={pageData?.ts} pageData={pageData} setPageData={setPageData} />
  124. {menu_add_items.length > 0 && showOtherTypes && <Row className="hidden sm:flex gap-3 flex-none">
  125. {menu_add_items.map((item, index) => (
  126. <NeoButton key={item.name} style="bordered" borderShape="circle" controlSize="regular" onPress={() => handleFormModal(item, null, setPageData, data.params)} image={item.icon} />
  127. ))}
  128. </Row>}
  129. </CardList>
  130. </BlockWrapper>
  131. )
  132. }