diff --git a/cmd/conversation.go b/cmd/conversation.go index 4436a7be..fe9cba19 100644 --- a/cmd/conversation.go +++ b/cmd/conversation.go @@ -11,7 +11,7 @@ import ( "github.com/zerodha/fastglue" ) -// handleGetAllConversations retrieves all conversations with pagination, sorting, and filtering. +// handleGetAllConversations retrieves all conversations. func handleGetAllConversations(r *fastglue.Request) error { var ( app = r.Context.(*App) @@ -20,12 +20,22 @@ func handleGetAllConversations(r *fastglue.Request) error { page, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page"))) pageSize, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page_size"))) filters = string(r.RequestCtx.QueryArgs().Peek("filters")) + total = 0 ) - c, err := app.conversation.GetAllConversationsList(order, orderBy, filters, page, pageSize) + conversations, pageSize, err := app.conversation.GetAllConversationsList(order, orderBy, filters, page, pageSize) if err != nil { return sendErrorEnvelope(r, err) } - return r.SendEnvelope(c) + if len(conversations) > 0 { + total = conversations[0].Total + } + return r.SendEnvelope(envelope.PageResults{ + Results: conversations, + Total: total, + PerPage: pageSize, + TotalPages: total / pageSize, + Page: page, + }) } // handleGetAssignedConversations retrieves conversations assigned to the current user. @@ -38,12 +48,22 @@ func handleGetAssignedConversations(r *fastglue.Request) error { page, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page"))) pageSize, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page_size"))) filters = string(r.RequestCtx.QueryArgs().Peek("filters")) + total = 0 ) - c, err := app.conversation.GetAssignedConversationsList(user.ID, order, orderBy, filters, page, pageSize) + conversations, pageSize, err := app.conversation.GetAssignedConversationsList(user.ID, order, orderBy, filters, page, pageSize) if err != nil { return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, err.Error(), nil, "") } - return r.SendEnvelope(c) + if len(conversations) > 0 { + total = conversations[0].Total + } + return r.SendEnvelope(envelope.PageResults{ + Results: conversations, + Total: total, + PerPage: pageSize, + TotalPages: total / pageSize, + Page: page, + }) } // handleGetUnassignedConversations retrieves unassigned conversations. @@ -56,12 +76,22 @@ func handleGetUnassignedConversations(r *fastglue.Request) error { page, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page"))) pageSize, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page_size"))) filters = string(r.RequestCtx.QueryArgs().Peek("filters")) + total = 0 ) - c, err := app.conversation.GetUnassignedConversationsList(user.ID, order, orderBy, filters, page, pageSize) + conversations, pageSize, err := app.conversation.GetUnassignedConversationsList(user.ID, order, orderBy, filters, page, pageSize) if err != nil { return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, err.Error(), nil, "") } - return r.SendEnvelope(c) + if len(conversations) > 0 { + total = conversations[0].Total + } + return r.SendEnvelope(envelope.PageResults{ + Results: conversations, + Total: total, + PerPage: pageSize, + TotalPages: total / pageSize, + Page: page, + }) } // handleGetConversation retrieves a single conversation by UUID with permission checks. diff --git a/cmd/messages.go b/cmd/messages.go index c7d58fa0..616136f3 100644 --- a/cmd/messages.go +++ b/cmd/messages.go @@ -5,6 +5,7 @@ import ( "github.com/abhinavxd/artemis/internal/conversation" cmodels "github.com/abhinavxd/artemis/internal/conversation/models" + "github.com/abhinavxd/artemis/internal/envelope" medModels "github.com/abhinavxd/artemis/internal/media/models" umodels "github.com/abhinavxd/artemis/internal/user/models" "github.com/valyala/fasthttp" @@ -24,6 +25,7 @@ func handleGetMessages(r *fastglue.Request) error { user = r.RequestCtx.UserValue("user").(umodels.User) page, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page"))) pageSize, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page_size"))) + total = 0 ) // Check permission @@ -32,17 +34,24 @@ func handleGetMessages(r *fastglue.Request) error { return sendErrorEnvelope(r, err) } - messages, err := app.conversation.GetConversationMessages(uuid, page, pageSize) + messages, pageSize, err := app.conversation.GetConversationMessages(uuid, page, pageSize) if err != nil { return sendErrorEnvelope(r, err) } for i := range messages { + total = messages[i].Total for j := range messages[i].Attachments { messages[i].Attachments[j].URL = app.media.GetURL(messages[i].Attachments[j].UUID) } } - return r.SendEnvelope(messages) + return r.SendEnvelope(envelope.PageResults{ + Total: total, + Results: messages, + Page: page, + PerPage: pageSize, + TotalPages: total / pageSize, + }) } func handleGetMessage(r *fastglue.Request) error { diff --git a/frontend/src/components/message/ContactMessageBubble.vue b/frontend/src/components/message/ContactMessageBubble.vue index dc63328f..95357259 100644 --- a/frontend/src/components/message/ContactMessageBubble.vue +++ b/frontend/src/components/message/ContactMessageBubble.vue @@ -66,7 +66,7 @@ const nonInlineAttachments = computed(() => ) const getFullName = computed(() => { - return convStore.current.contact.first_name + ' ' + convStore.current.last_name + return convStore.current.contact.first_name + ' ' + convStore.current.contact.last_name }) const avatarFallback = computed(() => { diff --git a/frontend/src/stores/conversation.js b/frontend/src/stores/conversation.js index 3fe03aec..5e4dc819 100644 --- a/frontend/src/stores/conversation.js +++ b/frontend/src/stores/conversation.js @@ -146,29 +146,19 @@ export const useConversationStore = defineStore('conversation', () => { messages.loading = true try { const response = await api.getConversationMessages(uuid, messages.page) - const fetchedMessages = response.data?.data || [] - + const result = response.data?.data || {} + const results = result.results || [] // Filter out messages already seen. - const newMessages = fetchedMessages.filter((message) => { + const newMessages = results.filter((message) => { if (!seenMessageUUIDs.has(message.uuid)) { seenMessageUUIDs.add(message.uuid) return true } return false }) - - if (newMessages.length === 0 && messages.page === 1) { - messages.data = [] - return - } - - if (newMessages.length === 0 && messages.page > 1) { - messages.hasMore = false - } - - // Add new messages to the messages state. + if (newMessages.length === 0 && messages.page === 1) messages.data = [] + if (result.total_pages <= messages.page) messages.hasMore = false messages.data.unshift(...newMessages) - } catch (error) { emitter.emit(EMITTER_EVENTS.SHOW_TOAST, { title: 'Something went wrong', @@ -250,22 +240,17 @@ export const useConversationStore = defineStore('conversation', () => { default: return } - - if (response?.data?.data) { - const newConversations = response.data.data.filter((conversation) => { - if (!seenConversationUUIDs.has(conversation.uuid)) { - seenConversationUUIDs.set(conversation.uuid, true) - return true - } - return false - }) - - if (!conversations.data) conversations.data = [] - if (newConversations.length === 0) conversations.hasMore = false - conversations.data.push(...newConversations) - } else { - conversations.hasMore = false - } + const apiResponse = response.data.data + const newConversations = apiResponse.results.filter((conversation) => { + if (!seenConversationUUIDs.has(conversation.uuid)) { + seenConversationUUIDs.set(conversation.uuid, true) + return true + } + return false + }) + if (apiResponse.total_pages <= conversations.page) conversations.hasMore = false + if (!conversations.data) conversations.data = [] + conversations.data.push(...newConversations) } catch (error) { conversations.errorMessage = handleHTTPError(error).message emitter.emit(EMITTER_EVENTS.SHOW_TOAST, { diff --git a/internal/conversation/conversation.go b/internal/conversation/conversation.go index c9bc3182..12c10838 100644 --- a/internal/conversation/conversation.go +++ b/internal/conversation/conversation.go @@ -289,59 +289,48 @@ func (c *Manager) GetConversationUUID(id int) (string, error) { } // GetAllConversationsList retrieves all conversations with optional filtering, ordering, and pagination. -func (c *Manager) GetAllConversationsList(order, orderBy, filters string, page, pageSize int) ([]models.Conversation, error) { +func (c *Manager) GetAllConversationsList(order, orderBy, filters string, page, pageSize int) ([]models.Conversation, int, error) { return c.GetConversations(0, models.AllConversations, order, orderBy, filters, page, pageSize) } // GetAssignedConversationsList retrieves conversations assigned to a specific user with optional filtering, ordering, and pagination. -func (c *Manager) GetAssignedConversationsList(userID int, order, orderBy, filters string, page, pageSize int) ([]models.Conversation, error) { +func (c *Manager) GetAssignedConversationsList(userID int, order, orderBy, filters string, page, pageSize int) ([]models.Conversation, int, error) { return c.GetConversations(userID, models.AssignedConversations, order, orderBy, filters, page, pageSize) } // GetUnassignedConversationsList retrieves conversations assigned to a team the user is part of with optional filtering, ordering, and pagination. -func (c *Manager) GetUnassignedConversationsList(userID int, order, orderBy, filters string, page, pageSize int) ([]models.Conversation, error) { +func (c *Manager) GetUnassignedConversationsList(userID int, order, orderBy, filters string, page, pageSize int) ([]models.Conversation, int, error) { return c.GetConversations(userID, models.UnassignedConversations, order, orderBy, filters, page, pageSize) } // GetConversations retrieves conversations list based on user ID, type, and optional filtering, ordering, and pagination. -func (c *Manager) GetConversations(userID int, listType, order, orderBy, filters string, page, pageSize int) ([]models.Conversation, error) { +func (c *Manager) GetConversations(userID int, listType, order, orderBy, filters string, page, pageSize int) ([]models.Conversation, int, error) { var conversations = make([]models.Conversation, 0) - if orderBy == "" { - orderBy = "last_message_at" - } - if order == "" { - order = "DESC" - } - - if filters == "" { - filters = "[]" - } - - query, qArgs, err := c.makeConversationsListQuery(userID, c.q.GetConversations, listType, order, orderBy, page, pageSize, filters) + query, pageSize, qArgs, err := c.makeConversationsListQuery(userID, c.q.GetConversations, listType, order, orderBy, page, pageSize, filters) if err != nil { - return conversations, envelope.NewError(envelope.GeneralError, c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.entities.conversations}"), nil) + return conversations, pageSize, envelope.NewError(envelope.GeneralError, c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.entities.conversations}"), nil) } tx, err := c.db.BeginTxx(context.Background(), nil) defer tx.Rollback() if err != nil { c.lo.Error("error preparing get conversations query", "error", err) - return conversations, envelope.NewError(envelope.GeneralError, c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.entities.conversations}"), nil) + return conversations, pageSize, envelope.NewError(envelope.GeneralError, c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.entities.conversations}"), nil) } if err := tx.Select(&conversations, query, qArgs...); err != nil { c.lo.Error("error fetching conversations", "error", err) - return conversations, envelope.NewError(envelope.GeneralError, c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.entities.conversations}"), nil) + return conversations, pageSize, envelope.NewError(envelope.GeneralError, c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.entities.conversations}"), nil) } - return conversations, nil + return conversations, pageSize, nil } // GetConversationsListUUIDs retrieves the UUIDs of conversations list. func (c *Manager) GetConversationsListUUIDs(userID, page, pageSize int, typ string) ([]string, error) { var ids = make([]string, 0) - query, qArgs, err := c.makeConversationsListQuery(userID, c.q.GetConversationsListUUIDs, typ, "", "", page, pageSize, "") + query, _, qArgs, err := c.makeConversationsListQuery(userID, c.q.GetConversationsListUUIDs, typ, "", "", page, pageSize, "") if err != nil { c.lo.Error("error generating conversations query", "error", err) return ids, err @@ -560,12 +549,27 @@ func (t *Manager) UpsertConversationTags(uuid string, tagIDs []int) error { } // makeConversationsListQuery prepares a SQL query string for conversations list -func (c *Manager) makeConversationsListQuery(userID int, baseQuery, listType, order, orderBy string, page, pageSize int, filtersJSON string) (string, []interface{}, error) { +func (c *Manager) makeConversationsListQuery(userID int, baseQuery, listType, order, orderBy string, page, pageSize int, filtersJSON string) (string, int, []interface{}, error) { var qArgs []interface{} + if orderBy == "" { + orderBy = "last_message_at" + } + if order == "" { + order = "DESC" + } + if filtersJSON == "" { + filtersJSON = "[]" + } if pageSize > conversationsListMaxPageSize { pageSize = conversationsListMaxPageSize } + if pageSize < 1 { + pageSize = 10 + } + if page < 1 { + page = 1 + } // Set condition based on the list type. switch listType { @@ -578,7 +582,7 @@ func (c *Manager) makeConversationsListQuery(userID int, baseQuery, listType, or case models.AllConversations: baseQuery = fmt.Sprintf(baseQuery, "") default: - return "", nil, fmt.Errorf("invalid conversation type %s", listType) + return "", pageSize, nil, fmt.Errorf("invalid conversation type %s", listType) } query, qArgs, err := dbutil.PaginateAndFilterQuery(baseQuery, qArgs, dbutil.PaginationOptions{ @@ -591,10 +595,10 @@ func (c *Manager) makeConversationsListQuery(userID int, baseQuery, listType, or }) if err != nil { c.lo.Error("error preparing query", "error", err) - return "", nil, err + return "", pageSize, nil, err } - return query, qArgs, err + return query, pageSize, qArgs, err } // GetToAddress retrieves the recipient addresses for a conversation. diff --git a/internal/conversation/message.go b/internal/conversation/message.go index fe613bee..e8b31f15 100644 --- a/internal/conversation/message.go +++ b/internal/conversation/message.go @@ -212,32 +212,32 @@ func (m *Manager) RenderContentInTemplate(channel string, message *models.Messag } // GetConversationMessages retrieves messages for a specific conversation. -func (m *Manager) GetConversationMessages(conversationUUID string, page, pageSize int) ([]models.Message, error) { +func (m *Manager) GetConversationMessages(conversationUUID string, page, pageSize int) ([]models.Message, int, error) { var ( messages = make([]models.Message, 0) qArgs []interface{} ) qArgs = append(qArgs, conversationUUID) - query, qArgs, err := m.generateMessagesQuery(m.q.GetMessages, qArgs, page, pageSize) + query, pageSize, qArgs, err := m.generateMessagesQuery(m.q.GetMessages, qArgs, page, pageSize) if err != nil { m.lo.Error("error generating messages query", "error", err) - return messages, envelope.NewError(envelope.GeneralError, "Error fetching messages", nil) + return messages, pageSize, envelope.NewError(envelope.GeneralError, "Error fetching messages", nil) } tx, err := m.db.BeginTxx(context.Background(), nil) 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) + return messages, pageSize, envelope.NewError(envelope.GeneralError, "Error fetching messages", nil) } if err := tx.Select(&messages, query, qArgs...); err != nil { m.lo.Error("error fetching conversations", "error", err) - return messages, envelope.NewError(envelope.GeneralError, "Error fetching messages", nil) + return messages, pageSize, envelope.NewError(envelope.GeneralError, "Error fetching messages", nil) } - return messages, nil + return messages, pageSize, nil } // GetMessage retrieves a message by UUID. @@ -489,14 +489,16 @@ func (m *Manager) GetConversationByMessageID(id int) (models.Conversation, error } // generateMessagesQuery generates the SQL query for fetching messages in a conversation. -func (c *Manager) generateMessagesQuery(baseQuery string, qArgs []interface{}, page, pageSize int) (string, []interface{}, error) { - // Set default values for page and page size if they are invalid +func (c *Manager) generateMessagesQuery(baseQuery string, qArgs []interface{}, page, pageSize int) (string, int, []interface{}, error) { if page <= 0 { page = 1 } - if pageSize <= 0 || pageSize > maxMessagesPerPage { + if pageSize > maxMessagesPerPage { pageSize = maxMessagesPerPage } + if pageSize <= 0 { + pageSize = 10 + } // Calculate the offset offset := (page - 1) * pageSize @@ -506,7 +508,7 @@ func (c *Manager) generateMessagesQuery(baseQuery string, qArgs []interface{}, p // Include LIMIT and OFFSET in the SQL query sqlQuery := fmt.Sprintf(baseQuery, fmt.Sprintf("LIMIT $%d OFFSET $%d", len(qArgs)-1, len(qArgs))) - return sqlQuery, qArgs, nil + return sqlQuery, pageSize, qArgs, nil } // uploadMessageAttachments uploads attachments for a message. diff --git a/internal/conversation/models/models.go b/internal/conversation/models/models.go index 2d3a852b..5b3ab191 100644 --- a/internal/conversation/models/models.go +++ b/internal/conversation/models/models.go @@ -44,6 +44,7 @@ type Conversation struct { LastMessageAt null.Time `db:"last_message_at" json:"last_message_at"` LastMessage string `db:"last_message" json:"last_message"` Contact cmodels.Contact `db:"contact" json:"contact"` + Total int `db:"total" json:"-"` } type ConversationParticipant struct { @@ -93,6 +94,7 @@ type Message struct { InReplyTo string `json:"-"` Headers textproto.MIMEHeader `json:"-"` Media []mmodels.Media `db:"-" json:"-"` + Total int `db:"total" json:"-"` } // IncomingMessage links a message with the contact information and inbox id. diff --git a/internal/conversation/queries.sql b/internal/conversation/queries.sql index e44cb770..0042460a 100644 --- a/internal/conversation/queries.sql +++ b/internal/conversation/queries.sql @@ -9,6 +9,7 @@ RETURNING id, uuid; -- name: get-conversations SELECT + COUNT(*) OVER() AS total, conversations.updated_at, conversations.uuid, conversations.assignee_last_seen_at, @@ -368,6 +369,7 @@ attachments AS ( GROUP BY message_id ) SELECT + COUNT(*) OVER() AS total, m.created_at, m.updated_at, m.status, diff --git a/internal/dbutil/paginate.go b/internal/dbutil/paginate.go index a965d063..c818648a 100644 --- a/internal/dbutil/paginate.go +++ b/internal/dbutil/paginate.go @@ -47,14 +47,6 @@ func PaginateAndFilterQuery(baseQuery string, existingArgs []interface{}, opts P return "", nil, err } - // Validate and set default values for pagination - if opts.Page <= 0 { - opts.Page = 1 - } - if opts.PageSize <= 0 { - opts.PageSize = 10 // Default page size - } - // Calculate offset offset := (opts.Page - 1) * opts.PageSize diff --git a/internal/envelope/envelope.go b/internal/envelope/envelope.go index c82a9a49..c02fb4eb 100644 --- a/internal/envelope/envelope.go +++ b/internal/envelope/envelope.go @@ -32,6 +32,15 @@ func (e Error) Error() string { return e.Message } +// PageResults is a generic struct for paginated results. +type PageResults struct { + Results interface{} `json:"results"` + Total int `json:"total"` + PerPage int `json:"per_page"` + TotalPages int `json:"total_pages"` + Page int `json:"page"` +} + // NewError creates and returns a new instance of Error with custom error metadata. func NewError(etype string, message string, data interface{}) error { err := Error{