diff --git a/chat-SDK-JS b/chat-SDK-JS index e4f28b4..f0c68a6 160000 --- a/chat-SDK-JS +++ b/chat-SDK-JS @@ -1 +1 @@ -Subproject commit e4f28b437a76d04e44f745760a07a68bd65da906 +Subproject commit f0c68a68c7b53cb8abf98cb64c6f15cbd7e7fdd2 diff --git a/src/components/App.jsx b/src/components/App.jsx index 2443d32..1832d31 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -1,7 +1,7 @@ import React from 'react'; import skygearChat from 'skygear-chat'; -import ManagedConversationList from '../utils/ManagedConversationList.jsx'; +import ManagedUserConversationList from '../utils/ManagedConversationList.jsx'; import Conversation from './Conversation.jsx'; import ConversationPreview from './ConversationPreview.jsx'; @@ -30,38 +30,45 @@ export default class App extends React.Component { this.state = { unreadCount: 0, // user's total unread message count (int) currentModal: null, // currently displayed modal dialog name (or null) - currentConversation: null // currently selected Conversion Record (or null) + activeID: null // currently selected UserConversion ID (or null) }; - this.conversationList = new ManagedConversationList(); + this.userConversationList = new ManagedUserConversationList(); } componentDidMount() { // subscribe conversation change - this.conversationList.subscribe(() => { - const {currentConversation} = this.state; - const {conversationList} = this; - if (currentConversation) { - this.setState({ - currentConversation: conversationList.get(currentConversation._id) - }); - } else { - this.forceUpdate(); - } + this.userConversationList.subscribe(() => { + this.forceUpdate(); + this.updateUnreadCount(); }); + this.updateUnreadCount(); + } + + updateUnreadCount() { skygearChat.getUnreadCount().then(result => { this.setState({unreadCount: result.message}); }); } + selectConversation(userConversation) { + this.setState({ + activeID: userConversation._id, + unreadCount: this.state.unreadCount - userConversation.unread_count + }); + userConversation.unread_count = 0; + this.updateUnreadCount(); + } + render() { const { state: { unreadCount, currentModal, - currentConversation + activeID }, - conversationList + userConversationList } = this; + const activeUC = userConversationList.get(activeID); return (
{ - conversationList - .map((c) => { + userConversationList + .map((uc) => { return this.setState({currentConversation: c})}/> + uc.id === activeID} + userConversation={uc} + conversation={uc.$transient.conversation} + onClick={() => this.selectConversation(uc)}/> }) }
- {currentConversation && + {activeID && this.setState({currentModal: 'details'})}/> } {(modal => { @@ -111,13 +119,13 @@ export default class App extends React.Component { case 'createGroup': return ( conversationList.add(c)} + addConversationDelegate={c => userConversationList.addConversation(c)} onClose={() => this.setState({currentModal: null})}/> ); case 'createChat': return ( conversationList.add(c)} + addConversationDelegate={c => userConversationList.addConversation(c)} onClose={() => this.setState({currentModal: null})}/> ); case 'settings': @@ -128,11 +136,10 @@ export default class App extends React.Component { case 'details': return ( conversationList.update(c)} - removeConversationDelegate={id => conversationList.remove(id)} + key={'DetailsModal-' + activeID.id} + conversation={activeUC.$transient.conversation} + updateConversationDelegate={c => userConversationList.updateConversation(c)} + removeConversationDelegate={c => userConversationList.removeConversation(c)} onClose={() => this.setState({currentModal: null})}/> ); default: diff --git a/src/components/Conversation.jsx b/src/components/Conversation.jsx index fcfd824..cc99d9e 100644 --- a/src/components/Conversation.jsx +++ b/src/components/Conversation.jsx @@ -108,9 +108,10 @@ export default class Conversation extends React.Component { createdBy: skygear.currentUser.id }); this.messageList.add(message); - skygear.privateDB.save(message); - // force update the conversation on new message to trigger pubsub event - skygearChat.updateConversation(conversation); + skygear.privateDB.save(message).then(() => { + // force update the conversation on new message to trigger pubsub event + skygearChat.updateConversation(conversation); + }); } } render() { diff --git a/src/components/ConversationPreview.jsx b/src/components/ConversationPreview.jsx index fe02f1e..f05a887 100644 --- a/src/components/ConversationPreview.jsx +++ b/src/components/ConversationPreview.jsx @@ -8,7 +8,7 @@ export default class ConversationPreview extends React.Component { super(props); const {title} = props.conversation; this.state = { - title: title || 'loading...', // conversation title (either group name or participant names) + title: title || 'loading...', // conversation title (either group name or participant names) imageURL: 'img/loading.svg' // conversation image URL }; } @@ -43,23 +43,17 @@ export default class ConversationPreview extends React.Component { } render() { const { - props: { - selected, - onClick, - conversation: { - unread_count, - $transient: { - last_message: { - body: lastMessage - } = {} - } - } - }, - state: { - title, - imageURL - } - } = this; + selected, + onClick, + } = this.props; + const { + unread_count + } = this.props.userConversation; + const { + title, + imageURL + } = this.state; + const lastMessage = this.props.conversation.$transient.last_message; return (
- { lastMessage.length > 23 ? - lastMessage.substring(0, 20) + '...' : lastMessage } + { lastMessage.body.length > 23 ? + lastMessage.body.substring(0, 20) + '...' : lastMessage.body } }
diff --git a/src/components/DetailsModal.jsx b/src/components/DetailsModal.jsx index c754522..e13d51a 100644 --- a/src/components/DetailsModal.jsx +++ b/src/components/DetailsModal.jsx @@ -77,7 +77,7 @@ export default class DetailsModal extends React.Component { ).then(() => { // close modal after leaving onClose(); - removeConversationDelegate(conversation._id); + removeConversationDelegate(conversation); }); } discoverAndAddUser(username) { diff --git a/src/utils/ManagedConversationList.jsx b/src/utils/ManagedConversationList.jsx index 4e979b9..f439b40 100644 --- a/src/utils/ManagedConversationList.jsx +++ b/src/utils/ManagedConversationList.jsx @@ -50,10 +50,10 @@ export class ConversationSorting { } /** - * A managed list of conversations for the current user. - * Note: You must be logged in to use this. + * A managed list of user conversations for the current user. + * Note: You must be logged in at skygear to use this. */ -export default class ManagedConversationList { +export default class ManagedUserConversationList { /** * @param {object} [options={}] * @param {boolean} [options.initialFetch=true] @@ -73,8 +73,10 @@ export default class ManagedConversationList { this._compare = sortBy; // conversation IDs in order (without type prefix) this._orderedIDs = []; - // map of conversaton ID => conversation object - this._conversations = {}; + // map of user conversaton ID => user conversation object + this._userConversations = {}; + // map of conversaton ID => user conversation object + this._ucByCId = {}; // map of subscription ID => event handler this._updateHandlers = {}; @@ -100,13 +102,13 @@ export default class ManagedConversationList { console.log('[conversation event]', event); switch (event.event_type) { case 'create': - this.add(event.record); + this.addConversation(event.record); break; case 'update': - this.update(event.record); + this.updateConversation(event.record); break; case 'delete': - this.remove(event.record._id); + this.removeConversation(event.record); break; } } @@ -114,10 +116,10 @@ export default class ManagedConversationList { /** * @private */ - _conversationsUpdated() { - const {_conversations, _compare, _updateHandlers} = this; - this._orderedIDs = Object.keys(_conversations) - .map(id => _conversations[id]) + _userConversationsUpdated() { + const {_userConversations, _compare, _updateHandlers} = this; + this._orderedIDs = Object.keys(_userConversations) + .map(id => _userConversations[id]) .sort(_compare) .map(conversation => conversation._id); Object.keys(_updateHandlers) @@ -125,27 +127,42 @@ export default class ManagedConversationList { .forEach(handler => handler(this)); } /** - * Fetches list of conversations from the server. - * @return {Promise} + * Fetches list of user conversations from the server. + * @return {Promise} * Promise of this object, resolves if the fetch is successful. */ fetch() { - // FIXME: use the getConversations() API when it is fixed return skygearChat .getUserConversations() - .then(results => results.map(uc => { - uc.$transient.conversation.unread_count = uc.unread_count; - return uc.$transient.conversation; - })) .then(results => { - console.log('[fetched conversations]', results); - results.forEach(conversation => { - this._conversations[conversation._id] = conversation; + console.log('[fetched user conversations]', results); + results.forEach((uc) => { + this._userConversations[uc._id] = uc; + this._ucByCId[uc.$transient.conversation._id] = uc; }); - this._conversationsUpdated(); + this._userConversationsUpdated(); return this; }); } + /** + * Update user conversations from the server by providing a conversation + * @param {Conversation} conversation + * @return {ManagedUserConversationList} + */ + updateOne(conversation) { + const { + _userConversations, + _ucByCId + } = this; + skygearChat + .getUserConversation(conversation) + .then((uc) => { + _userConversations[uc._id] = uc; + _ucByCId[conversation._id] = uc; + this._userConversationsUpdated(); + }); + return this; + } /** * List length (like the array length property) * @type {number} @@ -154,17 +171,17 @@ export default class ManagedConversationList { return this._orderedIDs.length; } /** - * Get a Conversation + * Get a User Conversation * @param {number|string} indexOrID * Either the list index (number) or conversation ID (string) without type prefix. * @return {Conversation} */ get(indexOrID) { - const {_conversations, _orderedIDs} = this; + const {_userConversations, _orderedIDs} = this; if (typeof indexOrID === 'number') { - return _conversations[_orderedIDs[indexOrID]]; + return _userConversations[_orderedIDs[indexOrID]]; } else { - return _conversations[indexOrID]; + return _userConversations[indexOrID]; } } /** @@ -174,7 +191,7 @@ export default class ManagedConversationList { */ map(mappingFunction) { return this._orderedIDs - .map(id => this._conversations[id]) + .map(id => this._userConversations[id]) .map(mappingFunction); } /** @@ -184,51 +201,59 @@ export default class ManagedConversationList { */ filter(predicate) { return this._orderedIDs - .map(id => this._conversations[id]) + .map(id => this._userConversations[id]) .filter(predicate); } /** * Add a conversation, no-op if the conversation already exists. * @param {Conversation} conversation - * @return {ManagedConversationList} + * @return {ManagedUserConversationList} */ - add(conversation) { - const {_conversations} = this; - if (!_conversations.hasOwnProperty(conversation._id)) { - _conversations[conversation._id] = conversation; - this._conversationsUpdated(); + addConversation(conversation) { + const { + _ucByCId + } = this; + if (_ucByCId.hasOwnProperty(conversation._id)) { + return this; } - return this; + return this.updateOne(conversation); } /** * Update a conversation, no-op if the conversation updatedAt date is older than existing. * Conversation will be added if it's not in the list. * @param {Conversation} conversation - * @return {ManagedConversationList} - */ - update(conversation) { - const {_conversations} = this; - if ( - _conversations.hasOwnProperty(conversation._id) && - conversation.updatedAt >= _conversations[conversation._id].updatedAt - ) { - _conversations[conversation._id] = conversation; - this._conversationsUpdated(); + * @return {ManagedUserConversationList} + */ + updateConversation(conversation) { + const {_ucByCId} = this; + if (_ucByCId.hasOwnProperty(conversation._id)) { + const uc = _ucByCId[conversation._id]; + const c = uc.$transient.conversation; + if (conversation.updatedAt <= c.updatedAt) { + return this + } + return this.updateOne(conversation); } else { - this.add(conversation); + this.addConversation(conversation); } return this; } /** * Remove a conversation, no-op if the conversation doesn't exist. - * @param {string} conversationID Conversation ID without type prefix. - * @return {ManagedConversationList} - */ - remove(conversationID) { - const {_conversations} = this; - if (_conversations.hasOwnProperty(conversationID)) { - delete _conversations[conversationID]; - this._conversationsUpdated(); + * @param {conversation} conversation Conversation without type prefix. + * @return {ManagedUserConversationList} + */ + removeConversation(conversation) { + const { + _userConversations, + _ucByCId + } = this; + const conversationID = conversation._id; + if (_ucById.hasOwnProperty(conversationID)) { + const uc = _ucByCId[conversationID] + delete _userConversations[uc._id]; + delete _ucById[conversationID]; + this._userConversationsUpdated(); } return this; } diff --git a/src/utils/ManagedMessageList.jsx b/src/utils/ManagedMessageList.jsx index cf5987f..693d41a 100644 --- a/src/utils/ManagedMessageList.jsx +++ b/src/utils/ManagedMessageList.jsx @@ -94,8 +94,9 @@ export default class ManagedMessageList { results.forEach(message => { this._messages[message._id] = message; }); - if (this._autoRead) { - skygearChat.markAsRead(results); + if (this._autoRead && results) { + skygearChat.markAsLastMessageRead( + this._conversation, results[0]); } this._messagesUpdated(); return this; @@ -154,7 +155,7 @@ export default class ManagedMessageList { _messages[message._id] = message; this._messagesUpdated(); if (this._autoRead) { - skygearChat.markAsRead([message]); + skygearChat.markAsLastMessageRead(this._conversation, message); } } return this;