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
? (