From 7f1f1f24d14e312a29ee71e12c077284a0a4e6d1 Mon Sep 17 00:00:00 2001 From: Alex Reichert Date: Fri, 12 Feb 2021 13:42:16 -0500 Subject: [PATCH] Load previous conversations within main conversation UI (#569) * First pass at refactoring conversations dashboard UI (WIP) * Remove unused code * Remove unused async * Add ability to load previous conversation in conversation UI --- assets/src/App.css | 8 + .../conversations/AllConversations.tsx | 15 +- .../conversations/ClosedConversations.tsx | 15 +- .../conversations/ConversationContainer.tsx | 180 ++++++++++++++ .../conversations/ConversationMessages.tsx | 126 +++++++++- ...ntainer.tsx => ConversationsDashboard.tsx} | 219 +++++------------- .../ConversationsPreviewList.tsx | 73 ++++++ .../conversations/ConversationsProvider.tsx | 131 ++++++----- .../conversations/MyConversations.tsx | 15 +- .../conversations/PriorityConversations.tsx | 15 +- assets/src/types.ts | 1 + lib/chat_api/conversations.ex | 30 +++ 12 files changed, 562 insertions(+), 266 deletions(-) create mode 100644 assets/src/components/conversations/ConversationContainer.tsx rename assets/src/components/conversations/{ConversationsContainer.tsx => ConversationsDashboard.tsx} (55%) create mode 100644 assets/src/components/conversations/ConversationsPreviewList.tsx diff --git a/assets/src/App.css b/assets/src/App.css index 5ddf160e0..e069c54dd 100644 --- a/assets/src/App.css +++ b/assets/src/App.css @@ -50,6 +50,14 @@ body { color: #fff; } +.Button--faded { + opacity: 0.6; +} + +.Button--faded:hover { + opacity: 1; +} + /* Mimic behavior of antd "text"-type Button */ a.RelatedCustomerConversation--link { display: block; diff --git a/assets/src/components/conversations/AllConversations.tsx b/assets/src/components/conversations/AllConversations.tsx index 98412ff4b..5c47d91a5 100644 --- a/assets/src/components/conversations/AllConversations.tsx +++ b/assets/src/components/conversations/AllConversations.tsx @@ -1,17 +1,14 @@ import React from 'react'; import {useConversations} from './ConversationsProvider'; -import ConversationsContainer from './ConversationsContainer'; +import ConversationsDashboard from './ConversationsDashboard'; const AllConversations = () => { const { loading, currentUser, account, - isNewUser, all = [], - conversationsById = {}, messagesByConversation = {}, - currentlyOnline = {}, fetchAllConversations, onSelectConversation, onUpdateConversation, @@ -19,16 +16,16 @@ const AllConversations = () => { onSendMessage, } = useConversations(); + if (!currentUser) { + return null; + } + return ( - { @@ -7,11 +7,8 @@ const ClosedConversations = () => { loading, currentUser, account, - isNewUser, closed = [], - conversationsById = {}, messagesByConversation = {}, - currentlyOnline = {}, fetchAllConversations, fetchClosedConversations, onSelectConversation, @@ -29,16 +26,16 @@ const ClosedConversations = () => { return results; }; + if (!currentUser) { + return null; + } + return ( - void; + onAssignUser: (conversationId: string, userId: string) => void; + onMarkPriority: (conversationId: string) => void; + onRemovePriority: (conversationId: string) => void; + onCloseConversation: (conversationId: string) => void; + onReopenConversation: (conversationId: string) => void; + onDeleteConversation: (conversationId: string) => void; + onSendMessage: (message: Partial) => void; +}) => { + // TODO: handle loading better? + const { + currentUser, + account, + conversationsById, + messagesByConversation, + isNewUser, + isCustomerOnline, + } = useConversations(); + const [history, setConversationHistory] = React.useState>( + [] + ); + const [ + isLoadingPreviousConversation, + setLoadingPreviousConversation, + ] = React.useState(false); + const [ + hasPreviousConversations, + setHasPreviousConversations, + ] = React.useState(false); + + React.useEffect(() => { + setConversationHistory([]); + setLoadingPreviousConversation(false); + setHasPreviousConversations(false); + + if (!selectedConversationId) { + return; + } + + API.fetchPreviousConversation(selectedConversationId) + .then((conversation) => setHasPreviousConversations(!!conversation)) + .catch((err) => + logger.error('Error retrieving previous conversation:', err) + ); + }, [selectedConversationId]); + + const users = (account && account.users) || []; + const messages = selectedConversationId + ? messagesByConversation[selectedConversationId] + : []; + const conversation = selectedConversationId + ? conversationsById[selectedConversationId] + : null; + const customer = conversation ? conversation.customer : null; + const isOnline = customer ? isCustomerOnline(customer.id) : false; + + const fetchPreviousConversation = async (conversationId: string) => { + if (!selectedConversationId) { + return; + } + + setLoadingPreviousConversation(true); + + API.fetchPreviousConversation(conversationId) + .then((conversation) => { + const previousConversationId = conversation && conversation.id; + + if (previousConversationId) { + setConversationHistory([conversation, ...history]); + + return API.fetchPreviousConversation(previousConversationId); + } + + return null; + }) + .then((conversation) => setHasPreviousConversations(!!conversation)) + .catch((err) => + logger.error('Error retrieving previous conversation:', err) + ) + .finally(() => setLoadingPreviousConversation(false)); + }; + + return ( + <> + + + + + {conversation && ( + // NB: the `key` forces a rerender so the input can clear + // any text from the last conversation and trigger autofocus + + )} + + {customer && conversation && ( + + + + )} + + + ); +}; + +export default ConversationContainer; diff --git a/assets/src/components/conversations/ConversationMessages.tsx b/assets/src/components/conversations/ConversationMessages.tsx index a5901ff55..4c4697efb 100644 --- a/assets/src/components/conversations/ConversationMessages.tsx +++ b/assets/src/components/conversations/ConversationMessages.tsx @@ -1,11 +1,13 @@ import React from 'react'; import {Link} from 'react-router-dom'; import {Box, Flex} from 'theme-ui'; -import {Button, colors, Result} from '../common'; -import {SmileOutlined} from '../icons'; +import {colors, Button, Divider, Result} from '../common'; +import {SmileOutlined, UpOutlined} from '../icons'; import Spinner from '../Spinner'; import ChatMessage from './ChatMessage'; -import {Message, User} from '../../types'; +import {Conversation, Message, User} from '../../types'; + +const noop = () => {}; const EmptyMessagesPlaceholder = () => { return ( @@ -38,24 +40,35 @@ const GettingStartedRedirect = () => { }; const ConversationMessages = ({ + conversationId, messages, currentUser, loading, isClosing, showGetStarted, + isLoadingPreviousConversation, + hasPreviousConversations, + history = [], sx = {}, setScrollRef, isAgentMessage, + onLoadPreviousConversation = noop, }: { + conversationId?: string | null; messages: Array; currentUser?: User | null; loading?: boolean; isClosing?: boolean; showGetStarted?: boolean; + isLoadingPreviousConversation?: boolean; + hasPreviousConversations?: boolean; + history?: Array; sx?: any; setScrollRef: (el: any) => void; isAgentMessage?: (message: Message) => boolean; + onLoadPreviousConversation?: (conversationId: string) => void; }) => { + const [historyRefs, setHistoryRefs] = React.useState>([]); // Sets old behavior as default, but eventually we may just want to show // any message with a `user_id` (as opposed to `customer_id`) as an agent // (Note that this will require an update to the UI component @@ -67,6 +80,38 @@ const ConversationMessages = ({ ? isAgentMessage : isAgentMessageDefaultFn; + const addToHistoryRefs = (el: any) => { + if (el && el.id) { + const ids = historyRefs.map((el) => el.id); + + if (ids.includes(el.id)) { + return; + } + + setHistoryRefs([el, ...historyRefs]); + + // TODO: figure out the best way to handle this scroll behavior... + // might be nice to add a nice animation when the previous conversation is loaded + el.scrollIntoView({ + behavior: 'auto', + block: 'start', + inline: 'nearest', + }); + } + }; + + const handleLoadPrevious = () => { + if (history && history.length) { + const [{id: earliestConversationId}] = history; + + onLoadPreviousConversation(earliestConversationId); + } else if (conversationId) { + onLoadPreviousConversation(conversationId); + } else { + // No conversation detected yet; do nothing + } + }; + return ( + {hasPreviousConversations ? ( + + + + ) : ( + + )} + + {history && history.length + ? history.map((conversation: Conversation) => { + const {id: conversationId, messages = []} = conversation; + + return ( + + + {messages.map((message: Message, key: number) => { + // Slight hack + const next = messages[key + 1]; + const { + id: messageId, + customer_id: customerId, + } = message; + const isMe = isAgentMsg(message); + const isLastInGroup = next + ? customerId !== next.customer_id + : true; + + // TODO: fix `isMe` logic for multiple agents + return ( + + ); + })} + +
+ + + ); + }) + : null} + {messages.length ? ( - messages.map((msg: Message, key: number) => { + messages.map((message: Message, key: number) => { // Slight hack const next = messages[key + 1]; - const isMe = isAgentMsg(msg); + const {id: messageId, customer_id: customerId} = message; + const isMe = isAgentMsg(message); const isLastInGroup = next - ? msg.customer_id !== next.customer_id + ? customerId !== next.customer_id : true; // TODO: fix `isMe` logic for multiple agents return ( ; - conversationsById: {[key: string]: Conversation}; messagesByConversation: {[key: string]: Array}; fetch: () => Promise>; onSelectConversation: (id: string | null, fn?: () => void) => void; @@ -31,14 +22,14 @@ type Props = { type State = { loading: boolean; - selected: string | null; + selectedConversationId: string | null; closing: Array; }; -class ConversationsContainer extends React.Component { +class ConversationsDashboard extends React.Component { scrollToEl: any = null; - state: State = {loading: true, selected: null, closing: []}; + state: State = {loading: true, selectedConversationId: null, closing: []}; componentDidMount() { const q = qs.parse(window.location.search); @@ -66,15 +57,16 @@ class ConversationsContainer extends React.Component { } componentDidUpdate(prev: Props) { - if (!this.state.selected) { + if (!this.state.selectedConversationId) { return null; } - const {selected} = this.state; + const {selectedConversationId} = this.state; const {messagesByConversation: prevMessagesByConversation} = prev; const {messagesByConversation} = this.props; - const prevMessages = prevMessagesByConversation[selected] || []; - const messages = messagesByConversation[selected] || []; + const prevMessages = + prevMessagesByConversation[selectedConversationId] || []; + const messages = messagesByConversation[selectedConversationId] || []; if (messages.length > prevMessages.length) { this.scrollIntoView(); @@ -101,6 +93,8 @@ class ConversationsContainer extends React.Component { return null; } + const {selectedConversationId} = this.state; + // TODO: clean up a bit switch (key) { case 'ArrowDown': @@ -115,27 +109,29 @@ class ConversationsContainer extends React.Component { e.preventDefault(); return ( - this.state.selected && - this.handleCloseConversation(this.state.selected) + selectedConversationId && + this.handleCloseConversation(selectedConversationId) ); case 'p': e.preventDefault(); return ( - this.state.selected && this.handleMarkPriority(this.state.selected) + selectedConversationId && + this.handleMarkPriority(selectedConversationId) ); case 'u': e.preventDefault(); return ( - this.state.selected && this.handleMarkUnpriority(this.state.selected) + selectedConversationId && + this.handleMarkUnpriority(selectedConversationId) ); case 'o': e.preventDefault(); return ( - this.state.selected && - this.handleReopenConversation(this.state.selected) + selectedConversationId && + this.handleReopenConversation(selectedConversationId) ); default: return null; @@ -143,7 +139,7 @@ class ConversationsContainer extends React.Component { }; getNextConversationId = () => { - const {selected} = this.state; + const {selectedConversationId} = this.state; const {conversationIds = []} = this.props; if (conversationIds.length === 0) { @@ -152,17 +148,17 @@ class ConversationsContainer extends React.Component { const lastConversationId = conversationIds[conversationIds.length - 1]; - if (!selected) { + if (!selectedConversationId) { return lastConversationId; } - const index = conversationIds.indexOf(selected); + const index = conversationIds.indexOf(selectedConversationId); return conversationIds[index + 1] || lastConversationId || null; }; getPreviousConversationId = () => { - const {selected} = this.state; + const {selectedConversationId} = this.state; const {conversationIds = []} = this.props; if (conversationIds.length === 0) { @@ -171,21 +167,23 @@ class ConversationsContainer extends React.Component { const firstConversationId = conversationIds[0]; - if (!selected) { + if (!selectedConversationId) { return firstConversationId; } - const index = conversationIds.indexOf(selected); + const index = conversationIds.indexOf(selectedConversationId); return conversationIds[index - 1] || firstConversationId; }; // TODO: make sure this works as expected refreshSelectedConversation = async () => { - const {selected} = this.state; + const {selectedConversationId} = this.state; const nextId = this.getNextConversationId(); const updatedIds = await this.props.fetch(); - const hasValidSelectedId = selected && updatedIds.indexOf(selected) !== -1; + const hasValidSelectedId = + selectedConversationId && + updatedIds.indexOf(selectedConversationId) !== -1; if (!hasValidSelectedId) { const hasValidNextId = nextId && updatedIds.indexOf(nextId) !== -1; @@ -195,19 +193,8 @@ class ConversationsContainer extends React.Component { } }; - isCustomerOnline = (customerId: string) => { - if (!customerId) { - return false; - } - - const {currentlyOnline = {}} = this.props; - const key = `customer:${customerId}`; - - return !!(currentlyOnline && currentlyOnline[key]); - }; - handleSelectConversation = (id: string | null) => { - this.setState({selected: id}, () => { + this.setState({selectedConversationId: id}, () => { this.scrollIntoView(); }); @@ -219,7 +206,7 @@ class ConversationsContainer extends React.Component { // TODO: figure out the best way to handle this when closing multiple // conversations in a row very quickly - await sleep(1000); + await sleep(400); await this.props.onUpdateConversation(conversationId, {status: 'closed'}); await this.refreshSelectedConversation(); @@ -283,7 +270,7 @@ class ConversationsContainer extends React.Component { }; handleSendMessage = (message: Partial) => { - const {selected: conversationId} = this.state; + const {selectedConversationId: conversationId} = this.state; if (!conversationId) { return null; @@ -298,35 +285,12 @@ class ConversationsContainer extends React.Component { }; render() { - const {selected: selectedConversationId, closing = []} = this.state; - const { - title, - account, - currentUser, - showGetStarted, - conversationIds = [], - conversationsById = {}, - messagesByConversation = {}, - } = this.props; - const users = (account && account.users) || []; - - const messages = selectedConversationId - ? messagesByConversation[selectedConversationId] - : []; - const selectedConversation = selectedConversationId - ? conversationsById[selectedConversationId] - : null; - const selectedCustomer = selectedConversation - ? selectedConversation.customer - : null; - + const {selectedConversationId, closing = []} = this.state; + const {title, conversationIds = []} = this.props; const loading = this.props.loading || this.state.loading; const isClosingSelected = !!selectedConversationId && closing.indexOf(selectedConversationId) !== -1; - const isSelectedCustomerOnline = selectedCustomer - ? this.isCustomerOnline(selectedCustomer.id) - : false; return ( @@ -347,109 +311,34 @@ class ConversationsContainer extends React.Component { - - {!loading && conversationIds.length ? ( - conversationIds.map((conversationId, idx) => { - const conversation = conversationsById[conversationId]; - const messages = messagesByConversation[conversationId]; - const {customer_id: customerId} = conversation; - const isCustomerOnline = this.isCustomerOnline(customerId); - const isHighlighted = conversationId === selectedConversationId; - const isClosing = closing.indexOf(conversationId) !== -1; - const color = getColorByUuid(customerId); - - if (isClosing) { - return ( - - ); - } - - return ( - - ); - }) - ) : ( - - - {loading ? 'Loading...' : 'No conversations'} - - - )} - + + closing.indexOf(conversationId) !== -1 + } + onSelectConversation={this.handleSelectConversation} + /> - (this.scrollToEl = el)} onAssignUser={this.handleAssignUser} onMarkPriority={this.handleMarkPriority} onRemovePriority={this.handleMarkUnpriority} onCloseConversation={this.handleCloseConversation} onReopenConversation={this.handleReopenConversation} onDeleteConversation={this.handleDeleteConversation} + onSendMessage={this.handleSendMessage} /> - - (this.scrollToEl = el)} - /> - - {selectedConversation && ( - // NB: the `key` forces a rerender so the input can clear - // any text from the last conversation and trigger autofocus - - )} - - {selectedCustomer && selectedConversation && ( - - - - )} - ); } } -export default ConversationsContainer; +export default ConversationsDashboard; diff --git a/assets/src/components/conversations/ConversationsPreviewList.tsx b/assets/src/components/conversations/ConversationsPreviewList.tsx new file mode 100644 index 000000000..6406da5ec --- /dev/null +++ b/assets/src/components/conversations/ConversationsPreviewList.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import {Box} from 'theme-ui'; +import {Text} from '../common'; +import ConversationItem from './ConversationItem'; +import ConversationClosing from './ConversationClosing'; +import {getColorByUuid} from './support'; +import {useConversations} from './ConversationsProvider'; + +const ConversationsPreviewList = ({ + loading, + selectedConversationId, + conversationIds, + isConversationClosing, + onSelectConversation, +}: { + loading: boolean; + selectedConversationId: string | null; + conversationIds: Array; + isConversationClosing: (conversationId: string) => boolean; + onSelectConversation: (conversationId: string | null) => any; +}) => { + const { + conversationsById, + messagesByConversation, + isCustomerOnline, + } = useConversations(); + + return ( + + {!loading && conversationIds.length ? ( + conversationIds.map((conversationId) => { + const conversation = conversationsById[conversationId]; + // TODO: we only care about the most recent message? + const messages = messagesByConversation[conversationId]; + const {customer_id: customerId} = conversation; + const isOnline = isCustomerOnline(customerId); + const isHighlighted = conversationId === selectedConversationId; + const isClosing = isConversationClosing(conversationId); + const color = getColorByUuid(customerId); + + if (isClosing) { + return ( + + ); + } + + return ( + + ); + }) + ) : ( + + + {loading ? 'Loading...' : 'No conversations'} + + + )} + + ); +}; + +export default ConversationsPreviewList; diff --git a/assets/src/components/conversations/ConversationsProvider.tsx b/assets/src/components/conversations/ConversationsProvider.tsx index aaf394ddd..f6f14adc1 100644 --- a/assets/src/components/conversations/ConversationsProvider.tsx +++ b/assets/src/components/conversations/ConversationsProvider.tsx @@ -1,6 +1,6 @@ import React, {useContext} from 'react'; import {Channel, Socket} from 'phoenix'; -import {throttle} from 'lodash'; +import {debounce, throttle} from 'lodash'; import * as API from '../../api'; import {notification} from '../common'; import {Account, Conversation, Message, User} from '../../types'; @@ -27,6 +27,8 @@ export const ConversationsContext = React.createContext<{ messagesByConversation: {[key: string]: any}; currentlyOnline: {[key: string]: any}; + isCustomerOnline: (customerId: string) => boolean; + onSelectConversation: (id: string | null) => any; onUpdateConversation: (id: string, params: any) => Promise; onDeleteConversation: (id: string) => Promise; @@ -53,6 +55,7 @@ export const ConversationsContext = React.createContext<{ messagesByConversation: {}, currentlyOnline: {}, + isCustomerOnline: () => false, onSelectConversation: () => {}, onSendMessage: () => {}, onUpdateConversation: () => Promise.resolve(), @@ -194,10 +197,9 @@ export class ConversationsProvider extends React.Component { account, isNewUser: numTotalMessages === 0, }); - const conversationIds = await this.fetchAllConversations(); const {id: accountId} = account; - this.joinNotificationChannel(accountId, conversationIds); + this.joinNotificationChannel(accountId); } componentWillUnmount() { @@ -210,10 +212,7 @@ export class ConversationsProvider extends React.Component { } } - joinNotificationChannel = ( - accountId: string, - conversationIds: Array - ) => { + joinNotificationChannel = (accountId: string) => { if (this.socket && this.socket.disconnect) { logger.debug('Existing socket:', this.socket); this.socket.disconnect(); @@ -240,9 +239,7 @@ export class ConversationsProvider extends React.Component { // TODO: If no conversations exist, should we create a conversation with us // so new users can play around with the chat right away and give us feedback? - this.channel = this.socket.channel(`notification:${accountId}`, { - ids: conversationIds, - }); + this.channel = this.socket.channel(`notification:${accountId}`, {}); // TODO: rename to message:created? this.channel.on('shout', (message) => { @@ -259,7 +256,7 @@ export class ConversationsProvider extends React.Component { // TODO: can probably use this for more things this.channel.on('conversation:updated', ({id, updates}) => { // Handle conversation updated - this.handleConversationUpdated(id, updates); + debounce(() => this.handleConversationUpdated(id, updates), 1000); }); this.channel.on('presence_state', (state) => { @@ -278,10 +275,7 @@ export class ConversationsProvider extends React.Component { .receive('error', (err) => { logger.error('Unable to join', err); // TODO: double check that this works (retries after 10s) - setTimeout( - () => this.joinNotificationChannel(accountId, conversationIds), - 10000 - ); + setTimeout(() => this.joinNotificationChannel(accountId), 10000); }); }; @@ -307,6 +301,17 @@ export class ConversationsProvider extends React.Component { this.setState({presence: latest}); }; + isCustomerOnline = (customerId: string) => { + if (!customerId) { + return false; + } + + const {presence = {}} = this.state; + const key = `customer:${customerId}`; + + return !!(presence && presence[key]); + }; + playNotificationSound = async (volume: number) => { try { const file = '/alert-v2.mp3'; @@ -328,12 +333,8 @@ export class ConversationsProvider extends React.Component { handleNewMessage = async (message: Message) => { logger.debug('New message!', message); - const { - messagesByConversation, - selectedConversationId, - conversationsById, - } = this.state; - const {conversation_id: conversationId, customer_id: customerId} = message; + const {messagesByConversation} = this.state; + const {conversation_id: conversationId} = message; const existingMessages = messagesByConversation[conversationId] || []; const updatedMessagesByConversation = { ...messagesByConversation, @@ -344,46 +345,60 @@ export class ConversationsProvider extends React.Component { { messagesByConversation: updatedMessagesByConversation, }, - () => { - if (isWindowHidden(document || window.document)) { - // Play a slightly louder sound if this is the first message - const volume = existingMessages.length === 0 ? 0.2 : 0.1; + () => + this.debouncedNewMessagesCallback(message, { + isFirstMessage: existingMessages.length === 0, + }) + ); + }; - this.throttledNotificationSound(volume); - } - // TODO: this is a bit hacky... there's probably a better way to - // handle listening for changes on conversation records... - if (selectedConversationId === conversationId) { - // If the new message matches the id of the selected conversation, - // mark it as read right away and scroll to the latest message - this.handleConversationRead(selectedConversationId); - } else { - // Otherwise, find the updated conversation and mark it as unread - const conversation = conversationsById[conversationId]; - const shouldDisplayAlert = - !!customerId && conversation && conversation.status === 'open'; - - this.setState({ - conversationsById: { - ...conversationsById, - [conversationId]: {...conversation, read: false}, - }, - }); + debouncedNewMessagesCallback = debounce( + (message: Message, {isFirstMessage}: {isFirstMessage: boolean}) => { + const {selectedConversationId, conversationsById} = this.state; + const { + conversation_id: conversationId, + customer_id: customerId, + } = message; + + if (isWindowHidden(document || window.document)) { + // Play a slightly louder sound if this is the first message + const volume = isFirstMessage ? 0.2 : 0.1; + + this.throttledNotificationSound(volume); + } + // TODO: this is a bit hacky... there's probably a better way to + // handle listening for changes on conversation records... + if (selectedConversationId === conversationId) { + // If the new message matches the id of the selected conversation, + // mark it as read right away and scroll to the latest message + this.handleConversationRead(selectedConversationId); + } else { + // Otherwise, find the updated conversation and mark it as unread + const conversation = conversationsById[conversationId]; + const shouldDisplayAlert = + !!customerId && conversation && conversation.status === 'open'; + + this.setState({ + conversationsById: { + ...conversationsById, + [conversationId]: {...conversation, read: false}, + }, + }); - if (shouldDisplayAlert) { - notification.open({ - message: 'New message', - description: ( - - {message.body} - - ), - }); - } + if (shouldDisplayAlert) { + notification.open({ + message: 'New message', + description: ( + + {message.body} + + ), + }); } } - ); - }; + }, + 1000 + ); handleConversationRead = (conversationId: string | null) => { if (!this.channel || !conversationId) { @@ -719,6 +734,8 @@ export class ConversationsProvider extends React.Component { messagesByConversation, currentlyOnline: presence, + isCustomerOnline: this.isCustomerOnline, + onSelectConversation: this.handleSelectConversation, onUpdateConversation: this.handleUpdateConversation, onDeleteConversation: this.handleDeleteConversation, diff --git a/assets/src/components/conversations/MyConversations.tsx b/assets/src/components/conversations/MyConversations.tsx index 86efdb75b..901068ab5 100644 --- a/assets/src/components/conversations/MyConversations.tsx +++ b/assets/src/components/conversations/MyConversations.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import ConversationsContainer from './ConversationsContainer'; +import ConversationsDashboard from './ConversationsDashboard'; import {useConversations} from './ConversationsProvider'; const MyConversations = () => { @@ -7,11 +7,8 @@ const MyConversations = () => { loading, currentUser, account, - isNewUser, mine = [], - conversationsById = {}, messagesByConversation = {}, - currentlyOnline = {}, fetchMyConversations, onSelectConversation, onUpdateConversation, @@ -19,16 +16,16 @@ const MyConversations = () => { onSendMessage, } = useConversations(); + if (!currentUser) { + return null; + } + return ( - { @@ -7,11 +7,8 @@ const PriorityConversations = () => { loading, currentUser, account, - isNewUser, priority = [], - conversationsById = {}, messagesByConversation = {}, - currentlyOnline = {}, fetchPriorityConversations, onSelectConversation, onUpdateConversation, @@ -19,16 +16,16 @@ const PriorityConversations = () => { onSendMessage, } = useConversations(); + if (!currentUser) { + return null; + } + return ( - Repo.all() end + @spec list_conversations_by_account_v2(binary(), map()) :: [Conversation.t()] + def list_conversations_by_account_v2(account_id, filters \\ %{}) do + # TODO: eventually DRY this up with `list_recent_by_customer/3` below... but for now + # we're cool with some code duplication while we're still figuring out the shape of our APIs + + # For more info, see https://hexdocs.pm/ecto/Ecto.Query.html#preload/3-preload-queries + # and https://hexdocs.pm/ecto/Ecto.Query.html#windows/3-window-expressions + ranking_query = + from m in Message, + select: %{id: m.id, row_number: row_number() |> over(:messages_partition)}, + windows: [ + messages_partition: [partition_by: :conversation_id, order_by: [desc: :inserted_at]] + ] + + # We just want to query the most recent message + messages_query = + from m in Message, + join: r in subquery(ranking_query), + on: m.id == r.id and r.row_number <= 1, + preload: [:attachments, :customer, user: :profile] + + Conversation + |> where(account_id: ^account_id) + |> where(^filter_where(filters)) + |> where([c], is_nil(c.archived_at)) + |> order_by_most_recent_message() + |> preload([:customer, messages: ^messages_query]) + |> Repo.all() + end + @spec list_other_recent_conversations(Conversation.t(), integer(), map()) :: [Conversation.t()] def list_other_recent_conversations( %Conversation{