mirror of
				https://github.com/abhinavxd/libredesk.git
				synced 2025-11-04 05:53:30 +00:00 
			
		
		
		
	fix: whitespace being trimmed in canned repsonse
- refactor: conversations store. - feat: Show conversation subject on top of conversation messages list - feat: adds shadow to panels - update: schema.sql adds insert statements for priority / status - fix: trim canned response content to 100 chars. - revert: rename team conversations tab to unassigned.
This commit is contained in:
		@@ -17,12 +17,10 @@ func handleGetAllConversations(r *fastglue.Request) error {
 | 
			
		||||
		app         = r.Context.(*App)
 | 
			
		||||
		order       = string(r.RequestCtx.QueryArgs().Peek("order"))
 | 
			
		||||
		orderBy     = string(r.RequestCtx.QueryArgs().Peek("order_by"))
 | 
			
		||||
		filter      = string(r.RequestCtx.QueryArgs().Peek("filter"))
 | 
			
		||||
		page, _     = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page")))
 | 
			
		||||
		pageSize, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page_size")))
 | 
			
		||||
	)
 | 
			
		||||
	// Fetch all conversations
 | 
			
		||||
	c, err := app.conversation.GetAllConversations(order, orderBy, filter, page, pageSize)
 | 
			
		||||
	c, err := app.conversation.GetAllConversationsList(order, orderBy, page, pageSize)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return sendErrorEnvelope(r, err)
 | 
			
		||||
	}
 | 
			
		||||
