add reply guards, gravatar fallbacks for avatars, draft preview in conversation list, clickable agent avatars. Show sent messages immediately instead of waiting for WebSocket response.

This commit is contained in:
Abhinav Raut
2026-03-11 14:52:21 +05:30
parent 678239027e
commit e7ab571c48
20 changed files with 313 additions and 87 deletions
@@ -1,4 +1,19 @@
<template>
<AlertDialog :open="showContactEmailWarning" @update:open="showContactEmailWarning = $event">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{{ $t('replyBox.contactEmailMissing') }}</AlertDialogTitle>
<AlertDialogDescription>
{{ $t('replyBox.contactEmailMissingDescription', { email: conversationStore.current?.contact?.email }) }}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{{ $t('globals.messages.cancel') }}</AlertDialogCancel>
<AlertDialogAction @click="processSend(true)">{{ $t('replyBox.sendAnyway') }}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<Dialog :open="openAIKeyPrompt" @update:open="openAIKeyPrompt = false">
<DialogContent class="sm:max-w-lg">
<DialogHeader class="space-y-2">
@@ -117,6 +132,16 @@ import { useDraftManager } from '@main/composables/useDraftManager'
import api from '@main/api'
import { useI18n } from 'vue-i18n'
import { useConversationStore } from '@main/stores/conversation'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle
} from '@shared-ui/components/ui/alert-dialog'
import { Button } from '@shared-ui/components/ui/button'
import {
Dialog,
@@ -189,6 +214,7 @@ const showBcc = ref(false)
const emailErrors = ref([])
const aiPrompts = ref([])
const replyBoxContentRef = ref(null)
const showContactEmailWarning = ref(false)
const mentions = ref([])
/**
@@ -265,47 +291,97 @@ const hasTextContent = computed(() => {
/**
* Processes the send action.
*/
const processSend = async () => {
const processSend = async (skipContactEmailCheck = false) => {
let hasMessageSendingErrored = false
isEditorFullscreen.value = false
try {
isSending.value = true
// Send message if there is text content in the editor or media files are attached.
if (hasTextContent.value > 0 || mediaFiles.value.length > 0) {
const message = htmlContent.value
await api.sendMessage(conversationStore.current.uuid, {
sender_type: UserTypeAgent,
private: messageType.value === 'private_note',
message: message,
attachments: mediaFiles.value.map((file) => file.id),
// Include mentions only for private notes
mentions: messageType.value === 'private_note' ? mentions.value : [],
// Convert email addresses to array and remove empty strings.
cc: cc.value
.split(',')
.map((email) => email.trim())
.filter((email) => email),
bcc: bcc.value
? bcc.value
.split(',')
.map((email) => email.trim())
.filter((email) => email)
: [],
to: to.value
? to.value
.split(',')
.map((email) => email.trim())
.filter((email) => email)
: []
const hasContent = hasTextContent.value > 0 || mediaFiles.value.length > 0
const convUUID = conversationStore.current.uuid
const isPrivate = messageType.value === 'private_note'
if (!isPrivate && conversationStore.current.inbox_channel === 'email') {
// Require at least one recipient in `to`.
if (!to.value.trim()) {
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
variant: 'destructive',
description: t('replyBox.toRequired')
})
return
}
// Apply macro actions if any, for macro errors just show toast and clear the editor.
// Warn if the contact's email is not in any recipient field.
if (!skipContactEmailCheck) {
const contactEmail = conversationStore.current.contact?.email?.toLowerCase()
if (contactEmail) {
const allRecipients = [to.value, cc.value, bcc.value].join(',').toLowerCase()
if (!allRecipients.split(',').map(e => e.trim()).includes(contactEmail)) {
showContactEmailWarning.value = true
return
}
}
}
}
let tempUUID = null
// Add pending message to cache for instant display.
if (hasContent) {
const savedContent = htmlContent.value
const author = {
id: userStore.userID,
first_name: userStore.firstName,
last_name: userStore.lastName,
avatar_url: userStore.avatar,
type: 'agent'
}
const parsedTo = !isPrivate && to.value ? to.value.split(',').map(e => e.trim()).filter(Boolean) : []
const parsedCC = !isPrivate && cc.value ? cc.value.split(',').map(e => e.trim()).filter(Boolean) : []
const parsedBCC = !isPrivate && bcc.value ? bcc.value.split(',').map(e => e.trim()).filter(Boolean) : []
const meta = {}
if (parsedTo.length) meta.to = parsedTo
if (parsedCC.length) meta.cc = parsedCC
if (parsedBCC.length) meta.bcc = parsedBCC
tempUUID = conversationStore.addPendingMessage(convUUID, savedContent, isPrivate, author, mediaFiles.value, textContent.value, meta)
// Clear editor immediately.
htmlContent.value = ''
try {
isSending.value = true
const response = await api.sendMessage(convUUID, {
sender_type: UserTypeAgent,
private: isPrivate,
message: savedContent,
attachments: mediaFiles.value.map((file) => file.id),
mentions: isPrivate ? mentions.value : [],
cc: parsedCC,
bcc: parsedBCC,
to: parsedTo
})
// Replace pending message with the real one from API response.
if (response?.data?.data) {
conversationStore.replacePendingMessage(convUUID, tempUUID, response.data.data)
}
} catch (error) {
hasMessageSendingErrored = true
// Remove pending message and restore editor content.
conversationStore.removePendingMessage(convUUID, tempUUID)
htmlContent.value = savedContent
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
variant: 'destructive',
description: handleHTTPError(error).message
})
}
}
// Apply macro actions if any.
if (!hasMessageSendingErrored) {
const macroID = conversationStore.getMacro(MACRO_CONTEXT.REPLY)?.id
const macroActions = conversationStore.getMacro(MACRO_CONTEXT.REPLY)?.actions || []
if (macroID > 0 && macroActions.length > 0) {
try {
await api.applyMacro(conversationStore.current.uuid, macroID, macroActions)
await api.applyMacro(convUUID, macroID, macroActions)
} catch (error) {
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
variant: 'destructive',
@@ -313,32 +389,17 @@ const processSend = async () => {
})
}
}
} catch (error) {
hasMessageSendingErrored = true
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
variant: 'destructive',
description: handleHTTPError(error).message
})
} finally {
// If API has NOT errored clear state.
if (hasMessageSendingErrored === false) {
// Clear draft from backend.
clearDraft(currentDraftKey.value)
// Clear macro for this conversation reply.
conversationStore.resetMacro(MACRO_CONTEXT.REPLY)
// Clear media files.
clearMediaFiles()
// Clear any email errors.
emailErrors.value = []
// Clear mentions.
mentions.value = []
}
isSending.value = false
}
// Clear state on success.
if (!hasMessageSendingErrored) {
clearDraft(currentDraftKey.value)
conversationStore.resetMacro(MACRO_CONTEXT.REPLY)
clearMediaFiles()
emailErrors.value = []
mentions.value = []
}
isSending.value = false
}
/**
@@ -12,7 +12,9 @@
<!-- Avatar -->
<Avatar class="w-12 h-12 rounded-full shadow">
<AvatarImage
:src="conversation.contact.avatar_url || ''"
:src="
conversation.contact.avatar_url || getGravatarUrl(conversation.contact.email) || ''
"
class="object-cover"
v-if="conversation.contact.avatar_url || ''"
/>
@@ -29,10 +31,6 @@
<h3 class="text-sm font-semibold truncate">
{{ contactFullName }}
</h3>
<Pencil
v-if="hasDraftForConversation"
class="w-3 h-3 text-muted-foreground flex-shrink-0"
/>
</div>
<span
class="text-xs text-gray-400 whitespace-nowrap"
@@ -43,7 +41,10 @@
</div>
<!-- Subject -->
<p v-if="conversation.subject" class="text-xs font-medium text-muted-foreground truncate">
<p
v-if="conversation.subject"
class="text-xs font-medium text-muted-foreground truncate"
>
{{ conversation.subject }}
</p>
@@ -55,15 +56,19 @@
<!-- Message preview and unread count -->
<div class="flex items-start justify-between gap-2">
<div
class="text-sm flex items-center gap-1.5 flex-1 break-all text-gray-600 dark:text-gray-300"
>
<Reply
class="text-green-600 flex-shrink-0"
size="15"
v-if="conversation.last_message_sender === 'agent'"
/>
{{ trimmedLastMessage }}
<div class="text-sm flex items-center gap-1.5 flex-1 break-all text-muted-foreground">
<template v-if="hasDraftForConversation">
<span class="font-medium text-primary">{{ $t('globals.terms.draft') }}:</span>
{{ draftPreview }}
</template>
<template v-else>
<Reply
class="text-green-600 flex-shrink-0"
size="15"
v-if="conversation.last_message_sender === 'agent'"
/>
{{ trimmedLastMessage }}
</template>
</div>
<div
v-if="conversation.unread_message_count > 0"
@@ -123,7 +128,8 @@
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { getRelativeTime } from '@shared-ui/utils/datetime.js'
import { Mail, Reply, Pencil, MailOpen } from 'lucide-vue-next'
import { getGravatarUrl } from '@shared-ui/utils/gravatar.js'
import { Mail, Reply, MailOpen } from 'lucide-vue-next'
import { Avatar, AvatarFallback, AvatarImage } from '@shared-ui/components/ui/avatar'
import {
ContextMenu,
@@ -197,4 +203,11 @@ const relativeLastMessageTime = computed(() => {
const hasDraftForConversation = computed(() => {
return conversationStore.hasDraft(props.conversation.uuid)
})
const draftPreview = computed(() => {
const draft = conversationStore.getDraft(props.conversation.uuid)
if (!draft?.content) return ''
const text = draft.content.replace(/<[^>]*>/g, '').trim()
return text.length > 100 ? text.slice(0, 100) + '...' : text
})
</script>
@@ -9,6 +9,13 @@
>
{{ getFullName }}
</router-link>
<router-link
v-else-if="canManageUsers"
:to="{ name: 'edit-agent', params: { id: message.author?.id } }"
class="text-muted-foreground text-sm font-medium hover:underline hover:text-primary"
>
{{ getFullName }}
</router-link>
<p v-else class="text-muted-foreground text-sm font-medium">
{{ getFullName }}
</p>
@@ -94,7 +101,19 @@
</div>
<!-- Avatar (right for outgoing) -->
<Avatar v-if="isOutgoing" class="cursor-pointer w-8 h-8">
<router-link
v-if="isOutgoing && canManageUsers"
:to="{ name: 'edit-agent', params: { id: message.author?.id } }"
class="flex-shrink-0"
>
<Avatar class="cursor-pointer w-8 h-8 hover:opacity-80 transition-opacity">
<AvatarImage :src="getAvatar" />
<AvatarFallback class="font-medium">
{{ avatarFallback }}
</AvatarFallback>
</Avatar>
</router-link>
<Avatar v-else-if="isOutgoing" class="w-8 h-8">
<AvatarImage :src="getAvatar" />
<AvatarFallback class="font-medium">
{{ avatarFallback }}
@@ -122,6 +141,7 @@
import { computed, ref } from 'vue'
import { useConversationStore } from '@main/stores/conversation'
import { useAppSettingsStore } from '@main/stores/appSettings'
import { useUserStore } from '@main/stores/user'
import { useI18n } from 'vue-i18n'
import { Lock, RotateCcw, Check } from 'lucide-vue-next'
import { revertCIDToImageSrc } from '@shared-ui/utils/string.js'
@@ -129,6 +149,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@shared-ui/components/u
import { Spinner } from '@shared-ui/components/ui/spinner'
import { formatMessageTimestamp, formatFullTimestamp } from '@shared-ui/utils/datetime.js'
import { Avatar, AvatarFallback, AvatarImage } from '@shared-ui/components/ui/avatar'
import { getGravatarUrl } from '@shared-ui/utils/gravatar.js'
import { Letter } from 'vue-letter'
import MessageAttachmentPreview from '@main/features/conversation/message/attachment/MessageAttachmentPreview.vue'
import MessageEnvelope from './MessageEnvelope.vue'
@@ -145,8 +166,12 @@ const props = defineProps({
const convStore = useConversationStore()
const settingsStore = useAppSettingsStore()
const userStore = useUserStore()
const { t } = useI18n()
const isSystemUser = computed(() => props.message.author?.email === 'System')
const canManageUsers = computed(() => !isSystemUser.value && userStore.can('users:manage'))
// Direction helpers
const isOutgoing = computed(() => props.direction === 'outgoing')
@@ -159,7 +184,11 @@ const getFullName = computed(() => {
})
const getAvatar = computed(() => {
return props.message.author?.avatar_url || ''
if (props.message.author?.avatar_url) return props.message.author.avatar_url
if (!isOutgoing.value && convStore.current?.contact?.email) {
return getGravatarUrl(convStore.current.contact.email)
}
return ''
})
const avatarFallback = computed(() => {
@@ -3,7 +3,7 @@
<div class="flex justify-between items-start">
<div class="relative">
<Avatar class="size-20">
<AvatarImage :src="conversation?.contact?.avatar_url || ''" />
<AvatarImage :src="conversation?.contact?.avatar_url || getGravatarUrl(conversation?.contact?.email)" />
<AvatarFallback>
{{ conversation?.contact?.first_name?.toUpperCase().substring(0, 2) }}
</AvatarFallback>
@@ -122,6 +122,7 @@ import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
import { useConversationStore } from '@/stores/conversation'
import { Skeleton } from '@shared-ui/components/ui/skeleton'
import { useUserStore } from '@/stores/user'
import { getGravatarUrl } from '@shared-ui/utils/gravatar.js'
import { useI18n } from 'vue-i18n'
const conversationStore = useConversationStore()
const emitter = useEmitter()
+57 -1
View File
@@ -645,6 +645,59 @@ export const useConversationStore = defineStore('conversation', () => {
}
}
function addPendingMessage (conversationUUID, content, isPrivate, author, attachments = [], textContent = '', meta = {}) {
const pendingMessage = {
uuid: `pending-${Date.now()}`,
type: 'outgoing',
status: 'pending',
content,
text_content: textContent,
content_type: 'html',
private: isPrivate,
sender_type: 'agent',
sender_id: author.id,
conversation_uuid: conversationUUID,
created_at: new Date().toISOString(),
author,
attachments,
meta
}
messages.data.addMessage(conversationUUID, pendingMessage)
incrementMessageVersion()
setTimeout(() => {
emitter.emit(EMITTER_EVENTS.NEW_MESSAGE, {
conversation_uuid: conversationUUID,
message: pendingMessage
})
}, 0)
// Safety net: auto-remove after 10 seconds if still pending.
const tempId = pendingMessage.uuid
setTimeout(() => {
if (messages.data.hasMessage(conversationUUID, tempId)) {
messages.data.removeMessage(conversationUUID, tempId)
incrementMessageVersion()
}
}, 10000)
return pendingMessage.uuid
}
function replacePendingMessage (conversationUUID, tempUUID, realMessage) {
if (messages.data.hasMessage(conversationUUID, realMessage.uuid)) {
// WS already delivered the real message, just remove the temp.
messages.data.removeMessage(conversationUUID, tempUUID)
} else {
messages.data.updateMessage(conversationUUID, tempUUID, realMessage)
}
incrementMessageVersion()
}
function removePendingMessage (conversationUUID, tempUUID) {
messages.data.removeMessage(conversationUUID, tempUUID)
incrementMessageVersion()
}
function addNewConversation (conversation) {
if (!conversationUUIDExists(conversation.uuid)) {
// Fetch list of conversations again.
@@ -874,6 +927,9 @@ export const useConversationStore = defineStore('conversation', () => {
getDraft,
setDraft,
removeDraft,
hasDraft
hasDraft,
addPendingMessage,
replacePendingMessage,
removePendingMessage
}
})
@@ -15,7 +15,7 @@
<AvatarUpload
@upload="onUpload"
@remove="onRemove"
:src="contact.avatar_url"
:src="contact.avatar_url || getGravatarUrl(contact.email)"
:initials="getInitials"
:label="t('globals.messages.upload')"
/>
@@ -114,6 +114,7 @@ import { createFormSchema } from '../../features/contact/formSchema.js'
import { useEmitter } from '../../composables/useEmitter'
import { EMITTER_EVENTS } from '../../constants/emitterEvents'
import { handleHTTPError } from '@shared-ui/utils/http.js'
import { getGravatarUrl } from '@shared-ui/utils/gravatar.js'
import { CustomBreadcrumb } from '@shared-ui/components/ui/breadcrumb'
import { Spinner } from '@shared-ui/components/ui/spinner'
+2 -1
View File
@@ -32,19 +32,20 @@
"@tiptap/extension-link": "^2.11.2",
"@tiptap/extension-mention": "^2.11.2",
"@tiptap/extension-placeholder": "^2.4.0",
"@tiptap/suggestion": "^2.11.2",
"@tiptap/extension-table": "^2.11.5",
"@tiptap/extension-table-cell": "^2.11.5",
"@tiptap/extension-table-header": "^2.11.5",
"@tiptap/extension-table-row": "^2.11.5",
"@tiptap/pm": "^2.4.0",
"@tiptap/starter-kit": "^2.4.0",
"@tiptap/suggestion": "^2.11.2",
"@tiptap/vue-3": "^2.4.0",
"@unovis/ts": "^1.4.4",
"@unovis/vue": "^1.4.4",
"@vee-validate/zod": "^4.15.0",
"@vueuse/core": "^12.4.0",
"axios": "^1.13.5",
"blueimp-md5": "^2.19.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"codemirror": "^6.0.2",
+8
View File
@@ -83,6 +83,9 @@ importers:
axios:
specifier: ^1.13.5
version: 1.13.5(debug@4.4.0)
blueimp-md5:
specifier: ^2.19.0
version: 2.19.0
class-variance-authority:
specifier: ^0.7.0
version: 0.7.1
@@ -1506,6 +1509,9 @@ packages:
bluebird@3.7.2:
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
blueimp-md5@2.19.0:
resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==}
boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
@@ -4994,6 +5000,8 @@ snapshots:
bluebird@3.7.2: {}
blueimp-md5@2.19.0: {}
boolbase@1.0.0: {}
brace-expansion@1.1.11:
+6
View File
@@ -0,0 +1,6 @@
import md5 from 'blueimp-md5'
export function getGravatarUrl (email) {
if (!email) return ''
return `https://www.gravatar.com/avatar/${md5(email.trim().toLowerCase())}?d=404`
}
+5
View File
@@ -663,6 +663,7 @@
"globals.terms.default": "Standard",
"globals.terms.description": "Beskrivelse | Beskrivelser",
"globals.terms.disabled": "Deaktiveret",
"globals.terms.draft": "Kladde",
"globals.terms.email": "E-mail | E-mails",
"globals.terms.enabled": "Aktiveret",
"globals.terms.error": "Fejl | Fejl",
@@ -870,6 +871,10 @@
"replyBox.bcc": "Blind kopi (BCC)",
"replyBox.emailAddresess": "E-mailadresser, adskilt med komma",
"replyBox.invalidEmailsIn": "Ugyldig(e) e-mail(s) i",
"replyBox.contactEmailMissing": "Kontaktens e-mail er ikke blandt modtagerne",
"replyBox.contactEmailMissingDescription": "Kontaktens e-mail ({email}) er ikke inkluderet i til, cc eller bcc. Kontakten vil ikke modtage dette svar.",
"replyBox.sendAnyway": "Send alligevel",
"replyBox.toRequired": "Mindst én modtager er påkrævet i Til-feltet.",
"replyBox.removeBCC": "Fjern BCC",
"report.agentStatus": "Agentstatus",
"report.chart.newConversations": "Nye samtaler",
+5
View File
@@ -663,6 +663,7 @@
"globals.terms.default": "Standard",
"globals.terms.description": "Beschreibung | Beschreibungen",
"globals.terms.disabled": "Deaktiviert",
"globals.terms.draft": "Entwurf",
"globals.terms.email": "E-Mail | E-Mails",
"globals.terms.enabled": "Aktiviert",
"globals.terms.error": "Fehler | Fehler",
@@ -870,6 +871,10 @@
"replyBox.bcc": "Blindkopie",
"replyBox.emailAddresess": "E-Mail-Adressen, durch Komma getrennt",
"replyBox.invalidEmailsIn": "Ungültige E-Mail(s) in",
"replyBox.contactEmailMissing": "Kontakt-E-Mail nicht in Empfängern",
"replyBox.contactEmailMissingDescription": "Die E-Mail des Kontakts ({email}) ist nicht in An, CC oder BCC enthalten. Der Kontakt wird diese Antwort nicht erhalten.",
"replyBox.sendAnyway": "Trotzdem senden",
"replyBox.toRequired": "Mindestens ein Empfänger im An-Feld ist erforderlich.",
"replyBox.removeBCC": "BCC entfernen",
"report.agentStatus": "Mitarbeiterstatus",
"report.chart.newConversations": "Neue Konversationen",
+5
View File
@@ -670,6 +670,7 @@
"globals.terms.default": "Default",
"globals.terms.description": "Description | Descriptions",
"globals.terms.disabled": "Disabled",
"globals.terms.draft": "Draft",
"globals.terms.email": "Email | Emails",
"globals.terms.enabled": "Enabled",
"globals.terms.error": "Error | Errors",
@@ -877,6 +878,10 @@
"replyBox.bcc": "BCC",
"replyBox.emailAddresess": "Email addresses separated by comma",
"replyBox.invalidEmailsIn": "Invalid email(s) in",
"replyBox.contactEmailMissing": "Contact email not in recipients",
"replyBox.contactEmailMissingDescription": "The contact's email ({email}) is not included in to, cc, or bcc. The contact won't receive this reply.",
"replyBox.sendAnyway": "Send anyway",
"replyBox.toRequired": "At least one recipient is required in the To field.",
"replyBox.removeBCC": "Remove BCC",
"report.agentStatus": "Agent Status",
"report.chart.newConversations": "New conversations",
+5
View File
@@ -663,6 +663,7 @@
"globals.terms.default": "Predeterminado",
"globals.terms.description": "Descripción | Descripciones",
"globals.terms.disabled": "Deshabilitado",
"globals.terms.draft": "Borrador",
"globals.terms.email": "Correo Electrónico | Correos Electrónicos",
"globals.terms.enabled": "Habilitado",
"globals.terms.error": "Error | Errores",
@@ -870,6 +871,10 @@
"replyBox.bcc": "BCC",
"replyBox.emailAddresess": "Direcciones de correo electrónico separadas por coma",
"replyBox.invalidEmailsIn": "Correo(s) electrónico(s) no válido(s) en",
"replyBox.contactEmailMissing": "El correo del contacto no está en los destinatarios",
"replyBox.contactEmailMissingDescription": "El correo del contacto ({email}) no está incluido en para, cc o bcc. El contacto no recibirá esta respuesta.",
"replyBox.sendAnyway": "Enviar de todos modos",
"replyBox.toRequired": "Se requiere al menos un destinatario en el campo Para.",
"replyBox.removeBCC": "Eliminar BCC",
"report.agentStatus": "Estado del agente",
"report.chart.newConversations": "Nuevas conversaciones",
+5
View File
@@ -663,6 +663,7 @@
"globals.terms.default": "پیش‌فرض",
"globals.terms.description": "توضیحات | توضیحات",
"globals.terms.disabled": "غیرفعال",
"globals.terms.draft": "پیش‌نویس",
"globals.terms.email": "ایمیل | ایمیل‌ها",
"globals.terms.enabled": "فعال",
"globals.terms.error": "خطا | خطاها",
@@ -870,6 +871,10 @@
"replyBox.bcc": "رونوشت مخفی",
"replyBox.emailAddresess": "آدرس‌های ایمیل جدا شده با کاما",
"replyBox.invalidEmailsIn": "ایمیل(های) نامعتبر در",
"replyBox.contactEmailMissing": "ایمیل مخاطب در گیرندگان نیست",
"replyBox.contactEmailMissingDescription": "ایمیل مخاطب ({email}) در فیلدهای به، رونوشت یا رونوشت مخفی وجود ندارد. مخاطب این پاسخ را دریافت نخواهد کرد.",
"replyBox.sendAnyway": "ارسال به هر حال",
"replyBox.toRequired": "حداقل یک گیرنده در فیلد به الزامی است.",
"replyBox.removeBCC": "حذف BCC",
"report.agentStatus": "وضعیت نماینده",
"report.chart.newConversations": "مکالمات جدید",
+5
View File
@@ -663,6 +663,7 @@
"globals.terms.default": "Défaut",
"globals.terms.description": "Description | Descriptions",
"globals.terms.disabled": "Désactivé",
"globals.terms.draft": "Brouillon",
"globals.terms.email": "E-mail | E-mails",
"globals.terms.enabled": "Activé",
"globals.terms.error": "Erreur | Erreurs",
@@ -870,6 +871,10 @@
"replyBox.bcc": "Copie cachée",
"replyBox.emailAddresess": "Adresses électroniques séparées par une virgule",
"replyBox.invalidEmailsIn": "Email(s) incorrect(s) dans",
"replyBox.contactEmailMissing": "L'e-mail du contact n'est pas dans les destinataires",
"replyBox.contactEmailMissingDescription": "L'e-mail du contact ({email}) n'est pas inclus dans à, cc ou bcc. Le contact ne recevra pas cette réponse.",
"replyBox.sendAnyway": "Envoyer quand même",
"replyBox.toRequired": "Au moins un destinataire est requis dans le champ À.",
"replyBox.removeBCC": "Supprimer le BCC",
"report.agentStatus": "Statut des agents",
"report.chart.newConversations": "Nouvelles conversations",
+5
View File
@@ -663,6 +663,7 @@
"globals.terms.default": "Predefinito",
"globals.terms.description": "Descrizione | Descrizioni",
"globals.terms.disabled": "Disattivato",
"globals.terms.draft": "Bozza",
"globals.terms.email": "Email | Email",
"globals.terms.enabled": "Attivo",
"globals.terms.error": "Errore | Errori",
@@ -870,6 +871,10 @@
"replyBox.bcc": "CCN",
"replyBox.emailAddresess": "Indirizzi email separati da virgola",
"replyBox.invalidEmailsIn": "Email non valida in",
"replyBox.contactEmailMissing": "L'email del contatto non è tra i destinatari",
"replyBox.contactEmailMissingDescription": "L'email del contatto ({email}) non è inclusa in a, cc o bcc. Il contatto non riceverà questa risposta.",
"replyBox.sendAnyway": "Invia comunque",
"replyBox.toRequired": "È richiesto almeno un destinatario nel campo A.",
"replyBox.removeBCC": "Rimuovi BCC",
"report.agentStatus": "Stato dellagente",
"report.chart.newConversations": "Nuove conversazioni",
+5
View File
@@ -663,6 +663,7 @@
"globals.terms.default": "デフォルト",
"globals.terms.description": "説明",
"globals.terms.disabled": "無効",
"globals.terms.draft": "下書き",
"globals.terms.email": "Eメール",
"globals.terms.enabled": "有効",
"globals.terms.error": "エラー",
@@ -870,6 +871,10 @@
"replyBox.bcc": "BCC",
"replyBox.emailAddresess": "メールアドレスはカンマで区切って入力してください",
"replyBox.invalidEmailsIn": "以下のメールアドレスが無効です",
"replyBox.contactEmailMissing": "連絡先のメールが宛先にありません",
"replyBox.contactEmailMissingDescription": "連絡先のメール ({email}) が宛先、CC、BCCに含まれていません。連絡先はこの返信を受け取れません。",
"replyBox.sendAnyway": "それでも送信",
"replyBox.toRequired": "宛先フィールドに少なくとも1つの受信者が必要です。",
"replyBox.removeBCC": "BCCを削除",
"report.agentStatus": "担当者の状態",
"report.chart.newConversations": "新しい会話",
+12 -7
View File
@@ -646,10 +646,10 @@
"globals.terms.clientID": "क्लायंट ID",
"globals.terms.clientSecret": "क्लायंट गुप्त",
"globals.terms.closedAt": "बंद केले",
"globals.terms.collapse": "संकुचित करा",
"globals.terms.collapse": "कोलॅप्स करा",
"globals.terms.configure": "कॉन्फिगर करा",
"globals.terms.contact": "संपर्क | संपर्क",
"globals.terms.content": "सामग्री | सामग्री",
"globals.terms.content": "कंटेंट | कंटेंट",
"globals.terms.continue": "सुरू ठेवा",
"globals.terms.conversation": "संभाषण | संभाषण",
"globals.terms.copy": "कॉपी करा",
@@ -663,12 +663,13 @@
"globals.terms.default": "डिफॉल्ट",
"globals.terms.description": "वर्णन | वर्णने",
"globals.terms.disabled": "अक्षम",
"globals.terms.draft": "ड्राफ्ट",
"globals.terms.email": "ईमेल | ईमेल",
"globals.terms.enabled": "सक्षम",
"globals.terms.error": "त्रुटी | त्रुटी",
"globals.terms.event": "इव्हेंट | इव्हेंट्स",
"globals.terms.excellent": "अतिउत्तम",
"globals.terms.expand": "विस्तृत करा",
"globals.terms.expand": "एक्सपांड करा",
"globals.terms.fair": "ठीक",
"globals.terms.feedback": "अभिप्राय | अभिप्राय",
"globals.terms.file": "फाइल | फाइल्स",
@@ -688,7 +689,7 @@
"globals.terms.inactive": "निष्क्रिय | निष्क्रिय",
"globals.terms.inbox": "इनबॉक्स | इनबॉक्स",
"globals.terms.initiatedAt": "सुरू केले",
"globals.terms.integration": "एकत्रीकरण | एकत्रीकरणे",
"globals.terms.integration": "इंटिग्रेशन | इंटिग्रेशन्स",
"globals.terms.ipAddress": "IP पत्ता | IP पते",
"globals.terms.isDefault": "डिफॉल्ट आहे",
"globals.terms.key": "की | की",
@@ -721,11 +722,11 @@
"globals.terms.open": "उघडे",
"globals.terms.openMenu": "मेनू उघडा",
"globals.terms.optional": "पर्यायी | पर्यायी",
"globals.terms.overdue": "मुदत संपली",
"globals.terms.overdue": "ओव्हरड्यू",
"globals.terms.overview": "आढावा",
"globals.terms.page": "पृष्ठ | पृष्ठे",
"globals.terms.password": "पासवर्ड | पासवर्ड्स",
"globals.terms.pending": "प्रलंबित",
"globals.terms.pending": "पेंडिंग",
"globals.terms.phoneNumber": "फोन नंबर | फोन नंबर",
"globals.terms.pickDate": "तारीख निवडा",
"globals.terms.placeholder": "प्लेसहोल्डर | प्लेसहोल्डर",
@@ -792,7 +793,7 @@
"globals.terms.visitor": "अभ्यागत | अभ्यागत",
"globals.terms.warning": "चेतावणी | चेतावणी",
"globals.terms.webhook": "वेबहुक | वेबहुक्स",
"globals.terms.workspace": "कार्यक्षेत्र",
"globals.terms.workspace": "वर्कस्पेस",
"globals.terms.you": "तुम्ही",
"importer.agentCaseSensitiveNote": "भूमिका आणि संघ नक्की जुळणे आवश्यक आहे (केस-सेन्सिटिव्ह)",
"importer.createdAgent": "पंक्ती {row}: एजंट {name} ({email}) तयार केला",
@@ -870,6 +871,10 @@
"replyBox.bcc": "बीसीसी",
"replyBox.emailAddresess": "ईमेल पत्ते कॉमा वेगळे केलेले",
"replyBox.invalidEmailsIn": "मध्ये अवैध ईमेल",
"replyBox.contactEmailMissing": "संपर्काचा ईमेल प्राप्तकर्त्यांमध्ये नाही",
"replyBox.contactEmailMissingDescription": "संपर्काचा ईमेल ({email}) to, cc, किंवा bcc मध्ये समाविष्ट नाही. संपर्काला हे उत्तर मिळणार नाही.",
"replyBox.sendAnyway": "तरीही पाठवा",
"replyBox.toRequired": "To फील्डमध्ये किमान एक प्राप्तकर्ता आवश्यक आहे.",
"replyBox.removeBCC": "BCC काढा",
"report.agentStatus": "एजंट स्थिती",
"report.chart.newConversations": "नवीन संभाषणे",
+2
View File
@@ -145,6 +145,7 @@ type ConversationListContact struct {
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
FirstName string `db:"first_name" json:"first_name"`
LastName string `db:"last_name" json:"last_name"`
Email null.String `db:"email" json:"email"`
AvatarURL null.String `db:"avatar_url" json:"avatar_url"`
}
@@ -246,6 +247,7 @@ type MessageAuthor struct {
ID int `db:"id" json:"id"`
FirstName string `db:"first_name" json:"first_name"`
LastName string `db:"last_name" json:"last_name"`
Email null.String `db:"email" json:"email"`
AvatarURL null.String `db:"avatar_url" json:"avatar_url"`
AvailabilityStatus string `db:"availability_status" json:"availability_status"`
Type string `db:"type" json:"type"`
+4 -1
View File
@@ -44,6 +44,7 @@ SELECT
users.updated_at as "contact.updated_at",
users.first_name as "contact.first_name",
users.last_name as "contact.last_name",
users.email as "contact.email",
users.avatar_url as "contact.avatar_url",
inboxes.channel as inbox_channel,
inboxes.name as inbox_name,
@@ -581,6 +582,7 @@ SELECT
u.id AS "author.id",
u.first_name AS "author.first_name",
u.last_name AS "author.last_name",
u.email AS "author.email",
u.avatar_url AS "author.avatar_url",
u.availability_status AS "author.availability_status",
u.type AS "author.type",
@@ -605,7 +607,7 @@ LEFT JOIN media ON media.model_type = 'messages' AND media.model_id = m.id
WHERE m.uuid = $1
GROUP BY
m.id, m.created_at, m.updated_at, m.status, m.type, m.content, m.uuid, m.private, m.sender_type, c.uuid,
u.id, u.first_name, u.last_name, u.avatar_url, u.availability_status, u.type, u.last_active_at
u.id, u.first_name, u.last_name, u.email, u.avatar_url, u.availability_status, u.type, u.last_active_at
ORDER BY m.created_at;
-- name: get-messages
@@ -629,6 +631,7 @@ SELECT
u.id AS "author.id",
u.first_name AS "author.first_name",
u.last_name AS "author.last_name",
u.email AS "author.email",
u.avatar_url AS "author.avatar_url",
u.availability_status AS "author.availability_status",
u.type AS "author.type",