From 004d79fe3a0423fa95c793186999625f4039f7cb Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Wed, 4 Nov 2020 18:03:40 -0800 Subject: [PATCH 01/44] Order by most recent along with report icons --- src/libs/API.js | 6 ++-- src/libs/actions/PersonalDetails.js | 7 ++-- src/libs/actions/Report.js | 41 ++++++++++++++++++---- src/pages/home/sidebar/ChatSwitcherList.js | 7 ++-- src/pages/home/sidebar/ChatSwitcherView.js | 29 +++++++++++---- 5 files changed, 65 insertions(+), 25 deletions(-) diff --git a/src/libs/API.js b/src/libs/API.js index dd75538b501c..f2015166cc07 100644 --- a/src/libs/API.js +++ b/src/libs/API.js @@ -404,8 +404,8 @@ function authenticate(parameters) { * @param {number} parameters.sequenceNumber * @returns {Promise} */ -function setLastReadActionID(parameters) { - return queueRequest('Report_SetLastReadActionID', { +function updateLastAccessed(parameters) { + return queueRequest('Report_UpdateLastAccessed', { authToken, accountID: parameters.accountID, reportID: parameters.reportID, @@ -526,7 +526,7 @@ export { getAuthToken, getPersonalDetails, getReportHistory, - setLastReadActionID, + updateLastAccessed, setNameValuePair, togglePinnedReport, logToServer, diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index 1d4c8604057a..bc5eafa453f4 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -131,11 +131,7 @@ function fetch() { * @param {String} emailList */ function getForEmails(emailList) { - API.getPersonalDetails(emailList) - .then((data) => { - const details = _.pick(data, emailList.split(',')); - Ion.merge(IONKEYS.PERSONAL_DETAILS, formatPersonalDetails(details)); - }); + return API.getPersonalDetails(emailList); } // When the app reconnects from being offline, fetch all of the personal details @@ -146,4 +142,5 @@ export { fetchTimezone, getForEmails, getDisplayName, + formatPersonalDetails, }; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index c4c1cfb88e6b..6307f4e43f08 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -65,6 +65,10 @@ function getUnreadActionCount(report) { const usersLastReadActionID = lodashGet(report, [ 'reportNameValuePairs', `lastReadActionID_${currentUserAccountID}`, + ]) || lodashGet(report, [ + 'reportNameValuePairs', + `lastAccessed_${currentUserAccountID}`, + 'readActionID', ]); // Save the lastReadActionID locally so we can access this later @@ -107,11 +111,15 @@ function getSimplifiedReportObject(report) { return { reportID: report.reportID, reportName: report.reportName, - reportNameValuePairs: report.reportNameValuePairs, unreadActionCount: getUnreadActionCount(report), maxSequenceNumber: report.reportActionList.length, participants: getParticipantEmailsFromReport(report), isPinned: report.isPinned, + lastVisited: lodashGet(report, [ + 'reportNameValuePairs', + `lastAccessed_${currentUserAccountID}`, + 'timestamp' + ], 0) }; } @@ -153,9 +161,11 @@ function fetchChatReportsByIDs(chatList) { let participantEmails = []; // Process the reports and store them in Ion + let simplifiedReports = []; _.each(fetchedReports, (report) => { const newReport = getSimplifiedReportObject(report); + simplifiedReports.push(newReport); participantEmails.push(newReport.participants); if (lodashGet(report, 'reportNameValuePairs.type') === 'chat') { @@ -169,7 +179,26 @@ function fetchChatReportsByIDs(chatList) { // Fetch the person details if there are any participantEmails = _.unique(participantEmails); if (participantEmails && participantEmails.length !== 0) { - PersonalDetails.getForEmails(participantEmails.join(',')); + PersonalDetails.getForEmails(participantEmails.join(',')) + .then((data) => { + const details = _.pick(data, participantEmails); + Ion.merge(IONKEYS.PERSONAL_DETAILS, PersonalDetails.formatPersonalDetails(details)); + + // Let's save report icons based on the the personalDetails of the participants + _.each(simplifiedReports, (report) => { + const avatars = _.chain(report.participants) + .filter(participant => participant.email !== currentUserEmail) + .map(participant => lodashGet(details, [participant, 'avatar'])) + .compact() + .value(); + + if (!_.isEmpty(avatars)) { + Ion.merge(`${IONKEYS.COLLECTION.REPORT}${report.reportID}`, { + icons: avatars.slice(0, 3), + }); + } + }); + }); } return _.map(fetchedReports, report => report.reportID); @@ -185,12 +214,10 @@ function fetchChatReportsByIDs(chatList) { function setLocalLastReadActionID(reportID, sequenceNumber) { lastReadActionIDs[reportID] = sequenceNumber; - // Update the lastReadActionID on the report optimistically + // Update the lastReadActionID on the report optimisticallys Ion.merge(`${IONKEYS.COLLECTION.REPORT}${reportID}`, { unreadActionCount: 0, - reportNameValuePairs: { - [`lastReadActionID_${currentUserAccountID}`]: sequenceNumber, - } + lastVisited: Date.now(), }); } @@ -536,7 +563,7 @@ function updateLastReadActionID(reportID, sequenceNumber) { setLocalLastReadActionID(reportID, sequenceNumber); // Mark the report as not having any unread items - API.setLastReadActionID({ + API.updateLastAccessed({ accountID: currentUserAccountID, reportID, sequenceNumber, diff --git a/src/pages/home/sidebar/ChatSwitcherList.js b/src/pages/home/sidebar/ChatSwitcherList.js index 291bed497bb2..2515edc639af 100644 --- a/src/pages/home/sidebar/ChatSwitcherList.js +++ b/src/pages/home/sidebar/ChatSwitcherList.js @@ -66,15 +66,14 @@ const ChatSwitcherList = ({ ]} > { - option.icon - && ( + _.map(option.icons, icon => ( - ) + )) } {option.text === option.alternateText ? ( diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index 191a1b87dc41..df0a7eaabdd0 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -2,6 +2,7 @@ import React from 'react'; import {View, Text} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; +import lodashOrderby from 'lodash.orderby'; import withIon from '../../../components/withIon'; import IONKEYS from '../../../IONKEYS'; import Str from '../../../libs/Str'; @@ -212,9 +213,27 @@ class ChatSwitcherView extends React.Component { * @param {boolean} blurAfterReset */ reset(blurAfterReset = true) { + let options = []; + if (blurAfterReset === false) { + const sortByLastVisited = lodashOrderby(this.props.reports, ['lastVisited'], ['desc']); + const recentReports = sortByLastVisited.slice(0, this.maxSearchResults); + options = _.chain(recentReports) + .values() + .map(report => ({ + text: report.reportName, + alternateText: report.reportName, + searchText: report.reportName, + reportID: report.reportID, + type: OPTION_TYPE.REPORT, + participants: report.participants, + icons: report.icons, + })) + .value(); + } + this.setState({ search: '', - options: [], + options, focusedIndex: 0, isLogoVisible: blurAfterReset, isClearButtonVisible: !blurAfterReset, @@ -233,10 +252,7 @@ class ChatSwitcherView extends React.Component { */ triggerOnFocusCallback() { this.props.onFocus(); - this.setState({ - isLogoVisible: false, - isClearButtonVisible: true, - }); + this.reset(false); } /** @@ -338,7 +354,7 @@ class ChatSwitcherView extends React.Component { alternateText: personalDetail.login, searchText: personalDetail.displayName === personalDetail.login ? personalDetail.login : `${personalDetail.displayName} ${personalDetail.login}`, - icon: personalDetail.avatarURL, + icons: [personalDetail.avatarURL], login: personalDetail.login, type: OPTION_TYPE.USER, })) @@ -354,6 +370,7 @@ class ChatSwitcherView extends React.Component { reportID: report.reportID, type: OPTION_TYPE.REPORT, participants: report.participants, + icons: report.icons, })) .value(); From 2afde4711d7696ea43bf4c498a5dfde3f65885a9 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Thu, 5 Nov 2020 16:45:03 -0800 Subject: [PATCH 02/44] code for chat report icons --- src/libs/actions/Report.js | 2 ++ src/pages/home/sidebar/ChatSwitcherList.js | 18 ++++++++---------- src/pages/home/sidebar/ChatSwitcherView.js | 1 + src/pages/home/sidebar/SidebarLink.js | 15 ++++++++++++++- src/pages/home/sidebar/SidebarLinks.js | 1 + 5 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 6307f4e43f08..a0f9d95edd39 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -474,6 +474,7 @@ function fetchOrCreateChatReport(participants) { // Store only the absolute bare minimum of data in Ion because space is limited const newReport = getSimplifiedReportObject(report); newReport.reportName = getChatReportName(report.sharedReportList); + newReport.lastVisited = Date.now(); // Merge the data into Ion. Don't use set() here or multiSet() because then that would // overwrite any existing data (like if they have unread messages) @@ -556,6 +557,7 @@ function addAction(reportID, text, file) { */ function updateLastReadActionID(reportID, sequenceNumber) { const currentMaxSequenceNumber = reportMaxSequenceNumbers[reportID]; + debugger; if (sequenceNumber < currentMaxSequenceNumber) { return; } diff --git a/src/pages/home/sidebar/ChatSwitcherList.js b/src/pages/home/sidebar/ChatSwitcherList.js index 2515edc639af..a109db0cd31b 100644 --- a/src/pages/home/sidebar/ChatSwitcherList.js +++ b/src/pages/home/sidebar/ChatSwitcherList.js @@ -65,16 +65,14 @@ const ChatSwitcherList = ({ styles.alignItemsCenter, ]} > - { - _.map(option.icons, icon => ( - - - - )) - } + {_.map(option.icons, icon => ( + + + + ))} {option.text === option.alternateText ? ( diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index df0a7eaabdd0..14475225d000 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -219,6 +219,7 @@ class ChatSwitcherView extends React.Component { const recentReports = sortByLastVisited.slice(0, this.maxSearchResults); options = _.chain(recentReports) .values() + .reject(report => !report.lastVisited) .map(report => ({ text: report.reportName, alternateText: report.reportName, diff --git a/src/pages/home/sidebar/SidebarLink.js b/src/pages/home/sidebar/SidebarLink.js index 00ac1888c1de..c323e65d77cd 100644 --- a/src/pages/home/sidebar/SidebarLink.js +++ b/src/pages/home/sidebar/SidebarLink.js @@ -1,10 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {View} from 'react-native'; +import {Image, View} from 'react-native'; import Text from '../../../components/Text'; import styles from '../../../styles/StyleSheet'; import PressableLink from '../../../components/PressableLink'; import ROUTES from '../../../ROUTES'; +import _ from "underscore"; const propTypes = { // The ID of the report for this link @@ -13,6 +14,9 @@ const propTypes = { // The name of the report to use as the text for this link reportName: PropTypes.string, + // + icons: PropTypes.arrayOf(PropTypes.string), + // Toggles the hamburger menu open and closed onLinkClick: PropTypes.func.isRequired, @@ -26,6 +30,7 @@ const propTypes = { const defaultProps = { isUnread: false, reportName: '', + icons: [], }; const SidebarLink = (props) => { @@ -44,6 +49,14 @@ const SidebarLink = (props) => { style={styles.textDecorationNoLine} > + {_.map(props.icons, icon => ( + + + + ))} {props.reportName} diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index ddf6dd5e82c7..e87f461c6f1d 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -105,6 +105,7 @@ class SidebarLinks extends React.Component { key={report.reportID} reportID={report.reportID} reportName={report.reportName} + icons={report.icons || []} isUnread={report.unreadActionCount > 0} onLinkClick={onLinkClick} isActiveReport={report.reportID === reportIDInUrl} From 4e73d87021c470e4d4960ac2bfef4dc43d1bb47b Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Fri, 6 Nov 2020 12:09:59 -0800 Subject: [PATCH 03/44] icon fix --- src/libs/actions/Report.js | 1 - src/pages/home/sidebar/ChatSwitcherList.js | 7 +++++-- src/pages/home/sidebar/SidebarLink.js | 15 ++++++++++----- src/styles/StyleSheet.js | 3 +++ 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index a0f9d95edd39..39624ae64be7 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -557,7 +557,6 @@ function addAction(reportID, text, file) { */ function updateLastReadActionID(reportID, sequenceNumber) { const currentMaxSequenceNumber = reportMaxSequenceNumbers[reportID]; - debugger; if (sequenceNumber < currentMaxSequenceNumber) { return; } diff --git a/src/pages/home/sidebar/ChatSwitcherList.js b/src/pages/home/sidebar/ChatSwitcherList.js index a109db0cd31b..60d5efdddda1 100644 --- a/src/pages/home/sidebar/ChatSwitcherList.js +++ b/src/pages/home/sidebar/ChatSwitcherList.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import _ from 'underscore'; +import lodashGet from 'lodash.get'; import { Image, Text, @@ -40,6 +41,8 @@ const ChatSwitcherList = ({ const textStyle = optionIsFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; + const paddingBeforeIcon = lodashGet(option, 'icons', []).length !== 0 ? styles.pl4 : null; + return ( {_.map(option.icons, icon => ( - + ))} - + {option.text === option.alternateText ? ( {option.alternateText} diff --git a/src/pages/home/sidebar/SidebarLink.js b/src/pages/home/sidebar/SidebarLink.js index c323e65d77cd..63602125bdfa 100644 --- a/src/pages/home/sidebar/SidebarLink.js +++ b/src/pages/home/sidebar/SidebarLink.js @@ -36,9 +36,14 @@ const defaultProps = { const SidebarLink = (props) => { const linkWrapperActiveStyle = props.isActiveReport && styles.sidebarLinkWrapperActive; const linkActiveStyle = props.isActiveReport ? styles.sidebarLinkActive : null; - const textActiveStyle = props.isActiveReport ? styles.sidebarLinkActiveText : styles.sidebarLinkText; - const textActiveUnreadStyle = props.isUnread - ? [textActiveStyle, styles.sidebarLinkTextUnread] : [textActiveStyle]; + const textStyles = []; + textStyles.push(props.isActiveReport ? styles.sidebarLinkActiveText : styles.sidebarLinkText); + if (props.isUnread) { + textStyles.push(styles.sidebarLinkTextUnread); + } + if (props.icons.length !== 0) { + textStyles.push(styles.pl4); + } return ( @@ -50,14 +55,14 @@ const SidebarLink = (props) => { > {_.map(props.icons, icon => ( - + ))} - + {props.reportName} diff --git a/src/styles/StyleSheet.js b/src/styles/StyleSheet.js index 613d3a72af81..7be23791d897 100644 --- a/src/styles/StyleSheet.js +++ b/src/styles/StyleSheet.js @@ -80,6 +80,9 @@ const styles = { pr2: { paddingRight: 8, }, + pl4: { + paddingLeft: 4, + }, h100p: { height: '100%', From 35918f7beb089ad4d9f9f0466968c33be0420fa7 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Fri, 6 Nov 2020 12:51:58 -0800 Subject: [PATCH 04/44] Fixes to support add to group button --- src/pages/home/sidebar/ChatSwitcherView.js | 47 ++++++++++++---------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index 14475225d000..43397d15e4dc 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -78,6 +78,7 @@ class ChatSwitcherView extends React.Component { this.reset = this.reset.bind(this); this.selectUser = this.selectUser.bind(this); this.selectReport = this.selectReport.bind(this); + this.getRecentVisitedOptions = this.getRecentVisitedOptions.bind(this); this.triggerOnFocusCallback = this.triggerOnFocusCallback.bind(this); this.updateSearch = this.updateSearch.bind(this); this.selectRow = this.selectRow.bind(this); @@ -213,28 +214,9 @@ class ChatSwitcherView extends React.Component { * @param {boolean} blurAfterReset */ reset(blurAfterReset = true) { - let options = []; - if (blurAfterReset === false) { - const sortByLastVisited = lodashOrderby(this.props.reports, ['lastVisited'], ['desc']); - const recentReports = sortByLastVisited.slice(0, this.maxSearchResults); - options = _.chain(recentReports) - .values() - .reject(report => !report.lastVisited) - .map(report => ({ - text: report.reportName, - alternateText: report.reportName, - searchText: report.reportName, - reportID: report.reportID, - type: OPTION_TYPE.REPORT, - participants: report.participants, - icons: report.icons, - })) - .value(); - } - this.setState({ search: '', - options, + options: this.getRecentVisitedOptions(), focusedIndex: 0, isLogoVisible: blurAfterReset, isClearButtonVisible: !blurAfterReset, @@ -247,13 +229,36 @@ class ChatSwitcherView extends React.Component { }); } + getRecentVisitedOptions() { + const sortByLastVisited = lodashOrderby(this.props.reports, ['lastVisited'], ['desc']); + const recentReports = sortByLastVisited.slice(0, this.maxSearchResults); + return _.chain(recentReports) + .values() + .reject(report => !report.lastVisited) + .map(report => ({ + text: report.reportName, + alternateText: report.reportName, + searchText: report.reportName, + reportID: report.reportID, + participants: report.participants, + icons: report.icons, + login: report.participants.length === 1 ? report.participants[0] : '', + type: report.participants.length === 1 ? OPTION_TYPE.USER : OPTION_TYPE.REPORT, + })) + .value(); + } + /** * When the text input gets focus, the onFocus() callback needs to be called, the keyboard shortcut is disabled * and the logo is hidden */ triggerOnFocusCallback() { this.props.onFocus(); - this.reset(false); + this.setState({ + options: this.getRecentVisitedOptions(), + isLogoVisible: false, + isClearButtonVisible: true, + }); } /** From 4a26675ccb2a347fee11b00d2710f6327ad1db84 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Fri, 6 Nov 2020 17:18:48 -0800 Subject: [PATCH 05/44] Fixes to search and ordering --- src/libs/actions/Report.js | 3 +- .../home/sidebar/ChatSwitcherSearchForm.js | 5 +- src/pages/home/sidebar/ChatSwitcherView.js | 124 +++++++----------- 3 files changed, 48 insertions(+), 84 deletions(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 39624ae64be7..874a9ad7a7ea 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -187,7 +187,6 @@ function fetchChatReportsByIDs(chatList) { // Let's save report icons based on the the personalDetails of the participants _.each(simplifiedReports, (report) => { const avatars = _.chain(report.participants) - .filter(participant => participant.email !== currentUserEmail) .map(participant => lodashGet(details, [participant, 'avatar'])) .compact() .value(); @@ -214,7 +213,7 @@ function fetchChatReportsByIDs(chatList) { function setLocalLastReadActionID(reportID, sequenceNumber) { lastReadActionIDs[reportID] = sequenceNumber; - // Update the lastReadActionID on the report optimisticallys + // Update the lastReadActionID on the report optimistically Ion.merge(`${IONKEYS.COLLECTION.REPORT}${reportID}`, { unreadActionCount: 0, lastVisited: Date.now(), diff --git a/src/pages/home/sidebar/ChatSwitcherSearchForm.js b/src/pages/home/sidebar/ChatSwitcherSearchForm.js index 076cca4d9241..92c0151dcfb7 100644 --- a/src/pages/home/sidebar/ChatSwitcherSearchForm.js +++ b/src/pages/home/sidebar/ChatSwitcherSearchForm.js @@ -28,9 +28,6 @@ const propTypes = { // The current value of the search input searchValue: PropTypes.string.isRequired, - // A function to call when the input has been blurred - onBlur: PropTypes.func.isRequired, - // A function to call when the text has changed in the input onChangeText: PropTypes.func.isRequired, @@ -141,7 +138,7 @@ const ChatSwitcherSearchForm = props => ( ref={props.forwardedRef} style={[styles.textInput, styles.textInputReversed, styles.flex1]} value={props.searchValue} - onBlur={props.onBlur} + onBlur={() => {}} onChangeText={props.onChangeText} onFocus={props.onFocus} onKeyPress={props.onKeyPress} diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index 43397d15e4dc..b11009048a4e 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -216,7 +216,7 @@ class ChatSwitcherView extends React.Component { reset(blurAfterReset = true) { this.setState({ search: '', - options: this.getRecentVisitedOptions(), + options: this.getRecentVisitedOptions(true), focusedIndex: 0, isLogoVisible: blurAfterReset, isClearButtonVisible: !blurAfterReset, @@ -229,22 +229,27 @@ class ChatSwitcherView extends React.Component { }); } - getRecentVisitedOptions() { + getRecentVisitedOptions(getMaxSearchResults = true) { const sortByLastVisited = lodashOrderby(this.props.reports, ['lastVisited'], ['desc']); - const recentReports = sortByLastVisited.slice(0, this.maxSearchResults); + const recentReports = getMaxSearchResults ? sortByLastVisited.slice(0, this.maxSearchResults) : sortByLastVisited; return _.chain(recentReports) .values() .reject(report => !report.lastVisited) - .map(report => ({ - text: report.reportName, - alternateText: report.reportName, - searchText: report.reportName, - reportID: report.reportID, - participants: report.participants, - icons: report.icons, - login: report.participants.length === 1 ? report.participants[0] : '', - type: report.participants.length === 1 ? OPTION_TYPE.USER : OPTION_TYPE.REPORT, - })) + .map(report => { + const isSingleUserPrivateDMReport = report.participants.length === 1; + const login = isSingleUserPrivateDMReport ? report.participants[0] : ''; + return { + text: report.reportName, + alternateText: report.reportName, + searchText: report.reportName === login ? login + : `${report.reportName} ${login}`, + reportID: report.reportID, + participants: report.participants, + icons: report.icons, + login, + type: report.participants.length === 1 ? OPTION_TYPE.USER : OPTION_TYPE.REPORT, + }; + }) .value(); } @@ -254,11 +259,14 @@ class ChatSwitcherView extends React.Component { */ triggerOnFocusCallback() { this.props.onFocus(); - this.setState({ - options: this.getRecentVisitedOptions(), + const params = { isLogoVisible: false, isClearButtonVisible: true, - }); + } + if (this.state.search === '') { + params.options = this.getRecentVisitedOptions(true); + } + this.setState(params); } /** @@ -337,24 +345,17 @@ class ChatSwitcherView extends React.Component { this.setState({search: value}); - // Search our full list of options. We want: - // 1) Exact matches first - // 2) beginning-of-string matches second - // 3) middle-of-string matches last - const matchRegexes = [ - new RegExp(`^${Str.escapeForRegExp(value)}$`, 'i'), - new RegExp(`^${Str.escapeForRegExp(value)}`, 'i'), - new RegExp(Str.escapeForRegExp(value), 'i'), - ]; - // Because we want to regexes above to be listed in a specific order, the for loop below will end up adding // duplicate options to the list (because one option can match multiple regex patterns). // A Set is used here so that duplicate values are automatically removed. const matches = new Set(); + const recentlyVisitedOptions = this.getRecentVisitedOptions(false); + // Get a list of all users we can send messages to and make their details generic const personalDetailOptions = _.chain(this.props.personalDetails) .values() + .reject(personalDetail => _.findWhere(recentlyVisitedOptions, {login: personalDetail.login})) .map(personalDetail => ({ text: personalDetail.displayName, alternateText: personalDetail.login, @@ -366,54 +367,26 @@ class ChatSwitcherView extends React.Component { })) .value(); - // Get a list of all reports we can send messages to - const reportOptions = _.chain(this.props.reports) - .values() - .map(report => ({ - text: report.reportName, - alternateText: report.reportName, - searchText: report.reportName, - reportID: report.reportID, - type: OPTION_TYPE.REPORT, - participants: report.participants, - icons: report.icons, - })) - .value(); + const searchOptions = _.union(recentlyVisitedOptions, personalDetailOptions) + const userEnteredText = value.toLowerCase(); - // If we have at least one group user then stop showing - // report options as we cannot add a report to a group DM - const searchOptions = this.state.groupUsers.length === 0 - ? _.union(personalDetailOptions, reportOptions) - : personalDetailOptions; - - for (let i = 0; i < matchRegexes.length; i++) { - if (matches.size < this.maxSearchResults) { - for (let j = 0; j < searchOptions.length; j++) { - const option = searchOptions[j]; - const valueToSearch = option.searchText.replace(new RegExp(/ /g), ''); - const isMatch = matchRegexes[i].test(valueToSearch); - - // We want to avoid adding single user private DM reports - // since we will prefer to show the user UI over the report name - const isSingleUserPrivateDMReport = option.participants - && option.participants.length === 1; - - // We must also filter out any users who are already in the Group DM list - // so they can't be selected more than once - const isInGroupUsers = _.some(this.state.groupUsers, groupOption => ( - groupOption.login === option.login - )); - - // Make sure we don't include the same option twice (automatically handled be using a `Set`) - if (isMatch && !isSingleUserPrivateDMReport && !isInGroupUsers) { - matches.add(option); - } - - if (matches.size === this.maxSearchResults) { - break; - } - } - } else { + for (let i = 0; i < searchOptions.length; i++) { + const option = searchOptions[i]; + const optionSearchText = option.searchText.toLowerCase(); + const isMatch = optionSearchText.includes(userEnteredText); + + // We must also filter out any users who are already in the Group DM list + // so they can't be selected more than once + const isInGroupUsers = _.some(this.state.groupUsers, groupOption => ( + groupOption.login === option.login + )); + + // Make sure we don't include the same option twice (automatically handled be using a `Set`) + if (isMatch && !isInGroupUsers) { + matches.add(option); + } + + if (matches.size === this.maxSearchResults) { break; } } @@ -431,11 +404,6 @@ class ChatSwitcherView extends React.Component { isClearButtonVisible={this.state.isClearButtonVisible} isLogoVisible={this.state.isLogoVisible} searchValue={this.state.search} - onBlur={() => { - if (this.state.search === '') { - this.reset(); - } - }} onChangeText={this.updateSearch} onClearButtonClick={this.reset} onFocus={this.triggerOnFocusCallback} From 62a157dcbf8f1251cd7568ed6ad87310cb3c1274 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Mon, 9 Nov 2020 17:59:21 -0800 Subject: [PATCH 06/44] record action properly and sorting improvements --- src/pages/home/report/ReportActionsView.js | 3 ++ src/pages/home/sidebar/ChatSwitcherView.js | 59 ++++++++++++---------- src/pages/home/sidebar/SidebarLink.js | 2 +- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index cb2e4b0ba8b6..ae9c332ae1b9 100644 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -53,6 +53,7 @@ class ReportActionsView extends React.Component { } componentDidMount() { + console.log(`componentDidMount: ${this.props.reportID}. IsActive: ${this.props.isActiveReport}`); this.visibilityChangeEvent = AppState.addEventListener('change', () => { if (this.props.isActiveReport && Visibility.isVisible()) { setTimeout(this.recordMaxAction, 3000); @@ -60,12 +61,14 @@ class ReportActionsView extends React.Component { }); if (this.props.isActiveReport) { this.keyboardEvent = Keyboard.addListener('keyboardDidShow', this.scrollToListBottom); + this.recordMaxAction(); } fetchActions(this.props.reportID); } componentDidUpdate(prevProps) { + console.log(`componentDidUpdate: ${this.props.reportID}. IsActive: ${this.props.isActiveReport}. Previous: ${prevProps.reportID}, IsActive: ${prevProps.isActiveReport}`); // If we previously had a value for reportActions but no longer have one // this can only mean that the reportActions have been deleted. So we must // refetch these actions the next time we switch to this chat. diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index b11009048a4e..6da2fb6ad60f 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -3,9 +3,9 @@ import {View, Text} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; import lodashOrderby from 'lodash.orderby'; +import lodashGet from 'lodash.get'; import withIon from '../../../components/withIon'; import IONKEYS from '../../../IONKEYS'; -import Str from '../../../libs/Str'; import KeyboardShortcut from '../../../libs/KeyboardShortcut'; import ChatSwitcherList from './ChatSwitcherList'; import ChatSwitcherSearchForm from './ChatSwitcherSearchForm'; @@ -109,6 +109,33 @@ class ChatSwitcherView extends React.Component { KeyboardShortcut.unsubscribe('K'); } + getRecentVisitedOptions(getMaxSearchResults = true) { + const sortByLastVisited = lodashOrderby(this.props.reports, ['lastVisited'], ['desc']); + const recentReports = getMaxSearchResults + ? sortByLastVisited.slice(0, this.maxSearchResults) + : sortByLastVisited; + return _.chain(recentReports) + .values() + .reject(report => !report.lastVisited || lodashGet(report, 'participants', []).length === 0) + .map((report) => { + const participants = lodashGet(report, 'participants', []); + const isSingleUserPrivateDMReport = participants.length === 1; + const login = isSingleUserPrivateDMReport ? report.participants[0] : ''; + return { + text: report.reportName, + alternateText: report.reportName, + searchText: report.reportName === login ? login + : `${report.reportName} ${login}`, + reportID: report.reportID, + participants: participants, + icons: report.icons, + login, + type: participants.length === 1 ? OPTION_TYPE.USER : OPTION_TYPE.REPORT, + }; + }) + .value(); + } + /** * Fires the correct method for the option type selected. * @@ -229,30 +256,6 @@ class ChatSwitcherView extends React.Component { }); } - getRecentVisitedOptions(getMaxSearchResults = true) { - const sortByLastVisited = lodashOrderby(this.props.reports, ['lastVisited'], ['desc']); - const recentReports = getMaxSearchResults ? sortByLastVisited.slice(0, this.maxSearchResults) : sortByLastVisited; - return _.chain(recentReports) - .values() - .reject(report => !report.lastVisited) - .map(report => { - const isSingleUserPrivateDMReport = report.participants.length === 1; - const login = isSingleUserPrivateDMReport ? report.participants[0] : ''; - return { - text: report.reportName, - alternateText: report.reportName, - searchText: report.reportName === login ? login - : `${report.reportName} ${login}`, - reportID: report.reportID, - participants: report.participants, - icons: report.icons, - login, - type: report.participants.length === 1 ? OPTION_TYPE.USER : OPTION_TYPE.REPORT, - }; - }) - .value(); - } - /** * When the text input gets focus, the onFocus() callback needs to be called, the keyboard shortcut is disabled * and the logo is hidden @@ -262,7 +265,7 @@ class ChatSwitcherView extends React.Component { const params = { isLogoVisible: false, isClearButtonVisible: true, - } + }; if (this.state.search === '') { params.options = this.getRecentVisitedOptions(true); } @@ -367,7 +370,7 @@ class ChatSwitcherView extends React.Component { })) .value(); - const searchOptions = _.union(recentlyVisitedOptions, personalDetailOptions) + const searchOptions = _.union(recentlyVisitedOptions, personalDetailOptions); const userEnteredText = value.toLowerCase(); for (let i = 0; i < searchOptions.length; i++) { @@ -405,7 +408,7 @@ class ChatSwitcherView extends React.Component { isLogoVisible={this.state.isLogoVisible} searchValue={this.state.search} onChangeText={this.updateSearch} - onClearButtonClick={this.reset} + onClearButtonClick={() => this.reset(true)} onFocus={this.triggerOnFocusCallback} onKeyPress={this.handleKeyPress} groupUsers={this.state.groupUsers} diff --git a/src/pages/home/sidebar/SidebarLink.js b/src/pages/home/sidebar/SidebarLink.js index 63602125bdfa..f6e33a45f640 100644 --- a/src/pages/home/sidebar/SidebarLink.js +++ b/src/pages/home/sidebar/SidebarLink.js @@ -1,11 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import {Image, View} from 'react-native'; +import _ from 'underscore'; import Text from '../../../components/Text'; import styles from '../../../styles/StyleSheet'; import PressableLink from '../../../components/PressableLink'; import ROUTES from '../../../ROUTES'; -import _ from "underscore"; const propTypes = { // The ID of the report for this link From 2da2772d7355785317831f453a3de2f45b602696 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Tue, 10 Nov 2020 11:55:23 -0800 Subject: [PATCH 07/44] cleanups --- src/libs/actions/PersonalDetails.js | 1 + src/libs/actions/Report.js | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index bc5eafa453f4..1732e8048b5a 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -129,6 +129,7 @@ function fetch() { * Get personal details for a list of emails. * * @param {String} emailList + * @return {Promise} */ function getForEmails(emailList) { return API.getPersonalDetails(emailList); diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 874a9ad7a7ea..d27f7b16927e 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -62,6 +62,7 @@ const lastReadActionIDs = {}; * @returns {boolean} */ function getUnreadActionCount(report) { + // @todo remove the first check as part of cleanup https://github.com/Expensify/Expensify/issues/145243 const usersLastReadActionID = lodashGet(report, [ 'reportNameValuePairs', `lastReadActionID_${currentUserAccountID}`, @@ -161,7 +162,7 @@ function fetchChatReportsByIDs(chatList) { let participantEmails = []; // Process the reports and store them in Ion - let simplifiedReports = []; + const simplifiedReports = []; _.each(fetchedReports, (report) => { const newReport = getSimplifiedReportObject(report); @@ -184,7 +185,8 @@ function fetchChatReportsByIDs(chatList) { const details = _.pick(data, participantEmails); Ion.merge(IONKEYS.PERSONAL_DETAILS, PersonalDetails.formatPersonalDetails(details)); - // Let's save report icons based on the the personalDetails of the participants + // The personalDetails of the participants contain their avatar images. Here we'll go over each + // report and based on the participants we'll link up their avatars to report icons. _.each(simplifiedReports, (report) => { const avatars = _.chain(report.participants) .map(participant => lodashGet(details, [participant, 'avatar'])) @@ -205,7 +207,7 @@ function fetchChatReportsByIDs(chatList) { } /** - * Update the lastReadActionID in Ion and local memory. + * Update the lastReadActionID in local memory * * @param {Number} reportID * @param {Number} sequenceNumber @@ -213,7 +215,7 @@ function fetchChatReportsByIDs(chatList) { function setLocalLastReadActionID(reportID, sequenceNumber) { lastReadActionIDs[reportID] = sequenceNumber; - // Update the lastReadActionID on the report optimistically + // Update the report optimistically Ion.merge(`${IONKEYS.COLLECTION.REPORT}${reportID}`, { unreadActionCount: 0, lastVisited: Date.now(), From 2cdf9a4b309a7c14d65c88f091194df3bb87aae8 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Tue, 10 Nov 2020 11:58:56 -0800 Subject: [PATCH 08/44] remove console --- src/pages/home/report/ReportActionsView.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index ae9c332ae1b9..afb22faf6b77 100644 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -53,7 +53,6 @@ class ReportActionsView extends React.Component { } componentDidMount() { - console.log(`componentDidMount: ${this.props.reportID}. IsActive: ${this.props.isActiveReport}`); this.visibilityChangeEvent = AppState.addEventListener('change', () => { if (this.props.isActiveReport && Visibility.isVisible()) { setTimeout(this.recordMaxAction, 3000); @@ -68,7 +67,6 @@ class ReportActionsView extends React.Component { } componentDidUpdate(prevProps) { - console.log(`componentDidUpdate: ${this.props.reportID}. IsActive: ${this.props.isActiveReport}. Previous: ${prevProps.reportID}, IsActive: ${prevProps.isActiveReport}`); // If we previously had a value for reportActions but no longer have one // this can only mean that the reportActions have been deleted. So we must // refetch these actions the next time we switch to this chat. From 68b4d76eb7f33a98d05e5fa5241674a7e3fc5ad2 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Tue, 10 Nov 2020 12:06:40 -0800 Subject: [PATCH 09/44] moving icon logic from switcherList to row --- src/pages/home/sidebar/ChatSwitcherRow.js | 25 ++++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/pages/home/sidebar/ChatSwitcherRow.js b/src/pages/home/sidebar/ChatSwitcherRow.js index 76dcf4bbbb4a..bb35d8761389 100644 --- a/src/pages/home/sidebar/ChatSwitcherRow.js +++ b/src/pages/home/sidebar/ChatSwitcherRow.js @@ -6,6 +6,8 @@ import { TouchableOpacity, View, } from 'react-native'; +import lodashGet from 'lodash.get'; +import _ from 'underscore'; import styles from '../../../styles/StyleSheet'; import ChatSwitcherOptionPropTypes from './ChatSwitcherOptionPropTypes'; @@ -33,6 +35,8 @@ const ChatSwitcherRow = ({ const textStyle = optionIsFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; + const paddingBeforeIcon = lodashGet(option, 'icons', []).length !== 0 ? styles.pl4 : null; + return ( - { - option.icon - && ( - - - - ) - } - + {_.map(option.icons, icon => ( + + + + ))} + {option.text === option.alternateText ? ( {option.alternateText} From 1746fc8b780158b3b0a58470fcc173f90c57ba6f Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Tue, 10 Nov 2020 12:17:05 -0800 Subject: [PATCH 10/44] style fix --- src/pages/home/sidebar/ChatSwitcherView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index 053da50c4e4c..470b8d6e8cf8 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -136,7 +136,7 @@ class ChatSwitcherView extends React.Component { searchText: report.reportName === login ? login : `${report.reportName} ${login}`, reportID: report.reportID, - participants: participants, + participants, icons: report.icons, login, type: participants.length === 1 ? OPTION_TYPE.USER : OPTION_TYPE.REPORT, From 5f11d83049cbc8b5cd6f52f013c33bf6b44e4a70 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Tue, 10 Nov 2020 13:27:07 -0800 Subject: [PATCH 11/44] minor correction --- src/pages/home/sidebar/ChatSwitcherView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index 470b8d6e8cf8..181e84e17ad4 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -139,7 +139,7 @@ class ChatSwitcherView extends React.Component { participants, icons: report.icons, login, - type: participants.length === 1 ? OPTION_TYPE.USER : OPTION_TYPE.REPORT, + type: isSingleUserPrivateDMReport ? OPTION_TYPE.USER : OPTION_TYPE.REPORT, }; }) .value(); From 78d43e8e76d858747d9ac2ba20f6aa713e11a7b8 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Tue, 10 Nov 2020 16:36:27 -0800 Subject: [PATCH 12/44] Update as per discussion to not sort during search --- src/pages/home/sidebar/ChatSwitcherView.js | 38 ++++++++++++++-------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index 181e84e17ad4..0db6965e3cfc 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -78,7 +78,7 @@ class ChatSwitcherView extends React.Component { this.reset = this.reset.bind(this); this.selectUser = this.selectUser.bind(this); this.selectReport = this.selectReport.bind(this); - this.getRecentVisitedOptions = this.getRecentVisitedOptions.bind(this); + this.getChatReportsOptions = this.getChatReportsOptions.bind(this); this.triggerOnFocusCallback = this.triggerOnFocusCallback.bind(this); this.updateSearch = this.updateSearch.bind(this); this.selectRow = this.selectRow.bind(this); @@ -118,14 +118,25 @@ class ChatSwitcherView extends React.Component { KeyboardShortcut.unsubscribe('K'); } - getRecentVisitedOptions(getMaxSearchResults = true) { - const sortByLastVisited = lodashOrderby(this.props.reports, ['lastVisited'], ['desc']); - const recentReports = getMaxSearchResults - ? sortByLastVisited.slice(0, this.maxSearchResults) - : sortByLastVisited; - return _.chain(recentReports) + /** + * Get the chat report options created from props.report. Additionally these chat report options will also determine + * if its a 1:1 DM or not. If it is a 1:1 DM we'll save the participant login and the type as user so that we can + * filter out the same in personalDetailOptions. + * + * @param {Boolean} sortByLastVisited + * @returns {Object} + */ + getChatReportsOptions(sortByLastVisited = true) { + let chatReports = this.props.reports; + if (sortByLastVisited) { + chatReports = lodashOrderby(this.props.reports, ['lastVisited'], ['desc']); + } + + return _.chain(chatReports) .values() - .reject(report => !report.lastVisited || lodashGet(report, 'participants', []).length === 0) + .reject(report => { + return sortByLastVisited ? !report.lastVisited : false; + }) .map((report) => { const participants = lodashGet(report, 'participants', []); const isSingleUserPrivateDMReport = participants.length === 1; @@ -140,6 +151,7 @@ class ChatSwitcherView extends React.Component { icons: report.icons, login, type: isSingleUserPrivateDMReport ? OPTION_TYPE.USER : OPTION_TYPE.REPORT, + isUnread: report.unreadActionCount > 0, }; }) .value(); @@ -252,7 +264,7 @@ class ChatSwitcherView extends React.Component { reset(blurAfterReset = true) { this.setState({ search: '', - options: this.getRecentVisitedOptions(true), + options: this.getChatReportsOptions(), focusedIndex: 0, isLogoVisible: blurAfterReset, isClearButtonVisible: !blurAfterReset, @@ -276,7 +288,7 @@ class ChatSwitcherView extends React.Component { isClearButtonVisible: true, }; if (this.state.search === '') { - params.options = this.getRecentVisitedOptions(true); + params.options = this.getChatReportsOptions(); } this.setState(params); } @@ -362,12 +374,12 @@ class ChatSwitcherView extends React.Component { // A Set is used here so that duplicate values are automatically removed. const matches = new Set(); - const recentlyVisitedOptions = this.getRecentVisitedOptions(false); + const chatReportOptions = this.getChatReportsOptions(false); // Get a list of all users we can send messages to and make their details generic const personalDetailOptions = _.chain(this.props.personalDetails) .values() - .reject(personalDetail => _.findWhere(recentlyVisitedOptions, {login: personalDetail.login})) + .reject(personalDetail => _.findWhere(chatReportOptions, {login: personalDetail.login})) .map(personalDetail => ({ text: personalDetail.displayName, alternateText: personalDetail.login, @@ -379,7 +391,7 @@ class ChatSwitcherView extends React.Component { })) .value(); - const searchOptions = _.union(recentlyVisitedOptions, personalDetailOptions); + const searchOptions = _.union(chatReportOptions, personalDetailOptions); const userEnteredText = value.toLowerCase(); for (let i = 0; i < searchOptions.length; i++) { From 8dc27d7f654bfbdd258055f730ca81593157a71c Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Tue, 10 Nov 2020 16:39:28 -0800 Subject: [PATCH 13/44] style fix --- src/pages/home/sidebar/ChatSwitcherView.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index 0db6965e3cfc..fe4ee22d3ff4 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -134,9 +134,7 @@ class ChatSwitcherView extends React.Component { return _.chain(chatReports) .values() - .reject(report => { - return sortByLastVisited ? !report.lastVisited : false; - }) + .reject(report => (sortByLastVisited ? !report.lastVisited : false)) .map((report) => { const participants = lodashGet(report, 'participants', []); const isSingleUserPrivateDMReport = participants.length === 1; From 6e8700786acb0e6412a41a33cc5aba9ef4d43798 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Wed, 11 Nov 2020 10:02:45 -0800 Subject: [PATCH 14/44] single icon --- src/libs/actions/Report.js | 15 ++++------ src/pages/home/sidebar/SidebarLink.js | 39 ++++++++++++-------------- src/pages/home/sidebar/SidebarLinks.js | 2 +- 3 files changed, 25 insertions(+), 31 deletions(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 6563a5ea4234..0e840133197e 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -188,15 +188,12 @@ function fetchChatReportsByIDs(chatList) { // The personalDetails of the participants contain their avatar images. Here we'll go over each // report and based on the participants we'll link up their avatars to report icons. _.each(simplifiedReports, (report) => { - const avatars = _.chain(report.participants) - .map(participant => lodashGet(details, [participant, 'avatar'])) - .compact() - .value(); - - if (!_.isEmpty(avatars)) { - Ion.merge(`${IONKEYS.COLLECTION.REPORT}${report.reportID}`, { - icons: avatars.slice(0, 3), - }); + if (lodashGet(report, 'participants', []).length === 1) { + const dmParticipant = report.participants[0]; + const icon = lodashGet(details, [dmParticipant, 'avatar']); + if (!_.isEmpty(icon)) { + Ion.merge(`${IONKEYS.COLLECTION.REPORT}${report.reportID}`, {icon}); + } } }); }); diff --git a/src/pages/home/sidebar/SidebarLink.js b/src/pages/home/sidebar/SidebarLink.js index f6e33a45f640..9329ed95ab50 100644 --- a/src/pages/home/sidebar/SidebarLink.js +++ b/src/pages/home/sidebar/SidebarLink.js @@ -1,7 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import {Image, View} from 'react-native'; -import _ from 'underscore'; import Text from '../../../components/Text'; import styles from '../../../styles/StyleSheet'; import PressableLink from '../../../components/PressableLink'; @@ -14,8 +13,8 @@ const propTypes = { // The name of the report to use as the text for this link reportName: PropTypes.string, - // - icons: PropTypes.arrayOf(PropTypes.string), + // The icon for the reports + icon: PropTypes.string, // Toggles the hamburger menu open and closed onLinkClick: PropTypes.func.isRequired, @@ -30,20 +29,15 @@ const propTypes = { const defaultProps = { isUnread: false, reportName: '', - icons: [], + icon: '', }; const SidebarLink = (props) => { const linkWrapperActiveStyle = props.isActiveReport && styles.sidebarLinkWrapperActive; const linkActiveStyle = props.isActiveReport ? styles.sidebarLinkActive : null; - const textStyles = []; - textStyles.push(props.isActiveReport ? styles.sidebarLinkActiveText : styles.sidebarLinkText); - if (props.isUnread) { - textStyles.push(styles.sidebarLinkTextUnread); - } - if (props.icons.length !== 0) { - textStyles.push(styles.pl4); - } + const textActiveStyle = props.isActiveReport ? styles.sidebarLinkActiveText : styles.sidebarLinkText; + const textActiveUnreadStyle = props.isUnread + ? [textActiveStyle, styles.sidebarLinkTextUnread] : [textActiveStyle]; return ( @@ -54,15 +48,18 @@ const SidebarLink = (props) => { style={styles.textDecorationNoLine} > - {_.map(props.icons, icon => ( - - - - ))} - + { + props.icon + && ( + + + + ) + } + {props.reportName} diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 971368c56380..1dbd31362a8e 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -85,7 +85,7 @@ const SidebarLinks = (props) => { key={report.reportID} reportID={report.reportID} reportName={report.reportName} - icons={report.icons || []} + icon={report.icon} isUnread={report.unreadActionCount > 0} onLinkClick={onLinkClick} isActiveReport={report.reportID === reportIDInUrl} From 306d88ab433e393feed1b701d467b7706a74a363 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Wed, 11 Nov 2020 11:10:23 -0800 Subject: [PATCH 15/44] Icon and search revert to regex with some improvements --- src/pages/home/sidebar/ChatSwitcherView.js | 82 +++++++++++++++------- src/pages/home/sidebar/SidebarLink.js | 20 +++--- 2 files changed, 67 insertions(+), 35 deletions(-) diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index fe4ee22d3ff4..be5c54e839ff 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -6,6 +6,7 @@ import lodashOrderby from 'lodash.orderby'; import lodashGet from 'lodash.get'; import withIon from '../../../components/withIon'; import IONKEYS from '../../../IONKEYS'; +import Str from '../../../libs/Str'; import KeyboardShortcut from '../../../libs/KeyboardShortcut'; import ChatSwitcherList from './ChatSwitcherList'; import ChatSwitcherSearchForm from './ChatSwitcherSearchForm'; @@ -121,12 +122,13 @@ class ChatSwitcherView extends React.Component { /** * Get the chat report options created from props.report. Additionally these chat report options will also determine * if its a 1:1 DM or not. If it is a 1:1 DM we'll save the participant login and the type as user so that we can - * filter out the same in personalDetailOptions. + * reject the same in personalDetailOptions to not deal with dupes. * * @param {Boolean} sortByLastVisited + * @param {Boolean} getOnlySingleUserPrivateDM * @returns {Object} */ - getChatReportsOptions(sortByLastVisited = true) { + getChatReportsOptions(sortByLastVisited = true, getOnlySingleUserPrivateDM = false) { let chatReports = this.props.reports; if (sortByLastVisited) { chatReports = lodashOrderby(this.props.reports, ['lastVisited'], ['desc']); @@ -134,7 +136,22 @@ class ChatSwitcherView extends React.Component { return _.chain(chatReports) .values() - .reject(report => (sortByLastVisited ? !report.lastVisited : false)) + .reject((report) => { + if (_.isEmpty(report.reportName)) { + return true; + } + if (sortByLastVisited && !report.lastVisited) { + return true; + } + if (getOnlySingleUserPrivateDM) { + const participants = lodashGet(report, 'participants', []); + const isSingleUserPrivateDMReport = participants.length === 1; + + // Since we only want 1:1 private DMs we'll have to return true (reject) when its not a 1:1 DM + return !isSingleUserPrivateDMReport; + } + return false; + }) .map((report) => { const participants = lodashGet(report, 'participants', []); const isSingleUserPrivateDMReport = participants.length === 1; @@ -146,7 +163,7 @@ class ChatSwitcherView extends React.Component { : `${report.reportName} ${login}`, reportID: report.reportID, participants, - icons: report.icons, + icon: report.icon, login, type: isSingleUserPrivateDMReport ? OPTION_TYPE.USER : OPTION_TYPE.REPORT, isUnread: report.unreadActionCount > 0, @@ -367,12 +384,24 @@ class ChatSwitcherView extends React.Component { this.setState({search: value}); + // Search our full list of options. We want: + // 1) Exact matches first + // 2) beginning-of-string matches second + // 3) middle-of-string matches last + const matchRegexes = [ + new RegExp(`^${Str.escapeForRegExp(value)}$`, 'i'), + new RegExp(`^${Str.escapeForRegExp(value)}`, 'i'), + new RegExp(Str.escapeForRegExp(value), 'i'), + ]; + // Because we want to regexes above to be listed in a specific order, the for loop below will end up adding // duplicate options to the list (because one option can match multiple regex patterns). // A Set is used here so that duplicate values are automatically removed. const matches = new Set(); - const chatReportOptions = this.getChatReportsOptions(false); + // If we have at least one group user then let's only get 1:1 DM chat options since we cannot add group + // DMs at this point + const chatReportOptions = this.getChatReportsOptions(false, this.state.groupUsers.length !== 0); // Get a list of all users we can send messages to and make their details generic const personalDetailOptions = _.chain(this.props.personalDetails) @@ -383,32 +412,37 @@ class ChatSwitcherView extends React.Component { alternateText: personalDetail.login, searchText: personalDetail.displayName === personalDetail.login ? personalDetail.login : `${personalDetail.displayName} ${personalDetail.login}`, - icons: [personalDetail.avatarURL], + icon: personalDetail.avatarURL, login: personalDetail.login, type: OPTION_TYPE.USER, })) .value(); const searchOptions = _.union(chatReportOptions, personalDetailOptions); - const userEnteredText = value.toLowerCase(); - - for (let i = 0; i < searchOptions.length; i++) { - const option = searchOptions[i]; - const optionSearchText = option.searchText.toLowerCase(); - const isMatch = optionSearchText.includes(userEnteredText); - - // We must also filter out any users who are already in the Group DM list - // so they can't be selected more than once - const isInGroupUsers = _.some(this.state.groupUsers, groupOption => ( - groupOption.login === option.login - )); - - // Make sure we don't include the same option twice (automatically handled be using a `Set`) - if (isMatch && !isInGroupUsers) { - matches.add(option); - } - if (matches.size === this.maxSearchResults) { + for (let i = 0; i < matchRegexes.length; i++) { + if (matches.size < this.maxSearchResults) { + for (let j = 0; j < searchOptions.length; j++) { + const option = searchOptions[j]; + const valueToSearch = option.searchText.replace(new RegExp(/ /g), ''); + const isMatch = matchRegexes[i].test(valueToSearch); + + // We must also filter out any users who are already in the Group DM list + // so they can't be selected more than once + const isInGroupUsers = _.some(this.state.groupUsers, groupOption => ( + groupOption.login === option.login + )); + + // Make sure we don't include the same option twice (automatically handled be using a `Set`) + if (isMatch && !isInGroupUsers) { + matches.add(option); + } + + if (matches.size === this.maxSearchResults) { + break; + } + } + } else { break; } } diff --git a/src/pages/home/sidebar/SidebarLink.js b/src/pages/home/sidebar/SidebarLink.js index 9329ed95ab50..cf665c5ee983 100644 --- a/src/pages/home/sidebar/SidebarLink.js +++ b/src/pages/home/sidebar/SidebarLink.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import {Image, View} from 'react-native'; +import _ from 'underscore'; import Text from '../../../components/Text'; import styles from '../../../styles/StyleSheet'; import PressableLink from '../../../components/PressableLink'; @@ -48,17 +49,14 @@ const SidebarLink = (props) => { style={styles.textDecorationNoLine} > - { - props.icon - && ( - - - - ) - } + {!_.isEmpty(props.icon) && ( + + + + )} {props.reportName} From 32f5988198285dd0db4c235a2d05f1e073192329 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Wed, 11 Nov 2020 11:49:19 -0800 Subject: [PATCH 16/44] cleanup --- src/pages/home/sidebar/ChatSwitcherView.js | 2 +- src/pages/home/sidebar/SidebarLink.js | 2 +- src/styles/StyleSheet.js | 3 --- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index be5c54e839ff..aa6d37d9d13d 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -433,7 +433,7 @@ class ChatSwitcherView extends React.Component { groupOption.login === option.login )); - // Make sure we don't include the same option twice (automatically handled be using a `Set`) + // Make sure we don't include the same option twice (automatically handled by using a `Set`) if (isMatch && !isInGroupUsers) { matches.add(option); } diff --git a/src/pages/home/sidebar/SidebarLink.js b/src/pages/home/sidebar/SidebarLink.js index cf665c5ee983..b06c01346f48 100644 --- a/src/pages/home/sidebar/SidebarLink.js +++ b/src/pages/home/sidebar/SidebarLink.js @@ -14,7 +14,7 @@ const propTypes = { // The name of the report to use as the text for this link reportName: PropTypes.string, - // The icon for the reports + // The icon for the report icon: PropTypes.string, // Toggles the hamburger menu open and closed diff --git a/src/styles/StyleSheet.js b/src/styles/StyleSheet.js index ff4abf3ff0e1..972bc51f5d92 100644 --- a/src/styles/StyleSheet.js +++ b/src/styles/StyleSheet.js @@ -80,9 +80,6 @@ const styles = { pr2: { paddingRight: 8, }, - pl4: { - paddingLeft: 4, - }, h100p: { height: '100%', From 40c04f58aeecd048b9d05c7925f151bf22574427 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Wed, 11 Nov 2020 12:37:54 -0800 Subject: [PATCH 17/44] correction to use Onyx --- src/libs/actions/Report.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index ada97aaf34e3..9ea2bf0a065a 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -192,7 +192,7 @@ function fetchChatReportsByIDs(chatList) { const dmParticipant = report.participants[0]; const icon = lodashGet(details, [dmParticipant, 'avatar']); if (!_.isEmpty(icon)) { - Ion.merge(`${IONKEYS.COLLECTION.REPORT}${report.reportID}`, {icon}); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, {icon}); } } }); From d0daf83b83d9d9cdecd1a370f7203014d0c8cce8 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Wed, 11 Nov 2020 17:17:50 -0800 Subject: [PATCH 18/44] Addressing review comments --- src/components/TextInputWithFocusStyles.js | 3 +- src/libs/actions/PersonalDetails.js | 22 +++++++++++++-- src/libs/actions/Report.js | 28 ++++--------------- .../home/sidebar/ChatSwitcherSearchForm.js | 2 -- src/pages/home/sidebar/ChatSwitcherView.js | 27 ++++++++---------- 5 files changed, 39 insertions(+), 43 deletions(-) diff --git a/src/components/TextInputWithFocusStyles.js b/src/components/TextInputWithFocusStyles.js index dce216bb3e2c..543efd55e96e 100644 --- a/src/components/TextInputWithFocusStyles.js +++ b/src/components/TextInputWithFocusStyles.js @@ -20,7 +20,7 @@ const propTypes = { style: PropTypes.any, // A function to call when the input has been blurred - onBlur: PropTypes.func.isRequired, + onBlur: PropTypes.func, // A function to call when the input has gotten focus onFocus: PropTypes.func.isRequired, @@ -29,6 +29,7 @@ const defaultProps = { styleFocusIn: null, styleFocusOut: null, style: null, + onBlur: () => {}, }; class TextInputWithFocusStyles extends React.Component { diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index b1ca0d704f3f..e4d148f73bc3 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -132,10 +132,26 @@ function fetch() { * Get personal details for a list of emails. * * @param {String} emailList - * @return {Promise} + * @param {Object} reports */ -function getForEmails(emailList) { - return API.getPersonalDetails(emailList); +function getForEmails(emailList, reports) { + API.getPersonalDetails(emailList) + .then((data) => { + const details = _.pick(data, emailList.split(',')); + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, formatPersonalDetails(details)); + + // The personalDetails of the participants contain their avatar images. Here we'll go over each + // report and based on the participants we'll link up their avatars to report icons. + _.each(reports, (report) => { + if (report.participants.length === 1) { + const dmParticipant = report.participants[0]; + const icon = lodashGet(details, [dmParticipant, 'avatar']); + if (icon) { + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, {icon}); + } + } + }); + }); } // When the app reconnects from being offline, fetch all of the personal details diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 9ea2bf0a065a..1617d10b313e 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -69,7 +69,7 @@ function getUnreadActionCount(report) { ]) || lodashGet(report, [ 'reportNameValuePairs', `lastAccessed_${currentUserAccountID}`, - 'readActionID', + 'actionID', ]); // Save the lastReadActionID locally so we can access this later @@ -116,7 +116,7 @@ function getSimplifiedReportObject(report) { maxSequenceNumber: report.reportActionList.length, participants: getParticipantEmailsFromReport(report), isPinned: report.isPinned, - lastVisited: lodashGet(report, [ + lastVisitedTimestamp: lodashGet(report, [ 'reportNameValuePairs', `lastAccessed_${currentUserAccountID}`, 'timestamp' @@ -179,24 +179,8 @@ function fetchChatReportsByIDs(chatList) { // Fetch the person details if there are any participantEmails = _.unique(participantEmails); - if (participantEmails && participantEmails.length !== 0) { - PersonalDetails.getForEmails(participantEmails.join(',')) - .then((data) => { - const details = _.pick(data, participantEmails); - Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, PersonalDetails.formatPersonalDetails(details)); - - // The personalDetails of the participants contain their avatar images. Here we'll go over each - // report and based on the participants we'll link up their avatars to report icons. - _.each(simplifiedReports, (report) => { - if (lodashGet(report, 'participants', []).length === 1) { - const dmParticipant = report.participants[0]; - const icon = lodashGet(details, [dmParticipant, 'avatar']); - if (!_.isEmpty(icon)) { - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, {icon}); - } - } - }); - }); + if (participantEmails && participantEmails.length > 0) { + PersonalDetails.getForEmails(participantEmails.join(','), simplifiedReports); } return _.map(fetchedReports, report => report.reportID); @@ -215,7 +199,7 @@ function setLocalLastReadActionID(reportID, sequenceNumber) { // Update the report optimistically Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { unreadActionCount: 0, - lastVisited: Date.now(), + lastVisitedTimestamp: Date.now(), }); } @@ -472,7 +456,7 @@ function fetchOrCreateChatReport(participants) { // Store only the absolute bare minimum of data in Onyx because space is limited const newReport = getSimplifiedReportObject(report); newReport.reportName = getChatReportName(report.sharedReportList); - newReport.lastVisited = Date.now(); + newReport.lastVisitedTimestamp = Date.now(); // Merge the data into Onyx. Don't use set() here or multiSet() because then that would // overwrite any existing data (like if they have unread messages) diff --git a/src/pages/home/sidebar/ChatSwitcherSearchForm.js b/src/pages/home/sidebar/ChatSwitcherSearchForm.js index a23c2d90e660..b7f342658d09 100644 --- a/src/pages/home/sidebar/ChatSwitcherSearchForm.js +++ b/src/pages/home/sidebar/ChatSwitcherSearchForm.js @@ -106,7 +106,6 @@ const ChatSwitcherSearchForm = props => ( // everything when we try to remove a user or start // the conversation // eslint-disable-next-line react/jsx-props-no-multi-spaces - onBlur={() => {}} onChangeText={props.onChangeText} onFocus={props.onFocus} onKeyPress={props.onKeyPress} @@ -139,7 +138,6 @@ const ChatSwitcherSearchForm = props => ( ref={props.forwardedRef} style={[styles.textInput, styles.textInputReversed, styles.flex1]} value={props.searchValue} - onBlur={() => {}} onChangeText={props.onChangeText} onFocus={props.onFocus} onKeyPress={props.onKeyPress} diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index 60bf01a20088..e740247b7e54 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -125,14 +125,13 @@ class ChatSwitcherView extends React.Component { * reject the same in personalDetailOptions to not deal with dupes. * * @param {Boolean} sortByLastVisited - * @param {Boolean} getOnlySingleUserPrivateDM + * @param {Boolean} onlyShowSingleParticipantChats * @returns {Object} */ - getChatReportsOptions(sortByLastVisited = true, getOnlySingleUserPrivateDM = false) { - let chatReports = this.props.reports; - if (sortByLastVisited) { - chatReports = lodashOrderby(this.props.reports, ['lastVisited'], ['desc']); - } + getChatReportsOptions(sortByLastVisited = true, onlyShowSingleParticipantChats = false) { + const chatReports = sortByLastVisited + ? lodashOrderby(this.props.reports, ['lastVisitedTimestamp'], ['desc']) + : this.props.reports; return _.chain(chatReports) .values() @@ -140,10 +139,10 @@ class ChatSwitcherView extends React.Component { if (_.isEmpty(report.reportName)) { return true; } - if (sortByLastVisited && !report.lastVisited) { + if (sortByLastVisited && !report.lastVisitedTimestamp) { return true; } - if (getOnlySingleUserPrivateDM) { + if (onlyShowSingleParticipantChats) { const participants = lodashGet(report, 'participants', []); const isSingleUserPrivateDMReport = participants.length === 1; @@ -298,14 +297,11 @@ class ChatSwitcherView extends React.Component { */ triggerOnFocusCallback() { ChatSwitcher.show(); - const params = { + this.setState(prevState => ({ isLogoVisible: false, isClearButtonVisible: true, - }; - if (this.state.search === '') { - params.options = this.getChatReportsOptions(); - } - this.setState(params); + options: prevState.search === '' ? this.getChatReportsOptions() : prevState.options, + })); } /** @@ -401,7 +397,8 @@ class ChatSwitcherView extends React.Component { // If we have at least one group user then let's only get 1:1 DM chat options since we cannot add group // DMs at this point - const chatReportOptions = this.getChatReportsOptions(false, this.state.groupUsers.length !== 0); + const isGroupChat = this.state.groupUsers.length > 0; + const chatReportOptions = this.getChatReportsOptions(false, isGroupChat); // Get a list of all users we can send messages to and make their details generic const personalDetailOptions = _.chain(this.props.personalDetails) From 35e5876913342a0a31a754a5f3d41c10b35bb3c8 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Thu, 12 Nov 2020 09:42:49 -0800 Subject: [PATCH 19/44] Update to use lastRead --- src/libs/API.js | 6 +++--- src/libs/actions/Report.js | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/libs/API.js b/src/libs/API.js index 32bafefeec36..77a0ff94e6e1 100644 --- a/src/libs/API.js +++ b/src/libs/API.js @@ -403,8 +403,8 @@ function authenticate(parameters) { * @param {number} parameters.sequenceNumber * @returns {Promise} */ -function updateLastAccessed(parameters) { - return queueRequest('Report_UpdateLastAccessed', { +function updateLastRead(parameters) { + return queueRequest('Report_UpdateLastRead', { authToken, accountID: parameters.accountID, reportID: parameters.reportID, @@ -525,7 +525,7 @@ export { getAuthToken, getPersonalDetails, getReportHistory, - updateLastAccessed, + updateLastRead, setNameValuePair, togglePinnedReport, logToServer, diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 1617d10b313e..28d4810f051b 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -68,7 +68,7 @@ function getUnreadActionCount(report) { `lastReadActionID_${currentUserAccountID}`, ]) || lodashGet(report, [ 'reportNameValuePairs', - `lastAccessed_${currentUserAccountID}`, + `lastRead_${currentUserAccountID}`, 'actionID', ]); @@ -118,7 +118,7 @@ function getSimplifiedReportObject(report) { isPinned: report.isPinned, lastVisitedTimestamp: lodashGet(report, [ 'reportNameValuePairs', - `lastAccessed_${currentUserAccountID}`, + `lastRead_${currentUserAccountID}`, 'timestamp' ], 0) }; @@ -188,12 +188,12 @@ function fetchChatReportsByIDs(chatList) { } /** - * Update the lastReadActionID in local memory + * Update the lastRead actionID and timestamp in local memory and Onyx * * @param {Number} reportID * @param {Number} sequenceNumber */ -function setLocalLastReadActionID(reportID, sequenceNumber) { +function setLocalLastRead(reportID, sequenceNumber) { lastReadActionIDs[reportID] = sequenceNumber; // Update the report optimistically @@ -217,7 +217,7 @@ function updateReportWithNewAction(reportID, reportAction) { // last read actionID has been updated in the server but not necessarily reflected // locally so we must first update it and then calculate the unread (which should be 0) if (isFromCurrentUser) { - setLocalLastReadActionID(reportID, newMaxSequenceNumber); + setLocalLastRead(reportID, newMaxSequenceNumber); } // Always merge the reportID into Onyx @@ -543,10 +543,10 @@ function updateLastReadActionID(reportID, sequenceNumber) { return; } - setLocalLastReadActionID(reportID, sequenceNumber); + setLocalLastRead(reportID, sequenceNumber); // Mark the report as not having any unread items - API.updateLastAccessed({ + API.updateLastRead({ accountID: currentUserAccountID, reportID, sequenceNumber, From decebefc27a55aa3cef73afdcbc78d37d601ad65 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Thu, 12 Nov 2020 10:17:02 -0800 Subject: [PATCH 20/44] moar comments and some cleanup --- src/libs/actions/Report.js | 1 + src/pages/home/sidebar/ChatSwitcherView.js | 23 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 28d4810f051b..19d228482fbc 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -63,6 +63,7 @@ const lastReadActionIDs = {}; */ function getUnreadActionCount(report) { // @todo remove the first check as part of cleanup https://github.com/Expensify/Expensify/issues/145243 + // since we migrating our data from lastReadActionID_ value to lastRead_ object. const usersLastReadActionID = lodashGet(report, [ 'reportNameValuePairs', `lastReadActionID_${currentUserAccountID}`, diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index e740247b7e54..952a8b96d0da 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -125,31 +125,29 @@ class ChatSwitcherView extends React.Component { * reject the same in personalDetailOptions to not deal with dupes. * * @param {Boolean} sortByLastVisited - * @param {Boolean} onlyShowSingleParticipantChats + * @param {Boolean} excludeGroupDMs * @returns {Object} */ - getChatReportsOptions(sortByLastVisited = true, onlyShowSingleParticipantChats = false) { + getChatReportsOptions(sortByLastVisited = true, excludeGroupDMs = false) { const chatReports = sortByLastVisited ? lodashOrderby(this.props.reports, ['lastVisitedTimestamp'], ['desc']) : this.props.reports; return _.chain(chatReports) .values() - .reject((report) => { + .filter((report) => { if (_.isEmpty(report.reportName)) { - return true; + return false; } if (sortByLastVisited && !report.lastVisitedTimestamp) { - return true; + return false; } - if (onlyShowSingleParticipantChats) { + if (excludeGroupDMs) { const participants = lodashGet(report, 'participants', []); - const isSingleUserPrivateDMReport = participants.length === 1; - - // Since we only want 1:1 private DMs we'll have to return true (reject) when its not a 1:1 DM - return !isSingleUserPrivateDMReport; + const isGroupDM = participants.length > 0; + return !isGroupDM; } - return false; + return true; }) .map((report) => { const participants = lodashGet(report, 'participants', []); @@ -396,7 +394,8 @@ class ChatSwitcherView extends React.Component { const matches = new Set(); // If we have at least one group user then let's only get 1:1 DM chat options since we cannot add group - // DMs at this point + // DMs at this point. We don't want to sort our chatReportOptions by lastVisited since we'll let the regex + // matches order our options. const isGroupChat = this.state.groupUsers.length > 0; const chatReportOptions = this.getChatReportsOptions(false, isGroupChat); From ff11e79eb514f2d056a2819bf256d19d3f48d9f6 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Thu, 12 Nov 2020 11:14:24 -0800 Subject: [PATCH 21/44] comment update --- src/pages/home/sidebar/ChatSwitcherView.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index 952a8b96d0da..ba0a40cb7bff 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -121,8 +121,7 @@ class ChatSwitcherView extends React.Component { /** * Get the chat report options created from props.report. Additionally these chat report options will also determine - * if its a 1:1 DM or not. If it is a 1:1 DM we'll save the participant login and the type as user so that we can - * reject the same in personalDetailOptions to not deal with dupes. + * if its a 1:1 DM or not. If it is a 1:1 DM we'll save the DM participant login and the type as user. * * @param {Boolean} sortByLastVisited * @param {Boolean} excludeGroupDMs @@ -399,7 +398,9 @@ class ChatSwitcherView extends React.Component { const isGroupChat = this.state.groupUsers.length > 0; const chatReportOptions = this.getChatReportsOptions(false, isGroupChat); - // Get a list of all users we can send messages to and make their details generic + // Get a list of all users we can send messages to and make their details generic. We will also reject any + // personalDetails logins that exist in chatReportOptions which will remove our dupes since we'll use + // chatReportOptions as our first source of truth if the 1:1 chat DM exists there. const personalDetailOptions = _.chain(this.props.personalDetails) .values() .reject(personalDetail => _.findWhere(chatReportOptions, {login: personalDetail.login})) From 85a5b8ca010262e06cb6458f6af63100d79ce3b1 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Wed, 25 Nov 2020 16:17:10 -0800 Subject: [PATCH 22/44] unneeded unread check --- src/pages/home/sidebar/ChatSwitcherView.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index 617c83cfaaee..17ca9c146ef4 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -435,15 +435,6 @@ class ChatSwitcherView extends React.Component { matches.add(option); } - // If is is a single user private DM report, add the isUnread property to the - // user UI equivalent. - if (isSingleUserPrivateDMReport) { - const userOption = _.find(searchOptions, opt => opt.login === option.participants[0]); - if (userOption) { - userOption.isUnread = option.isUnread; - } - } - if (matches.size === this.maxSearchResults) { break; } From fd27e337fe742c40eb1a08a63096c1b6eb3a5e7f Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Wed, 25 Nov 2020 16:30:08 -0800 Subject: [PATCH 23/44] Correcting logic --- src/pages/home/sidebar/ChatSwitcherView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index 17ca9c146ef4..48a983db24de 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -275,7 +275,7 @@ class ChatSwitcherView extends React.Component { reset(blurAfterReset = true) { this.setState({ search: '', - options: this.getChatReportsOptions(), + options: blurAfterReset ? [] : this.getChatReportsOptions(), focusedIndex: 0, isLogoVisible: blurAfterReset, isClearButtonVisible: !blurAfterReset, From 0d2a024d1a3558f1c11ef68e6ee72b41eb8d58ee Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Thu, 26 Nov 2020 12:45:31 -0800 Subject: [PATCH 24/44] Some corrections around the group chat and sorting logic --- ios/Podfile.lock | 2 +- src/pages/home/sidebar/ChatSwitcherView.js | 37 +++++++++++++++------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 612f33396b68..a1dc27174bf7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -672,4 +672,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: d03a22d8299d9564ca7fc55d0a779f6fbf0d2b37 -COCOAPODS: 1.8.4 +COCOAPODS: 1.10.0 diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index 48a983db24de..89193564f4dd 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -124,15 +124,14 @@ class ChatSwitcherView extends React.Component { * if its a 1:1 DM or not. If it is a 1:1 DM we'll save the DM participant login and the type as user. * * @param {Boolean} sortByLastVisited - * @param {Boolean} excludeGroupDMs * @returns {Object} */ - getChatReportsOptions(sortByLastVisited = true, excludeGroupDMs = false) { - const chatReports = sortByLastVisited - ? lodashOrderby(this.props.reports, ['lastVisitedTimestamp'], ['desc']) - : this.props.reports; + getChatReportsOptions(sortByLastVisited = true) { + // If we have at least one group user then let's only get 1:1 DM chat options since we cannot add group + // DMs at this point. + const excludeGroupDMs = this.state.groupUsers.length > 0; - return _.chain(chatReports) + const chatReports = _.chain(this.props.reports) .values() .filter((report) => { if (_.isEmpty(report.reportName)) { @@ -141,9 +140,18 @@ class ChatSwitcherView extends React.Component { if (sortByLastVisited && !report.lastVisitedTimestamp) { return false; } + // Remove any previously selected group user so that it doesn't show as a dupe + const isInGroupUsers = _.some(this.state.groupUsers, ({login}) => { + const participants = lodashGet(report, 'participants', []); + const isSingleUserPrivateDMReport = participants.length === 1; + return isSingleUserPrivateDMReport && login === participants[0]; + }); + if (isInGroupUsers) { + return false; + } if (excludeGroupDMs) { const participants = lodashGet(report, 'participants', []); - const isGroupDM = participants.length > 0; + const isGroupDM = participants.length > 1; return !isGroupDM; } return true; @@ -163,9 +171,16 @@ class ChatSwitcherView extends React.Component { login, type: isSingleUserPrivateDMReport ? OPTION_TYPE.USER : OPTION_TYPE.REPORT, isUnread: report.unreadActionCount > 0, + lastVisitedTimestamp: report.lastVisitedTimestamp, }; }) .value(); + + // If we are not sorting by lastVisited then let's sort it such that 1:1 user reports are on top with group DMs + // on the bottom. This would ensure our search UI is clean than having 1:1 reports show up in the middle. + return sortByLastVisited + ? lodashOrderby(chatReports, ['lastVisitedTimestamp'], ['desc']) + : lodashOrderby(chatReports, ['type'], ['desc']); } /** @@ -367,7 +382,7 @@ class ChatSwitcherView extends React.Component { if (this.state.groupUsers.length > 0) { // If we have groupLogins we only want to reset the options not // the entire state which would clear out the list of groupUsers - this.setState({options: [], search: ''}); + this.setState({options: this.getChatReportsOptions(true), search: ''}); return; } @@ -392,11 +407,9 @@ class ChatSwitcherView extends React.Component { // A Set is used here so that duplicate values are automatically removed. const matches = new Set(); - // If we have at least one group user then let's only get 1:1 DM chat options since we cannot add group - // DMs at this point. We don't want to sort our chatReportOptions by lastVisited since we'll let the regex + // We don't want to sort our chatReportOptions by lastVisited since we'll let the regex // matches order our options. - const isGroupChat = this.state.groupUsers.length > 0; - const chatReportOptions = this.getChatReportsOptions(false, isGroupChat); + const chatReportOptions = this.getChatReportsOptions(false); // Get a list of all users we can send messages to and make their details generic. We will also reject any // personalDetails logins that exist in chatReportOptions which will remove our dupes since we'll use From 9a5cf517a3bf0bdcde46c10401cbffe1b333ac0e Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Fri, 27 Nov 2020 15:03:50 -0800 Subject: [PATCH 25/44] revert accidently pushed change --- ios/Podfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a1dc27174bf7..612f33396b68 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -672,4 +672,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: d03a22d8299d9564ca7fc55d0a779f6fbf0d2b37 -COCOAPODS: 1.10.0 +COCOAPODS: 1.8.4 From 8a7c43cc860ed5abd1cd1b5e600c2dd5ec19bd57 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Fri, 27 Nov 2020 15:23:37 -0800 Subject: [PATCH 26/44] Some readability improvements --- src/CONST.js | 4 ++++ src/pages/home/sidebar/ChatLinkRow.js | 5 +++-- src/pages/home/sidebar/ChatSwitcherList.js | 5 ++++- src/pages/home/sidebar/ChatSwitcherView.js | 19 ++++++++----------- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index aec83345c7d2..8845ff9bbc57 100644 --- a/src/CONST.js +++ b/src/CONST.js @@ -5,6 +5,10 @@ const CONST = { CLOUDFRONT_URL, MOZILLA_PDF_VIEWER_URL, EXPENSIFY_ICON_URL: `${CLOUDFRONT_URL}/images/favicon-2019.png`, + REPORT: { + SINGLE_USER_CHAT: 'singleUserChat', + GROUP_CHAT: 'groupChats', + }, }; export default CONST; diff --git a/src/pages/home/sidebar/ChatLinkRow.js b/src/pages/home/sidebar/ChatLinkRow.js index 425f0468e917..81edf9646235 100644 --- a/src/pages/home/sidebar/ChatLinkRow.js +++ b/src/pages/home/sidebar/ChatLinkRow.js @@ -12,6 +12,7 @@ import styles, {colors} from '../../../styles/StyleSheet'; import ChatSwitcherOptionPropTypes from './ChatSwitcherOptionPropTypes'; import ROUTES from '../../../ROUTES'; import PressableLink from '../../../components/PressableLink'; +import CONST from '../../../CONST'; const propTypes = { // Option to allow the user to choose from can be type 'report' or 'user' @@ -42,7 +43,7 @@ const ChatLinkRow = ({ onAddToGroup, isChatSwitcher, }) => { - const isUserRow = option.type === 'user'; + const isSingleUserChat = option.type === CONST.REPORT.SINGLE_USER_CHAT; const textStyle = optionIsFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; @@ -107,7 +108,7 @@ const ChatLinkRow = ({ - {isUserRow && isChatSwitcher && ( + {isSingleUserChat && isChatSwitcher && ( (option.type === 'user' ? option.alternateText : String(option.reportID))} + keyExtractor={option => ( + option.type === CONST.REPORT.SINGLE_USER_CHAT ? option.alternateText : String(option.reportID) + )} renderItem={({item, index}) => ( { const participants = lodashGet(report, 'participants', []); @@ -158,8 +155,8 @@ class ChatSwitcherView extends React.Component { }) .map((report) => { const participants = lodashGet(report, 'participants', []); - const isSingleUserPrivateDMReport = participants.length === 1; - const login = isSingleUserPrivateDMReport ? report.participants[0] : ''; + const isSingleUserChat = participants.length === 1; + const login = isSingleUserChat ? report.participants[0] : ''; return { text: report.reportName, alternateText: report.reportName, @@ -169,7 +166,7 @@ class ChatSwitcherView extends React.Component { participants, icon: report.icon, login, - type: isSingleUserPrivateDMReport ? OPTION_TYPE.USER : OPTION_TYPE.REPORT, + type: isSingleUserChat ? CONST.REPORT.SINGLE_USER_CHAT : CONST.REPORT.GROUP_CHAT, isUnread: report.unreadActionCount > 0, lastVisitedTimestamp: report.lastVisitedTimestamp, }; @@ -190,10 +187,10 @@ class ChatSwitcherView extends React.Component { */ selectRow(option) { switch (option.type) { - case OPTION_TYPE.USER: + case CONST.REPORT.SINGLE_USER_CHAT: this.selectUser(option); break; - case OPTION_TYPE.REPORT: + case CONST.REPORT.GROUP_CHAT: this.selectReport(option); break; default: @@ -424,7 +421,7 @@ class ChatSwitcherView extends React.Component { : `${personalDetail.displayName} ${personalDetail.login}`, icon: personalDetail.avatarURL, login: personalDetail.login, - type: OPTION_TYPE.USER, + type: CONST.REPORT.SINGLE_USER_CHAT, })) .value(); From 69bf6ab45fe02a2278df7a12e260dabb2cdc3771 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Mon, 30 Nov 2020 12:09:17 -0800 Subject: [PATCH 27/44] addressing comments --- src/libs/actions/PersonalDetails.js | 22 +++++++++++++------ src/libs/actions/Report.js | 13 ++--------- src/pages/home/sidebar/ChatSwitcherList.js | 5 +---- .../sidebar/ChatSwitcherOptionPropTypes.js | 3 +++ src/pages/home/sidebar/ChatSwitcherView.js | 7 +++++- 5 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index e4d148f73bc3..d398d33de50d 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -129,15 +129,24 @@ function fetch() { } /** - * Get personal details for a list of emails. + * Get personal details from report participants. * - * @param {String} emailList * @param {Object} reports */ -function getForEmails(emailList, reports) { - API.getPersonalDetails(emailList) +function getFromReportParticipants(reports) { + const participantEmails = _.chain(reports) + .pluck('participants') + .flatten() + .unique() + .value(); + + if (participantEmails.length === 0) { + return; + } + + API.getPersonalDetails(participantEmails.join(',')) .then((data) => { - const details = _.pick(data, emailList.split(',')); + const details = _.pick(data, participantEmails); Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, formatPersonalDetails(details)); // The personalDetails of the participants contain their avatar images. Here we'll go over each @@ -160,7 +169,6 @@ NetworkConnection.onReconnect(fetch); export { fetch, fetchTimezone, - getForEmails, + getFromReportParticipants, getDisplayName, - formatPersonalDetails, }; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 9538172cd105..dcadd6ef75f5 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -158,17 +158,11 @@ function fetchChatReportsByIDs(chatList) { .then(({reports}) => { fetchedReports = reports; - // Build array of all participant emails so we can - // get the personal details. - let participantEmails = []; - // Process the reports and store them in Onyx const simplifiedReports = []; _.each(fetchedReports, (report) => { const newReport = getSimplifiedReportObject(report); - simplifiedReports.push(newReport); - participantEmails.push(newReport.participants); if (lodashGet(report, 'reportNameValuePairs.type') === 'chat') { newReport.reportName = getChatReportName(report.sharedReportList); @@ -178,11 +172,8 @@ function fetchChatReportsByIDs(chatList) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, newReport); }); - // Fetch the person details if there are any - participantEmails = _.unique(participantEmails); - if (participantEmails && participantEmails.length > 0) { - PersonalDetails.getForEmails(participantEmails.join(','), simplifiedReports); - } + // Fetch the personal details if there are any + PersonalDetails.getFromReportParticipants(simplifiedReports); return _.map(fetchedReports, report => report.reportID); }); diff --git a/src/pages/home/sidebar/ChatSwitcherList.js b/src/pages/home/sidebar/ChatSwitcherList.js index 74cbd1254ea9..f2df14bef2b9 100644 --- a/src/pages/home/sidebar/ChatSwitcherList.js +++ b/src/pages/home/sidebar/ChatSwitcherList.js @@ -5,7 +5,6 @@ import styles from '../../../styles/StyleSheet'; import ChatSwitcherOptionPropTypes from './ChatSwitcherOptionPropTypes'; import ChatLinkRow from './ChatLinkRow'; import KeyboardSpacer from '../../../components/KeyboardSpacer'; -import CONST from '../../../CONST'; const propTypes = { // The index of the option that is currently in focus @@ -35,9 +34,7 @@ const ChatSwitcherList = ({ ( - option.type === CONST.REPORT.SINGLE_USER_CHAT ? option.alternateText : String(option.reportID) - )} + keyExtractor={option => option.keyForList} renderItem={({item, index}) => ( 0, lastVisitedTimestamp: report.lastVisitedTimestamp, + keyForList: String(report.reportID), }; }) .value(); @@ -309,6 +310,9 @@ class ChatSwitcherView extends React.Component { this.setState(prevState => ({ isLogoVisible: false, isClearButtonVisible: true, + + // When the search bar is empty let's show the default chat report options sorted by last visited. If the + // search bar is not empty that means some text is present hence on focus let's not update the options. options: prevState.search === '' ? this.getChatReportsOptions() : prevState.options, })); } @@ -379,7 +383,7 @@ class ChatSwitcherView extends React.Component { if (this.state.groupUsers.length > 0) { // If we have groupLogins we only want to reset the options not // the entire state which would clear out the list of groupUsers - this.setState({options: this.getChatReportsOptions(true), search: ''}); + this.setState({options: this.getChatReportsOptions(), search: ''}); return; } @@ -422,6 +426,7 @@ class ChatSwitcherView extends React.Component { icon: personalDetail.avatarURL, login: personalDetail.login, type: CONST.REPORT.SINGLE_USER_CHAT, + keyForList: personalDetail.login, })) .value(); From f2d9f355dc17f511c95ea8980631c8b6b6e09923 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Mon, 30 Nov 2020 19:45:52 -0800 Subject: [PATCH 28/44] updating variable name --- src/pages/home/sidebar/ChatSwitcherView.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index 08e4e1a00d85..21bbae711d02 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -281,20 +281,20 @@ class ChatSwitcherView extends React.Component { } /** - * Reset the component to it's default state and blur the input + * Reset the component to it's default state and blur the input if we are no longer searching * - * @param {boolean} blurAfterReset + * @param {boolean} continueSearchingAfterReset */ - reset(blurAfterReset = true) { + reset(continueSearchingAfterReset = false) { this.setState({ search: '', - options: blurAfterReset ? [] : this.getChatReportsOptions(), + options: continueSearchingAfterReset ? this.getChatReportsOptions() : [], focusedIndex: 0, - isLogoVisible: blurAfterReset, - isClearButtonVisible: !blurAfterReset, + isLogoVisible: !continueSearchingAfterReset, + isClearButtonVisible: continueSearchingAfterReset, groupUsers: [], }, () => { - if (blurAfterReset) { + if (!continueSearchingAfterReset) { this.textInput.blur(); ChatSwitcher.hide(); } From 47c8f959a0bb78ea2628b0a78e1893d0c8282f82 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Tue, 1 Dec 2020 15:54:29 -0800 Subject: [PATCH 29/44] updating key to sequenceNumber to match web-e --- src/libs/actions/Report.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index dcadd6ef75f5..24a3753d2c19 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -53,7 +53,7 @@ const typingWatchTimers = {}; const reportMaxSequenceNumbers = {}; // Keeps track of the last read for each report -const lastReadActionIDs = {}; +const lastReadSequenceNumbers = {}; /** * Checks the report to see if there are any unread action items @@ -64,29 +64,29 @@ const lastReadActionIDs = {}; function getUnreadActionCount(report) { // @todo remove the first check as part of cleanup https://github.com/Expensify/Expensify/issues/145243 // since we migrating our data from lastReadActionID_ value to lastRead_ object. - const usersLastReadActionID = lodashGet(report, [ + const lastReadSequenceNumber = lodashGet(report, [ 'reportNameValuePairs', `lastReadActionID_${currentUserAccountID}`, ]) || lodashGet(report, [ 'reportNameValuePairs', `lastRead_${currentUserAccountID}`, - 'actionID', + 'sequenceNumber', ]); // Save the lastReadActionID locally so we can access this later - lastReadActionIDs[report.reportID] = usersLastReadActionID; + lastReadSequenceNumbers[report.reportID] = lastReadSequenceNumber; if (report.reportActionList.length === 0) { return 0; } - if (!usersLastReadActionID) { + if (!lastReadSequenceNumber) { return report.reportActionList.length; } // There are unread items if the last one the user has read is less // than the highest sequence number we have - const unreadActionCount = report.reportActionList.length - usersLastReadActionID; + const unreadActionCount = report.reportActionList.length - lastReadSequenceNumber; return Math.max(0, unreadActionCount); } @@ -186,7 +186,7 @@ function fetchChatReportsByIDs(chatList) { * @param {Number} sequenceNumber */ function setLocalLastRead(reportID, sequenceNumber) { - lastReadActionIDs[reportID] = sequenceNumber; + lastReadSequenceNumbers[reportID] = sequenceNumber; // Update the report optimistically Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { @@ -217,7 +217,7 @@ function updateReportWithNewAction(reportID, reportAction) { // by handleReportChanged Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { reportID, - unreadActionCount: newMaxSequenceNumber - (lastReadActionIDs[reportID] || 0), + unreadActionCount: newMaxSequenceNumber - (lastReadSequenceNumbers[reportID] || 0), maxSequenceNumber: reportAction.sequenceNumber, }); From dd483ac5dd113b48c9259207a4aafad296780cd4 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Thu, 3 Dec 2020 17:36:39 -0800 Subject: [PATCH 30/44] corrections and a bug fix --- src/libs/actions/PersonalDetails.js | 2 +- src/libs/actions/Report.js | 2 +- src/pages/home/sidebar/ChatSwitcherView.js | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index 8204bf37fca0..20734561e114 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -144,7 +144,7 @@ function getFromReportParticipants(reports) { return; } - API.PersonalDetails_GetForEmails(participantEmails.join(',')) + API.PersonalDetails_GetForEmails({emailList: participantEmails.join(',')}) .then((data) => { const details = _.pick(data, participantEmails); Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, formatPersonalDetails(details)); diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 072192806650..2904f3c05ba1 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -541,7 +541,7 @@ function updateLastReadActionID(reportID, sequenceNumber) { setLocalLastRead(reportID, sequenceNumber); // Mark the report as not having any unread items - API.Report_SetLastReadActionID({ + API.Report_UpdateLastRead({ accountID: currentUserAccountID, reportID, sequenceNumber, diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index 5769eeab4438..045965823fcf 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -234,7 +234,11 @@ class ChatSwitcherView extends React.Component { : [...users, option] ), []), }), () => { - this.updateSearch(this.state.search); + if (this.state.groupUsers.length > 0) { + this.updateSearch(this.state.search); + } else { + this.reset(true); + } this.textInput.focus(); }); } From 5b1d9a9bdce2fae267a3ef0356d71c6e86e393f8 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Tue, 8 Dec 2020 10:02:00 -0800 Subject: [PATCH 31/44] Update to match code style --- src/libs/API.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/libs/API.js b/src/libs/API.js index 6019c83361ad..44fb9203b2a7 100644 --- a/src/libs/API.js +++ b/src/libs/API.js @@ -241,7 +241,6 @@ function Authenticate(parameters) { }); } - /** * @param {object} parameters * @param {string} parameters.emailList @@ -389,12 +388,11 @@ function Report_TogglePinned(parameters) { * @returns {Promise} */ function Report_UpdateLastRead(parameters) { - return request('Report_UpdateLastRead', { - authToken, - accountID: parameters.accountID, - reportID: parameters.reportID, - sequenceNumber: parameters.sequenceNumber, - }); + const commandName = 'Report_UpdateLastRead'; + const params = {...parameters}; + requireParameters(['accountID', 'reportID', 'sequenceNumber'], parameters, commandName); + params.authToken = authToken; + return request(commandName, params); } export { From d437b3e88d860f42e22a530ce49adf746bf2eee8 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Tue, 8 Dec 2020 14:45:52 -0800 Subject: [PATCH 32/44] cleaning up some flows --- src/libs/API.js | 4 +--- src/libs/actions/PersonalDetails.js | 16 ++++++++------ src/libs/actions/Report.js | 5 ++++- src/pages/home/sidebar/ChatSwitcherView.js | 25 ++++++++++------------ 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/libs/API.js b/src/libs/API.js index 44fb9203b2a7..76d8d7fe1ef1 100644 --- a/src/libs/API.js +++ b/src/libs/API.js @@ -389,10 +389,8 @@ function Report_TogglePinned(parameters) { */ function Report_UpdateLastRead(parameters) { const commandName = 'Report_UpdateLastRead'; - const params = {...parameters}; requireParameters(['accountID', 'reportID', 'sequenceNumber'], parameters, commandName); - params.authToken = authToken; - return request(commandName, params); + return request(commandName, parameters); } export { diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index 20734561e114..dbba15554d80 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -19,6 +19,13 @@ Onyx.connect({ callback: val => personalDetails = val, }); +function getDefaultAvatar(login) { + // There are 8 possible default avatars, so we choose which one this user has based + // on a simple hash of their login (which is converted from HEX to INT) + const loginHashBucket = (parseInt(md5(login).substring(0, 4), 16) % 8) + 1; + return `${CONST.CLOUDFRONT_URL}/images/avatars/avatar_${loginHashBucket}.png`; +} + /** * Returns the URL for a user's avatar and handles someone not having any avatar at all * @@ -31,10 +38,7 @@ function getAvatar(personalDetail, login) { return personalDetail.avatar.replace(/&d=404$/, ''); } - // There are 8 possible default avatars, so we choose which one this user has based - // on a simple hash of their login (which is converted from HEX to INT) - const loginHashBucket = (parseInt(md5(login).substring(0, 4), 16) % 8) + 1; - return `${CONST.CLOUDFRONT_URL}/images/avatars/avatar_${loginHashBucket}.png`; + return getDefaultAvatar(login); } /** @@ -147,14 +151,14 @@ function getFromReportParticipants(reports) { API.PersonalDetails_GetForEmails({emailList: participantEmails.join(',')}) .then((data) => { const details = _.pick(data, participantEmails); - Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, formatPersonalDetails(details)); + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, details); // The personalDetails of the participants contain their avatar images. Here we'll go over each // report and based on the participants we'll link up their avatars to report icons. _.each(reports, (report) => { if (report.participants.length === 1) { const dmParticipant = report.participants[0]; - const icon = lodashGet(details, [dmParticipant, 'avatar']); + const icon = lodashGet(details, [dmParticipant, 'avatar'], getDefaultAvatar(dmParticipant)); if (icon) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, {icon}); } diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 2904f3c05ba1..cd4cd2382c71 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -158,7 +158,10 @@ function fetchChatReportsByIDs(chatList) { .then(({reports}) => { fetchedReports = reports; - // Process the reports and store them in Onyx + // Process the reports and store them in Onyx. At the same time we'll save the simplified reports in this + // variable called simplifiedReports which hold the participants (minus the current user) for each report. + // Using this simplifiedReport we can call PersonalDetails.getFromReportParticipants to get the + // personal details of all the participants and even link up their avatars to report icons. const simplifiedReports = []; _.each(fetchedReports, (report) => { const newReport = getSimplifiedReportObject(report); diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index 71c677a17852..c6224ae664fa 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -116,8 +116,8 @@ class ChatSwitcherView extends React.Component { } /** - * Get the chat report options created from props.report. Additionally these chat report options will also determine - * if its a 1:1 DM or not. If it is a 1:1 DM we'll save the DM participant login and the type as user. + * Get the chat report options created from props.reports. Additionally these chat report options will also + * determine if its a 1:1 DM or not. If it is a 1:1 DM we'll save the DM participant login and the type as user. * * @param {Boolean} sortByLastVisited * @returns {Object} @@ -234,11 +234,7 @@ class ChatSwitcherView extends React.Component { : [...users, option] ), []), }), () => { - if (this.state.groupUsers.length > 0) { - this.updateSearch(this.state.search); - } else { - this.reset(true); - } + this.updateSearch(this.state.search); this.textInput.focus(); }); } @@ -288,18 +284,19 @@ class ChatSwitcherView extends React.Component { /** * Reset the component to it's default state and blur the input if we are no longer searching * - * @param {Boolean} continueSearchingAfterReset + * @param {Boolean} blurAfterReset + * @param {Boolean} resetOptions */ - reset(continueSearchingAfterReset = false) { + reset(blurAfterReset = true, resetOptions = false) { this.setState({ search: '', - options: continueSearchingAfterReset ? this.getChatReportsOptions() : [], + options: resetOptions ? this.getChatReportsOptions() : [], focusedIndex: 0, - isLogoVisible: !continueSearchingAfterReset, - isClearButtonVisible: continueSearchingAfterReset, + isLogoVisible: blurAfterReset, + isClearButtonVisible: !blurAfterReset, groupUsers: [], }, () => { - if (!continueSearchingAfterReset) { + if (blurAfterReset) { this.textInput.blur(); ChatSwitcher.hide(); } @@ -392,7 +389,7 @@ class ChatSwitcherView extends React.Component { return; } - this.reset(false); + this.reset(false, true); return; } From f5406fb9bb8aaebaec8d0c23c77dc45f45a28b2c Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Tue, 8 Dec 2020 14:54:54 -0800 Subject: [PATCH 33/44] moar cleanup --- src/pages/home/sidebar/ChatSwitcherView.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index c6224ae664fa..62e3e854b0a1 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -309,14 +309,7 @@ class ChatSwitcherView extends React.Component { */ triggerOnFocusCallback() { ChatSwitcher.show(); - this.setState(prevState => ({ - isLogoVisible: false, - isClearButtonVisible: true, - - // When the search bar is empty let's show the default chat report options sorted by last visited. If the - // search bar is not empty that means some text is present hence on focus let's not update the options. - options: prevState.search === '' ? this.getChatReportsOptions() : prevState.options, - })); + this.updateSearch(this.state.search); } /** From a9ebd927e99662e6ea77bcb28be95cb3fed67ca3 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Wed, 9 Dec 2020 08:56:04 -0800 Subject: [PATCH 34/44] not calling updateSearch twice since the same happens in focus --- src/pages/home/sidebar/ChatSwitcherView.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index 62e3e854b0a1..d7ab30eae616 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -234,7 +234,6 @@ class ChatSwitcherView extends React.Component { : [...users, option] ), []), }), () => { - this.updateSearch(this.state.search); this.textInput.focus(); }); } From 5161b3edd6e5a9118376d7806d00f9e44471d320 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Thu, 10 Dec 2020 11:26:40 -0800 Subject: [PATCH 35/44] comment updates --- src/libs/actions/PersonalDetails.js | 6 ++++++ src/pages/home/sidebar/ChatSwitcherView.js | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index dbba15554d80..4be49b73e73b 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -19,6 +19,12 @@ Onyx.connect({ callback: val => personalDetails = val, }); +/** + * Helper method to return a default avatar + * + * @param {String} login + * @returns {String} + */ function getDefaultAvatar(login) { // There are 8 possible default avatars, so we choose which one this user has based // on a simple hash of their login (which is converted from HEX to INT) diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index d7ab30eae616..9d70c4e862ad 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -117,9 +117,13 @@ class ChatSwitcherView extends React.Component { /** * Get the chat report options created from props.reports. Additionally these chat report options will also - * determine if its a 1:1 DM or not. If it is a 1:1 DM we'll save the DM participant login and the type as user. + * determine if its a 1:1 DM or not by checking if report.participant is with just one other person. + * If it is a 1:1 DM we'll save the DM participant login and the type as user in the report option, this way + * we can filter out the same options from personalDetailOptions since we already have that 1:1 DM chat report and + * wouldn't need to make an unnecessary fetchOrCreateChatReport request via personalDetailOptions. * - * @param {Boolean} sortByLastVisited + * @param {Boolean} sortByLastVisited We set this to true when search text is empty and we set this false when + * search text is not empty since at that time we sort using matchRegexes. * @returns {Object} */ getChatReportsOptions(sortByLastVisited = true) { From 4c52b724cf89f939947f647d5adf35676b2f4d02 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Thu, 10 Dec 2020 12:02:14 -0800 Subject: [PATCH 36/44] comment correction --- src/pages/home/sidebar/ChatSwitcherView.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index 9d70c4e862ad..6491ec231a89 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -119,8 +119,7 @@ class ChatSwitcherView extends React.Component { * Get the chat report options created from props.reports. Additionally these chat report options will also * determine if its a 1:1 DM or not by checking if report.participant is with just one other person. * If it is a 1:1 DM we'll save the DM participant login and the type as user in the report option, this way - * we can filter out the same options from personalDetailOptions since we already have that 1:1 DM chat report and - * wouldn't need to make an unnecessary fetchOrCreateChatReport request via personalDetailOptions. + * we can filter out the same options from personalDetailOptions since we already have that 1:1 DM chat report. * * @param {Boolean} sortByLastVisited We set this to true when search text is empty and we set this false when * search text is not empty since at that time we sort using matchRegexes. From 34690121a1c68652c1e230cd0deb4fd3df9ec079 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Thu, 10 Dec 2020 13:05:19 -0800 Subject: [PATCH 37/44] updating language --- src/CONST.js | 4 +- src/pages/home/sidebar/ChatLinkRow.js | 4 +- .../home/sidebar/ChatSwitcherSearchForm.js | 10 +-- src/pages/home/sidebar/ChatSwitcherView.js | 79 ++++++++++--------- 4 files changed, 49 insertions(+), 48 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index e43d20086130..6f719b4bb169 100644 --- a/src/CONST.js +++ b/src/CONST.js @@ -5,8 +5,8 @@ const CONST = { PDF_VIEWER_URL: '/pdf/web/viewer.html', EXPENSIFY_ICON_URL: `${CLOUDFRONT_URL}/images/favicon-2019.png`, REPORT: { - SINGLE_USER_CHAT: 'singleUserChat', - GROUP_CHAT: 'groupChats', + SINGLE_USER_DM: 'singleUserDM', + GROUP_USERS_DM: 'groupUsersDM', }, }; diff --git a/src/pages/home/sidebar/ChatLinkRow.js b/src/pages/home/sidebar/ChatLinkRow.js index ea95724f3aae..e75898b8c98e 100644 --- a/src/pages/home/sidebar/ChatLinkRow.js +++ b/src/pages/home/sidebar/ChatLinkRow.js @@ -44,7 +44,7 @@ const ChatLinkRow = ({ onAddToGroup, isChatSwitcher, }) => { - const isSingleUserChat = option.type === CONST.REPORT.SINGLE_USER_CHAT; + const isSingleUserDM = option.type === CONST.REPORT.SINGLE_USER_DM; const textStyle = optionIsFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; @@ -109,7 +109,7 @@ const ChatLinkRow = ({ - {isSingleUserChat && isChatSwitcher && ( + {isSingleUserDM && isChatSwitcher && ( ( @@ -66,7 +66,7 @@ const ChatSwitcherSearchForm = props => ( )} - {props.groupUsers.length > 0 + {props.usersToStartGroupReportWith.length > 0 ? ( ( > - {_.map(props.groupUsers, user => ( + {_.map(props.usersToStartGroupReportWith, user => ( 0; + getReportsOptions(sortByLastVisited = true) { + // If the user has already started creating a group DM, then only the single user DM options should + // be shown because only single users can be added to a group DM. An existing group + // DM cannot be added to a new group DM. + const onlyShowSingleUserDMs = this.state.usersToStartGroupReportWith.length > 0; - const chatReports = _.chain(this.props.reports) + const reports = _.chain(this.props.reports) .values() .filter((report) => { if (_.isEmpty(report.reportName)) { @@ -141,7 +142,7 @@ class ChatSwitcherView extends React.Component { } // Remove any previously selected group user so that it doesn't show as a dupe - const isInGroupUsers = _.some(this.state.groupUsers, ({login}) => { + const isInGroupUsers = _.some(this.state.usersToStartGroupReportWith, ({login}) => { const participants = lodashGet(report, 'participants', []); const isSingleUserPrivateDMReport = participants.length === 1; return isSingleUserPrivateDMReport && login === participants[0]; @@ -149,7 +150,7 @@ class ChatSwitcherView extends React.Component { if (isInGroupUsers) { return false; } - if (excludeGroupDMs) { + if (onlyShowSingleUserDMs) { const participants = lodashGet(report, 'participants', []); const isGroupDM = participants.length > 1; return !isGroupDM; @@ -158,8 +159,8 @@ class ChatSwitcherView extends React.Component { }) .map((report) => { const participants = lodashGet(report, 'participants', []); - const isSingleUserChat = participants.length === 1; - const login = isSingleUserChat ? report.participants[0] : ''; + const isSingleUserDM = participants.length === 1; + const login = isSingleUserDM ? report.participants[0] : ''; return { text: report.reportName, alternateText: report.reportName, @@ -170,7 +171,7 @@ class ChatSwitcherView extends React.Component { participants, icon: report.icon, login, - type: isSingleUserChat ? CONST.REPORT.SINGLE_USER_CHAT : CONST.REPORT.GROUP_CHAT, + type: isSingleUserDM ? CONST.REPORT.SINGLE_USER_DM : CONST.REPORT.GROUP_USERS_DM, isUnread: report.unreadActionCount > 0, lastVisitedTimestamp: report.lastVisitedTimestamp, keyForList: String(report.reportID), @@ -181,8 +182,8 @@ class ChatSwitcherView extends React.Component { // If we are not sorting by lastVisited then let's sort it such that 1:1 user reports are on top with group DMs // on the bottom. This would ensure our search UI is clean than having 1:1 reports show up in the middle. return sortByLastVisited - ? lodashOrderby(chatReports, ['lastVisitedTimestamp'], ['desc']) - : lodashOrderby(chatReports, ['type'], ['desc']); + ? lodashOrderby(reports, ['lastVisitedTimestamp'], ['desc']) + : lodashOrderby(reports, ['type'], ['desc']); } /** @@ -192,10 +193,10 @@ class ChatSwitcherView extends React.Component { */ selectRow(option) { switch (option.type) { - case CONST.REPORT.SINGLE_USER_CHAT: + case CONST.REPORT.SINGLE_USER_DM: this.selectUser(option); break; - case CONST.REPORT.GROUP_CHAT: + case CONST.REPORT.GROUP_USERS_DM: this.selectReport(option); break; default: @@ -203,14 +204,14 @@ class ChatSwitcherView extends React.Component { } /** - * Adds a user to the groupUsers array and + * Adds a user to the usersToStartGroupReportWith array and * updates the options. * * @param {Object} option */ addUserToGroup(option) { this.setState(prevState => ({ - groupUsers: [...prevState.groupUsers, option], + usersToStartGroupReportWith: [...prevState.usersToStartGroupReportWith, option], search: '', }), () => { this.updateSearch(''); @@ -220,18 +221,18 @@ class ChatSwitcherView extends React.Component { } /** - * Removes a user from the groupUsers array and + * Removes a user from the usersToStartGroupReportWith array and * updates the options. * * @param {Object} [optionToRemove] remove last when no option provided */ removeUserFromGroup(optionToRemove) { const selectedOption = !optionToRemove - ? _.last(this.state.groupUsers) + ? _.last(this.state.usersToStartGroupReportWith) : optionToRemove; this.setState(prevState => ({ - groupUsers: _.reduce(prevState.groupUsers, (users, option) => ( + usersToStartGroupReportWith: _.reduce(prevState.usersToStartGroupReportWith, (users, option) => ( option.login === selectedOption.login ? users : [...users, option] @@ -245,7 +246,7 @@ class ChatSwitcherView extends React.Component { * Begins the group */ startGroupChat() { - const userLogins = _.map(this.state.groupUsers, option => option.login); + const userLogins = _.map(this.state.usersToStartGroupReportWith, option => option.login); fetchOrCreateChatReport([this.props.session.email, ...userLogins]); this.props.onLinkClick(); this.reset(); @@ -260,8 +261,8 @@ class ChatSwitcherView extends React.Component { selectUser(selectedOption) { // If there are group users saved start a group chat between // the user that was just selected and everyone in the list - if (this.state.groupUsers.length > 0) { - const userLogins = _.map(this.state.groupUsers, option => option.login); + if (this.state.usersToStartGroupReportWith.length > 0) { + const userLogins = _.map(this.state.usersToStartGroupReportWith, option => option.login); fetchOrCreateChatReport([this.props.session.email, ...userLogins, selectedOption.login]); } else { fetchOrCreateChatReport([this.props.session.email, selectedOption.login]); @@ -292,11 +293,11 @@ class ChatSwitcherView extends React.Component { reset(blurAfterReset = true, resetOptions = false) { this.setState({ search: '', - options: resetOptions ? this.getChatReportsOptions() : [], + options: resetOptions ? this.getReportsOptions() : [], focusedIndex: 0, isLogoVisible: blurAfterReset, isClearButtonVisible: !blurAfterReset, - groupUsers: [], + usersToStartGroupReportWith: [], }, () => { if (blurAfterReset) { this.textInput.blur(); @@ -331,7 +332,7 @@ class ChatSwitcherView extends React.Component { e.preventDefault(); break; case 'Backspace': - if (this.state.groupUsers.length > 0 && this.state.search === '') { + if (this.state.usersToStartGroupReportWith.length > 0 && this.state.search === '') { // Remove the last user this.removeUserFromGroup(); } @@ -377,10 +378,10 @@ class ChatSwitcherView extends React.Component { */ updateSearch(value) { if (value === '') { - if (this.state.groupUsers.length > 0) { + if (this.state.usersToStartGroupReportWith.length > 0) { // If we have groupLogins we only want to reset the options not - // the entire state which would clear out the list of groupUsers - this.setState({options: this.getChatReportsOptions(), search: ''}); + // the entire state which would clear out the list of usersToStartGroupReportWith + this.setState({options: this.getReportsOptions(), search: ''}); return; } @@ -407,14 +408,14 @@ class ChatSwitcherView extends React.Component { // We don't want to sort our chatReportOptions by lastVisited since we'll let the regex // matches order our options. - const chatReportOptions = this.getChatReportsOptions(false); + const reportOptions = this.getReportsOptions(false); // Get a list of all users we can send messages to and make their details generic. We will also reject any // personalDetails logins that exist in chatReportOptions which will remove our dupes since we'll use // chatReportOptions as our first source of truth if the 1:1 chat DM exists there. const personalDetailOptions = _.chain(this.props.personalDetails) .values() - .reject(personalDetail => _.findWhere(chatReportOptions, {login: personalDetail.login})) + .reject(personalDetail => _.findWhere(reportOptions, {login: personalDetail.login})) .map(personalDetail => ({ text: personalDetail.displayName, alternateText: personalDetail.login, @@ -422,12 +423,12 @@ class ChatSwitcherView extends React.Component { : `${personalDetail.displayName} ${personalDetail.login}`, icon: personalDetail.avatarURL, login: personalDetail.login, - type: CONST.REPORT.SINGLE_USER_CHAT, + type: CONST.REPORT.SINGLE_USER_DM, keyForList: personalDetail.login, })) .value(); - const searchOptions = _.union(chatReportOptions, personalDetailOptions); + const searchOptions = _.union(reportOptions, personalDetailOptions); for (let i = 0; i < matchRegexes.length; i++) { if (matches.size < this.maxSearchResults) { @@ -438,7 +439,7 @@ class ChatSwitcherView extends React.Component { // We must also filter out any users who are already in the Group DM list // so they can't be selected more than once - const isInGroupUsers = _.some(this.state.groupUsers, groupOption => ( + const isInGroupUsers = _.some(this.state.usersToStartGroupReportWith, groupOption => ( groupOption.login === option.login )); @@ -473,12 +474,12 @@ class ChatSwitcherView extends React.Component { onClearButtonClick={() => this.reset()} onFocus={this.triggerOnFocusCallback} onKeyPress={this.handleKeyPress} - groupUsers={this.state.groupUsers} + usersToStartGroupReportWith={this.state.usersToStartGroupReportWith} onRemoveFromGroup={this.removeUserFromGroup} onConfirmUsers={this.startGroupChat} /> - {this.state.groupUsers.length === MAX_GROUP_DM_LENGTH + {this.state.usersToStartGroupReportWith.length === MAX_GROUP_DM_LENGTH ? ( From 5bff415ec745f65314eff3217d5df78a9f0fc612 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Thu, 10 Dec 2020 13:11:03 -0800 Subject: [PATCH 38/44] correcting comment --- src/pages/home/sidebar/ChatSwitcherSearchForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/sidebar/ChatSwitcherSearchForm.js b/src/pages/home/sidebar/ChatSwitcherSearchForm.js index 50aa805ed2ff..5e3551b7f7d0 100644 --- a/src/pages/home/sidebar/ChatSwitcherSearchForm.js +++ b/src/pages/home/sidebar/ChatSwitcherSearchForm.js @@ -46,7 +46,7 @@ const propTypes = { // Begins / navigates to the chat between the various group users onConfirmUsers: PropTypes.func.isRequired, - // Users selected to begin a group chat + // Users selected to begin a group report DM usersToStartGroupReportWith: PropTypes.arrayOf(ChatSwitcherOptionPropTypes), }; From 625097f1995ba9067e340896ae6eec16e1f15a68 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Thu, 10 Dec 2020 13:42:58 -0800 Subject: [PATCH 39/44] cleanup --- src/pages/home/sidebar/ChatSwitcherView.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index f15c3082fb38..2f79717248fb 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -142,18 +142,17 @@ class ChatSwitcherView extends React.Component { } // Remove any previously selected group user so that it doesn't show as a dupe - const isInGroupUsers = _.some(this.state.usersToStartGroupReportWith, ({login}) => { + const isParticipantAlreadySelected = _.some(this.state.usersToStartGroupReportWith, ({login}) => { const participants = lodashGet(report, 'participants', []); - const isSingleUserPrivateDMReport = participants.length === 1; - return isSingleUserPrivateDMReport && login === participants[0]; + const isSingleUserDM = participants.length === 1; + return isSingleUserDM && login === participants[0]; }); - if (isInGroupUsers) { + if (isParticipantAlreadySelected) { return false; } if (onlyShowSingleUserDMs) { const participants = lodashGet(report, 'participants', []); - const isGroupDM = participants.length > 1; - return !isGroupDM; + return participants.length === 1; } return true; }) From 2f5447d4ee1e6ebe5ff8de58042746d20824ce1b Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Thu, 10 Dec 2020 14:16:03 -0800 Subject: [PATCH 40/44] adding back formatPersonalDetails that i mistakenly removed --- src/libs/actions/PersonalDetails.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index 4be49b73e73b..857948a941b4 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -157,7 +157,7 @@ function getFromReportParticipants(reports) { API.PersonalDetails_GetForEmails({emailList: participantEmails.join(',')}) .then((data) => { const details = _.pick(data, participantEmails); - Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, details); + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, formatPersonalDetails(details)); // The personalDetails of the participants contain their avatar images. Here we'll go over each // report and based on the participants we'll link up their avatars to report icons. From d1d5329cb60c5b315c6779e51dc7f43bf62724c4 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Thu, 10 Dec 2020 14:35:07 -0800 Subject: [PATCH 41/44] Fix for empty avatar icon --- src/libs/actions/PersonalDetails.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index 857948a941b4..d3bcf52c7e73 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -164,10 +164,11 @@ function getFromReportParticipants(reports) { _.each(reports, (report) => { if (report.participants.length === 1) { const dmParticipant = report.participants[0]; - const icon = lodashGet(details, [dmParticipant, 'avatar'], getDefaultAvatar(dmParticipant)); - if (icon) { - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, {icon}); + let icon = lodashGet(details, [dmParticipant, 'avatar'], '') + if (!icon) { + icon = getDefaultAvatar(dmParticipant); } + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, {icon}); } }); }); From fdb673e9372ba56eec479e97d217d2b9ed05cf65 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Thu, 10 Dec 2020 14:38:35 -0800 Subject: [PATCH 42/44] missed colon --- src/libs/actions/PersonalDetails.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index d3bcf52c7e73..18a799300173 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -164,7 +164,7 @@ function getFromReportParticipants(reports) { _.each(reports, (report) => { if (report.participants.length === 1) { const dmParticipant = report.participants[0]; - let icon = lodashGet(details, [dmParticipant, 'avatar'], '') + let icon = lodashGet(details, [dmParticipant, 'avatar'], ''); if (!icon) { icon = getDefaultAvatar(dmParticipant); } From 22104c9cf9bc2d66916a99b198e8e57c6566fc22 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Thu, 10 Dec 2020 14:46:50 -0800 Subject: [PATCH 43/44] Added comment --- src/libs/actions/Report.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index cd4cd2382c71..4272498a8182 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -454,6 +454,9 @@ function fetchOrCreateChatReport(participants) { // Store only the absolute bare minimum of data in Onyx because space is limited const newReport = getSimplifiedReportObject(report); newReport.reportName = getChatReportName(report.sharedReportList); + + // Optimistically update the last visited timestamp such that if the user immediately switches to another + // report the order is still maintained. newReport.lastVisitedTimestamp = Date.now(); // Merge the data into Onyx. Don't use set() here or multiSet() because then that would From 022ca53a7a3b3eb854d8d39faf1b28d5e715e2f6 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Thu, 10 Dec 2020 14:50:11 -0800 Subject: [PATCH 44/44] correction --- src/libs/actions/Report.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 4272498a8182..04f8b2166093 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -456,7 +456,7 @@ function fetchOrCreateChatReport(participants) { newReport.reportName = getChatReportName(report.sharedReportList); // Optimistically update the last visited timestamp such that if the user immediately switches to another - // report the order is still maintained. + // report the last visited order is still maintained. newReport.lastVisitedTimestamp = Date.now(); // Merge the data into Onyx. Don't use set() here or multiSet() because then that would