entity-cover.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import { View } from 'app/design/view'
  2. import Image from 'app/ui/atoms/image'
  3. import { Text } from 'app/design/typography'
  4. import { CoverMenuMeta, CoverMenu } from 'app/components/elements/covers/menu-cover'
  5. import { appSetting, stripTags, LAYOUT_BREAKPOINTS } from 'app/lib/util'
  6. import { BlockWrapper } from 'app/components/block-wrapper'
  7. import { NeoButton } from 'app/design/controls/neo-button/neo-button'
  8. import { useProfileImageUpload } from 'app/lib/image/use-profile-image-upload'
  9. import { getCoverAspectClass } from 'app/lib/image/upload-settings'
  10. import Loading from 'app/ui/atoms/loading'
  11. import { useCurrentUser } from 'app/context/user'
  12. /**
  13. * entity_cover with the page-header cover's upload: the same crop, camera
  14. * button and `useProfileImageUpload` flow. The header hides that button when
  15. * `cover.view_by_module` is `min` (projects), so the block is where the cover
  16. * is changed.
  17. *
  18. * UNA puts `allow_edit` on `cover_block`, not on this block. The edit-profile
  19. * action is the same check (`checkAllowedEdit`: owner account or moderator).
  20. */
  21. function canChangeCover(data, currentUser) {
  22. if (!appSetting('cover', 'allow_edit')) return false
  23. if (data?.allow_edit) return true
  24. if (menuHasEditProfile(data?.actions_menu)) return true
  25. if (!currentUser) return false
  26. const profileId = data?.profile?.id
  27. if (profileId != null && String(currentUser.id) === String(profileId)) return true
  28. return !!(currentUser.moderator || currentUser.operator)
  29. }
  30. function menuHasEditProfile(menu) {
  31. const items = Array.isArray(menu?.items) ? menu.items : []
  32. for (const item of items) {
  33. const name = String(item?.name || '')
  34. const link = String(item?.link || item?.url || '')
  35. if (/^edit-.+-profile$/.test(name) || /edit-.+-profile/.test(link)) return true
  36. if (item?.list && menuHasEditProfile({ items: item.list })) return true
  37. }
  38. return false
  39. }
  40. export default function EntityCover({ data, block, blockWrapperProps, uri }) {
  41. const { currentUser } = useCurrentUser()
  42. const moduleName = data?.profile?.module || block?.module || ''
  43. const profile = data?.profile
  44. const allowEdit = canChangeCover(data, currentUser)
  45. const { imageUrl, isUploading, pick, modal } = useProfileImageUpload({
  46. kind: 'cover',
  47. profileId: data?.id || profile?.info?.id || 0,
  48. moduleName,
  49. storage: data?.cover?.storage || '',
  50. initialUrl: data?.cover?.src || '',
  51. isProfile: true,
  52. })
  53. const showPicture = moduleName === 'bx_persons'
  54. || (!!moduleName && !!appSetting('cover', 'show_pic_by_module', moduleName))
  55. const profileTitle = profile?.display_name || profile?.title || ''
  56. const showCover = !!imageUrl || allowEdit
  57. return (
  58. <BlockWrapper {...blockWrapperProps}>
  59. {showCover ? (
  60. <View className={`relative w-full overflow-hidden rounded-t-2xl bg-accent/50 ${getCoverAspectClass(moduleName, true)}`}>
  61. {imageUrl ? (
  62. <Image
  63. alt={profileTitle}
  64. view="cover"
  65. sizes={LAYOUT_BREAKPOINTS.xl}
  66. className="u-cover"
  67. src={imageUrl}
  68. priority
  69. />
  70. ) : null}
  71. {imageUrl?.includes('data:') || isUploading ? (
  72. <View className="absolute inset-0 h-full w-full opacity-60 bg-card justify-center items-center">
  73. <Loading />
  74. </View>
  75. ) : null}
  76. {allowEdit ? (
  77. <View className="absolute top-3 right-3">
  78. <NeoButton
  79. image="Camera"
  80. style="glass"
  81. controlSize="regular"
  82. borderShape="circle"
  83. disabled={isUploading}
  84. onPress={pick}
  85. />
  86. </View>
  87. ) : null}
  88. </View>
  89. ) : null}
  90. <View className="p-4 w-full mx-auto bg-card h-min overflow-hidden rounded-2xl shadow-sm">
  91. {showPicture && !!data.image?.src ? (
  92. <View className="w-28 h-28 overflow-hidden bg-background rounded-full">
  93. <Image alt={profileTitle} className="rounded-full" view="cover" src={data.image.src} priority={!imageUrl} />
  94. </View>
  95. ) : null}
  96. <View className="flex-row flex-wrap">
  97. <View className="flex-auto">
  98. <Text className="text-2xl pt-4 font-bold text-secondary-foreground">
  99. {profileTitle}
  100. </Text>
  101. </View>
  102. </View>
  103. <View className="text-center flex-row py-3 gap-3 items-center">
  104. <CoverMenuMeta {...data.meta_menu} />
  105. </View>
  106. <View className="w-full">
  107. <CoverMenu isSplitMenu={false} size="sm" {...data.actions_menu} uri={uri} />
  108. </View>
  109. <Text numberOfLines={3} className="mt-2 text-base text-muted-foreground">{stripTags(data.description || data.space_desc)}</Text>
  110. </View>
  111. {modal}
  112. </BlockWrapper>
  113. )
  114. }