ArticleBubble.vue 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { computed, defineAsyncComponent, ref } from 'vue'
  4. import CommonUserAvatar from '#shared/components/CommonUserAvatar/CommonUserAvatar.vue'
  5. import { useAttachments } from '#shared/composables/useAttachments.ts'
  6. import type { TicketArticle } from '#shared/entities/ticket/types.ts'
  7. import { EnumTicketArticleSenderName } from '#shared/graphql/types.ts'
  8. import {
  9. useFilePreviewViewer,
  10. type ViewerFile,
  11. } from '#desktop/composables/useFilePreviewViewer.ts'
  12. import ArticleBubbleActionList from '#desktop/pages/ticket/components/TicketDetailView/ArticleBubble/ArticleBubbleActionList.vue'
  13. import ArticleBubbleBlockedContentWarning from '#desktop/pages/ticket/components/TicketDetailView/ArticleBubble/ArticleBubbleBlockedContentWarning.vue'
  14. import ArticleBubbleBody from '#desktop/pages/ticket/components/TicketDetailView/ArticleBubble/ArticleBubbleBody.vue'
  15. import ArticleBubbleFooter from '#desktop/pages/ticket/components/TicketDetailView/ArticleBubble/ArticleBubbleFooter.vue'
  16. import ArticleBubbleMediaError from '#desktop/pages/ticket/components/TicketDetailView/ArticleBubble/ArticleBubbleMediaError.vue'
  17. import ArticleBubbleSecurityStatusBar from '#desktop/pages/ticket/components/TicketDetailView/ArticleBubble/ArticleBubbleSecurityStatusBar.vue'
  18. import ArticleBubbleSecurityWarning from '#desktop/pages/ticket/components/TicketDetailView/ArticleBubble/ArticleBubbleSecurityWarning.vue'
  19. import { useBubbleHeader } from '#desktop/pages/ticket/components/TicketDetailView/ArticleBubble/useBubbleHeader.ts'
  20. import { useBubbleStyleGuide } from '#desktop/pages/ticket/components/TicketDetailView/ArticleBubble/useBubbleStyleGuide.ts'
  21. import ArticleReactionBadge from '#desktop/pages/ticket/components/TicketDetailView/ArticleReactionBadge.vue'
  22. const ArticleBubbleHeader = defineAsyncComponent(
  23. () =>
  24. import(
  25. '#desktop/pages/ticket/components/TicketDetailView/ArticleBubble/ArticleBubbleHeader.vue'
  26. ),
  27. )
  28. interface Props {
  29. article: TicketArticle
  30. }
  31. const props = defineProps<Props>()
  32. const { showMetaInformation, toggleHeader } = useBubbleHeader()
  33. const position = computed(() => {
  34. switch (props.article.sender?.name) {
  35. case EnumTicketArticleSenderName.Customer:
  36. return 'right'
  37. case EnumTicketArticleSenderName.System:
  38. return 'left'
  39. case EnumTicketArticleSenderName.Agent:
  40. return 'left'
  41. default:
  42. return 'left'
  43. }
  44. })
  45. const hasInternalNote = computed(
  46. () =>
  47. (props.article.type?.name === 'note' && props.article.internal) ||
  48. props.article.internal,
  49. )
  50. const {
  51. frameBorderClass,
  52. dividerClass,
  53. bodyClasses,
  54. headerAndIconBarBackgroundClass,
  55. articleWrapperBorderClass,
  56. internalNoteClass,
  57. } = useBubbleStyleGuide(position, hasInternalNote)
  58. const filteredAttachments = computed(() => {
  59. return props.article.attachmentsWithoutInline.filter(
  60. (file) => !file.preferences || !file.preferences['original-format'],
  61. )
  62. })
  63. const { attachments: articleAttachments } = useAttachments({
  64. attachments: filteredAttachments,
  65. })
  66. const inlineImages = ref<ViewerFile[]>([])
  67. const { showPreview } = useFilePreviewViewer(
  68. computed(() => [...inlineImages.value, ...articleAttachments.value]),
  69. )
  70. </script>
  71. <template>
  72. <div
  73. class="group/article backface-hidden relative rounded-t-xl"
  74. :data-test-id="`article-bubble-container-${article.internalId}`"
  75. :class="[
  76. {
  77. 'ltr:rounded-bl-xl rtl:rounded-br-xl': position === 'right',
  78. 'ltr:rounded-br-xl rtl:rounded-bl-xl': position === 'left',
  79. },
  80. frameBorderClass,
  81. internalNoteClass,
  82. ]"
  83. >
  84. <CommonUserAvatar
  85. class="!absolute bottom-0"
  86. :class="{
  87. 'ltr:-right-2.5 ltr:translate-x-full rtl:-left-2.5 rtl:-translate-x-full':
  88. position === 'right',
  89. 'ltr:-left-2.5 ltr:-translate-x-full rtl:-right-2.5 rtl:translate-x-full':
  90. position === 'left',
  91. }"
  92. :entity="article.author"
  93. size="small"
  94. />
  95. <div
  96. class="grid w-full grid-rows-[0fr] overflow-hidden rounded-xl transition-[grid-template-rows]"
  97. :class="[
  98. {
  99. 'grid-rows-[1fr]': showMetaInformation,
  100. },
  101. articleWrapperBorderClass,
  102. ]"
  103. >
  104. <div
  105. :aria-hidden="!showMetaInformation"
  106. class="grid w-full grid-rows-[0fr] overflow-hidden"
  107. >
  108. <Transition name="pseudo-transition">
  109. <ArticleBubbleHeader
  110. v-if="showMetaInformation"
  111. :aria-label="$t('Article meta information')"
  112. :class="headerAndIconBarBackgroundClass"
  113. :show-meta-information="showMetaInformation"
  114. :position="position"
  115. :article="article"
  116. />
  117. </Transition>
  118. </div>
  119. <ArticleBubbleSecurityStatusBar
  120. v-if="!showMetaInformation"
  121. :class="[
  122. headerAndIconBarBackgroundClass,
  123. showMetaInformation ? dividerClass : '',
  124. ]"
  125. :article="article"
  126. />
  127. <ArticleBubbleSecurityWarning :article="article" />
  128. <ArticleBubbleMediaError :article="article" />
  129. <ArticleBubbleBody
  130. tabindex="0"
  131. :data-test-id="`article-bubble-body-${article.internalId}`"
  132. class="last:rounded-b-xl focus:outline-none focus-visible:-outline-offset-2 focus-visible:outline-blue-800"
  133. :class="[
  134. bodyClasses,
  135. {
  136. 'pt-3': showMetaInformation,
  137. '[&:nth-child(2)]:rounded-t-xl': !showMetaInformation,
  138. 'rtl:rounded-br-none [&:nth-child(2)]:ltr:rounded-br-none':
  139. position === 'right',
  140. 'rtl:rounded-br-none [&:nth-child(2)]:ltr:rounded-bl-none':
  141. position === 'left',
  142. },
  143. ]"
  144. :position="position"
  145. :show-meta-information="showMetaInformation"
  146. :inline-images="inlineImages"
  147. :article="article"
  148. @click="toggleHeader"
  149. @keydown.enter="toggleHeader"
  150. @preview="showPreview('image', $event)"
  151. />
  152. <ArticleBubbleBlockedContentWarning
  153. :class="[
  154. dividerClass,
  155. bodyClasses,
  156. {
  157. 'pt-3': showMetaInformation,
  158. },
  159. ]"
  160. :article="article"
  161. />
  162. <ArticleBubbleFooter
  163. :article="article"
  164. :article-attachments="articleAttachments"
  165. @preview="showPreview"
  166. />
  167. </div>
  168. <ArticleBubbleActionList :article="article" :position="position" />
  169. <ArticleReactionBadge
  170. :position="position"
  171. :reaction="article.preferences?.whatsapp?.reaction?.emoji"
  172. />
  173. </div>
  174. </template>