ArticleBubble.vue 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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. no-indicator
  95. />
  96. <div
  97. class="grid w-full grid-rows-[0fr] overflow-hidden rounded-xl transition-[grid-template-rows]"
  98. :class="[
  99. {
  100. 'grid-rows-[1fr]': showMetaInformation,
  101. },
  102. articleWrapperBorderClass,
  103. ]"
  104. >
  105. <div
  106. :aria-hidden="!showMetaInformation"
  107. class="grid w-full grid-rows-[0fr] overflow-hidden"
  108. >
  109. <Transition name="pseudo-transition">
  110. <ArticleBubbleHeader
  111. v-if="showMetaInformation"
  112. :aria-label="$t('Article meta information')"
  113. :class="headerAndIconBarBackgroundClass"
  114. :show-meta-information="showMetaInformation"
  115. :position="position"
  116. :article="article"
  117. />
  118. </Transition>
  119. </div>
  120. <ArticleBubbleSecurityStatusBar
  121. v-if="!showMetaInformation"
  122. :class="[
  123. headerAndIconBarBackgroundClass,
  124. showMetaInformation ? dividerClass : '',
  125. ]"
  126. :article="article"
  127. />
  128. <ArticleBubbleSecurityWarning :article="article" />
  129. <ArticleBubbleMediaError :article="article" />
  130. <ArticleBubbleBody
  131. tabindex="0"
  132. :data-test-id="`article-bubble-body-${article.internalId}`"
  133. class="last:rounded-b-xl focus:outline-none focus-visible:-outline-offset-2 focus-visible:outline-blue-800"
  134. :class="[
  135. bodyClasses,
  136. {
  137. 'pt-3': showMetaInformation,
  138. '[&:nth-child(2)]:rounded-t-xl': !showMetaInformation,
  139. 'rtl:rounded-br-none [&:nth-child(2)]:ltr:rounded-br-none':
  140. position === 'right',
  141. 'rtl:rounded-br-none [&:nth-child(2)]:ltr:rounded-bl-none':
  142. position === 'left',
  143. },
  144. ]"
  145. :position="position"
  146. :show-meta-information="showMetaInformation"
  147. :inline-images="inlineImages"
  148. :article="article"
  149. @click="toggleHeader"
  150. @keydown.enter="toggleHeader"
  151. @preview="showPreview('image', $event)"
  152. />
  153. <ArticleBubbleBlockedContentWarning
  154. :class="[
  155. dividerClass,
  156. bodyClasses,
  157. {
  158. 'pt-3': showMetaInformation,
  159. },
  160. ]"
  161. :article="article"
  162. />
  163. <ArticleBubbleFooter
  164. :article="article"
  165. :article-attachments="articleAttachments"
  166. @preview="showPreview"
  167. />
  168. </div>
  169. <ArticleBubbleActionList :article="article" :position="position" />
  170. <ArticleReactionBadge
  171. :position="position"
  172. :reaction="article.preferences?.whatsapp?.reaction?.emoji"
  173. />
  174. </div>
  175. </template>