whatsapp.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import { createMessage, type FormKitNode } from '@formkit/core'
  3. import { useFormKitNodeById } from '@formkit/vue'
  4. import type { FileUploaded } from '#shared/components/Form/fields/FieldFile/types.ts'
  5. import {
  6. FormValidationVisibility,
  7. type FormRef,
  8. } from '#shared/components/Form/types.ts'
  9. import { getNodeId } from '#shared/components/Form/utils.ts'
  10. import { getTicketChannelPlugin } from '#shared/entities/ticket/channel/plugins/index.ts'
  11. import type { TicketById } from '#shared/entities/ticket/types.ts'
  12. import { EnumTicketArticleSenderName } from '#shared/graphql/types.ts'
  13. import { i18n } from '#shared/i18n.ts'
  14. import { getAcceptableFileTypesString } from '#shared/utils/files.ts'
  15. import type {
  16. TicketArticleAction,
  17. TicketArticleActionPlugin,
  18. TicketArticleType,
  19. } from './types.ts'
  20. const allowedFiles = [
  21. {
  22. label: __('Audio file'),
  23. types: ['audio/aac', 'audio/mp4', 'audio/amr', 'audio/mpeg', 'audio/ogg'],
  24. size: 16 * 1024 * 1024,
  25. },
  26. {
  27. label: __('Sticker file'),
  28. types: ['image/webp'],
  29. size: 500 * 1024,
  30. },
  31. {
  32. label: __('Image file'),
  33. types: ['image/jpeg', 'image/png'],
  34. size: 5 * 1024 * 1024,
  35. },
  36. {
  37. label: __('Video file'),
  38. types: ['video/mp4', 'video/3gpp'],
  39. size: 16 * 1024 * 1024,
  40. },
  41. {
  42. label: __('Document file'),
  43. types: [
  44. 'text/plain',
  45. 'application/pdf',
  46. 'application/vnd.ms-powerpoint',
  47. 'application/msword',
  48. 'application/vnd.ms-excel',
  49. 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  50. 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
  51. 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  52. ],
  53. size: 100 * 1024 * 1024,
  54. },
  55. ]
  56. const acceptableFileTypes = getAcceptableFileTypesString(allowedFiles)
  57. const canUseWhatsapp = (ticket: TicketById) => {
  58. const channelPlugin = getTicketChannelPlugin(ticket.initialChannel)
  59. const channelAlert = channelPlugin?.channelAlert(ticket)
  60. return Boolean(channelAlert) && Boolean(channelAlert?.variant !== 'danger')
  61. }
  62. const actionPlugin: TicketArticleActionPlugin = {
  63. order: 300,
  64. addActions(ticket, article) {
  65. const sender = article.sender?.name
  66. const type = article.type?.name // 'whatsapp message'
  67. if (
  68. sender !== EnumTicketArticleSenderName.Customer ||
  69. type !== 'whatsapp message'
  70. )
  71. return []
  72. if (!canUseWhatsapp(ticket)) return []
  73. const action: TicketArticleAction = {
  74. apps: ['mobile', 'desktop'],
  75. label: __('Reply'),
  76. name: 'whatsapp message',
  77. icon: 'reply',
  78. alwaysVisible: true,
  79. view: {
  80. agent: ['change'],
  81. },
  82. perform(ticket, article, { openReplyForm }) {
  83. const articleData = {
  84. articleType: type,
  85. inReplyTo: article.messageId,
  86. }
  87. openReplyForm(articleData)
  88. },
  89. }
  90. return [action]
  91. },
  92. addTypes(ticket) {
  93. const descriptionType = ticket.createArticleType?.name
  94. if (descriptionType !== 'whatsapp message') return []
  95. if (!canUseWhatsapp(ticket)) return []
  96. let attachmentsFieldNode: FormKitNode
  97. let attachmentsCommitEvent: string
  98. let bodyCommitEventNode: FormKitNode
  99. let bodyCommitEventListener: string
  100. const setBodyNotAllowedMessage = (body: FormKitNode) => {
  101. body.emit('prop:validationVisibility', FormValidationVisibility.Live)
  102. body.store.set(
  103. createMessage({
  104. key: 'bodyNotAllowedForMediaType',
  105. blocking: true,
  106. value: i18n.t(
  107. 'No additional text can be sent with this media type. Please remove the text.',
  108. ),
  109. type: 'validation',
  110. visible: true,
  111. }),
  112. )
  113. }
  114. const removeBodyNotAllowedMessage = (body: FormKitNode) => {
  115. body.emit('prop:validationVisibility', FormValidationVisibility.Submit)
  116. body.store.remove('bodyNotAllowedForMediaType')
  117. }
  118. const deRegisterListeners = () => {
  119. if (attachmentsFieldNode) {
  120. attachmentsFieldNode.off(attachmentsCommitEvent)
  121. }
  122. if (bodyCommitEventNode) {
  123. bodyCommitEventNode.off(bodyCommitEventListener)
  124. removeBodyNotAllowedMessage(bodyCommitEventNode)
  125. }
  126. }
  127. const handleAllowedBody = (form?: FormRef) => {
  128. if (!form) return
  129. const checkAllowedForFileType = (currentFiles: FileUploaded[]) => {
  130. const body = form.getNodeByName('body')
  131. if (!body) return
  132. bodyCommitEventNode = body
  133. if (
  134. currentFiles &&
  135. currentFiles.length > 0 &&
  136. currentFiles[0].type &&
  137. (currentFiles[0].type === 'image/webp' ||
  138. currentFiles[0].type.startsWith('audio'))
  139. ) {
  140. bodyCommitEventListener = bodyCommitEventNode.on(
  141. 'commit',
  142. ({ payload: newValue }) => {
  143. if (newValue) {
  144. setBodyNotAllowedMessage(bodyCommitEventNode)
  145. } else {
  146. removeBodyNotAllowedMessage(bodyCommitEventNode)
  147. }
  148. },
  149. )
  150. if (bodyCommitEventNode.value) {
  151. setBodyNotAllowedMessage(bodyCommitEventNode)
  152. }
  153. } else {
  154. removeBodyNotAllowedMessage(bodyCommitEventNode)
  155. bodyCommitEventNode.off(bodyCommitEventListener)
  156. }
  157. }
  158. useFormKitNodeById(getNodeId(form.formId, 'attachments'), (node) => {
  159. attachmentsFieldNode = node
  160. // Check if the attachments are already present (e.g. after article type switch).
  161. if (attachmentsFieldNode.value) {
  162. checkAllowedForFileType(attachmentsFieldNode.value as FileUploaded[])
  163. }
  164. attachmentsCommitEvent = node.on('commit', ({ payload: newValue }) => {
  165. checkAllowedForFileType(newValue)
  166. })
  167. })
  168. }
  169. const type: TicketArticleType = {
  170. apps: ['mobile', 'desktop'],
  171. value: 'whatsapp message',
  172. label: __('WhatsApp'),
  173. buttonLabel: __('Add message'),
  174. icon: 'whatsapp',
  175. view: {
  176. agent: ['change'],
  177. },
  178. fields: {
  179. body: {
  180. required: false,
  181. validation: 'require_one:attachments|length:1,4096',
  182. },
  183. attachments: {
  184. validation: 'require_one:body',
  185. accept: acceptableFileTypes,
  186. multiple: false,
  187. allowedFiles,
  188. },
  189. },
  190. internal: false,
  191. contentType: 'text/plain',
  192. onDeselected: () => {
  193. deRegisterListeners()
  194. },
  195. onSelected: (ticket, context, form) => handleAllowedBody(form),
  196. onOpened: (ticket, context, form) => handleAllowedBody(form),
  197. }
  198. return [type]
  199. },
  200. }
  201. export default actionPlugin