CommonUserAvatar.vue 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { computed, toRef } from 'vue'
  4. import { useAppName } from '#shared/composables/useAppName.ts'
  5. import { useAvatarIndicator } from '#shared/composables/useAvatarIndicator.ts'
  6. import { EnumTaskbarApp } from '#shared/graphql/types.ts'
  7. import { getIdFromGraphQLId } from '#shared/graphql/utils.ts'
  8. import { i18n } from '#shared/i18n.ts'
  9. import { getUserAvatarClasses } from '#shared/initializer/initializeUserAvatarClasses.ts'
  10. import { useApplicationStore } from '#shared/stores/application.ts'
  11. import {
  12. SYSTEM_USER_ID,
  13. SYSTEM_USER_INTERNAL_ID,
  14. } from '#shared/utils/constants.ts'
  15. import { getInitials } from '#shared/utils/formatter.ts'
  16. import CommonAvatar from '../CommonAvatar/CommonAvatar.vue'
  17. import logo from './assets/logo.svg'
  18. import type { AvatarUserAccess, AvatarUserLive, AvatarUser } from './types.ts'
  19. import type { AvatarSize } from '../CommonAvatar/index.ts'
  20. export interface Props {
  21. entity: AvatarUser
  22. size?: AvatarSize
  23. personal?: boolean
  24. decorative?: boolean
  25. initialsOnly?: boolean
  26. live?: AvatarUserLive
  27. access?: AvatarUserAccess
  28. noMuted?: boolean
  29. noIndicator?: boolean
  30. }
  31. const props = withDefaults(defineProps<Props>(), {
  32. size: 'medium',
  33. })
  34. const initials = computed(() => {
  35. const { lastname, firstname, email, phone, mobile } = props.entity
  36. return getInitials(firstname, lastname, email, phone, mobile)
  37. })
  38. const { backgroundColors } = getUserAvatarClasses()
  39. const fullName = computed(() => {
  40. const { lastname, firstname, fullname } = props.entity
  41. if (fullname) return fullname
  42. return [firstname, lastname].filter(Boolean).join(' ')
  43. })
  44. const colorClass = computed(() => {
  45. const { id } = props.entity
  46. const internalId = getIdFromGraphQLId(id)
  47. if (internalId === SYSTEM_USER_INTERNAL_ID) return 'bg-white'
  48. // get color based on mod of the integer ID
  49. // so it stays consistent between different interfaces and logins
  50. return backgroundColors[internalId % (backgroundColors.length - 1)]
  51. })
  52. const sources = ['facebook', 'twitter']
  53. const icon = computed(() => {
  54. const { source } = props.entity
  55. if (source && sources.includes(source)) return source
  56. return null
  57. })
  58. const appName = useAppName()
  59. const application = useApplicationStore()
  60. const image = computed(() => {
  61. if (icon.value || props.initialsOnly) return null
  62. if (props.entity.id === SYSTEM_USER_ID) return logo
  63. if (!props.entity.image) return null
  64. // Support the inline data URI as an image source.
  65. if (props.entity.image.startsWith('data:')) return props.entity.image
  66. // we're using the REST api here to get the image and to also use the browser image cache
  67. // TODO: this should be re-evaluated when the desktop app is going to be implemented
  68. const apiUrl = String(application.config.api_path)
  69. return `${apiUrl}/users/image/${props.entity.image}`
  70. })
  71. const isVip = computed(() => {
  72. return !props.personal && props.entity.vip
  73. })
  74. const { indicatorIcon, indicatorLabel, indicatorIsIdle } = useAvatarIndicator(
  75. toRef(props, 'entity'),
  76. toRef(props, 'personal'),
  77. toRef(props, 'live'),
  78. toRef(props, 'access'),
  79. )
  80. const isMuted = computed(() => !props.noMuted && indicatorIsIdle.value)
  81. const className = computed(() => {
  82. const classes = [colorClass.value]
  83. if (isMuted.value) {
  84. classes.push('opacity-60')
  85. }
  86. return classes
  87. })
  88. const label = computed(() => {
  89. let label = i18n.t('Avatar')
  90. const name = fullName.value || props.entity.email
  91. if (name) label += ` (${name})`
  92. if (isVip.value) label += ` (${i18n.t('VIP')})`
  93. return label
  94. })
  95. const indicator = computed(() => {
  96. if (appName === EnumTaskbarApp.Mobile || props.noIndicator) return null
  97. return indicatorIcon.value
  98. })
  99. const indicatorClass = computed(() => {
  100. if (isMuted.value) return 'fill-stone-200 dark:fill-neutral-500'
  101. return 'text-black dark:text-white'
  102. })
  103. const indicatorSizes = {
  104. xs: 'xs',
  105. small: 'xs',
  106. medium: 'xs',
  107. normal: 'tiny',
  108. large: 'small',
  109. xl: 'medium',
  110. } as const
  111. const indicatorSize = computed(() => indicatorSizes[props.size])
  112. </script>
  113. <template>
  114. <div class="relative">
  115. <CommonAvatar
  116. :initials="initials"
  117. :size="size"
  118. :icon="icon"
  119. :class="className"
  120. :image="image"
  121. :vip-icon="isVip ? 'vip-user' : undefined"
  122. :decorative="decorative"
  123. :aria-label="label"
  124. />
  125. <div
  126. v-if="indicator"
  127. v-tooltip="indicatorLabel"
  128. class="absolute bottom-0 end-0 flex translate-y-1 items-center justify-center rounded-full bg-blue-200 p-[3px] outline outline-1 -outline-offset-1 outline-neutral-100 ltr:translate-x-2 rtl:-translate-x-2 dark:bg-gray-700 dark:outline-gray-900"
  129. >
  130. <CommonIcon
  131. :class="indicatorClass"
  132. :label="indicatorLabel"
  133. :size="indicatorSize"
  134. :name="indicator"
  135. />
  136. </div>
  137. </div>
  138. </template>