cover-elements.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import { View, Row } from 'app/design/view'
  2. import Image from 'app/ui/atoms/image'
  3. import {
  4. appSetting,
  5. LAYOUT_BREAKPOINTS,
  6. isNativeTabsEnabled,
  7. FeedbackHaptics,
  8. } from 'app/lib/util'
  9. import Profile from 'app/ui/molecules/profile/profile'
  10. import { NeoButton } from 'app/design/controls/neo-button/neo-button'
  11. import { fetcher } from 'app/lib/fetcher'
  12. import { useProfileImageUpload } from 'app/lib/image/use-profile-image-upload'
  13. import { getCoverAspectClass } from 'app/lib/image/upload-settings'
  14. import { useCurrentUser } from 'app/context/user'
  15. import Loading from 'app/ui/atoms/loading'
  16. import { Platform } from 'react-native'
  17. import { getWindowSafeAreaInsets, useRouter } from 'app/lib/hooks/router'
  18. import { navigateBackInTab } from 'app/lib/navigation/tab-history'
  19. import { useCoverBackTabKey, useShowCoverBackButton } from 'app/components/elements/use-cover-back'
  20. import { useIsPushedScreen } from 'app/context/tab-chrome'
  21. import { useTranslation } from 'react-i18next'
  22. import { playCue } from 'app/lib/sound'
  23. // Matches `top-3` overlay spacing on the full cover.
  24. const COVER_CONTROL_TOP = 10
  25. function getNativeTabsTopInset() {
  26. return isNativeTabsEnabled() ? (getWindowSafeAreaInsets().top || 0) : 0
  27. }
  28. export function nativeTabsTopPadStyle() {
  29. const top = getNativeTabsTopInset()
  30. return top > 0 ? { paddingTop: top } : undefined
  31. }
  32. function BackButton() {
  33. const { t } = useTranslation()
  34. const router = useRouter()
  35. const { currentUser } = useCurrentUser()
  36. const currentTab = useCoverBackTabKey()
  37. const showBack = useShowCoverBackButton()
  38. const isPushedScreen = useIsPushedScreen()
  39. const handleBackPress = () => {
  40. FeedbackHaptics('Medium')
  41. if (Platform.OS === 'web') {
  42. playCue('navigate', { direction: 'back' })
  43. if (typeof window !== 'undefined' && window.history.length > 2) {
  44. window.history.back()
  45. } else if (router?.replace) {
  46. router.replace('/')
  47. }
  48. return
  49. }
  50. // A pushed page pops the tab's stack; a tab root shows its root page.
  51. if (isPushedScreen) {
  52. router.back()
  53. return
  54. }
  55. navigateBackInTab(router, currentTab, currentUser)
  56. }
  57. if (!showBack) {
  58. return null
  59. }
  60. return (
  61. <View className="self-center">
  62. <NeoButton
  63. image="ArrowLeft"
  64. style="glass"
  65. controlSize="regular"
  66. borderShape="circle"
  67. accessibilityLabel={t('Back')}
  68. sound={false}
  69. onPress={handleBackPress}
  70. />
  71. </View>
  72. )
  73. }
  74. export function CoverBackButton() {
  75. return <BackButton />
  76. }
  77. export function getCoverBackButton() {
  78. return <CoverBackButton />
  79. }
  80. export function CoverImage({
  81. mode,
  82. profileData,
  83. coverData,
  84. allowEdit,
  85. allowSwitch,
  86. title,
  87. profileDisplaySize,
  88. is_person,
  89. suppressCoverBackButton = false,
  90. priority = false,
  91. animated = false,
  92. }) {
  93. const { imageUrl, isUploading, pick, modal } = useProfileImageUpload({
  94. kind: mode,
  95. profileId: profileData?.info?.id || profileData?.id || 0,
  96. moduleName: profileData?.module || '',
  97. storage: coverData?.storage || '',
  98. initialUrl: mode == 'cover' ? coverData?.src : profileData?.url_avatar,
  99. isProfile: true,
  100. })
  101. const handleSwitch = async (id) => {
  102. await fetcher(
  103. '/api.php?r=system/switch_profile/TemplServiceAccount&params[]=' +
  104. id,
  105. )
  106. location.reload()
  107. }
  108. if (mode == 'cover') {
  109. const isCover = !!imageUrl
  110. const overlayTop = COVER_CONTROL_TOP + getNativeTabsTopInset()
  111. return (
  112. <>
  113. <View
  114. className={`bg-accent/50 lg:rounded-xl mx-auto gap-2 flex-1 overflow-hidden w-full
  115. ${isCover ? getCoverAspectClass(profileData?.module, true) : 'pb-32'}`}
  116. >
  117. {isCover ? (
  118. <Image
  119. alt={title}
  120. view="cover"
  121. sizes={LAYOUT_BREAKPOINTS.xl}
  122. className="u-cover"
  123. src={imageUrl}
  124. priority={priority}
  125. />
  126. ) : null}
  127. {imageUrl?.includes('data:') || isUploading ? (
  128. <View className="absolute inset-0 h-full w-full opacity-60 bg-card justify-center items-center">
  129. <Loading />
  130. </View>
  131. ) : null}
  132. <Row
  133. className="justify-end gap-2 absolute right-3"
  134. style={{ top: overlayTop }}
  135. >
  136. {allowSwitch ? (
  137. <NeoButton
  138. image="RefreshCw"
  139. style="glass"
  140. controlSize="regular"
  141. borderShape="circle"
  142. onPress={() => handleSwitch(allowSwitch)}
  143. />
  144. ) : null}
  145. {allowEdit ? (
  146. <NeoButton
  147. image="Camera"
  148. style="glass"
  149. controlSize="regular"
  150. borderShape="circle"
  151. disabled={isUploading}
  152. onPress={pick}
  153. />
  154. ) : null}
  155. </Row>
  156. <View
  157. className="absolute lg:hidden left-3 z-50"
  158. style={{ top: overlayTop }}
  159. >
  160. {!suppressCoverBackButton ? getCoverBackButton() : null}
  161. </View>
  162. </View>
  163. {modal}
  164. </>
  165. )
  166. }
  167. if (mode == 'picture') {
  168. return (
  169. <>
  170. <View className="relative inline-flex">
  171. <Profile
  172. {...profileData}
  173. url_avatar={imageUrl}
  174. displayType="unit_wo_info"
  175. displaySize={profileDisplaySize}
  176. priority={priority}
  177. animated={!!animated}
  178. />
  179. {isUploading ? (
  180. <View className="absolute inset-0 rounded-full bg-card/80 items-center justify-center">
  181. <Loading />
  182. </View>
  183. ) : null}
  184. {allowEdit ? (
  185. <View className="absolute bottom-0.5 lg:bottom-1 right-0.5 lg:right-1 rounded-full bg-card p-1">
  186. <View className="lg:hidden">
  187. <NeoButton
  188. image="Camera"
  189. style="glass"
  190. controlSize="mini"
  191. borderShape="circle"
  192. disabled={isUploading}
  193. onPress={pick}
  194. />
  195. </View>
  196. <View className="hidden lg:flex">
  197. <NeoButton
  198. image="Camera"
  199. style="glass"
  200. controlSize="small"
  201. borderShape="circle"
  202. disabled={isUploading}
  203. onPress={pick}
  204. />
  205. </View>
  206. </View>
  207. ) : null}
  208. </View>
  209. {modal}
  210. </>
  211. )
  212. }
  213. return null
  214. }