mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-11 13:28:57 +00:00
more commits.
This commit is contained in:
@@ -21,7 +21,6 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
|
||||
g.GET("/api/conversations/all", auth(handleGetAllConversations, "conversation:all"))
|
||||
g.GET("/api/conversations/team", auth(handleGetTeamConversations, "conversation:team"))
|
||||
g.GET("/api/conversations/assigned", auth(handleGetAssignedConversations, "conversation:assigned"))
|
||||
|
||||
g.GET("/api/conversations/{uuid}", auth(handleGetConversation))
|
||||
g.GET("/api/conversations/{uuid}/participants", auth(handleGetConversationParticipants))
|
||||
g.PUT("/api/conversations/{uuid}/last-seen", auth(handleUpdateAssigneeLastSeen))
|
||||
@@ -30,8 +29,6 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
|
||||
g.PUT("/api/conversations/{uuid}/priority", auth(handleUpdatePriority, "conversation:edit_priority"))
|
||||
g.PUT("/api/conversations/{uuid}/status", auth(handleUpdateStatus, "conversation:edit_status"))
|
||||
g.POST("/api/conversations/{uuid}/tags", auth(handleAddConversationTags))
|
||||
|
||||
// Message.
|
||||
g.GET("/api/conversations/{uuid}/messages", auth(handleGetMessages))
|
||||
g.GET("/api/message/{uuid}/retry", auth(handleRetryMessage))
|
||||
g.GET("/api/message/{uuid}", auth(handleGetMessage))
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
? (() => {
|
||||
console.log(' ->', conversationStore.conversation.data.assigned_user_id , 'agents ', agents)
|
||||
const agent = agents.find(agent => agent.id === conversationStore.conversation.data.assigned_user_id);
|
||||
console.log('Foud agent ', agent)
|
||||
return agent ? `${agent.first_name} ${agent.last_name}` : "Select agent...";
|
||||
})()
|
||||
: "Select agent..."
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<template>
|
||||
<div ref="threadEl" class="overflow-y-scroll">
|
||||
<div class="text-center mt-3" v-if="conversationStore.messages.hasMore">
|
||||
<Button variant="secondary" @click="conversationStore.fetchNextMessages">Fetch more</Button>
|
||||
<div class="text-center mt-3" v-if="conversationStore.messages.hasMore && !conversationStore.messages.loading">
|
||||
<Button variant="ghost" @click="conversationStore.fetchNextMessages">
|
||||
<RefreshCw size="17" class="mr-2" />
|
||||
Load more
|
||||
</Button>
|
||||
</div>
|
||||
<div v-for="message in conversationStore.sortedMessages" :key="message.uuid" :class="message.type === 'activity' ? 'm-4' : 'm-6'">
|
||||
<div v-for="message in conversationStore.sortedMessages" :key="message.uuid"
|
||||
:class="message.type === 'activity' ? 'm-4' : 'm-6'">
|
||||
<div v-if="!message.private">
|
||||
<ContactMessageBubble :message="message" v-if="message.type === 'incoming'" />
|
||||
<AgentMessageBubble :message="message" v-if="message.type === 'outgoing'" />
|
||||
@@ -19,30 +23,37 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import ContactMessageBubble from "./ContactMessageBubble.vue";
|
||||
import ActivityMessageBubble from "./ActivityMessageBubble.vue";
|
||||
import AgentMessageBubble from "./AgentMessageBubble.vue";
|
||||
import { useConversationStore } from '@/stores/conversation'
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { RefreshCw } from 'lucide-vue-next';
|
||||
|
||||
const conversationStore = useConversationStore()
|
||||
const threadEl = ref(null);
|
||||
|
||||
|
||||
const scrollToBottom = () => {
|
||||
const thread = threadEl.value;
|
||||
if (thread) {
|
||||
thread.scrollTop = thread.scrollHeight;
|
||||
}
|
||||
setTimeout(() => {
|
||||
console.log("Scrolling bottom")
|
||||
const thread = threadEl.value;
|
||||
if (thread) {
|
||||
thread.scrollTop = thread.scrollHeight;
|
||||
}
|
||||
}, 0)
|
||||
};
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 0);
|
||||
scrollToBottom();
|
||||
});
|
||||
|
||||
// On conversation change scroll to the bottom
|
||||
watch(() => conversationStore.conversation.data, () => {
|
||||
scrollToBottom();
|
||||
});
|
||||
|
||||
const isPrivateNote = (message) => {
|
||||
return message.type === "outgoing" && message.private;
|
||||
|
||||
@@ -58,15 +58,14 @@ export const useConversationStore = defineStore('conversation', () => {
|
||||
);
|
||||
})
|
||||
|
||||
// Computed property to sort message by created_at
|
||||
// Computed property to sort messages by created_at
|
||||
const sortedMessages = computed(() => {
|
||||
if (!messages.data) {
|
||||
return [];
|
||||
}
|
||||
return [...messages.data];
|
||||
return [...messages.data].sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
||||
});
|
||||
|
||||
|
||||
const getContactFullName = (uuid) => {
|
||||
const conv = conversations.data.find(conv => conv.uuid === uuid);
|
||||
return conv ? `${conv.first_name} ${conv.last_name}` : '';
|
||||
@@ -84,7 +83,10 @@ export const useConversationStore = defineStore('conversation', () => {
|
||||
try {
|
||||
const resp = await api.getConversation(uuid);
|
||||
conversation.data = resp.data.data
|
||||
// mark this conversation as read.
|
||||
markAsRead(uuid)
|
||||
// reset messages state on new conversation fetch.
|
||||
resetMessages()
|
||||
} catch (error) {
|
||||
conversation.errorMessage = handleHTTPError(error).message;
|
||||
} finally {
|
||||
@@ -131,9 +133,6 @@ export const useConversationStore = defineStore('conversation', () => {
|
||||
|
||||
// Add new messages to the messages data
|
||||
messages.data.unshift(...newMessages);
|
||||
|
||||
// Sort messages immediately after updating the state
|
||||
messages.data.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Could not fetch messages, Please try again.',
|
||||
@@ -355,9 +354,16 @@ export const useConversationStore = defineStore('conversation', () => {
|
||||
conversation.errorMessage = ""
|
||||
|
||||
// Reset messages state
|
||||
resetMessages()
|
||||
}
|
||||
|
||||
function resetMessages () {
|
||||
messages.data = []
|
||||
messages.loading = false
|
||||
messages.page = 1
|
||||
messages.hasMore = true
|
||||
messages.errorMessage = ""
|
||||
seenMessageUUIDs = new Set()
|
||||
}
|
||||
|
||||
return { conversations, conversation, messages, sortedConversations, sortedMessages, conversationUUIDExists, updateConversationProp, addNewConversation, getContactFullName, fetchParticipants, fetchNextMessages, fetchNextConversations, updateMessageProp, updateAssigneeLastSeen, updateMessageList, fetchConversation, fetchConversations, fetchMessages, upsertTags, updateAssignee, updatePriority, updateStatus, updateConversationList, $reset };
|
||||
|
||||
@@ -16,7 +16,6 @@ export function initWS() {
|
||||
|
||||
// Connection opened event
|
||||
socket.addEventListener('open', function () {
|
||||
console.log('WebSocket connection established')
|
||||
reconnectInterval = 1000 // Reset the reconnection interval
|
||||
if (reconnectTimeout) {
|
||||
clearTimeout(reconnectTimeout) // Clear any existing reconnection timeout
|
||||
@@ -26,7 +25,6 @@ export function initWS() {
|
||||
|
||||
// Listen for messages from the server
|
||||
socket.addEventListener('message', function (e) {
|
||||
console.log('Message from server:', e.data)
|
||||
if (e.data) {
|
||||
let event = JSON.parse(e.data)
|
||||
// TODO: move event type to consts.
|
||||
@@ -57,7 +55,6 @@ export function initWS() {
|
||||
|
||||
// Handle the connection close event
|
||||
socket.addEventListener('close', function (event) {
|
||||
console.log('WebSocket connection closed:', event)
|
||||
if (!manualClose) {
|
||||
reconnect()
|
||||
}
|
||||
|
||||
@@ -138,14 +138,14 @@ func New(
|
||||
|
||||
type queries struct {
|
||||
// Conversation queries.
|
||||
GetInReplyTo *sqlx.Stmt `query:"get-in-reply-to"`
|
||||
GetLatestReceivedMessageSourceID *sqlx.Stmt `query:"get-latest-received-message-source-id"`
|
||||
GetToAddress *sqlx.Stmt `query:"get-to-address"`
|
||||
GetConversationID *sqlx.Stmt `query:"get-conversation-id"`
|
||||
GetConversationUUID *sqlx.Stmt `query:"get-conversation-uuid"`
|
||||
GetConversation *sqlx.Stmt `query:"get-conversation"`
|
||||
GetRecentConversations *sqlx.Stmt `query:"get-recent-conversations"`
|
||||
GetUnassignedConversations *sqlx.Stmt `query:"get-unassigned-conversations"`
|
||||
GetConversations string `query:"get-conversations-conversations"`
|
||||
GetConversations string `query:"get-conversations"`
|
||||
GetConversationsUUIDs string `query:"get-conversations-uuids"`
|
||||
GetConversationParticipants *sqlx.Stmt `query:"get-conversation-participants"`
|
||||
GetAssignedConversations *sqlx.Stmt `query:"get-assigned-conversations"`
|
||||
@@ -163,7 +163,7 @@ type queries struct {
|
||||
AddConversationTag *sqlx.Stmt `query:"add-conversation-tag"`
|
||||
DeleteConversationTags *sqlx.Stmt `query:"delete-conversation-tags"`
|
||||
|
||||
// Message queries
|
||||
// Message queries.
|
||||
GetMessage *sqlx.Stmt `query:"get-message"`
|
||||
GetMessages string `query:"get-messages"`
|
||||
GetPendingMessages *sqlx.Stmt `query:"get-pending-messages"`
|
||||
@@ -247,39 +247,6 @@ func (c *Manager) AddConversationParticipant(userID int, conversationUUID string
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateConversationMeta updates the metadata of a conversation.
|
||||
func (c *Manager) UpdateConversationMeta(conversationID int, conversationUUID string, meta map[string]string) error {
|
||||
metaJSON, err := json.Marshal(meta)
|
||||
if err != nil {
|
||||
c.lo.Error("error marshalling meta", "meta", meta, "error", err)
|
||||
return err
|
||||
}
|
||||
if _, err := c.q.UpdateConversationMeta.Exec(conversationID, conversationUUID, metaJSON); err != nil {
|
||||
c.lo.Error("error updating conversation meta", "error", "error")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateConversationLastMessage updates the last message details in the conversation meta.
|
||||
func (c *Manager) UpdateConversationLastMessage(conversationID int, conversationUUID, lastMessage string, lastMessageAt time.Time) error {
|
||||
return c.UpdateConversationMeta(conversationID, conversationUUID, map[string]string{
|
||||
"last_message": lastMessage,
|
||||
"last_message_at": lastMessageAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateConversationFirstReplyAt updates the first reply timestamp for a conversation.
|
||||
func (c *Manager) UpdateConversationFirstReplyAt(conversationUUID string, conversationID int, at time.Time) error {
|
||||
if _, err := c.q.UpdateConversationFirstReplyAt.Exec(conversationID, at); err != nil {
|
||||
c.lo.Error("error updating conversation first reply at", "error", err)
|
||||
return err
|
||||
}
|
||||
// Send ws update.
|
||||
c.wsHub.BroadcastConversationPropertyUpdate(conversationUUID, "first_reply_at", time.Now().Format(time.RFC3339))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUnassignedConversations retrieves unassigned conversations.
|
||||
func (c *Manager) GetUnassignedConversations() ([]models.Conversation, error) {
|
||||
var conv []models.Conversation
|
||||
@@ -387,6 +354,39 @@ func (c *Manager) GetConversationUUIDs(userID, page, pageSize int, typ, filter s
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// UpdateConversationMeta updates the metadata of a conversation.
|
||||
func (c *Manager) UpdateConversationMeta(conversationID int, conversationUUID string, meta map[string]string) error {
|
||||
metaJSON, err := json.Marshal(meta)
|
||||
if err != nil {
|
||||
c.lo.Error("error marshalling meta", "meta", meta, "error", err)
|
||||
return err
|
||||
}
|
||||
if _, err := c.q.UpdateConversationMeta.Exec(conversationID, conversationUUID, metaJSON); err != nil {
|
||||
c.lo.Error("error updating conversation meta", "error", "error")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateConversationLastMessage updates the last message details in the conversation meta.
|
||||
func (c *Manager) UpdateConversationLastMessage(conversationID int, conversationUUID, lastMessage string, lastMessageAt time.Time) error {
|
||||
return c.UpdateConversationMeta(conversationID, conversationUUID, map[string]string{
|
||||
"last_message": lastMessage,
|
||||
"last_message_at": lastMessageAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateConversationFirstReplyAt updates the first reply timestamp for a conversation.
|
||||
func (c *Manager) UpdateConversationFirstReplyAt(conversationUUID string, conversationID int, at time.Time) error {
|
||||
if _, err := c.q.UpdateConversationFirstReplyAt.Exec(conversationID, at); err != nil {
|
||||
c.lo.Error("error updating conversation first reply at", "error", err)
|
||||
return err
|
||||
}
|
||||
// Broadcast update to all subscribers.
|
||||
c.wsHub.BroadcastConversationPropertyUpdate(conversationUUID, "first_reply_at", at.Format(time.RFC3339))
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateConversationUserAssignee sets the assignee of a conversation to a specifc user.
|
||||
func (c *Manager) UpdateConversationUserAssignee(uuid string, assigneeID int, actor umodels.User) error {
|
||||
if err := c.UpdateAssignee(uuid, assigneeID, models.AssigneeTypeUser); err != nil {
|
||||
@@ -529,7 +529,7 @@ func (c *Manager) generateConversationsListQuery(userID int, baseQuery, typ, ord
|
||||
case models.AllConversations:
|
||||
// No conditions.
|
||||
default:
|
||||
return "", nil, errors.New("invalid type of conversation")
|
||||
return "", nil, fmt.Errorf("invalid conversation type %s", typ)
|
||||
}
|
||||
|
||||
if filterClause, ok := models.ValidFilters[filter]; ok {
|
||||
@@ -571,10 +571,10 @@ func (m *Manager) GetToAddress(conversationID int, channel string) ([]string, er
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// GetInReplyTo retrieves the `In-Reply-To` for given conversation.
|
||||
func (m *Manager) GetInReplyTo(conversationID int) (string, error) {
|
||||
// GetLatestReceivedMessageSourceID returns the last received message source ID.
|
||||
func (m *Manager) GetLatestReceivedMessageSourceID(conversationID int) (string, error) {
|
||||
var out string
|
||||
if err := m.q.GetInReplyTo.Get(&out, conversationID); err != nil {
|
||||
if err := m.q.GetLatestReceivedMessageSourceID.Get(&out, conversationID); err != nil {
|
||||
m.lo.Error("error fetching `in reply to`", "error", err, "conversation_id", conversationID)
|
||||
return out, err
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ func (m *Manager) MessageDispatchWorker(ctx context.Context) {
|
||||
// Get from, to addresses and inReplyTo.
|
||||
message.From = inbox.FromAddress()
|
||||
message.To, _ = m.GetToAddress(message.ConversationID, inbox.Channel())
|
||||
message.InReplyTo, _ = m.GetInReplyTo(message.ConversationID)
|
||||
message.InReplyTo, _ = m.GetLatestReceivedMessageSourceID(message.ConversationID)
|
||||
|
||||
// Send.
|
||||
err = inbox.Send(message)
|
||||
@@ -182,6 +182,7 @@ func (m *Manager) GetConversationMessages(conversationUUID string, page, pageSiz
|
||||
messages = make([]models.Message, 0)
|
||||
qArgs []interface{}
|
||||
)
|
||||
|
||||
qArgs = append(qArgs, conversationUUID)
|
||||
query, qArgs, err := m.generateMessagesQuery(m.q.GetMessages, qArgs, page, pageSize)
|
||||
if err != nil {
|
||||
@@ -190,7 +191,7 @@ func (m *Manager) GetConversationMessages(conversationUUID string, page, pageSiz
|
||||
}
|
||||
|
||||
tx, err := m.db.BeginTxx(context.Background(), nil)
|
||||
defer tx.Commit()
|
||||
defer tx.Rollback()
|
||||
if err != nil {
|
||||
m.lo.Error("error preparing get messages query", "error", err)
|
||||
return messages, envelope.NewError(envelope.GeneralError, "Error fetching messages", nil)
|
||||
|
||||
@@ -228,7 +228,7 @@ WHERE conversation_id = (
|
||||
-- name: get-to-address
|
||||
SELECT cm.source_id from conversations c inner join contact_methods cm on cm.contact_id = c.contact_id where c.id = $1 and cm.source = $2;
|
||||
|
||||
-- name: get-in-reply-to
|
||||
-- name: get-latest-received-message-source-id
|
||||
SELECT source_id
|
||||
FROM messages
|
||||
WHERE conversation_id = $1 and status = 'received'
|
||||
|
||||
+2
-2
@@ -110,7 +110,7 @@ func (h *Hub) PushMessage(msg PushMessage) {
|
||||
}
|
||||
|
||||
// BroadcastNewConversationMessage broadcasts a new conversation message.
|
||||
func (h *Hub) BroadcastNewConversationMessage(conversationUUID, trimmedMessage, messageUUID, lastMessageAt string, private bool) {
|
||||
func (h *Hub) BroadcastNewConversationMessage(conversationUUID, content, messageUUID, lastMessageAt string, private bool) {
|
||||
userIDs, ok := h.conversationSubs[conversationUUID]
|
||||
if !ok || len(userIDs) == 0 {
|
||||
return
|
||||
@@ -120,7 +120,7 @@ func (h *Hub) BroadcastNewConversationMessage(conversationUUID, trimmedMessage,
|
||||
Type: models.MessageTypeNewMessage,
|
||||
Data: map[string]interface{}{
|
||||
"conversation_uuid": conversationUUID,
|
||||
"last_message": trimmedMessage,
|
||||
"last_message": content,
|
||||
"uuid": messageUUID,
|
||||
"last_message_at": lastMessageAt,
|
||||
"private": private,
|
||||
|
||||
Reference in New Issue
Block a user