mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-22 02:23:32 +00:00
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:
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+8
@@ -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:
|
||||
|
||||
@@ -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`
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "مکالمات جدید",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 dell’agente",
|
||||
"report.chart.newConversations": "Nuove conversazioni",
|
||||
|
||||
@@ -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
@@ -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": "नवीन संभाषणे",
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user