diff --git a/src/CONST.js b/src/CONST.js index a425f0330e1a..6f719b4bb169 100644 --- a/src/CONST.js +++ b/src/CONST.js @@ -4,6 +4,10 @@ const CONST = { CLOUDFRONT_URL, PDF_VIEWER_URL: '/pdf/web/viewer.html', EXPENSIFY_ICON_URL: `${CLOUDFRONT_URL}/images/favicon-2019.png`, + REPORT: { + SINGLE_USER_DM: 'singleUserDM', + GROUP_USERS_DM: 'groupUsersDM', + }, }; export default CONST; 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/API.js b/src/libs/API.js index 23509741e235..76d8d7fe1ef1 100644 --- a/src/libs/API.js +++ b/src/libs/API.js @@ -387,10 +387,9 @@ function Report_TogglePinned(parameters) { * @param {Number} parameters.sequenceNumber * @returns {Promise} */ -function Report_SetLastReadActionID(parameters) { - const commandName = 'Report_SetLastReadActionID'; - requireParameters(['accountID', 'reportID', 'sequenceNumber'], - parameters, commandName); +function Report_UpdateLastRead(parameters) { + const commandName = 'Report_UpdateLastRead'; + requireParameters(['accountID', 'reportID', 'sequenceNumber'], parameters, commandName); return request(commandName, parameters); } @@ -407,5 +406,5 @@ export { Report_AddComment, Report_GetHistory, Report_TogglePinned, - Report_SetLastReadActionID + Report_UpdateLastRead }; diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index a70c12b23c61..18a799300173 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -19,6 +19,19 @@ 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) + 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 +44,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); } /** @@ -129,15 +139,38 @@ 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) { - API.PersonalDetails_GetForEmails({emailList}) +function getFromReportParticipants(reports) { + const participantEmails = _.chain(reports) + .pluck('participants') + .flatten() + .unique() + .value(); + + if (participantEmails.length === 0) { + return; + } + + API.PersonalDetails_GetForEmails({emailList: 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 + // 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]; + let icon = lodashGet(details, [dmParticipant, 'avatar'], ''); + if (!icon) { + icon = getDefaultAvatar(dmParticipant); + } + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, {icon}); + } + }); }); } @@ -147,6 +180,6 @@ NetworkConnection.onReconnect(fetch); export { fetch, fetchTimezone, - getForEmails, + getFromReportParticipants, getDisplayName, }; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 58e1a490c067..04f8b2166093 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 @@ -62,25 +62,31 @@ const lastReadActionIDs = {}; * @returns {Boolean} */ function getUnreadActionCount(report) { - const usersLastReadActionID = lodashGet(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 lastReadSequenceNumber = lodashGet(report, [ 'reportNameValuePairs', `lastReadActionID_${currentUserAccountID}`, + ]) || lodashGet(report, [ + 'reportNameValuePairs', + `lastRead_${currentUserAccountID}`, + '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); } @@ -107,11 +113,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, + lastVisitedTimestamp: lodashGet(report, [ + 'reportNameValuePairs', + `lastRead_${currentUserAccountID}`, + 'timestamp' + ], 0) }; } @@ -148,15 +158,14 @@ 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 + // 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); - - participantEmails.push(newReport.participants); + simplifiedReports.push(newReport); if (lodashGet(report, 'reportNameValuePairs.type') === 'chat') { newReport.reportName = getChatReportName(report.sharedReportList); @@ -166,31 +175,26 @@ 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(',')); - } + // Fetch the personal details if there are any + PersonalDetails.getFromReportParticipants(simplifiedReports); return _.map(fetchedReports, report => report.reportID); }); } /** - * Update the lastReadActionID in Onyx and local memory. + * Update the lastRead actionID and timestamp in local memory and Onyx * * @param {Number} reportID * @param {Number} sequenceNumber */ -function setLocalLastReadActionID(reportID, sequenceNumber) { - lastReadActionIDs[reportID] = sequenceNumber; +function setLocalLastRead(reportID, sequenceNumber) { + lastReadSequenceNumbers[reportID] = sequenceNumber; - // Update the lastReadActionID on the report optimistically + // Update the report optimistically Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { unreadActionCount: 0, - reportNameValuePairs: { - [`lastReadActionID_${currentUserAccountID}`]: sequenceNumber, - } + lastVisitedTimestamp: Date.now(), }); } @@ -208,7 +212,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 @@ -216,7 +220,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, }); @@ -451,6 +455,10 @@ function fetchOrCreateChatReport(participants) { 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 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 // overwrite any existing data (like if they have unread messages) Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, newReport); @@ -536,10 +544,10 @@ function updateLastReadActionID(reportID, sequenceNumber) { return; } - setLocalLastReadActionID(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/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 1d2fc377411c..6155a03ff947 100644 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -60,6 +60,7 @@ class ReportActionsView extends React.Component { }); if (this.props.isActiveReport) { this.keyboardEvent = Keyboard.addListener('keyboardDidShow', this.scrollToListBottom); + this.recordMaxAction(); } fetchActions(this.props.reportID); diff --git a/src/pages/home/sidebar/ChatLinkRow.js b/src/pages/home/sidebar/ChatLinkRow.js index 579e73851e9a..e75898b8c98e 100644 --- a/src/pages/home/sidebar/ChatLinkRow.js +++ b/src/pages/home/sidebar/ChatLinkRow.js @@ -13,6 +13,7 @@ import ChatSwitcherOptionPropTypes from './ChatSwitcherOptionPropTypes'; import ROUTES from '../../../ROUTES'; import pencilIcon from '../../../../assets/images/icon-pencil.png'; 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' @@ -43,7 +44,7 @@ const ChatLinkRow = ({ onAddToGroup, isChatSwitcher, }) => { - const isUserRow = option.type === 'user'; + const isSingleUserDM = option.type === CONST.REPORT.SINGLE_USER_DM; const textStyle = optionIsFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; @@ -108,7 +109,7 @@ const ChatLinkRow = ({ - {isUserRow && isChatSwitcher && ( + {isSingleUserDM && isChatSwitcher && ( (option.type === 'user' ? option.alternateText : String(option.reportID))} + keyExtractor={option => option.keyForList} renderItem={({item, index}) => ( ( @@ -69,7 +66,7 @@ const ChatSwitcherSearchForm = props => ( )} - {props.groupUsers.length > 0 + {props.usersToStartGroupReportWith.length > 0 ? ( ( > - {_.map(props.groupUsers, user => ( + {_.map(props.usersToStartGroupReportWith, user => ( ( // 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} @@ -142,7 +138,6 @@ const ChatSwitcherSearchForm = props => ( ref={props.forwardedRef} style={[styles.textInput, styles.textInputReversed, styles.flex1]} value={props.searchValue} - onBlur={props.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 65576511ead0..2f79717248fb 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -2,6 +2,8 @@ import React from 'react'; 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 {withOnyx} from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; import ONYXKEYS from '../../../ONYXKEYS'; @@ -13,11 +15,7 @@ import {redirect} from '../../../libs/actions/App'; import ROUTES from '../../../ROUTES'; import styles from '../../../styles/StyleSheet'; import * as ChatSwitcher from '../../../libs/actions/ChatSwitcher'; - -const OPTION_TYPE = { - USER: 'user', - REPORT: 'report', -}; +import CONST from '../../../CONST'; const MAX_GROUP_DM_LENGTH = 8; @@ -77,6 +75,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.getReportsOptions = this.getReportsOptions.bind(this); this.triggerOnFocusCallback = this.triggerOnFocusCallback.bind(this); this.updateSearch = this.updateSearch.bind(this); this.selectRow = this.selectRow.bind(this); @@ -89,7 +88,7 @@ class ChatSwitcherView extends React.Component { focusedIndex: 0, isLogoVisible: true, isClearButtonVisible: false, - groupUsers: [], + usersToStartGroupReportWith: [], }; } @@ -116,6 +115,76 @@ class ChatSwitcherView extends React.Component { KeyboardShortcut.unsubscribe('K'); } + /** + * Get the report options created from props.reports. Additionally these 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 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. + * @returns {Object} + */ + 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 reports = _.chain(this.props.reports) + .values() + .filter((report) => { + if (_.isEmpty(report.reportName)) { + return false; + } + if (sortByLastVisited && !report.lastVisitedTimestamp) { + return false; + } + + // Remove any previously selected group user so that it doesn't show as a dupe + const isParticipantAlreadySelected = _.some(this.state.usersToStartGroupReportWith, ({login}) => { + const participants = lodashGet(report, 'participants', []); + const isSingleUserDM = participants.length === 1; + return isSingleUserDM && login === participants[0]; + }); + if (isParticipantAlreadySelected) { + return false; + } + if (onlyShowSingleUserDMs) { + const participants = lodashGet(report, 'participants', []); + return participants.length === 1; + } + return true; + }) + .map((report) => { + const participants = lodashGet(report, 'participants', []); + const isSingleUserDM = participants.length === 1; + const login = isSingleUserDM ? report.participants[0] : ''; + return { + text: report.reportName, + alternateText: report.reportName, + searchText: report.participants < 10 + ? `${report.reportName} ${report.participants.join(' ')}` + : report.reportName ?? '', + reportID: report.reportID, + participants, + icon: report.icon, + login, + type: isSingleUserDM ? CONST.REPORT.SINGLE_USER_DM : CONST.REPORT.GROUP_USERS_DM, + isUnread: report.unreadActionCount > 0, + lastVisitedTimestamp: report.lastVisitedTimestamp, + keyForList: String(report.reportID), + }; + }) + .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(reports, ['lastVisitedTimestamp'], ['desc']) + : lodashOrderby(reports, ['type'], ['desc']); + } + /** * Fires the correct method for the option type selected. * @@ -123,10 +192,10 @@ class ChatSwitcherView extends React.Component { */ selectRow(option) { switch (option.type) { - case OPTION_TYPE.USER: + case CONST.REPORT.SINGLE_USER_DM: this.selectUser(option); break; - case OPTION_TYPE.REPORT: + case CONST.REPORT.GROUP_USERS_DM: this.selectReport(option); break; default: @@ -134,14 +203,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(''); @@ -151,24 +220,23 @@ 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] ), []), }), () => { - this.updateSearch(this.state.search); this.textInput.focus(); }); } @@ -177,7 +245,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(); @@ -192,8 +260,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]); @@ -216,18 +284,19 @@ 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} resetOptions */ - reset(blurAfterReset = true) { + reset(blurAfterReset = true, resetOptions = false) { this.setState({ search: '', - options: [], + options: resetOptions ? this.getReportsOptions() : [], focusedIndex: 0, isLogoVisible: blurAfterReset, isClearButtonVisible: !blurAfterReset, - groupUsers: [], + usersToStartGroupReportWith: [], }, () => { if (blurAfterReset) { this.textInput.blur(); @@ -242,10 +311,7 @@ class ChatSwitcherView extends React.Component { */ triggerOnFocusCallback() { ChatSwitcher.show(); - this.setState({ - isLogoVisible: false, - isClearButtonVisible: true, - }); + this.updateSearch(this.state.search); } /** @@ -265,7 +331,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(); } @@ -311,14 +377,14 @@ 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: [], search: ''}); + // the entire state which would clear out the list of usersToStartGroupReportWith + this.setState({options: this.getReportsOptions(), search: ''}); return; } - this.reset(false); + this.reset(false, true); return; } @@ -339,9 +405,16 @@ class ChatSwitcherView extends React.Component { // A Set is used here so that duplicate values are automatically removed. const matches = new Set(); - // Get a list of all users we can send messages to and make their details generic + // We don't want to sort our chatReportOptions by lastVisited since we'll let the regex + // matches order our options. + 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(reportOptions, {login: personalDetail.login})) .map(personalDetail => ({ text: personalDetail.displayName, alternateText: personalDetail.login, @@ -349,31 +422,12 @@ class ChatSwitcherView extends React.Component { : `${personalDetail.displayName} ${personalDetail.login}`, icon: personalDetail.avatarURL, login: personalDetail.login, - type: OPTION_TYPE.USER, + type: CONST.REPORT.SINGLE_USER_DM, + keyForList: personalDetail.login, })) .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.participants - ? `${report.reportName} ${report.participants.join(' ')}` - : report.reportName ?? '', - reportID: report.reportID, - type: OPTION_TYPE.REPORT, - participants: report.participants, - isUnread: report.unreadActionCount > 0 - })) - .value(); - - // 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; + const searchOptions = _.union(reportOptions, personalDetailOptions); for (let i = 0; i < matchRegexes.length; i++) { if (matches.size < this.maxSearchResults) { @@ -382,31 +436,17 @@ class ChatSwitcherView extends React.Component { const valueToSearch = option.searchText && 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 => ( + const isInGroupUsers = _.some(this.state.usersToStartGroupReportWith, 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) { + // Make sure we don't include the same option twice (automatically handled by using a `Set`) + if (isMatch && !isInGroupUsers) { 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; } @@ -429,21 +469,16 @@ 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} 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 ? (