@@ -36,31 +34,27 @@ func handleGetAssignedConversations(r *fastglue.Request) error {
 | 
			
		||||
		user        = r.RequestCtx.UserValue("user").(umodels.User)
 | 
			
		||||
		order       = string(r.RequestCtx.QueryArgs().Peek("order"))
 | 
			
		||||
		orderBy     = string(r.RequestCtx.QueryArgs().Peek("order_by"))
 | 
			
		||||
		filter      = string(r.RequestCtx.QueryArgs().Peek("filter"))
 | 
			
		||||
		page, _     = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page")))
 | 
			
		||||
		pageSize, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page_size")))
 | 
			
		||||
	)
 | 
			
		||||
	// Fetch conversations assigned to the user
 | 
			
		||||
	c, err := app.conversation.GetAssignedConversations(user.ID, order, orderBy, filter, page, pageSize)
 | 
			
		||||
	c, err := app.conversation.GetAssignedConversationsList(user.ID, order, orderBy, page, pageSize)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, err.Error(), nil, "")
 | 
			
		||||
	}
 | 
			
		||||
	return r.SendEnvelope(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleGetTeamConversations retrieves team-specific conversations.
 | 
			
		||||
func handleGetTeamConversations(r *fastglue.Request) error {
 | 
			
		||||
// handleGetUnassignedConversations retrieves unassigned conversations.
 | 
			
		||||
func handleGetUnassignedConversations(r *fastglue.Request) error {
 | 
			
		||||
	var (
 | 
			
		||||
		app         = r.Context.(*App)
 | 
			
		||||
		user        = r.RequestCtx.UserValue("user").(umodels.User)
 | 
			
		||||
		order       = string(r.RequestCtx.QueryArgs().Peek("order"))
 | 
			
		||||
		orderBy     = string(r.RequestCtx.QueryArgs().Peek("order_by"))
 | 
			
		||||
		filter      = string(r.RequestCtx.QueryArgs().Peek("filter"))
 | 
			
		||||
		page, _     = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page")))
 | 
			
		||||
		pageSize, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page_size")))
 | 
			
		||||
	)
 | 
			
		||||
	// Fetch conversations assigned to the user's team
 | 
			
		||||
	c, err := app.conversation.GetTeamConversations(user.ID, order, orderBy, filter, page, pageSize)
 | 
			
		||||
	c, err := app.conversation.GetUnassignedConversationsList(user.ID, order, orderBy, page, pageSize)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, err.Error(), nil, "")
 | 
			
		||||
	}
 | 
			
		||||
@@ -74,8 +68,6 @@ func handleGetConversation(r *fastglue.Request) error {
 | 
			
		||||
		uuid = r.RequestCtx.UserValue("uuid").(string)
 | 
			
		||||
		user = r.RequestCtx.UserValue("user").(umodels.User)
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// Check if the user has access to the conversation
 | 
			
		||||
	conversation, err := enforceConversationAccess(app, uuid, user)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return sendErrorEnvelope(r, err)
 | 
			
		||||
@@ -91,14 +83,10 @@ func handleUpdateConversationAssigneeLastSeen(r *fastglue.Request) error {
 | 
			
		||||
		uuid = r.RequestCtx.UserValue("uuid").(string)
 | 
			
		||||
		user = r.RequestCtx.UserValue("user").(umodels.User)
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// Check permission
 | 
			
		||||
	_, err := enforceConversationAccess(app, uuid, user)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return sendErrorEnvelope(r, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Update last seen
 | 
			
		||||
	if err = app.conversation.UpdateConversationAssigneeLastSeen(uuid); err != nil {
 | 
			
		||||
		return sendErrorEnvelope(r, err)
 | 
			
		||||
	}
 | 
			
		||||
@@ -112,14 +100,10 @@ func handleGetConversationParticipants(r *fastglue.Request) error {
 | 
			
		||||
		uuid = r.RequestCtx.UserValue("uuid").(string)
 | 
			
		||||
		user = r.RequestCtx.UserValue("user").(umodels.User)
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// Check permission
 | 
			
		||||
	_, err := enforceConversationAccess(app, uuid, user)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return sendErrorEnvelope(r, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Fetch participants
 | 
			
		||||
	p, err := app.conversation.GetConversationParticipants(uuid)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return sendErrorEnvelope(r, err)
 | 
			
		||||
@@ -139,13 +123,11 @@ func handleUpdateConversationUserAssignee(r *fastglue.Request) error {
 | 
			
		||||
		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Invalid assignee `id`.", nil, envelope.InputError)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check permission
 | 
			
		||||
	_, err = enforceConversationAccess(app, uuid, user)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return sendErrorEnvelope(r, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Update the user assignee
 | 
			
		||||
	if err := app.conversation.UpdateConversationUserAssignee(uuid, assigneeID, user); err != nil {
 | 
			
		||||
		return sendErrorEnvelope(r, err)
 | 
			
		||||
	}
 | 
			
		||||
@@ -163,14 +145,10 @@ func handleUpdateTeamAssignee(r *fastglue.Request) error {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Invalid assignee `id`.", nil, envelope.InputError)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check permission
 | 
			
		||||
	_, err = enforceConversationAccess(app, uuid, user)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return sendErrorEnvelope(r, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Update the team assignee
 | 
			
		||||
	if err := app.conversation.UpdateConversationTeamAssignee(uuid, assigneeID, user); err != nil {
 | 
			
		||||
		return sendErrorEnvelope(r, err)
 | 
			
		||||
	}
 | 
			
		||||
@@ -186,14 +164,10 @@ func handleUpdateConversationPriority(r *fastglue.Request) error {
 | 
			
		||||
		uuid     = r.RequestCtx.UserValue("uuid").(string)
 | 
			
		||||
		user     = r.RequestCtx.UserValue("user").(umodels.User)
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// Check permission
 | 
			
		||||
	_, err := enforceConversationAccess(app, uuid, user)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return sendErrorEnvelope(r, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Update priority
 | 
			
		||||
	if err := app.conversation.UpdateConversationPriority(uuid, priority, user); err != nil {
 | 
			
		||||
		return sendErrorEnvelope(r, err)
 | 
			
		||||
	}
 | 
			
		||||
@@ -209,14 +183,10 @@ func handleUpdateConversationStatus(r *fastglue.Request) error {
 | 
			
		||||
		uuid   = r.RequestCtx.UserValue("uuid").(string)
 | 
			
		||||
		user   = r.RequestCtx.UserValue("user").(umodels.User)
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// Check permission
 | 
			
		||||
	_, err := enforceConversationAccess(app, uuid, user)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return sendErrorEnvelope(r, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Update status
 | 
			
		||||
	if err := app.conversation.UpdateConversationStatus(uuid, status, user); err != nil {
 | 
			
		||||
		return sendErrorEnvelope(r, err)
 | 
			
		||||
	}
 | 
			
		||||
@@ -241,13 +211,11 @@ func handleAddConversationTags(r *fastglue.Request) error {
 | 
			
		||||
		return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "error adding tags", nil, "")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check permission
 | 
			
		||||
	_, err = enforceConversationAccess(app, uuid, user)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return sendErrorEnvelope(r, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Upsert tags
 | 
			
		||||
	if err := app.conversation.UpsertConversationTags(uuid, tagIDs); err != nil {
 | 
			
		||||
		return sendErrorEnvelope(r, err)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -40,7 +40,7 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
 | 
			
		||||
 | 
			
		||||
	// Conversation and message.
 | 
			
		||||
	g.GET("/api/conversations/all", perm(handleGetAllConversations, "conversations", "read_all"))
 | 
			
		||||
	g.GET("/api/conversations/team", perm(handleGetTeamConversations, "conversations", "read_team"))
 | 
			
		||||
	g.GET("/api/conversations/unassigned", perm(handleGetUnassignedConversations, "conversations", "read_unassigned"))
 | 
			
		||||
	g.GET("/api/conversations/assigned", perm(handleGetAssignedConversations, "conversations", "read_assigned"))
 | 
			
		||||
 | 
			
		||||
	g.PUT("/api/conversations/{uuid}/assignee/user", perm(handleUpdateConversationUserAssignee, "conversations", "update_user_assignee"))
 | 
			
		||||
 
 | 
			
		||||
@@ -78,6 +78,7 @@ func main() {
 | 
			
		||||
	// Installer.
 | 
			
		||||
	if ko.Bool("install") {
 | 
			
		||||
		install(db, fs)
 | 
			
		||||
		setSystemUserPass(db)
 | 
			
		||||
		os.Exit(0)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <Toaster />
 | 
			
		||||
  <TooltipProvider :delay-duration="200">
 | 
			
		||||
    <div class="bg-background text-foreground">
 | 
			
		||||
    <div>
 | 
			
		||||
      <div v-if="$route.path !== '/'">
 | 
			
		||||
        <ResizablePanelGroup direction="horizontal" auto-save-id="app.vue.resizable.panel">
 | 
			
		||||
          <ResizablePanel id="resize-panel-1" collapsible :default-size="10" :collapsed-size="1" :min-size="7"
 | 
			
		||||
          <ResizablePanel class="shadow shadow-gray-300" id="resize-panel-1" collapsible :default-size="10" :collapsed-size="1" :min-size="7"
 | 
			
		||||
            :max-size="20" :class="cn(isCollapsed && 'min-w-[50px] transition-all duration-200 ease-in-out')"
 | 
			
		||||
            @expand="toggleNav(false)" @collapse="toggleNav(true)">
 | 
			
		||||
            <NavBar :is-collapsed="isCollapsed" :links="navLinks" :bottom-links="bottomLinks" />
 | 
			
		||||
 
 | 
			
		||||
@@ -119,9 +119,9 @@ const updateAssignee = (uuid, assignee_type, data) =>
 | 
			
		||||
const updateConversationStatus = (uuid, data) => http.put(`/api/conversations/${uuid}/status`, data)
 | 
			
		||||
const updateConversationPriority = (uuid, data) => http.put(`/api/conversations/${uuid}/priority`, data)
 | 
			
		||||
const updateAssigneeLastSeen = (uuid) => http.put(`/api/conversations/${uuid}/last-seen`)
 | 
			
		||||
const getMessage = (cuuid, uuid) => http.get(`/api/conversations/${cuuid}/messages/${uuid}`)
 | 
			
		||||
const getConversationMessage = (cuuid, uuid) => http.get(`/api/conversations/${cuuid}/messages/${uuid}`)
 | 
			
		||||
const retryMessage = (cuuid, uuid) => http.put(`/api/conversations/${cuuid}/messages/${uuid}/retry`)
 | 
			
		||||
const getMessages = (uuid, page) =>
 | 
			
		||||
const getConversationMessages = (uuid, page) =>
 | 
			
		||||
  http.get(`/api/conversations/${uuid}/messages`, {
 | 
			
		||||
    params: { page: page }
 | 
			
		||||
  })
 | 
			
		||||
@@ -137,12 +137,12 @@ const getCannedResponses = () => http.get('/api/canned-responses')
 | 
			
		||||
const createCannedResponse = (data) => http.post('/api/canned-responses', data)
 | 
			
		||||
const updateCannedResponse = (id, data) => http.put(`/api/canned-responses/${id}`, data)
 | 
			
		||||
const deleteCannedResponse = (id) => http.delete(`/api/canned-responses/${id}`)
 | 
			
		||||
const getAssignedConversations = (page, filter) =>
 | 
			
		||||
  http.get(`/api/conversations/assigned?page=${page}&filter=${filter}`)
 | 
			
		||||
const getTeamConversations = (page, filter) =>
 | 
			
		||||
  http.get(`/api/conversations/team?page=${page}&filter=${filter}`)
 | 
			
		||||
const getAllConversations = (page, filter) =>
 | 
			
		||||
  http.get(`/api/conversations/all?page=${page}&filter=${filter}`)
 | 
			
		||||
const getAssignedConversations = (page) =>
 | 
			
		||||
  http.get(`/api/conversations/assigned?page=${page}`)
 | 
			
		||||
const getUnassignedConversations = (page) =>
 | 
			
		||||
  http.get(`/api/conversations/unassigned?page=${page}`)
 | 
			
		||||
const getAllConversations = (page) =>
 | 
			
		||||
  http.get(`/api/conversations/all?page=${page}`)
 | 
			
		||||
const uploadMedia = (data) =>
 | 
			
		||||
  http.post('/api/media', data, {
 | 
			
		||||
    headers: {
 | 
			
		||||
@@ -204,15 +204,15 @@ export default {
 | 
			
		||||
  getAutomationRule,
 | 
			
		||||
  getAutomationRules,
 | 
			
		||||
  getAssignedConversations,
 | 
			
		||||
  getTeamConversations,
 | 
			
		||||
  getUnassignedConversations,
 | 
			
		||||
  getAllConversations,
 | 
			
		||||
  getGlobalDashboardCharts,
 | 
			
		||||
  getGlobalDashboardCounts,
 | 
			
		||||
  getUserDashboardCounts,
 | 
			
		||||
  getUserDashboardCharts,
 | 
			
		||||
  getConversationParticipants,
 | 
			
		||||
  getMessage,
 | 
			
		||||
  getMessages,
 | 
			
		||||
  getConversationMessage,
 | 
			
		||||
  getConversationMessages,
 | 
			
		||||
  getCurrentUser,
 | 
			
		||||
  getCannedResponses,
 | 
			
		||||
  createCannedResponse,
 | 
			
		||||
 
 | 
			
		||||
@@ -166,10 +166,6 @@ body {
 | 
			
		||||
  @apply text-muted-foreground text-sm;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-xs-muted {
 | 
			
		||||
  @apply text-muted-foreground text-xs;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.box {
 | 
			
		||||
  @apply border shadow;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,10 @@
 | 
			
		||||
    @click="handleClick"
 | 
			
		||||
  >
 | 
			
		||||
    <div class="flex items-center mb-4">
 | 
			
		||||
      <component :is="icon" size="16" class="mr-2" />
 | 
			
		||||
      <component :is="icon" size="25" class="mr-2" />
 | 
			
		||||
      <p class="text-lg">{{ title }}</p>
 | 
			
		||||
    </div>
 | 
			
		||||
    <p class="text-xs-muted">{{ subTitle }}</p>
 | 
			
		||||
    <p class="text-sm text-muted-foreground">{{ subTitle }}</p>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="flex flex-col space-y-1">
 | 
			
		||||
    <span class="text-2xl">{{ title }}</span>
 | 
			
		||||
    <p class="text-xs-muted">{{ description }}</p>
 | 
			
		||||
    <p class="text-muted-foreground text-lg">{{ description }}</p>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -76,9 +76,9 @@ const permissions = ref([
 | 
			
		||||
  {
 | 
			
		||||
    name: 'Conversation',
 | 
			
		||||
    permissions: [
 | 
			
		||||
      { name: 'conversations:read_all', label: 'View all conversations list' },
 | 
			
		||||
      { name: 'conversations:read_team', label: 'View team conversations list' },
 | 
			
		||||
      { name: 'conversations:read_assigned', label: 'View assigned conversations list' },
 | 
			
		||||
      { name: 'conversations:read_all', label: 'View all conversations' },
 | 
			
		||||
      { name: 'conversations:read_unassigned', label: 'View unassigned conversations' },
 | 
			
		||||
      { name: 'conversations:read_assigned', label: 'View assigned conversations' },
 | 
			
		||||
      { name: 'conversations:read', label: 'View conversation' },
 | 
			
		||||
      { name: 'conversations:update_user_assignee', label: 'Edit assigned user' },
 | 
			
		||||
      { name: 'conversations:update_team_assignee', label: 'Edit assigned team' },
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@
 | 
			
		||||
        >
 | 
			
		||||
          <div class="flex flex-col items-start space-y-1">
 | 
			
		||||
            <span class="text-sm">{{ item.title }}</span>
 | 
			
		||||
            <p class="text-xs-muted break-words whitespace-normal">{{ item.description }}</p>
 | 
			
		||||
            <p class="text-muted-foreground text-xs break-words whitespace-normal">{{ item.description }}</p>
 | 
			
		||||
          </div>
 | 
			
		||||
        </Button>
 | 
			
		||||
      </template>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,26 +2,18 @@
 | 
			
		||||
  <div class="relative" v-if="conversationStore.messages.data">
 | 
			
		||||
 | 
			
		||||
    <!-- Header -->
 | 
			
		||||
    <div class="px-4 border-b h-[47px] flex items-center justify-between">
 | 
			
		||||
    <div class="px-4 border-b h-[47px] flex items-center justify-between shadow shadow-gray-100">
 | 
			
		||||
      <div class="flex items-center space-x-3 text-sm">
 | 
			
		||||
        <div class="font-bold">
 | 
			
		||||
          {{ contactFullName }}
 | 
			
		||||
          {{ conversationStore.current.subject }}
 | 
			
		||||
        </div>
 | 
			
		||||
        <Tooltip>
 | 
			
		||||
          <TooltipTrigger>
 | 
			
		||||
            <Badge :variant="getBadgeVariant">
 | 
			
		||||
              {{ conversationStore.conversation.data.status }}
 | 
			
		||||
            </Badge>
 | 
			
		||||
          </TooltipTrigger>
 | 
			
		||||
          <TooltipContent>
 | 
			
		||||
            <p>Status</p>
 | 
			
		||||
          </TooltipContent>
 | 
			
		||||
        </Tooltip>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div>
 | 
			
		||||
        <DropdownMenu>
 | 
			
		||||
          <DropdownMenuTrigger>
 | 
			
		||||
            <Icon icon="lucide:ellipsis-vertical" class="mt-2 size-6"></Icon>
 | 
			
		||||
            <Badge :variant="getBadgeVariant">
 | 
			
		||||
              {{ conversationStore.current.status }}
 | 
			
		||||
            </Badge>
 | 
			
		||||
          </DropdownMenuTrigger>
 | 
			
		||||
          <DropdownMenuContent>
 | 
			
		||||
            <DropdownMenuItem v-for="status in statuses" :key="status.name" @click="handleUpdateStatus(status.name)">
 | 
			
		||||
@@ -33,8 +25,6 @@
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- Header end -->
 | 
			
		||||
 | 
			
		||||
    <Error class="sticky" :error-message="conversationStore.messages.errorMessage"></Error>
 | 
			
		||||
    
 | 
			
		||||
    <!-- Messages & reply box -->
 | 
			
		||||
    <div class="flex flex-col h-screen">
 | 
			
		||||
      <MessageList class="flex-1" />
 | 
			
		||||
@@ -48,7 +38,6 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
import { computed, ref, onMounted } from 'vue'
 | 
			
		||||
import { useConversationStore } from '@/stores/conversation'
 | 
			
		||||
import { Error } from '@/components/ui/error'
 | 
			
		||||
import { Badge } from '@/components/ui/badge'
 | 
			
		||||
import {
 | 
			
		||||
  DropdownMenu,
 | 
			
		||||
@@ -56,11 +45,9 @@ import {
 | 
			
		||||
  DropdownMenuItem,
 | 
			
		||||
  DropdownMenuTrigger
 | 
			
		||||
} from '@/components/ui/dropdown-menu'
 | 
			
		||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
 | 
			
		||||
import MessageList from '@/components/message/MessageList.vue'
 | 
			
		||||
import ReplyBox from './ReplyBox.vue'
 | 
			
		||||
import api from '@/api'
 | 
			
		||||
import { Icon } from '@iconify/vue'
 | 
			
		||||
 | 
			
		||||
const conversationStore = useConversationStore()
 | 
			
		||||
const statuses = ref([])
 | 
			
		||||
@@ -75,14 +62,10 @@ const getStatuses = async () => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getBadgeVariant = computed(() => {
 | 
			
		||||
  return conversationStore.conversation.data?.status == 'Spam' ? 'destructive' : 'primary'
 | 
			
		||||
  return conversationStore.current?.status == 'Spam' ? 'destructive' : 'primary'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const handleUpdateStatus = (status) => {
 | 
			
		||||
  conversationStore.updateStatus(status)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const contactFullName = computed(() => {
 | 
			
		||||
  return conversationStore.getContactFullName(conversationStore.conversation.data?.uuid)
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -111,7 +111,9 @@ watch(
 | 
			
		||||
  () => props.contentToSet,
 | 
			
		||||
  (newContent) => {
 | 
			
		||||
    if (newContent) {
 | 
			
		||||
      editor.value.commands.setContent(newContent)
 | 
			
		||||
      editor.value.commands.setContent(newContent, false, {
 | 
			
		||||
        preserveWhitespace: "full"
 | 
			
		||||
      })
 | 
			
		||||
      editor.value.commands.focus()
 | 
			
		||||
      emit('contentSet')
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
          'cursor-pointer rounded p-1 hover:bg-secondary',
 | 
			
		||||
          { 'bg-secondary': index === selectedResponseIndex }
 | 
			
		||||
        ]" @click="selectCannedResponse(response.content)" @mouseenter="selectedResponseIndex = index">
 | 
			
		||||
          <span class="font-semibold">{{ response.title }}</span> - {{ response.content }}
 | 
			
		||||
          <span class="font-semibold">{{ response.title }}</span> - {{ response.content.slice(0, 100) }}...
 | 
			
		||||
        </li>
 | 
			
		||||
      </ul>
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -184,7 +184,7 @@ const handleSend = async () => {
 | 
			
		||||
  try {
 | 
			
		||||
    // Replace image source url with cid.
 | 
			
		||||
    const message = transformImageSrcToCID(editorHTML.value)
 | 
			
		||||
    await api.sendMessage(conversationStore.conversation.data.uuid, {
 | 
			
		||||
    await api.sendMessage(conversationStore.current.uuid, {
 | 
			
		||||
      private: messageType.value === 'private_note',
 | 
			
		||||
      message: message,
 | 
			
		||||
      attachments: uploadedFiles.value.map((file) => file.id)
 | 
			
		||||
@@ -199,7 +199,7 @@ const handleSend = async () => {
 | 
			
		||||
    clearContent.value = true
 | 
			
		||||
    uploadedFiles.value = []
 | 
			
		||||
  }
 | 
			
		||||
  api.updateAssigneeLastSeen(conversationStore.conversation.data.uuid)
 | 
			
		||||
  api.updateAssigneeLastSeen(conversationStore.current.uuid)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const handleOnFileDelete = (uuid) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
defineProps({
 | 
			
		||||
  icon: {
 | 
			
		||||
    type: Object,
 | 
			
		||||
    type: Function,
 | 
			
		||||
    required: true
 | 
			
		||||
  },
 | 
			
		||||
  title: {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,7 @@
 | 
			
		||||
  <div class="h-screen">
 | 
			
		||||
 | 
			
		||||
    <!-- Filters -->
 | 
			
		||||
    <ConversationListFilters v-model:type="conversationType" v-model:filter="conversationFilter"
 | 
			
		||||
      :handleFilterChange="handleFilterChange">
 | 
			
		||||
    </ConversationListFilters>
 | 
			
		||||
    <ConversationListFilters v-model:type="conversationType"></ConversationListFilters>
 | 
			
		||||
 | 
			
		||||
    <!-- Error / Empty list -->
 | 
			
		||||
    <EmptyList v-if="emptyConversations" title="No conversations found" message="Try adjusting filters."
 | 
			
		||||
@@ -13,14 +11,14 @@
 | 
			
		||||
      :message="conversationStore.conversations.errorMessage" :icon="MessageCircleWarning"></EmptyList>
 | 
			
		||||
 | 
			
		||||
    <div class="h-screen overflow-y-scroll pb-[180px] flex flex-col">
 | 
			
		||||
      <!-- Item -->
 | 
			
		||||
      <ConversationListItem />
 | 
			
		||||
 | 
			
		||||
      <!-- List skeleton -->
 | 
			
		||||
      <div v-if="conversationsLoading">
 | 
			
		||||
        <ConversationListItemSkeleton v-for="index in 8" :key="index"></ConversationListItemSkeleton>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- Item -->
 | 
			
		||||
      <ConversationListItem />
 | 
			
		||||
 | 
			
		||||
      <!-- Load more  -->
 | 
			
		||||
      <div class="flex justify-center items-center mt-5 relative">
 | 
			
		||||
        <div v-if="conversationStore.conversations.hasMore && !hasErrored && hasConversations">
 | 
			
		||||
@@ -40,7 +38,7 @@
 | 
			
		||||
import { onMounted, watch, computed, onUnmounted } from 'vue'
 | 
			
		||||
import { useConversationStore } from '@/stores/conversation'
 | 
			
		||||
import { subscribeConversationsList } from '@/websocket.js'
 | 
			
		||||
import { CONVERSATION_LIST_TYPE, CONVERSATION_FILTERS } from '@/constants/conversation'
 | 
			
		||||
import { CONVERSATION_LIST_TYPE } from '@/constants/conversation'
 | 
			
		||||
import { MessageCircleWarning, MessageCircleQuestion } from 'lucide-vue-next'
 | 
			
		||||
import { Button } from '@/components/ui/button'
 | 
			
		||||
import Spinner from '@/components/ui/spinner/Spinner.vue'
 | 
			
		||||
@@ -52,15 +50,14 @@ import { useStorage } from '@vueuse/core'
 | 
			
		||||
 | 
			
		||||
const conversationStore = useConversationStore()
 | 
			
		||||
const conversationType = useStorage('conversation_type', CONVERSATION_LIST_TYPE.ASSIGNED)
 | 
			
		||||
const conversationFilter = useStorage('conversation_filter', CONVERSATION_FILTERS.ALL)
 | 
			
		||||
let listRefreshInterval = null
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  conversationStore.fetchConversations(conversationType.value, conversationFilter.value)
 | 
			
		||||
  subscribeConversationsList(conversationType.value, conversationFilter.value)
 | 
			
		||||
  conversationStore.fetchConversationsList(conversationType.value)
 | 
			
		||||
  subscribeConversationsList(conversationType.value)
 | 
			
		||||
  // Refresh list every min.
 | 
			
		||||
  listRefreshInterval = setInterval(() => {
 | 
			
		||||
    conversationStore.fetchConversations(conversationType.value, conversationFilter.value, false)
 | 
			
		||||
    conversationStore.fetchConversationsList(conversationType.value, false)
 | 
			
		||||
  }, 60000)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@@ -69,18 +66,12 @@ onUnmounted(() => {
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
watch(conversationType, (newType) => {
 | 
			
		||||
  conversationStore.fetchConversations(newType, conversationFilter.value)
 | 
			
		||||
  subscribeConversationsList(newType, conversationFilter.value)
 | 
			
		||||
  conversationStore.fetchConversationsList(newType)
 | 
			
		||||
  subscribeConversationsList(newType)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const handleFilterChange = (filter) => {
 | 
			
		||||
  conversationFilter.value = filter
 | 
			
		||||
  conversationStore.fetchConversations(conversationType.value, filter)
 | 
			
		||||
  subscribeConversationsList(conversationType.value, conversationFilter.value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const loadNextPage = () => {
 | 
			
		||||
  conversationStore.fetchNextConversations(conversationType.value, conversationFilter.value)
 | 
			
		||||
  conversationStore.fetchNextConversations(conversationType.value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const hasConversations = computed(() => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,12 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="flex justify-between px-2 py-2 border-b">
 | 
			
		||||
 | 
			
		||||
        <Tabs v-model="conversationType">
 | 
			
		||||
            <TabsList class="w-full flex justify-evenly">
 | 
			
		||||
                <TabsTrigger value="assigned" class="w-full"> Assigned </TabsTrigger>
 | 
			
		||||
                <TabsTrigger value="team" class="w-full"> Unassigned </TabsTrigger>
 | 
			
		||||
                <TabsTrigger value="unassigned" class="w-full"> Unassigned </TabsTrigger>
 | 
			
		||||
                <TabsTrigger value="all" class="w-full"> All </TabsTrigger>
 | 
			
		||||
            </TabsList>
 | 
			
		||||
        </Tabs>
 | 
			
		||||
 | 
			
		||||
        <Popover>
 | 
			
		||||
            <PopoverTrigger as-child>
 | 
			
		||||
                <div class="flex items-center mr-2">
 | 
			
		||||
@@ -17,20 +15,7 @@
 | 
			
		||||
            </PopoverTrigger>
 | 
			
		||||
            <PopoverContent class="w-52">
 | 
			
		||||
                <div>
 | 
			
		||||
                    <Select v-model="conversationFilter">
 | 
			
		||||
                        <SelectTrigger>
 | 
			
		||||
                            <SelectValue placeholder="Select a filter" />
 | 
			
		||||
                        </SelectTrigger>
 | 
			
		||||
                        <SelectContent>
 | 
			
		||||
                            <SelectGroup>
 | 
			
		||||
                                <SelectItem value="status_all"> All </SelectItem>
 | 
			
		||||
                                <SelectItem value="status_open"> Open </SelectItem>
 | 
			
		||||
                                <SelectItem value="status_processing"> Processing </SelectItem>
 | 
			
		||||
                                <SelectItem value="status_spam"> Spam </SelectItem>
 | 
			
		||||
                                <SelectItem value="status_resolved"> Resolved </SelectItem>
 | 
			
		||||
                            </SelectGroup>
 | 
			
		||||
                        </SelectContent>
 | 
			
		||||
                    </Select>
 | 
			
		||||
                    Work in progress.
 | 
			
		||||
                </div>
 | 
			
		||||
            </PopoverContent>
 | 
			
		||||
        </Popover>
 | 
			
		||||
@@ -38,30 +23,10 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import { defineModel, watch } from 'vue'
 | 
			
		||||
import { defineModel } from 'vue'
 | 
			
		||||
import { ListFilter } from 'lucide-vue-next'
 | 
			
		||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
 | 
			
		||||
import {
 | 
			
		||||
    Select,
 | 
			
		||||
    SelectContent,
 | 
			
		||||
    SelectGroup,
 | 
			
		||||
    SelectItem,
 | 
			
		||||
    SelectTrigger,
 | 
			
		||||
    SelectValue
 | 
			
		||||
} from '@/components/ui/select'
 | 
			
		||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
 | 
			
		||||
 | 
			
		||||
const conversationType = defineModel('type')
 | 
			
		||||
const conversationFilter = defineModel('filter')
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    handleFilterChange: {
 | 
			
		||||
        type: Function,
 | 
			
		||||
        required: true
 | 
			
		||||
    },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
watch(conversationFilter, (newValue) => {
 | 
			
		||||
    props.handleFilterChange(newValue)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="flex items-center cursor-pointer flex-row hover:bg-slate-50"
 | 
			
		||||
    :class="{ 'bg-slate-100': conversation.uuid === conversationStore.conversation.data?.uuid }"
 | 
			
		||||
    :class="{ 'bg-slate-100': conversation.uuid === conversationStore.current?.uuid }"
 | 
			
		||||
    v-for="conversation in conversationStore.sortedConversations" :key="conversation.uuid"
 | 
			
		||||
    @click="router.push('/conversations/' + conversation.uuid)">
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -53,9 +53,9 @@ const filteredAgents = ref([])
 | 
			
		||||
 | 
			
		||||
const assignedAgentName = computed(() => {
 | 
			
		||||
    const assignedUserId = props.conversation.assigned_user_id
 | 
			
		||||
    if (!assignedUserId) return 'Select agent...'
 | 
			
		||||
    if (!assignedUserId) return 'Select agent'
 | 
			
		||||
    const agent = props.agents.find(agent => agent.id === assignedUserId)
 | 
			
		||||
    return agent ? `${agent.first_name} ${agent.last_name}` : 'Select agent...'
 | 
			
		||||
    return agent ? `${agent.first_name} ${agent.last_name}` : 'Select agent'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const handleFilterAgents = (search) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -52,9 +52,9 @@ const filteredTeams = ref([])
 | 
			
		||||
 | 
			
		||||
const assignedTeamName = computed(() => {
 | 
			
		||||
    const assignedTeamId = props.conversation.assigned_team_id
 | 
			
		||||
    if (!assignedTeamId) return 'Select team...'
 | 
			
		||||
    if (!assignedTeamId) return 'Select team'
 | 
			
		||||
    const team = props.teams.find(team => team.id === assignedTeamId)
 | 
			
		||||
    return team ? team.name : 'Select team...'
 | 
			
		||||
    return team ? team.name : 'Select team'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const handleFilterTeams = (search) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="p-3">
 | 
			
		||||
    <ConversationSideBarContact :conversation="conversationStore.conversation.data"></ConversationSideBarContact>
 | 
			
		||||
    <Accordion type="multiple" collapsible class="border-t mt-4">
 | 
			
		||||
    <ConversationSideBarContact :conversation="conversationStore.current"></ConversationSideBarContact>
 | 
			
		||||
    <Accordion type="multiple" collapsible class="border-t mt-4" :default-value="['Actions', 'Information']">
 | 
			
		||||
      <AccordionItem value="Actions">
 | 
			
		||||
        <AccordionTrigger>
 | 
			
		||||
          <h4 class="scroll-m-20 text-base font-medium tracking-tight">
 | 
			
		||||
@@ -10,16 +10,16 @@
 | 
			
		||||
        </AccordionTrigger>
 | 
			
		||||
        <AccordionContent class="space-y-5">
 | 
			
		||||
          <!-- Agent -->
 | 
			
		||||
          <AssignAgent :agents="agents" :conversation="conversationStore.conversation.data" :selectAgent="selectAgent">
 | 
			
		||||
          <AssignAgent :agents="agents" :conversation="conversationStore.current" :selectAgent="selectAgent">
 | 
			
		||||
          </AssignAgent>
 | 
			
		||||
          <!-- Team -->
 | 
			
		||||
          <AssignTeam :teams="teams" :conversation="conversationStore.conversation.data" :selectTeam="selectTeam">
 | 
			
		||||
          <AssignTeam :teams="teams" :conversation="conversationStore.current" :selectTeam="selectTeam">
 | 
			
		||||
          </AssignTeam>
 | 
			
		||||
          <!-- Priority  -->
 | 
			
		||||
          <PriorityChange :priorities="priorities" :conversation="conversationStore.conversation.data"
 | 
			
		||||
          <PriorityChange :priorities="priorities" :conversation="conversationStore.current"
 | 
			
		||||
            :selectPriority="selectPriority"></PriorityChange>
 | 
			
		||||
          <!-- Tags -->
 | 
			
		||||
          <SelectTag :initialValue="conversationStore.conversation.data.tags" v-model="selectedTags" :items="tags"
 | 
			
		||||
          <SelectTag :initialValue="conversationStore.current.tags" v-model="selectedTags" :items="tags"
 | 
			
		||||
            placeHolder="Select tags"></SelectTag>
 | 
			
		||||
        </AccordionContent>
 | 
			
		||||
      </AccordionItem>
 | 
			
		||||
@@ -30,7 +30,7 @@
 | 
			
		||||
          </span>
 | 
			
		||||
        </AccordionTrigger>
 | 
			
		||||
        <AccordionContent>
 | 
			
		||||
          <ConversationInfo :conversation="conversationStore.conversation.data"></ConversationInfo>
 | 
			
		||||
          <ConversationInfo :conversation="conversationStore.current"></ConversationInfo>
 | 
			
		||||
        </AccordionContent>
 | 
			
		||||
      </AccordionItem>
 | 
			
		||||
    </Accordion>
 | 
			
		||||
@@ -154,17 +154,17 @@ const handleUpsertTags = () => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const selectAgent = (id) => {
 | 
			
		||||
  conversationStore.conversation.data.assigned_user_id = id
 | 
			
		||||
  conversationStore.current.assigned_user_id = id
 | 
			
		||||
  handleAssignedUserChange(id)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const selectTeam = (id) => {
 | 
			
		||||
  conversationStore.conversation.data.assigned_team_id = id
 | 
			
		||||
  conversationStore.current.assigned_team_id = id
 | 
			
		||||
  handleAssignedTeamChange(id)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const selectPriority = (priority) => {
 | 
			
		||||
  conversationStore.conversation.data.priority = priority
 | 
			
		||||
  conversationStore.current.priority = priority
 | 
			
		||||
  handlePriorityChange(priority)
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,8 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <LineChart
 | 
			
		||||
    :data="renamedData"
 | 
			
		||||
    index="date"
 | 
			
		||||
    :categories="['New conversations']"
 | 
			
		||||
    :y-formatter="
 | 
			
		||||
      (tick) => {
 | 
			
		||||
        return tick
 | 
			
		||||
      }
 | 
			
		||||
    "
 | 
			
		||||
  />
 | 
			
		||||
  <LineChart :data="chartData" index="date" :categories="['New conversations']" :y-formatter="(tick) => {
 | 
			
		||||
    return tick
 | 
			
		||||
  }
 | 
			
		||||
    " />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
@@ -22,10 +16,10 @@ const props = defineProps({
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const renamedData = computed(() =>
 | 
			
		||||
  props.data.map((item) => ({
 | 
			
		||||
    ...item,
 | 
			
		||||
    'New conversations': item.new_conversations
 | 
			
		||||
const chartData = computed(() =>
 | 
			
		||||
  props.data.map(item => ({
 | 
			
		||||
    date: item.date,
 | 
			
		||||
    "New conversations": item.new_conversations
 | 
			
		||||
  }))
 | 
			
		||||
)
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -107,6 +107,6 @@ const avatarFallback = computed(() => {
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const retryMessage = (msg) => {
 | 
			
		||||
  api.retryMessage(convStore.conversation.data.uuid, msg.uuid)
 | 
			
		||||
  api.retryMessage(convStore.current.uuid, msg.uuid)
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,7 @@ const props = defineProps({
 | 
			
		||||
const convStore = useConversationStore()
 | 
			
		||||
 | 
			
		||||
const getAvatar = computed(() => {
 | 
			
		||||
  return convStore.conversation.data.avatar_url ? convStore.conversation.avatar_url : ''
 | 
			
		||||
  return convStore.current.avatar_url ? convStore.conversation.avatar_url : ''
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const messageContent = computed(() => 
 | 
			
		||||
@@ -66,10 +66,10 @@ const nonInlineAttachments = computed(() =>
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const getFullName = computed(() => {
 | 
			
		||||
  return convStore.conversation.data.first_name + ' ' + convStore.conversation.data.last_name
 | 
			
		||||
  return convStore.current.first_name + ' ' + convStore.current.last_name
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const avatarFallback = computed(() => {
 | 
			
		||||
  return convStore.conversation.data.first_name.toUpperCase().substring(0, 2)
 | 
			
		||||
  return convStore.current.first_name.toUpperCase().substring(0, 2)
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -37,6 +37,7 @@ import { useConversationStore } from '@/stores/conversation'
 | 
			
		||||
import { Button } from '@/components/ui/button'
 | 
			
		||||
import { RefreshCw } from 'lucide-vue-next'
 | 
			
		||||
import { useEmitter } from '@/composables/useEmitter'
 | 
			
		||||
import { EMITTER_EVENTS } from '@/constants/emitterEvents'
 | 
			
		||||
 | 
			
		||||
const conversationStore = useConversationStore()
 | 
			
		||||
const threadEl = ref(null)
 | 
			
		||||
@@ -44,18 +45,19 @@ const emitter = useEmitter()
 | 
			
		||||
 | 
			
		||||
const scrollToBottom = () => {
 | 
			
		||||
  setTimeout(() => {
 | 
			
		||||
    console.log('scrolling..')
 | 
			
		||||
    const thread = threadEl.value
 | 
			
		||||
    if (thread) {
 | 
			
		||||
      thread.scrollTop = thread.scrollHeight
 | 
			
		||||
    }
 | 
			
		||||
  }, 0)
 | 
			
		||||
  }, 50)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  scrollToBottom()
 | 
			
		||||
  // On new outgoing message to the current conversation, scroll to the bottom.
 | 
			
		||||
  emitter.on('new-outgoing-message', (data) => {
 | 
			
		||||
    if (data.conversation_uuid === conversationStore.conversation.data.uuid) {
 | 
			
		||||
  emitter.on(EMITTER_EVENTS.NEW_OUTGOING_MESSAGE, (data) => {
 | 
			
		||||
    if (data.conversation_uuid === conversationStore.current.uuid) {
 | 
			
		||||
      scrollToBottom()
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
@@ -63,7 +65,7 @@ onMounted(() => {
 | 
			
		||||
 | 
			
		||||
// On conversation change scroll to the bottom
 | 
			
		||||
watch(
 | 
			
		||||
  () => conversationStore.conversation.data,
 | 
			
		||||
  () => conversationStore.current.uuid,
 | 
			
		||||
  () => {
 | 
			
		||||
    scrollToBottom()
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,5 @@
 | 
			
		||||
export const CONVERSATION_FILTERS = {
 | 
			
		||||
  ALL: 'status_all',
 | 
			
		||||
  STATUS_OPEN: 'status_open',
 | 
			
		||||
  STATUS_PROCESSING: 'status_processing',
 | 
			
		||||
  STATUS_SPAM: 'status_spam',
 | 
			
		||||
  STATUS_RESOLVED: 'status_resolved'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const CONVERSATION_LIST_TYPE = {
 | 
			
		||||
  ASSIGNED: 'assigned',
 | 
			
		||||
  UNASSIGNED: 'team',
 | 
			
		||||
  UNASSIGNED: 'unassigned',
 | 
			
		||||
  ALL: 'all'
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,6 @@
 | 
			
		||||
export const EMITTER_EVENTS = {
 | 
			
		||||
    REFRESH_LIST: 'refresh-list',
 | 
			
		||||
    SHOW_TOAST: 'show-toast'
 | 
			
		||||
    SHOW_TOAST: 'show-toast',
 | 
			
		||||
    NEW_OUTGOING_MESSAGE: 'new-outgoing-message',
 | 
			
		||||
    NEW_INCOMING_MESSAGE: 'new-incoming-message',
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
import { defineStore } from 'pinia'
 | 
			
		||||
import { computed, reactive, onUnmounted } from 'vue'
 | 
			
		||||
import { handleHTTPError } from '@/utils/http'
 | 
			
		||||
import { useToast } from '@/components/ui/toast/use-toast'
 | 
			
		||||
import { CONVERSATION_LIST_TYPE } from '@/constants/conversation'
 | 
			
		||||
import api from '@/api'
 | 
			
		||||
import { useEmitter } from '@/composables/useEmitter'
 | 
			
		||||
import { EMITTER_EVENTS } from '@/constants/emitterEvents'
 | 
			
		||||
import api from '@/api'
 | 
			
		||||
 | 
			
		||||
export const useConversationStore = defineStore('conversation', () => {
 | 
			
		||||
  // List of conversations
 | 
			
		||||
@@ -36,20 +36,18 @@ export const useConversationStore = defineStore('conversation', () => {
 | 
			
		||||
  // Map to track seen msg UUIDs for deduplication
 | 
			
		||||
  let seenConversationUUIDs = new Map()
 | 
			
		||||
  let seenMessageUUIDs = new Set()
 | 
			
		||||
  let previousConvListType = ''
 | 
			
		||||
  let previousFilter = ''
 | 
			
		||||
  let previousConversationListType = ''
 | 
			
		||||
  let reRenderInterval = setInterval(() => {
 | 
			
		||||
    conversations.data = [...conversations.data]
 | 
			
		||||
  }, 60000)
 | 
			
		||||
  const { toast } = useToast()
 | 
			
		||||
  }, 120000)
 | 
			
		||||
  const emitter = useEmitter()
 | 
			
		||||
 | 
			
		||||
  // Clear the reRenderInterval when the store is destroyed.
 | 
			
		||||
  // Cleanup.
 | 
			
		||||
  onUnmounted(() => {
 | 
			
		||||
    clearInterval(reRenderInterval)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // Computed property to sort conversations by last_message_at
 | 
			
		||||
  // Sort conversations by last_message_at
 | 
			
		||||
  const sortedConversations = computed(() => {
 | 
			
		||||
    if (!conversations.data) {
 | 
			
		||||
      return []
 | 
			
		||||
@@ -59,7 +57,7 @@ export const useConversationStore = defineStore('conversation', () => {
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // Computed property to sort messages by created_at
 | 
			
		||||
  // Sort messages by created_at
 | 
			
		||||
  const sortedMessages = computed(() => {
 | 
			
		||||
    if (!messages.data) {
 | 
			
		||||
      return []
 | 
			
		||||
@@ -67,6 +65,18 @@ export const useConversationStore = defineStore('conversation', () => {
 | 
			
		||||
    return [...messages.data].sort((a, b) => new Date(a.created_at) - new Date(b.created_at))
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // Marks a conversation as read.
 | 
			
		||||
  function markAsRead (uuid) {
 | 
			
		||||
    const index = conversations.data.findIndex((conv) => conv.uuid === uuid)
 | 
			
		||||
    if (index !== -1) {
 | 
			
		||||
      conversations.data[index].unread_message_count = 0
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const currentContactName = computed(() => {
 | 
			
		||||
    return conversation.data?.first_name + " " + conversation.data?.last_name
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const getContactFullName = (uuid) => {
 | 
			
		||||
    if (conversations?.data) {
 | 
			
		||||
      const conv = conversations.data.find((conv) => conv.uuid === uuid)
 | 
			
		||||
@@ -74,29 +84,31 @@ export const useConversationStore = defineStore('conversation', () => {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function markAsRead (uuid) {
 | 
			
		||||
    const index = conversations.data.findIndex((conv) => conv.uuid === uuid)
 | 
			
		||||
    if (index !== -1) {
 | 
			
		||||
      conversations.data[index].unread_message_count = 0
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const current = computed(() => {
 | 
			
		||||
    return conversation.data
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // Fetches conversation by uuid.
 | 
			
		||||
  async function fetchConversation (uuid) {
 | 
			
		||||
    conversation.loading = true
 | 
			
		||||
    try {
 | 
			
		||||
      const resp = await api.getConversation(uuid)
 | 
			
		||||
      conversation.data = resp.data.data
 | 
			
		||||
      // mark this conversation as read.
 | 
			
		||||
      // Mark this conversation as read.
 | 
			
		||||
      markAsRead(uuid)
 | 
			
		||||
      // reset messages state on new conversation fetch.
 | 
			
		||||
      resetMessages()
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      conversation.errorMessage = handleHTTPError(error).message
 | 
			
		||||
      emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
 | 
			
		||||
        title: 'Something went wrong',
 | 
			
		||||
        variant: 'destructive',
 | 
			
		||||
        description: conversation.errorMessage
 | 
			
		||||
      })
 | 
			
		||||
    } finally {
 | 
			
		||||
      conversation.loading = false
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Fetches participants of conversation by uuid.
 | 
			
		||||
  async function fetchParticipants (uuid) {
 | 
			
		||||
    try {
 | 
			
		||||
      const resp = await api.getConversationParticipants(uuid)
 | 
			
		||||
@@ -106,17 +118,24 @@ export const useConversationStore = defineStore('conversation', () => {
 | 
			
		||||
      }, {})
 | 
			
		||||
      updateParticipants(participants)
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('Error fetching participants:', error)
 | 
			
		||||
      emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
 | 
			
		||||
        title: 'Something went wrong',
 | 
			
		||||
        variant: 'destructive',
 | 
			
		||||
        description: handleHTTPError(error).message
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Fetches messages of a conversation.
 | 
			
		||||
  async function fetchMessages (uuid) {
 | 
			
		||||
    // Reset state.
 | 
			
		||||
    resetMessages()
 | 
			
		||||
    messages.loading = true
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await api.getMessages(uuid, messages.page)
 | 
			
		||||
      const response = await api.getConversationMessages(uuid, messages.page)
 | 
			
		||||
      const fetchedMessages = response.data?.data || []
 | 
			
		||||
 | 
			
		||||
      // Filter out messages that have already been seen
 | 
			
		||||
      // Filter out messages already seen.
 | 
			
		||||
      const newMessages = fetchedMessages.filter((message) => {
 | 
			
		||||
        if (!seenMessageUUIDs.has(message.uuid)) {
 | 
			
		||||
          seenMessageUUIDs.add(message.uuid)
 | 
			
		||||
@@ -134,11 +153,12 @@ export const useConversationStore = defineStore('conversation', () => {
 | 
			
		||||
        messages.hasMore = false
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Add new messages to the messages data
 | 
			
		||||
      // Add new messages to the messages state.
 | 
			
		||||
      messages.data.unshift(...newMessages)
 | 
			
		||||
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      toast({
 | 
			
		||||
        title: 'Could not fetch messages, Please try again.',
 | 
			
		||||
      emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
 | 
			
		||||
        title: 'Something went wrong',
 | 
			
		||||
        variant: 'destructive',
 | 
			
		||||
        description: handleHTTPError(error).message
 | 
			
		||||
      })
 | 
			
		||||
@@ -149,14 +169,16 @@ export const useConversationStore = defineStore('conversation', () => {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Fetches next page of messages by incrementing the page number.
 | 
			
		||||
  async function fetchNextMessages () {
 | 
			
		||||
    messages.page++
 | 
			
		||||
    fetchMessages(conversation.data.uuid)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Fetches a specific message of conversation
 | 
			
		||||
  async function fetchMessage (cuuid, uuid) {
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await api.getMessage(cuuid, uuid)
 | 
			
		||||
      const response = await api.getConversationMessage(cuuid, uuid)
 | 
			
		||||
      if (response?.data?.data) {
 | 
			
		||||
        const message = response.data.data
 | 
			
		||||
        if (!messages.data.some((m) => m.uuid === message.uuid)) {
 | 
			
		||||
@@ -165,45 +187,51 @@ export const useConversationStore = defineStore('conversation', () => {
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      messages.errorMessage = handleHTTPError(error).message
 | 
			
		||||
      emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
 | 
			
		||||
        title: 'Could not fetch message',
 | 
			
		||||
        variant: 'destructive',
 | 
			
		||||
        description: messages.errorMessage
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function onFilterchange () {
 | 
			
		||||
    conversations.data = null
 | 
			
		||||
  // On conversation type(tab) change reset state.
 | 
			
		||||
  function onConversationTypeChange () {
 | 
			
		||||
    conversations.data = []
 | 
			
		||||
    conversations.page = 1
 | 
			
		||||
    conversations.hasMore = true
 | 
			
		||||
    seenConversationUUIDs.clear()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function fetchConversations (type, filter, showLoader = true) {
 | 
			
		||||
  // fetchConversationsList fetches list of conversations based on type(tab).
 | 
			
		||||
  async function fetchConversationsList (type, showLoader = true) {
 | 
			
		||||
    conversations.errorMessage = ''
 | 
			
		||||
    if (showLoader)
 | 
			
		||||
      conversations.loading = true
 | 
			
		||||
    conversations.errorMessage = ''
 | 
			
		||||
 | 
			
		||||
    if (type !== previousConvListType || filter !== previousFilter) {
 | 
			
		||||
      onFilterchange()
 | 
			
		||||
      previousConvListType = type
 | 
			
		||||
      previousFilter = filter
 | 
			
		||||
    if (type !== previousConversationListType) {
 | 
			
		||||
      onConversationTypeChange()
 | 
			
		||||
      previousConversationListType = type
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      let response
 | 
			
		||||
      switch (type) {
 | 
			
		||||
        case CONVERSATION_LIST_TYPE.ASSIGNED:
 | 
			
		||||
          response = await api.getAssignedConversations(conversations.page, filter)
 | 
			
		||||
          response = await api.getAssignedConversations(conversations.page)
 | 
			
		||||
          break
 | 
			
		||||
        case CONVERSATION_LIST_TYPE.UNASSIGNED:
 | 
			
		||||
          response = await api.getTeamConversations(conversations.page, filter)
 | 
			
		||||
          response = await api.getUnassignedConversations(conversations.page)
 | 
			
		||||
          break
 | 
			
		||||
        case CONVERSATION_LIST_TYPE.ALL:
 | 
			
		||||
          response = await api.getAllConversations(conversations.page, filter)
 | 
			
		||||
          response = await api.getAllConversations(conversations.page)
 | 
			
		||||
          break
 | 
			
		||||
        default:
 | 
			
		||||
          console.warn(`Invalid type ${type}`)
 | 
			
		||||
          return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Merge new conversations if any
 | 
			
		||||
      // Merge new conversations if any.
 | 
			
		||||
      if (response?.data?.data) {
 | 
			
		||||
        const newConversations = response.data.data.filter((conversation) => {
 | 
			
		||||
          if (!seenConversationUUIDs.has(conversation.uuid)) {
 | 
			
		||||
@@ -217,9 +245,8 @@ export const useConversationStore = defineStore('conversation', () => {
 | 
			
		||||
          conversations.data = []
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (newConversations.length === 0) {
 | 
			
		||||
        if (newConversations.length === 0)
 | 
			
		||||
          conversations.hasMore = false
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        conversations.data.push(...newConversations)
 | 
			
		||||
      } else {
 | 
			
		||||
@@ -235,14 +262,18 @@ export const useConversationStore = defineStore('conversation', () => {
 | 
			
		||||
  // Increments the page and fetches the next set of conversations.
 | 
			
		||||
  function fetchNextConversations (type, filter) {
 | 
			
		||||
    conversations.page++
 | 
			
		||||
    fetchConversations(type, filter)
 | 
			
		||||
    fetchConversationsList(type, filter)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function updatePriority (v) {
 | 
			
		||||
    try {
 | 
			
		||||
      await api.updateConversationPriority(conversation.data.uuid, { priority: v })
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      // Pass.
 | 
			
		||||
      emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
 | 
			
		||||
        title: 'Something went wrong',
 | 
			
		||||
        variant: 'destructive',
 | 
			
		||||
        description: handleHTTPError(error).message
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -250,8 +281,8 @@ export const useConversationStore = defineStore('conversation', () => {
 | 
			
		||||
    try {
 | 
			
		||||
      await api.updateConversationStatus(conversation.data.uuid, { status: v })
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      toast({
 | 
			
		||||
        title: 'Uh oh! Could not update status, Please try again.',
 | 
			
		||||
      emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
 | 
			
		||||
        title: 'Something went wrong',
 | 
			
		||||
        variant: 'destructive',
 | 
			
		||||
        description: handleHTTPError(error).message
 | 
			
		||||
      })
 | 
			
		||||
@@ -262,8 +293,8 @@ export const useConversationStore = defineStore('conversation', () => {
 | 
			
		||||
    try {
 | 
			
		||||
      await api.upsertTags(conversation.data.uuid, v)
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      toast({
 | 
			
		||||
        title: 'Uh oh! Could not add tags, Please try again.',
 | 
			
		||||
      emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
 | 
			
		||||
        title: 'Something went wrong',
 | 
			
		||||
        variant: 'destructive',
 | 
			
		||||
        description: handleHTTPError(error).message
 | 
			
		||||
      })
 | 
			
		||||
@@ -274,7 +305,11 @@ export const useConversationStore = defineStore('conversation', () => {
 | 
			
		||||
    try {
 | 
			
		||||
      await api.updateAssignee(conversation.data.uuid, type, v)
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      // Pass.
 | 
			
		||||
      emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
 | 
			
		||||
        title: 'Something went wrong',
 | 
			
		||||
        variant: 'destructive',
 | 
			
		||||
        description: handleHTTPError(error).message
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -301,18 +336,18 @@ export const useConversationStore = defineStore('conversation', () => {
 | 
			
		||||
 | 
			
		||||
  // Update the last message for a conversation.
 | 
			
		||||
  function updateConversationLastMessage (message) {
 | 
			
		||||
    const conv = conversations.data.find((c) => c.uuid === message.conversation_uuid)
 | 
			
		||||
    if (conv) {
 | 
			
		||||
      conv.last_message = message.content
 | 
			
		||||
      conv.last_message_at = message.created_at
 | 
			
		||||
    const listConversation = conversations.data.find((c) => c.uuid === message.conversation_uuid)
 | 
			
		||||
    if (listConversation) {
 | 
			
		||||
      listConversation.last_message = message.content
 | 
			
		||||
      listConversation.last_message_at = message.created_at
 | 
			
		||||
      // Increment unread count only if conversation is not open.
 | 
			
		||||
      if (conv.uuid !== conversation?.data?.uuid) {
 | 
			
		||||
        conv.unread_message_count += 1
 | 
			
		||||
      if (listConversation.uuid !== conversation?.data?.uuid) {
 | 
			
		||||
        listConversation.unread_message_count += 1
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Update message in a conversation.
 | 
			
		||||
  // Adds a new message to conversation.
 | 
			
		||||
  function updateConversationMessageList (message) {
 | 
			
		||||
    // Fetch entire message only if the convesation is open and the message is not present in the list.
 | 
			
		||||
    if (conversation?.data?.uuid === message.conversation_uuid) {
 | 
			
		||||
@@ -322,7 +357,12 @@ export const useConversationStore = defineStore('conversation', () => {
 | 
			
		||||
        updateAssigneeLastSeen(message.conversation_uuid)
 | 
			
		||||
        if (message.type === 'outgoing') {
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            emitter.emit('new-outgoing-message', { conversation_uuid: message.conversation_uuid })
 | 
			
		||||
            emitter.emit(EMITTER_EVENTS.NEW_OUTGOING_MESSAGE, { conversation_uuid: message.conversation_uuid })
 | 
			
		||||
          }, 50)
 | 
			
		||||
        }
 | 
			
		||||
        if (message.type === 'incoming') {
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            emitter.emit(EMITTER_EVENTS.NEW_INCOMING_MESSAGE, { conversation_uuid: message.conversation_uuid })
 | 
			
		||||
          }, 50)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
@@ -336,21 +376,19 @@ export const useConversationStore = defineStore('conversation', () => {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function updateMessageProp (message) {
 | 
			
		||||
    // Update prop in list.
 | 
			
		||||
    const existingMessage = messages.data.find((m) => m.uuid === message.uuid)
 | 
			
		||||
    if (existingMessage) {
 | 
			
		||||
      existingMessage[message.prop] = message.value
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function updateConversationProp (conversation) {
 | 
			
		||||
    // Update prop if conversation is open.
 | 
			
		||||
    if (conversation?.data?.uuid === conversation.uuid) {
 | 
			
		||||
      conversation.data[conversation.prop] = conversation.value
 | 
			
		||||
  function updateConversationProp (update) {
 | 
			
		||||
    // Update prop in open conversation.
 | 
			
		||||
    if (conversation.data?.uuid === update.uuid) {
 | 
			
		||||
      conversation.data[update.prop] = update.value
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Update prop in list.
 | 
			
		||||
    const existingConversation = conversations?.data?.find((c) => c.uuid === conversation.uuid)
 | 
			
		||||
    // Update prop in conversation list.
 | 
			
		||||
    const existingConversation = conversations?.data?.find((c) => c.uuid === update.uuid)
 | 
			
		||||
    if (existingConversation) {
 | 
			
		||||
      existingConversation[conversation.prop] = conversation.value
 | 
			
		||||
    }
 | 
			
		||||
@@ -389,6 +427,8 @@ export const useConversationStore = defineStore('conversation', () => {
 | 
			
		||||
    messages,
 | 
			
		||||
    sortedConversations,
 | 
			
		||||
    sortedMessages,
 | 
			
		||||
    current,
 | 
			
		||||
    currentContactName,
 | 
			
		||||
    conversationUUIDExists,
 | 
			
		||||
    updateConversationProp,
 | 
			
		||||
    addNewConversation,
 | 
			
		||||
@@ -400,7 +440,7 @@ export const useConversationStore = defineStore('conversation', () => {
 | 
			
		||||
    updateAssigneeLastSeen,
 | 
			
		||||
    updateConversationMessageList,
 | 
			
		||||
    fetchConversation,
 | 
			
		||||
    fetchConversations,
 | 
			
		||||
    fetchConversationsList,
 | 
			
		||||
    fetchMessages,
 | 
			
		||||
    upsertTags,
 | 
			
		||||
    updateAssignee,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <ResizablePanelGroup direction="horizontal" auto-save-id="conversation.vue.resizable.panel">
 | 
			
		||||
    <ResizablePanel :min-size="23" :default-size="23" :max-size="40">
 | 
			
		||||
    <ResizablePanel :min-size="23" :default-size="23" :max-size="40" class="shadow-md shadow-gray-300">
 | 
			
		||||
      <ConversationList></ConversationList>
 | 
			
		||||
    </ResizablePanel>
 | 
			
		||||
    <ResizableHandle />
 | 
			
		||||
    <ResizablePanel>
 | 
			
		||||
      <Conversation v-if="conversationStore.conversation.data"></Conversation>
 | 
			
		||||
      <Conversation v-if="conversationStore.current"></Conversation>
 | 
			
		||||
      <ConversationPlaceholder v-else></ConversationPlaceholder>
 | 
			
		||||
    </ResizablePanel>
 | 
			
		||||
    <ResizableHandle />
 | 
			
		||||
@@ -13,7 +13,8 @@
 | 
			
		||||
      :min-size="10"
 | 
			
		||||
      :default-size="16"
 | 
			
		||||
      :max-size="30"
 | 
			
		||||
      v-if="conversationStore.conversation.data"
 | 
			
		||||
      v-if="conversationStore.current"
 | 
			
		||||
      class="shadow shadow-gray-300"
 | 
			
		||||
    >
 | 
			
		||||
      <ConversationSideBar></ConversationSideBar>
 | 
			
		||||
    </ResizablePanel>
 | 
			
		||||
 
 | 
			
		||||
@@ -288,23 +288,23 @@ func (c *Manager) GetConversationUUID(id int) (string, error) {
 | 
			
		||||
	return uuid, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetAllConversations retrieves all conversations with optional filtering, ordering, and pagination.
 | 
			
		||||
func (c *Manager) GetAllConversations(order, orderBy, filter string, page, pageSize int) ([]models.Conversation, error) {
 | 
			
		||||
	return c.GetConversations(0, models.AllConversations, order, orderBy, filter, page, pageSize)
 | 
			
		||||
// GetAllConversationsList retrieves all conversations with optional filtering, ordering, and pagination.
 | 
			
		||||
func (c *Manager) GetAllConversationsList(order, orderBy string, page, pageSize int) ([]models.Conversation, error) {
 | 
			
		||||
	return c.GetConversations(0, models.AllConversations, order, orderBy, page, pageSize)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetAssignedConversations retrieves conversations assigned to a specific user with optional filtering, ordering, and pagination.
 | 
			
		||||
func (c *Manager) GetAssignedConversations(userID int, order, orderBy, filter string, page, pageSize int) ([]models.Conversation, error) {
 | 
			
		||||
	return c.GetConversations(userID, models.AssignedConversations, order, orderBy, filter, page, pageSize)
 | 
			
		||||
// GetAssignedConversationsList retrieves conversations assigned to a specific user with optional filtering, ordering, and pagination.
 | 
			
		||||
func (c *Manager) GetAssignedConversationsList(userID int, order, orderBy string, page, pageSize int) ([]models.Conversation, error) {
 | 
			
		||||
	return c.GetConversations(userID, models.AssignedConversations, order, orderBy, page, pageSize)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetTeamConversations retrieves conversations assigned to a team the user is part of with optional filtering, ordering, and pagination.
 | 
			
		||||
func (c *Manager) GetTeamConversations(userID int, order, orderBy, filter string, page, pageSize int) ([]models.Conversation, error) {
 | 
			
		||||
	return c.GetConversations(userID, models.AssigneeTypeTeam, order, orderBy, filter, 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 string, page, pageSize int) ([]models.Conversation, error) {
 | 
			
		||||
	return c.GetConversations(userID, models.UnassignedConversations, order, orderBy, page, pageSize)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetConversations retrieves conversations based on user ID, type, and optional filtering, ordering, and pagination.
 | 
			
		||||
func (c *Manager) GetConversations(userID int, typ, order, orderBy, filter string, page, pageSize int) ([]models.Conversation, error) {
 | 
			
		||||
func (c *Manager) GetConversations(userID int, typ, order, orderBy string, page, pageSize int) ([]models.Conversation, error) {
 | 
			
		||||
	var conversations = make([]models.Conversation, 0)
 | 
			
		||||
 | 
			
		||||
	if orderBy == "" {
 | 
			
		||||
@@ -314,7 +314,7 @@ func (c *Manager) GetConversations(userID int, typ, order, orderBy, filter strin
 | 
			
		||||
		order = "DESC"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	query, qArgs, err := c.generateConversationsListQuery(userID, c.q.GetConversations, typ, order, orderBy, filter, page, pageSize)
 | 
			
		||||
	query, qArgs, err := c.generateConversationsListQuery(userID, c.q.GetConversations, typ, order, orderBy, page, pageSize)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.lo.Error("error generating conversations query", "error", err)
 | 
			
		||||
		return conversations, envelope.NewError(envelope.GeneralError, c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.entities.conversations}"), nil)
 | 
			
		||||
@@ -335,10 +335,10 @@ func (c *Manager) GetConversations(userID int, typ, order, orderBy, filter strin
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetConversationUUIDs retrieves the UUIDs of conversations based on user ID, type, and optional filtering, ordering, and pagination.
 | 
			
		||||
func (c *Manager) GetConversationUUIDs(userID, page, pageSize int, typ, filter string) ([]string, error) {
 | 
			
		||||
func (c *Manager) GetConversationUUIDs(userID, page, pageSize int, typ string) ([]string, error) {
 | 
			
		||||
	var ids = make([]string, 0)
 | 
			
		||||
 | 
			
		||||
	query, qArgs, err := c.generateConversationsListQuery(userID, c.q.GetConversationsUUIDs, typ, "", "", filter, page, pageSize)
 | 
			
		||||
	query, qArgs, err := c.generateConversationsListQuery(userID, c.q.GetConversationsUUIDs, typ, "", "", page, pageSize)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.lo.Error("error generating conversations query", "error", err)
 | 
			
		||||
		return ids, err
 | 
			
		||||
@@ -465,6 +465,7 @@ func (c *Manager) UpdateConversationStatus(uuid string, status []byte, actor umo
 | 
			
		||||
	if err := c.RecordStatusChange(statusStr, uuid, actor); err != nil {
 | 
			
		||||
		return envelope.NewError(envelope.GeneralError, "Error recording status change", nil)
 | 
			
		||||
	}
 | 
			
		||||
	c.BroadcastConversationPropertyUpdate(uuid, "status", string(status))
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -555,7 +556,7 @@ func (t *Manager) UpsertConversationTags(uuid string, tagIDs []int) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// generateConversationsListQuery generates the SQL query to list conversations with optional filtering, ordering, and pagination.
 | 
			
		||||
func (c *Manager) generateConversationsListQuery(userID int, baseQuery, typ, order, orderBy, filter string, page, pageSize int) (string, []interface{}, error) {
 | 
			
		||||
func (c *Manager) generateConversationsListQuery(userID int, baseQuery, typ, order, orderBy string, page, pageSize int) (string, []interface{}, error) {
 | 
			
		||||
	var (
 | 
			
		||||
		qArgs []interface{}
 | 
			
		||||
		cond  string
 | 
			
		||||
@@ -566,7 +567,7 @@ func (c *Manager) generateConversationsListQuery(userID int, baseQuery, typ, ord
 | 
			
		||||
	case models.AssignedConversations:
 | 
			
		||||
		cond = "AND c.assigned_user_id = $1"
 | 
			
		||||
		qArgs = append(qArgs, userID)
 | 
			
		||||
	case models.TeamConversations:
 | 
			
		||||
	case models.UnassignedConversations:
 | 
			
		||||
		cond = "AND c.assigned_user_id IS NULL AND c.assigned_team_id IN (SELECT team_id FROM team_members WHERE user_id = $1)"
 | 
			
		||||
		qArgs = append(qArgs, userID)
 | 
			
		||||
	case models.AllConversations:
 | 
			
		||||
@@ -575,10 +576,6 @@ func (c *Manager) generateConversationsListQuery(userID int, baseQuery, typ, ord
 | 
			
		||||
		return "", nil, fmt.Errorf("invalid conversation type %s", typ)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if filterClause, ok := models.ValidFilters[filter]; ok {
 | 
			
		||||
		cond += " AND " + filterClause
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Ensure orderBy & order is valid.
 | 
			
		||||
	var orderByClause = ""
 | 
			
		||||
	if slices.Contains(models.ValidOrderBy, orderBy) {
 | 
			
		||||
 
 | 
			
		||||
@@ -27,9 +27,9 @@ var (
 | 
			
		||||
	AssigneeTypeTeam = "team"
 | 
			
		||||
	AssigneeTypeUser = "user"
 | 
			
		||||
 | 
			
		||||
	AllConversations      = "all"
 | 
			
		||||
	AssignedConversations = "assigned"
 | 
			
		||||
	TeamConversations     = "team"
 | 
			
		||||
	AllConversations        = "all"
 | 
			
		||||
	AssignedConversations   = "assigned"
 | 
			
		||||
	UnassignedConversations = "unassigned"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Conversation struct {
 | 
			
		||||
 
 | 
			
		||||
@@ -118,7 +118,7 @@ func (c *Client) processIncomingMessage(data []byte) {
 | 
			
		||||
 | 
			
		||||
		// Add the new subscriptions.
 | 
			
		||||
		for page := 1; page <= maxConversationsPagesToSub; page++ {
 | 
			
		||||
			conversationUUIDs, err := c.Hub.conversationStore.GetConversationUUIDs(c.ID, page, maxConversationsPageSize, subReq.Type, subReq.Filter)
 | 
			
		||||
			conversationUUIDs, err := c.Hub.conversationStore.GetConversationUUIDs(c.ID, page, maxConversationsPageSize, subReq.Type)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Println("error fetching conversation ids", err)
 | 
			
		||||
				continue
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ type Hub struct {
 | 
			
		||||
 | 
			
		||||
// ConversationStore defines the interface for retrieving conversation UUIDs.
 | 
			
		||||
type ConversationStore interface {
 | 
			
		||||
	GetConversationUUIDs(userID, page, pageSize int, typ, predefinedFilter string) ([]string, error)
 | 
			
		||||
	GetConversationUUIDs(userID, page, pageSize int, typ string) ([]string, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewHub creates a new Hub.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								schema.sql
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								schema.sql
									
									
									
									
									
								
							@@ -123,10 +123,10 @@ CREATE TABLE roles (
 | 
			
		||||
-- Create roles.
 | 
			
		||||
INSERT INTO roles
 | 
			
		||||
(permissions, "name", description)
 | 
			
		||||
VALUES('{conversations:read_team,conversations:read_all,conversations:read,conversations:read_assigned,conversations:update_user_assignee,conversations:update_team_assignee,conversations:update_priority,conversations:update_status,conversations:update_tags,messages:read,messages:write,templates:write,templates:read,roles:delete,roles:write,roles:read,inboxes:delete,inboxes:write,inboxes:read,automations:write,automations:delete,automations:read,teams:write,teams:read,users:write,users:read,dashboard_global:read,canned_responses:delete,tags:delete,canned_responses:write,tags:write,status:delete,status:write,status:read,oidc:delete,oidc:read,oidc:write,settings_notifications:read,settings_notifications:write,settings_general:write,templates:delete,admin:read}', 'Admin', 'Role for users who have access to the admin panel.');
 | 
			
		||||
VALUES('{conversations:read_unassigned,conversations:read_all,conversations:read,conversations:read_assigned,conversations:update_user_assignee,conversations:update_team_assignee,conversations:update_priority,conversations:update_status,conversations:update_tags,messages:read,messages:write,templates:write,templates:read,roles:delete,roles:write,roles:read,inboxes:delete,inboxes:write,inboxes:read,automations:write,automations:delete,automations:read,teams:write,teams:read,users:write,users:read,dashboard_global:read,canned_responses:delete,tags:delete,canned_responses:write,tags:write,status:delete,status:write,status:read,oidc:delete,oidc:read,oidc:write,settings_notifications:read,settings_notifications:write,settings_general:write,templates:delete,admin:read}', 'Admin', 'Role for users who have access to the admin panel.');
 | 
			
		||||
INSERT INTO roles
 | 
			
		||||
(permissions, "name", description)
 | 
			
		||||
VALUES('{conversations:read,conversations:read_team,conversations:read_assigned,conversations:update_user_assignee,conversations:update_team_assignee,conversations:update_priority,conversations:update_status,conversations:update_tags,status:write,status:delete,tags:write,tags:delete,canned_responses:write,canned_responses:delete,dashboard:global,users:write,users:read,teams:read,teams:write,automations:read,automations:write,automations:delete,inboxes:read,inboxes:write,inboxes:delete,roles:read,roles:write,roles:delete,templates:read,templates:write,messages:read,messages:write,dashboard_global:read,oidc:delete,status:read,oidc:write,settings_notifications:read,oidc:read,settings_general:write,settings_notifications:write,conversations:read_all,templates:delete}', 'Agent', 'Role for all agents with limited access.');
 | 
			
		||||
VALUES('{conversations:read,conversations:read_unassigned,conversations:read_assigned,conversations:update_user_assignee,conversations:update_team_assignee,conversations:update_priority,conversations:update_status,conversations:update_tags,status:write,status:delete,tags:write,tags:delete,canned_responses:write,canned_responses:delete,dashboard:global,users:write,users:read,teams:read,teams:write,automations:read,automations:write,automations:delete,inboxes:read,inboxes:write,inboxes:delete,roles:read,roles:write,roles:delete,templates:read,templates:write,messages:read,messages:write,dashboard_global:read,oidc:delete,status:read,oidc:write,settings_notifications:read,oidc:read,settings_general:write,settings_notifications:write,conversations:read_all,templates:delete}', 'Agent', 'Role for all agents with limited access.');
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS settings CASCADE;
 | 
			
		||||
CREATE TABLE settings (
 | 
			
		||||
@@ -285,3 +285,27 @@ VALUES
 | 
			
		||||
    ('notification.email.email_address', '""'::jsonb),
 | 
			
		||||
    ('notification.email.max_msg_retries', '3'::jsonb),
 | 
			
		||||
    ('notification.email.enabled', 'false'::jsonb);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
INSERT INTO priority
 | 
			
		||||
(id, "name")
 | 
			
		||||
VALUES(1, 'Low');
 | 
			
		||||
INSERT INTO priority
 | 
			
		||||
(id, "name")
 | 
			
		||||
VALUES(2, 'Medium');
 | 
			
		||||
INSERT INTO priority
 | 
			
		||||
(id, "name")
 | 
			
		||||
VALUES(3, 'High');
 | 
			
		||||
 | 
			
		||||
INSERT INTO status
 | 
			
		||||
(id, "name")
 | 
			
		||||
VALUES(1, 'Open');
 | 
			
		||||
INSERT INTO status
 | 
			
		||||
(id, "name")
 | 
			
		||||
VALUES(2, 'Replied');
 | 
			
		||||
INSERT INTO status
 | 
			
		||||
(id, "name")
 | 
			
		||||
VALUES(3, 'Resolved');
 | 
			
		||||
INSERT INTO status
 | 
			
		||||
(id, "name")
 | 
			
		||||
VALUES(4, 'Closed');
 | 
			
		||||
		Reference in New Issue
	
	Block a user