whatsapp.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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'],
  75. label: __('Reply'),
  76. name: 'whatsapp message',
  77. icon: 'reply',
  78. view: {
  79. agent: ['change'],
  80. },
  81. perform(ticket, article, { openReplyDialog }) {
  82. const articleData = {
  83. articleType: type,
  84. inReplyTo: article.messageId,
  85. }
  86. openReplyDialog(articleData)
  87. },
  88. }
  89. return [action]
  90. },
  91. addTypes(ticket) {
  92. const descriptionType = ticket.createArticleType?.name
  93. if (descriptionType !== 'whatsapp message') return []
  94. if (!canUseWhatsapp(ticket)) return []
  95. let attachmentsFieldNode: FormKitNode
  96. let attachmentsCommitEvent: string
  97. let bodyCommitEventNode: FormKitNode
  98. let bodyCommitEventListener: string
  99. const setBodyNotAllowedMessage = (body: FormKitNode) => {
  100. body.emit('prop:validationVisibility', FormValidationVisibility.Live)
  101. body.store.set(
  102. createMessage({
  103. key: 'bodyNotAllowedForMediaType',
  104. blocking: true,
  105. value: i18n.t(
  106. 'No additional text can be sent with this media type. Please remove the text.',
  107. ),
  108. type: 'validation',
  109. visible: true,
  110. }),
  111. )
  112. }
  113. const removeBodyNotAllowedMessage = (body: FormKitNode) => {
  114. body.emit('prop:validationVisibility', FormValidationVisibility.Submit)
  115. body.store.remove('bodyNotAllowedForMediaType')
  116. }
  117. const deRegisterListeners = () => {
  118. if (attachmentsFieldNode) {
  119. attachmentsFieldNode.off(attachmentsCommitEvent)
  120. }
  121. if (bodyCommitEventNode) {
  122. bodyCommitEventNode.off(bodyCommitEventListener)
  123. removeBodyNotAllowedMessage(bodyCommitEventNode)
  124. }
  125. }
  126. const handleAllowedBody = (form?: FormRef) => {
  127. if (!form) return
  128. const checkAllowedForFileType = (currentFiles: FileUploaded[]) => {
  129. const body = form.getNodeByName('body')
  130. if (!body) return
  131. bodyCommitEventNode = body
  132. if (
  133. currentFiles &&
  134. currentFiles.length > 0 &&
  135. currentFiles[0].type &&
  136. (currentFiles[0].type === 'image/webp' ||
  137. currentFiles[0].type.startsWith('audio'))
  138. ) {
  139. bodyCommitEventListener = bodyCommitEventNode.on(
  140. 'commit',
  141. ({ payload: newValue }) => {
  142. if (newValue) {
  143. setBodyNotAllowedMessage(bodyCommitEventNode)
  144. } else {
  145. removeBodyNotAllowedMessage(bodyCommitEventNode)
  146. }
  147. },
  148. )
  149. if (bodyCommitEventNode.value) {
  150. setBodyNotAllowedMessage(bodyCommitEventNode)
  151. }
  152. } else {
  153. removeBodyNotAllowedMessage(bodyCommitEventNode)
  154. bodyCommitEventNode.off(bodyCommitEventListener)
  155. }
  156. }
  157. useFormKitNodeById(getNodeId(form.formId, 'attachments'), (node) => {
  158. attachmentsFieldNode = node
  159. // Check if the attachments are already present (e.g. after article type switch).
  160. if (attachmentsFieldNode.value) {
  161. checkAllowedForFileType(attachmentsFieldNode.value as FileUploaded[])
  162. }
  163. attachmentsCommitEvent = node.on('commit', ({ payload: newValue }) => {
  164. checkAllowedForFileType(newValue)
  165. })
  166. })
  167. }
  168. const type: TicketArticleType = {
  169. apps: ['mobile'],
  170. value: 'whatsapp message',
  171. label: __('WhatsApp'),
  172. icon: 'whatsapp',
  173. view: {
  174. agent: ['change'],
  175. },
  176. fields: {
  177. body: {
  178. required: false,
  179. validation: 'require_one:attachments|length:1,4096',
  180. },
  181. attachments: {
  182. validation: 'require_one:body',
  183. accept: acceptableFileTypes,
  184. multiple: false,
  185. allowedFiles,
  186. },
  187. },
  188. internal: false,
  189. contentType: 'text/plain',
  190. onDeselected: () => {
  191. deRegisterListeners()
  192. },
  193. onSelected: (ticket, context, form) => handleAllowedBody(form),
  194. onOpened: (ticket, context, form) => handleAllowedBody(form),
  195. }
  196. return [type]
  197. },
  198. }
  199. export default actionPlugin