diff --git a/assets/images/avatars/group/default-avatar_1.svg b/assets/images/avatars/group/default-avatar_1.svg
new file mode 100644
index 000000000000..5d97c5bf855b
--- /dev/null
+++ b/assets/images/avatars/group/default-avatar_1.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/images/avatars/group/default-avatar_10.svg b/assets/images/avatars/group/default-avatar_10.svg
new file mode 100644
index 000000000000..12c9dd76ae31
--- /dev/null
+++ b/assets/images/avatars/group/default-avatar_10.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/images/avatars/group/default-avatar_11.svg b/assets/images/avatars/group/default-avatar_11.svg
new file mode 100644
index 000000000000..97f17f30f3a7
--- /dev/null
+++ b/assets/images/avatars/group/default-avatar_11.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/images/avatars/group/default-avatar_12.svg b/assets/images/avatars/group/default-avatar_12.svg
new file mode 100644
index 000000000000..f917fb136582
--- /dev/null
+++ b/assets/images/avatars/group/default-avatar_12.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/images/avatars/group/default-avatar_13.svg b/assets/images/avatars/group/default-avatar_13.svg
new file mode 100644
index 000000000000..9e59fb9123a5
--- /dev/null
+++ b/assets/images/avatars/group/default-avatar_13.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/images/avatars/group/default-avatar_14.svg b/assets/images/avatars/group/default-avatar_14.svg
new file mode 100644
index 000000000000..ca071e488416
--- /dev/null
+++ b/assets/images/avatars/group/default-avatar_14.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/images/avatars/group/default-avatar_15.svg b/assets/images/avatars/group/default-avatar_15.svg
new file mode 100644
index 000000000000..f227cc0717be
--- /dev/null
+++ b/assets/images/avatars/group/default-avatar_15.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/images/avatars/group/default-avatar_16.svg b/assets/images/avatars/group/default-avatar_16.svg
new file mode 100644
index 000000000000..efbb85f0b13d
--- /dev/null
+++ b/assets/images/avatars/group/default-avatar_16.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/images/avatars/group/default-avatar_17.svg b/assets/images/avatars/group/default-avatar_17.svg
new file mode 100644
index 000000000000..25c015c595ca
--- /dev/null
+++ b/assets/images/avatars/group/default-avatar_17.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/images/avatars/group/default-avatar_18.svg b/assets/images/avatars/group/default-avatar_18.svg
new file mode 100644
index 000000000000..a58ee6e66eff
--- /dev/null
+++ b/assets/images/avatars/group/default-avatar_18.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/images/avatars/group/default-avatar_2.svg b/assets/images/avatars/group/default-avatar_2.svg
new file mode 100644
index 000000000000..ff1cc3e6dd2d
--- /dev/null
+++ b/assets/images/avatars/group/default-avatar_2.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/images/avatars/group/default-avatar_3.svg b/assets/images/avatars/group/default-avatar_3.svg
new file mode 100644
index 000000000000..dde31b5d02a0
--- /dev/null
+++ b/assets/images/avatars/group/default-avatar_3.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/images/avatars/group/default-avatar_4.svg b/assets/images/avatars/group/default-avatar_4.svg
new file mode 100644
index 000000000000..f6d02801bc6b
--- /dev/null
+++ b/assets/images/avatars/group/default-avatar_4.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/images/avatars/group/default-avatar_5.svg b/assets/images/avatars/group/default-avatar_5.svg
new file mode 100644
index 000000000000..fdabd36e2058
--- /dev/null
+++ b/assets/images/avatars/group/default-avatar_5.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/images/avatars/group/default-avatar_6.svg b/assets/images/avatars/group/default-avatar_6.svg
new file mode 100644
index 000000000000..6f1c6b80eda6
--- /dev/null
+++ b/assets/images/avatars/group/default-avatar_6.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/images/avatars/group/default-avatar_7.svg b/assets/images/avatars/group/default-avatar_7.svg
new file mode 100644
index 000000000000..62d9a8b76bb8
--- /dev/null
+++ b/assets/images/avatars/group/default-avatar_7.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/images/avatars/group/default-avatar_8.svg b/assets/images/avatars/group/default-avatar_8.svg
new file mode 100644
index 000000000000..206b10c2322b
--- /dev/null
+++ b/assets/images/avatars/group/default-avatar_8.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/images/avatars/group/default-avatar_9.svg b/assets/images/avatars/group/default-avatar_9.svg
new file mode 100644
index 000000000000..ffbe02ce57e8
--- /dev/null
+++ b/assets/images/avatars/group/default-avatar_9.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/CONST.ts b/src/CONST.ts
index c57ac575f7e6..0fa1c64be44d 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -46,6 +46,7 @@ const KEYBOARD_SHORTCUT_NAVIGATION_TYPE = 'NAVIGATION_SHORTCUT';
const chatTypes = {
POLICY_ANNOUNCE: 'policyAnnounce',
POLICY_ADMINS: 'policyAdmins',
+ GROUP: 'group',
DOMAIN_ALL: 'domainAll',
POLICY_ROOM: 'policyRoom',
POLICY_EXPENSE_CHAT: 'policyExpenseChat',
@@ -117,6 +118,7 @@ const CONST = {
NORMAL: 'normal',
},
+ DEFAULT_GROUP_AVATAR_COUNT: 18,
DEFAULT_AVATAR_COUNT: 24,
OLD_DEFAULT_AVATAR_COUNT: 8,
@@ -606,6 +608,10 @@ const CONST = {
MAX_REPORT_PREVIEW_RECEIPTS: 3,
},
REPORT: {
+ ROLE: {
+ ADMIN: 'admin',
+ MEMBER: 'member',
+ },
MAX_COUNT_BEFORE_FOCUS_UPDATE: 30,
MAXIMUM_PARTICIPANTS: 8,
SPLIT_REPORTID: '-2',
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index b38e191a2a98..066350e5cbb4 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -287,6 +287,9 @@ const ONYXKEYS = {
/** Indicates whether we should store logs or not */
SHOULD_STORE_LOGS: 'shouldStoreLogs',
+ /** Stores new group chat draft */
+ NEW_GROUP_CHAT_DRAFT: 'newGroupChatDraft',
+
// Paths of PDF file that has been cached during one session
CACHED_PDF_PATHS: 'cachedPDFPaths',
@@ -545,6 +548,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.IOU]: OnyxTypes.IOU;
[ONYXKEYS.MODAL]: OnyxTypes.Modal;
[ONYXKEYS.NETWORK]: OnyxTypes.Network;
+ [ONYXKEYS.NEW_GROUP_CHAT_DRAFT]: OnyxTypes.NewGroupChatDraft;
[ONYXKEYS.CUSTOM_STATUS_DRAFT]: OnyxTypes.CustomStatusDraft;
[ONYXKEYS.INPUT_FOCUSED]: boolean;
[ONYXKEYS.PERSONAL_DETAILS_LIST]: OnyxTypes.PersonalDetailsList;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 23bb2ee845ad..7b28a2672ba6 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -174,6 +174,7 @@ const ROUTES = {
NEW: 'new',
NEW_CHAT: 'new/chat',
+ NEW_CHAT_CONFIRM: 'new/chat/confirm',
NEW_ROOM: 'new/room',
REPORT: 'r',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index ffb18391c980..f5891b042e49 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -261,6 +261,7 @@ const SCREENS = {
NEW_CHAT: {
ROOT: 'NewChat_Root',
NEW_CHAT: 'chat',
+ NEW_CHAT_CONFIRM: 'NewChat_Confirm',
NEW_ROOM: 'room',
},
diff --git a/src/components/Icon/GroupDefaultAvatars.ts b/src/components/Icon/GroupDefaultAvatars.ts
new file mode 100644
index 000000000000..7b4afb7c7309
--- /dev/null
+++ b/src/components/Icon/GroupDefaultAvatars.ts
@@ -0,0 +1,20 @@
+import Avatar1 from '@assets/images/avatars/group/default-avatar_1.svg';
+import Avatar2 from '@assets/images/avatars/group/default-avatar_2.svg';
+import Avatar3 from '@assets/images/avatars/group/default-avatar_3.svg';
+import Avatar4 from '@assets/images/avatars/group/default-avatar_4.svg';
+import Avatar5 from '@assets/images/avatars/group/default-avatar_5.svg';
+import Avatar6 from '@assets/images/avatars/group/default-avatar_6.svg';
+import Avatar7 from '@assets/images/avatars/group/default-avatar_7.svg';
+import Avatar8 from '@assets/images/avatars/group/default-avatar_8.svg';
+import Avatar9 from '@assets/images/avatars/group/default-avatar_9.svg';
+import Avatar10 from '@assets/images/avatars/group/default-avatar_10.svg';
+import Avatar11 from '@assets/images/avatars/group/default-avatar_11.svg';
+import Avatar12 from '@assets/images/avatars/group/default-avatar_12.svg';
+import Avatar13 from '@assets/images/avatars/group/default-avatar_13.svg';
+import Avatar14 from '@assets/images/avatars/group/default-avatar_14.svg';
+import Avatar15 from '@assets/images/avatars/group/default-avatar_15.svg';
+import Avatar16 from '@assets/images/avatars/group/default-avatar_16.svg';
+import Avatar17 from '@assets/images/avatars/group/default-avatar_17.svg';
+import Avatar18 from '@assets/images/avatars/group/default-avatar_18.svg';
+
+export {Avatar1, Avatar2, Avatar3, Avatar4, Avatar5, Avatar6, Avatar7, Avatar8, Avatar9, Avatar10, Avatar11, Avatar12, Avatar13, Avatar14, Avatar15, Avatar16, Avatar17, Avatar18};
diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx
index 5065d1cc7c13..b1abaf3f0b5b 100644
--- a/src/components/LHNOptionsList/OptionRowLHN.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHN.tsx
@@ -19,7 +19,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import DateUtils from '@libs/DateUtils';
import DomUtils from '@libs/DomUtils';
-import {getGroupChatName} from '@libs/GroupChatUtils';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import * as ReportUtils from '@libs/ReportUtils';
@@ -107,8 +106,9 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
const report = ReportUtils.getReport(optionItem.reportID ?? '');
const isStatusVisible = !!emojiCode && ReportUtils.isOneOnOneChat(!isEmptyObject(report) ? report : null);
- const isGroupChat = optionItem.type === CONST.REPORT.TYPE.CHAT && !optionItem.chatType && !optionItem.isThread && (optionItem.displayNamesWithTooltips?.length ?? 0) > 2;
- const fullTitle = isGroupChat ? getGroupChatName(!isEmptyObject(report) ? report : null) : optionItem.text;
+ const isGroupChat = ReportUtils.isGroupChat(optionItem) || ReportUtils.isDeprecatedGroupDM(optionItem);
+
+ const fullTitle = isGroupChat ? ReportUtils.getGroupChatName(report?.participantAccountIDs ?? []) : optionItem.text;
const subscriptAvatarBorderColor = isFocused ? focusedBackgroundColor : theme.sidebar;
return (
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 4badcddbc03d..eb26dfaf6b59 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -144,6 +144,7 @@ export default {
twoFactorCode: 'Two-factor code',
workspaces: 'Workspaces',
chats: 'Chats',
+ group: 'Group',
profile: 'Profile',
referral: 'Referral',
payments: 'Payments',
@@ -1201,6 +1202,9 @@ export default {
roomDescriptionOptional: 'Room description (optional)',
explainerText: 'Set a custom decription for the room.',
},
+ groupConfirmPage: {
+ groupName: 'Group name',
+ },
languagePage: {
language: 'Language',
languages: {
@@ -1339,7 +1343,7 @@ export default {
},
newChatPage: {
createChat: 'Create chat',
- createGroup: 'Create group',
+ startGroup: 'Start group',
addToGroup: 'Add to group',
},
yearPickerPage: {
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 8167633c2d64..a3304eaf34e9 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -134,6 +134,7 @@ export default {
twoFactorCode: 'Autenticación de dos factores',
workspaces: 'Espacios de trabajo',
chats: 'Chats',
+ group: 'Grupo',
profile: 'Perfil',
referral: 'Remisión',
payments: 'Pagos',
@@ -1203,6 +1204,9 @@ export default {
roomDescriptionOptional: 'Descripción de la sala de chat (opcional)',
explainerText: 'Establece una descripción personalizada para la sala de chat.',
},
+ groupConfirmPage: {
+ groupName: 'Nombre del grupo',
+ },
languagePage: {
language: 'Idioma',
languages: {
@@ -1343,7 +1347,7 @@ export default {
},
newChatPage: {
createChat: 'Crear chat',
- createGroup: 'Crear grupo',
+ startGroup: 'Grupo de inicio',
addToGroup: 'Añadir al grupo',
},
yearPickerPage: {
diff --git a/src/libs/API/parameters/OpenReportParams.ts b/src/libs/API/parameters/OpenReportParams.ts
index 8eaed6bc0fde..8b9d46a035d1 100644
--- a/src/libs/API/parameters/OpenReportParams.ts
+++ b/src/libs/API/parameters/OpenReportParams.ts
@@ -8,6 +8,10 @@ type OpenReportParams = {
createdReportActionID?: string;
clientLastReadTime?: string;
idempotencyKey?: string;
+ groupChatAdminLogins?: string;
+ reportName?: string;
+ chatType?: string;
+ optimisticAccountIDList?: string;
};
export default OpenReportParams;
diff --git a/src/libs/API/parameters/SplitBillParams.ts b/src/libs/API/parameters/SplitBillParams.ts
index 0ed7d252a2c6..310923093d5e 100644
--- a/src/libs/API/parameters/SplitBillParams.ts
+++ b/src/libs/API/parameters/SplitBillParams.ts
@@ -12,7 +12,8 @@ type SplitBillParams = {
transactionID: string;
reportActionID: string;
createdReportActionID?: string;
- policyID?: string;
+ policyID: string | undefined;
+ chatType: string | undefined;
};
export default SplitBillParams;
diff --git a/src/libs/GroupChatUtils.ts b/src/libs/GroupChatUtils.ts
deleted file mode 100644
index a18de0fdcbbf..000000000000
--- a/src/libs/GroupChatUtils.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import type {OnyxEntry} from 'react-native-onyx';
-import type {Report} from '@src/types/onyx';
-import localeCompare from './LocaleCompare';
-import * as ReportUtils from './ReportUtils';
-
-/**
- * Returns the report name if the report is a group chat
- */
-function getGroupChatName(report: OnyxEntry, shouldApplyLimit = false): string | undefined {
- let participants = report?.participantAccountIDs ?? [];
- if (shouldApplyLimit) {
- participants = participants.slice(0, 5);
- }
- const isMultipleParticipantReport = participants.length > 1;
-
- return participants
- .map((participant) => ReportUtils.getDisplayNameForParticipant(participant, isMultipleParticipantReport))
- .sort((first, second) => localeCompare(first ?? '', second ?? ''))
- .filter(Boolean)
- .join(', ');
-}
-
-export {
- // eslint-disable-next-line import/prefer-default-export
- getGroupChatName,
-};
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index ef1fc3c2dfb0..6583097ac9b8 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -152,6 +152,7 @@ const SearchModalStackNavigator = createModalStackNavigator({
[SCREENS.NEW_CHAT.ROOT]: () => require('../../../../pages/NewChatSelectorPage').default as React.ComponentType,
+ [SCREENS.NEW_CHAT.NEW_CHAT_CONFIRM]: () => require('../../../../pages/NewChatConfirmPage').default as React.ComponentType,
});
const NewTaskModalStackNavigator = createModalStackNavigator({
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index c9c5d47a2df3..6106f2aad419 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -432,6 +432,10 @@ const config: LinkingOptions['config'] = {
},
},
},
+ [SCREENS.NEW_CHAT.NEW_CHAT_CONFIRM]: {
+ path: ROUTES.NEW_CHAT_CONFIRM,
+ exact: true,
+ },
},
},
[SCREENS.RIGHT_MODAL.NEW_TASK]: {
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index ce4264b32141..dad7c1dd9ce8 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -11,6 +11,7 @@ import Onyx from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import type {FileObject} from '@components/AttachmentModal';
import * as Expensicons from '@components/Icon/Expensicons';
+import * as defaultGroupAvatars from '@components/Icon/GroupDefaultAvatars';
import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars';
import CONST from '@src/CONST';
import type {ParentNavigationSummaryParams, TranslationPaths} from '@src/languages/types';
@@ -45,7 +46,7 @@ import type {
ReimbursementDeQueuedMessage,
} from '@src/types/onyx/OriginalMessage';
import type {Status} from '@src/types/onyx/PersonalDetails';
-import type {NotificationPreference, PendingChatMember} from '@src/types/onyx/Report';
+import type {NotificationPreference, Participants, PendingChatMember, Participant as ReportParticipant} from '@src/types/onyx/Report';
import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction';
import type {Receipt, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
@@ -76,6 +77,8 @@ import * as TransactionUtils from './TransactionUtils';
import * as Url from './Url';
import * as UserUtils from './UserUtils';
+type AvatarRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18;
+
type WelcomeMessage = {showReportName: boolean; phrase1?: string; phrase2?: string};
type ExpenseOriginalMessage = {
@@ -247,6 +250,7 @@ type OptimisticChatReport = Pick<
| 'pendingFields'
| 'parentReportActionID'
| 'parentReportID'
+ | 'participants'
| 'participantAccountIDs'
| 'visibleChatMemberAccountIDs'
| 'policyID'
@@ -924,6 +928,10 @@ function isSelfDM(report: OnyxEntry): boolean {
return getChatType(report) === CONST.REPORT.CHAT_TYPE.SELF_DM;
}
+function isGroupChat(report: OnyxEntry | Partial): boolean {
+ return getChatType(report) === CONST.REPORT.CHAT_TYPE.GROUP;
+}
+
/**
* Only returns true if this is our main 1:1 DM report with Concierge
*/
@@ -1529,6 +1537,17 @@ function getWorkspaceAvatar(report: OnyxEntry): UserUtils.AvatarSource {
return !isEmpty(avatar) ? avatar : getDefaultWorkspaceAvatar(workspaceName);
}
+/**
+ * Helper method to return the default avatar associated with the given reportID
+ */
+function getDefaultGroupAvatar(reportID?: string): IconAsset {
+ if (!reportID) {
+ return defaultGroupAvatars.Avatar1;
+ }
+ const reportIDHashBucket: AvatarRange = ((Number(reportID) % CONST.DEFAULT_GROUP_AVATAR_COUNT) + 1) as AvatarRange;
+ return defaultGroupAvatars[`Avatar${reportIDHashBucket}`];
+}
+
/**
* Returns the appropriate icons for the given chat report using the stored personalDetails.
* The Avatar sources can be URLs or Icon components according to the chat type.
@@ -1591,6 +1610,71 @@ function getWorkspaceIcon(report: OnyxEntry, policy: OnyxEntry =
return workspaceIcon;
}
+/**
+ * Gets the personal details for a login by looking in the ONYXKEYS.PERSONAL_DETAILS_LIST Onyx key (stored in the local variable, allPersonalDetails). If it doesn't exist in Onyx,
+ * then a default object is constructed.
+ */
+function getPersonalDetailsForAccountID(accountID: number): Partial {
+ if (!accountID) {
+ return {};
+ }
+ return (
+ allPersonalDetails?.[accountID] ?? {
+ avatar: UserUtils.getDefaultAvatar(accountID),
+ isOptimisticPersonalDetail: true,
+ }
+ );
+}
+
+/**
+ * Get the displayName for a single report participant.
+ */
+function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = false, shouldFallbackToHidden = true, shouldAddCurrentUserPostfix = false): string {
+ if (!accountID) {
+ return '';
+ }
+
+ const personalDetails = getPersonalDetailsForAccountID(accountID);
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetails.login || '');
+ // This is to check if account is an invite/optimistically created one
+ // and prevent from falling back to 'Hidden', so a correct value is shown
+ // when searching for a new user
+ if (personalDetails.isOptimisticPersonalDetail === true) {
+ return formattedLogin;
+ }
+
+ // For selfDM, we display the user's displayName followed by '(you)' as a postfix
+ const shouldAddPostfix = shouldAddCurrentUserPostfix && accountID === currentUserAccountID;
+
+ const longName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, formattedLogin, shouldFallbackToHidden, shouldAddPostfix);
+
+ // If the user's personal details (first name) should be hidden, make sure we return "hidden" instead of the short name
+ if (shouldFallbackToHidden && longName === Localize.translateLocal('common.hidden')) {
+ return longName;
+ }
+
+ const shortName = personalDetails.firstName ? personalDetails.firstName : longName;
+ return shouldUseShortForm ? shortName : longName;
+}
+
+/**
+ * Returns the report name if the report is a group chat
+ */
+function getGroupChatName(participantAccountIDs: number[], shouldApplyLimit = false): string | undefined {
+ let participants = participantAccountIDs;
+ if (shouldApplyLimit) {
+ participants = participants.slice(0, 5);
+ }
+ const isMultipleParticipantReport = participants.length > 1;
+
+ return participants
+ .map((participant) => getDisplayNameForParticipant(participant, isMultipleParticipantReport))
+ .sort((first, second) => localeCompare(first ?? '', second ?? ''))
+ .filter(Boolean)
+ .join(', ');
+}
+
/**
* Returns the appropriate icons for the given chat report using the stored personalDetails.
* The Avatar sources can be URLs or Icon components according to the chat type.
@@ -1711,55 +1795,17 @@ function getIcons(
return getIconsForParticipants([currentUserAccountID ?? 0], personalDetails);
}
- return getIconsForParticipants(report?.participantAccountIDs ?? [], personalDetails);
-}
-
-/**
- * Gets the personal details for a login by looking in the ONYXKEYS.PERSONAL_DETAILS_LIST Onyx key (stored in the local variable, allPersonalDetails). If it doesn't exist in Onyx,
- * then a default object is constructed.
- */
-function getPersonalDetailsForAccountID(accountID: number): Partial {
- if (!accountID) {
- return {};
- }
- return (
- allPersonalDetails?.[accountID] ?? {
- avatar: UserUtils.getDefaultAvatar(accountID),
- isOptimisticPersonalDetail: true,
- }
- );
-}
-
-/**
- * Get the displayName for a single report participant.
- */
-function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = false, shouldFallbackToHidden = true, shouldAddCurrentUserPostfix = false): string {
- if (!accountID) {
- return '';
- }
-
- const personalDetails = getPersonalDetailsForAccountID(accountID);
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetails.login || '');
- // This is to check if account is an invite/optimistically created one
- // and prevent from falling back to 'Hidden', so a correct value is shown
- // when searching for a new user
- if (personalDetails.isOptimisticPersonalDetail === true) {
- return formattedLogin;
- }
-
- // For selfDM, we display the user's displayName followed by '(you)' as a postfix
- const shouldAddPostfix = shouldAddCurrentUserPostfix && accountID === currentUserAccountID;
-
- const longName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, formattedLogin, shouldFallbackToHidden, shouldAddPostfix);
-
- // If the user's personal details (first name) should be hidden, make sure we return "hidden" instead of the short name
- if (shouldFallbackToHidden && longName === Localize.translateLocal('common.hidden')) {
- return longName;
+ if (isGroupChat(report)) {
+ const groupChatIcon = {
+ source: getDefaultGroupAvatar(report.reportID),
+ id: -1,
+ type: CONST.ICON_TYPE_AVATAR,
+ name: getGroupChatName(report.participantAccountIDs ?? []),
+ };
+ return [groupChatIcon];
}
- const shortName = personalDetails.firstName ? personalDetails.firstName : longName;
- return shouldUseShortForm ? shortName : longName;
+ return getIconsForParticipants(report?.participantAccountIDs ?? [], personalDetails);
}
function getDisplayNamesWithTooltips(
@@ -1829,8 +1875,6 @@ function getDeletedParentActionMessageForChatReport(reportAction: OnyxEntry, report: OnyxEntry, shouldUseShortDisplayName = true): string {
const submitterDisplayName = getDisplayNameForParticipant(report?.ownerAccountID, shouldUseShortDisplayName) ?? '';
@@ -3649,6 +3693,15 @@ function buildOptimisticChatReport(
parentReportID = '',
description = '',
): OptimisticChatReport {
+ const participants = participantList.reduce((reportParticipants: Participants, accountID: number) => {
+ const participant: ReportParticipant = {
+ hidden: false,
+ role: accountID === currentUserAccountID ? CONST.REPORT.ROLE.ADMIN : CONST.REPORT.ROLE.MEMBER,
+ };
+ // eslint-disable-next-line no-param-reassign
+ reportParticipants[accountID] = participant;
+ return reportParticipants;
+ }, {} as Participants);
const currentTime = DateUtils.getDBTime();
const isNewlyCreatedWorkspaceChat = chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT && isOwnPolicyExpenseChat;
return {
@@ -3671,6 +3724,8 @@ function buildOptimisticChatReport(
// When creating a report the participantsAccountIDs and visibleChatMemberAccountIDs are the same
participantAccountIDs: participantList,
visibleChatMemberAccountIDs: participantList,
+ // For group chats we need to have participants object as we are migrating away from `participantAccountIDs` and `visibleChatMemberAccountIDs`. See https://github.com/Expensify/App/issues/34692
+ participants,
policyID,
reportID: generateReportID(),
reportName,
@@ -4326,7 +4381,8 @@ function shouldReportBeInOptionList({
!isArchivedRoom(report) &&
!isMoneyRequestReport(report) &&
!isTaskReport(report) &&
- !isSelfDM(report))
+ !isSelfDM(report) &&
+ !isGroupChat(report))
) {
return false;
}
@@ -4410,7 +4466,8 @@ function getChatByParticipants(newParticipantList: number[], reports: OnyxCollec
isTaskReport(report) ||
isMoneyRequestReport(report) ||
isChatRoom(report) ||
- isPolicyExpenseChat(report)
+ isPolicyExpenseChat(report) ||
+ isGroupChat(report)
) {
return false;
}
@@ -4625,7 +4682,7 @@ function hasIOUWaitingOnCurrentUserBankAccount(chatReport: OnyxEntry): b
*/
function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, otherParticipants: number[]): boolean {
// User cannot request money in chat thread or in task report or in chat room
- if (isChatThread(report) || isTaskReport(report) || isChatRoom(report) || isSelfDM(report)) {
+ if (isChatThread(report) || isTaskReport(report) || isChatRoom(report) || isSelfDM(report) || isGroupChat(report)) {
return false;
}
@@ -4707,7 +4764,12 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry 0) || (isDM(report) && hasMultipleOtherParticipants) || (isPolicyExpenseChat(report) && report?.isOwnPolicyExpenseChat)) {
+ if (
+ (isChatRoom(report) && otherParticipants.length > 0) ||
+ (isDM(report) && hasMultipleOtherParticipants) ||
+ (isGroupChat(report) && otherParticipants.length > 0) ||
+ (isPolicyExpenseChat(report) && report?.isOwnPolicyExpenseChat)
+ ) {
options = [CONST.IOU.TYPE.SPLIT];
}
@@ -5164,7 +5226,7 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry)
* - More than 2 participants.
*
*/
-function isGroupChat(report: OnyxEntry): boolean {
+function isDeprecatedGroupDM(report: OnyxEntry): boolean {
return Boolean(
report &&
!isChatThread(report) &&
@@ -5695,7 +5757,7 @@ export {
hasMissingSmartscanFields,
getIOUReportActionDisplayMessage,
isWaitingForAssigneeToCompleteTask,
- isGroupChat,
+ isDeprecatedGroupDM,
isOpenExpenseReport,
shouldUseFullTitleToDisplay,
parseReportRouteParams,
@@ -5737,6 +5799,7 @@ export {
canEditRoomVisibility,
canEditPolicyDescription,
getPolicyDescriptionText,
+ getDefaultGroupAvatar,
isAllowedToSubmitDraftExpenseReport,
isAllowedToApproveExpenseReport,
findSelfDMReportID,
@@ -5744,8 +5807,10 @@ export {
isJoinRequestInAdminRoom,
canAddOrDeleteTransactions,
shouldCreateNewMoneyRequestReport,
+ isGroupChat,
isTrackExpenseReport,
hasActionsWithErrors,
+ getGroupChatName,
};
export type {
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index 63b907a42e25..4803067267d0 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -388,7 +388,10 @@ function getOptionData({
.join(' ');
}
- result.alternateText = ReportUtils.isGroupChat(report) && lastActorDisplayName ? `${lastActorDisplayName}: ${lastMessageText}` : lastMessageText || formattedLogin;
+ result.alternateText =
+ (ReportUtils.isGroupChat(report) || ReportUtils.isDeprecatedGroupDM(report)) && lastActorDisplayName
+ ? `${lastActorDisplayName}: ${lastMessageText}`
+ : lastMessageText || formattedLogin;
}
result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result as Report);
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index b67e44fcf5ad..24a2605cca93 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -103,6 +103,7 @@ type SplitData = {
reportActionID: string;
policyID?: string;
createdReportActionID?: string;
+ chatType?: string;
};
type SplitsAndOnyxData = {
@@ -2278,11 +2279,32 @@ function createSplitsAndOnyxData(
): SplitsAndOnyxData {
const currentUserEmailForIOUSplit = PhoneNumber.addSMSDomainIfPhoneNumber(currentUserLogin);
const participantAccountIDs = participants.map((participant) => Number(participant.accountID));
- const existingSplitChatReport =
- existingSplitChatReportID || participants[0].reportID
- ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${existingSplitChatReportID || participants[0].reportID}`]
- : ReportUtils.getChatByParticipants(participantAccountIDs);
- const splitChatReport = existingSplitChatReport ?? ReportUtils.buildOptimisticChatReport(participantAccountIDs);
+
+ const existingChatReportID = existingSplitChatReportID || participants[0].reportID;
+ let existingSplitChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${existingChatReportID}`];
+ if (!existingSplitChatReport) {
+ existingSplitChatReport = participants.length < 2 ? ReportUtils.getChatByParticipants(participantAccountIDs) : null;
+ }
+ let newChat: ReportUtils.OptimisticChatReport | EmptyObject = {};
+ const allParticipantsAccountIDs = [...participantAccountIDs, currentUserAccountID];
+ if (!existingSplitChatReport && participants.length > 1) {
+ newChat = ReportUtils.buildOptimisticChatReport(
+ allParticipantsAccountIDs,
+ '',
+ CONST.REPORT.CHAT_TYPE.GROUP,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
+ );
+ }
+ if (isEmptyObject(newChat)) {
+ newChat = ReportUtils.buildOptimisticChatReport(allParticipantsAccountIDs);
+ }
+ const splitChatReport = existingSplitChatReport ?? newChat;
const isOwnPolicyExpenseChat = !!splitChatReport.isOwnPolicyExpenseChat;
const splitTransaction = TransactionUtils.buildOptimisticTransaction(
@@ -2611,6 +2633,7 @@ function createSplitsAndOnyxData(
transactionID: splitTransaction.transactionID,
reportActionID: splitIOUReportAction.reportActionID,
policyID: splitChatReport.policyID,
+ chatType: splitChatReport.chatType,
};
if (!existingSplitChatReport) {
@@ -2675,6 +2698,7 @@ function splitBill(
reportActionID: splitData.reportActionID,
createdReportActionID: splitData.createdReportActionID,
policyID: splitData.policyID,
+ chatType: splitData.chatType,
};
API.write(WRITE_COMMANDS.SPLIT_BILL, parameters, onyxData);
@@ -2733,6 +2757,7 @@ function splitBillAndOpenReport(
reportActionID: splitData.reportActionID,
createdReportActionID: splitData.createdReportActionID,
policyID: splitData.policyID,
+ chatType: splitData.chatType,
};
API.write(WRITE_COMMANDS.SPLIT_BILL_AND_OPEN_REPORT, parameters, onyxData);
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 225022665ddc..0c49845490ab 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -70,7 +70,16 @@ import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
import INPUT_IDS from '@src/types/form/NewRoomForm';
-import type {PersonalDetails, PersonalDetailsList, PolicyReportField, RecentlyUsedReportFields, ReportActionReactions, ReportMetadata, ReportUserIsTyping} from '@src/types/onyx';
+import type {
+ NewGroupChatDraft,
+ PersonalDetails,
+ PersonalDetailsList,
+ PolicyReportField,
+ RecentlyUsedReportFields,
+ ReportActionReactions,
+ ReportMetadata,
+ ReportUserIsTyping,
+} from '@src/types/onyx';
import type {Decision, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage';
import type {NotificationPreference, RoomVisibility, WriteCapability} from '@src/types/onyx/Report';
import type Report from '@src/types/onyx/Report';
@@ -92,6 +101,7 @@ type ActionSubscriber = {
let conciergeChatReportID: string | undefined;
let currentUserAccountID = -1;
+let currentUserEmail: string | undefined;
Onyx.connect({
key: ONYXKEYS.SESSION,
callback: (value) => {
@@ -100,7 +110,7 @@ Onyx.connect({
conciergeChatReportID = undefined;
return;
}
-
+ currentUserEmail = value.email;
currentUserAccountID = value.accountID;
},
});
@@ -202,6 +212,21 @@ Onyx.connect({
callback: (val) => (allRecentlyUsedReportFields = val),
});
+let newGroupDraft: OnyxEntry;
+Onyx.connect({
+ key: ONYXKEYS.NEW_GROUP_CHAT_DRAFT,
+ callback: (value) => (newGroupDraft = value),
+});
+
+function clearGroupChat() {
+ Onyx.set(ONYXKEYS.NEW_GROUP_CHAT_DRAFT, null);
+}
+
+function startNewChat() {
+ clearGroupChat();
+ Navigation.navigate(ROUTES.NEW);
+}
+
/** Get the private pusher channel name for a Report. */
function getReportChannelName(reportID: string): string {
return `${CONST.PUSHER.PRIVATE_REPORT_CHANNEL_PREFIX}${reportID}${CONFIG.PUSHER.SUFFIX}`;
@@ -620,6 +645,13 @@ function openReport(
idempotencyKey: `${SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT}_${reportID}`,
};
+ if (ReportUtils.isGroupChat(newReportObject)) {
+ parameters.chatType = CONST.REPORT.CHAT_TYPE.GROUP;
+ parameters.groupChatAdminLogins = currentUserEmail;
+ parameters.optimisticAccountIDList = participantAccountIDList.join(',');
+ parameters.reportName = newReportObject.reportName ?? '';
+ }
+
if (isFromDeepLink) {
parameters.shouldRetry = false;
}
@@ -752,16 +784,35 @@ function openReport(
* @param userLogins list of user logins to start a chat report with.
* @param shouldDismissModal a flag to determine if we should dismiss modal before navigate to report or navigate to report directly.
*/
-function navigateToAndOpenReport(userLogins: string[], shouldDismissModal = true) {
+function navigateToAndOpenReport(userLogins: string[], shouldDismissModal = true, reportName?: string) {
let newChat: ReportUtils.OptimisticChatReport | EmptyObject = {};
-
+ let chat: OnyxEntry | EmptyObject = {};
const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins(userLogins);
- const chat = ReportUtils.getChatByParticipants(participantAccountIDs);
- if (!chat) {
- newChat = ReportUtils.buildOptimisticChatReport(participantAccountIDs);
+ // If we are not creating a new Group Chat then we are creating a 1:1 DM and will look for an existing chat
+ if (!newGroupDraft) {
+ chat = ReportUtils.getChatByParticipants(participantAccountIDs);
+ }
+
+ if (isEmptyObject(chat)) {
+ if (newGroupDraft) {
+ newChat = ReportUtils.buildOptimisticChatReport(
+ participantAccountIDs,
+ reportName,
+ CONST.REPORT.CHAT_TYPE.GROUP,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
+ );
+ } else {
+ newChat = ReportUtils.buildOptimisticChatReport(participantAccountIDs);
+ }
}
- const report = chat ?? newChat;
+ const report = isEmptyObject(chat) ? newChat : chat;
// We want to pass newChat here because if anything is passed in that param (even an existing chat), we will try to create a chat on the server
openReport(report.reportID, '', userLogins, newChat);
@@ -2942,6 +2993,10 @@ function resolveActionableMentionWhisper(reportId: string, reportAction: OnyxEnt
API.write(WRITE_COMMANDS.RESOLVE_ACTIONABLE_MENTION_WHISPER, parameters, {optimisticData, failureData});
}
+function setGroupDraft(participants: Array<{login: string; accountID: number}>, reportName = '') {
+ Onyx.merge(ONYXKEYS.NEW_GROUP_CHAT_DRAFT, {participants, reportName});
+}
+
export {
getReportDraftStatus,
searchInServer,
@@ -3013,4 +3068,7 @@ export {
updateReportName,
resolveActionableMentionWhisper,
updateRoomVisibility,
+ setGroupDraft,
+ clearGroupChat,
+ startNewChat,
};
diff --git a/src/pages/NewChatConfirmPage.tsx b/src/pages/NewChatConfirmPage.tsx
new file mode 100644
index 000000000000..8570c061ebce
--- /dev/null
+++ b/src/pages/NewChatConfirmPage.tsx
@@ -0,0 +1,152 @@
+import React, {useMemo} from 'react';
+import {View} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
+import Avatar from '@components/Avatar';
+import Badge from '@components/Badge';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import ScreenWrapper from '@components/ScreenWrapper';
+import SelectionList from '@components/SelectionList';
+import TableListItem from '@components/SelectionList/TableListItem';
+import type {ListItem} from '@components/SelectionList/types';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import useLocalize from '@hooks/useLocalize';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import * as OptionsListUtils from '@libs/OptionsListUtils';
+import * as ReportUtils from '@libs/ReportUtils';
+import * as Report from '@userActions/Report';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type * as OnyxTypes from '@src/types/onyx';
+import type {Participant} from '@src/types/onyx/IOU';
+
+type NewChatConfirmPageOnyxProps = {
+ /** New group chat draft data */
+ newGroupDraft: OnyxEntry;
+
+ /** All of the personal details for everyone */
+ allPersonalDetails: OnyxEntry;
+};
+
+type NewChatConfirmPageProps = NewChatConfirmPageOnyxProps;
+
+function NewChatConfirmPage({newGroupDraft, allPersonalDetails}: NewChatConfirmPageProps) {
+ const {translate} = useLocalize();
+ const StyleUtils = useStyleUtils();
+ const styles = useThemeStyles();
+ const personalData = useCurrentUserPersonalDetails();
+ const participantAccountIDs = newGroupDraft?.participants.map((participant) => participant.accountID);
+ const selectedOptions = useMemo((): Participant[] => {
+ if (!newGroupDraft?.participants) {
+ return [];
+ }
+ const options: Participant[] = newGroupDraft.participants.map((participant) =>
+ OptionsListUtils.getParticipantsOption({accountID: participant.accountID, login: participant.login, reportID: ''}, allPersonalDetails),
+ );
+ return options;
+ }, [allPersonalDetails, newGroupDraft?.participants]);
+
+ const groupName = ReportUtils.getGroupChatName(participantAccountIDs ?? []);
+
+ const sections: ListItem[] = useMemo(
+ () =>
+ selectedOptions
+ .map((selectedOption: Participant) => {
+ const accountID = selectedOption.accountID;
+ const isAdmin = personalData.accountID === accountID;
+ let roleBadge = null;
+ if (isAdmin) {
+ roleBadge = (
+
+ );
+ }
+
+ const section: ListItem = {
+ login: selectedOption?.login ?? '',
+ text: selectedOption?.text ?? '',
+ keyForList: selectedOption?.keyForList ?? '',
+ isSelected: !isAdmin,
+ isDisabled: isAdmin,
+ rightElement: roleBadge,
+ accountID,
+ icons: selectedOption?.icons,
+ };
+ return section;
+ })
+ .sort((a, b) => a.text?.toLowerCase().localeCompare(b.text?.toLowerCase() ?? '') ?? -1),
+ [selectedOptions, personalData.accountID, translate, styles.textStrong, styles.justifyContentCenter, styles.badgeBordered, styles.activeItemBadge, StyleUtils],
+ );
+
+ /**
+ * Removes a selected option from list if already selected.
+ */
+ const unselectOption = (option: ListItem) => {
+ if (!newGroupDraft) {
+ return;
+ }
+ const newSelectedParticipants = newGroupDraft.participants.filter((participant) => participant.login !== option.login);
+ Report.setGroupDraft(newSelectedParticipants);
+ };
+
+ const createGroup = () => {
+ if (!newGroupDraft) {
+ return;
+ }
+ const logins: string[] = newGroupDraft.participants.map((participant) => participant.login);
+ Report.navigateToAndOpenReport(logins, true, groupName);
+ };
+
+ const navigateBack = () => {
+ Navigation.goBack(ROUTES.NEW_CHAT);
+ };
+
+ return (
+
+
+
+
+
+
+ 1}
+ confirmButtonText={translate('newChatPage.startGroup')}
+ onConfirm={createGroup}
+ />
+
+ );
+}
+
+NewChatConfirmPage.displayName = 'NewChatConfirmPage';
+
+export default withOnyx({
+ newGroupDraft: {
+ key: ONYXKEYS.NEW_GROUP_CHAT_DRAFT,
+ },
+ allPersonalDetails: {
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ },
+})(NewChatConfirmPage);
diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx
index f4eccd52c78e..9c2d47f684ab 100755
--- a/src/pages/NewChatPage.tsx
+++ b/src/pages/NewChatPage.tsx
@@ -7,6 +7,7 @@ import OfflineIndicator from '@components/OfflineIndicator';
import OptionsSelector from '@components/OptionsSelector';
import ScreenWrapper from '@components/ScreenWrapper';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useSearchTermAndSearch from '@hooks/useSearchTermAndSearch';
@@ -14,6 +15,8 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import doInteractionTask from '@libs/DoInteractionTask';
+import Log from '@libs/Log';
+import Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as ReportUtils from '@libs/ReportUtils';
import type {OptionData} from '@libs/ReportUtils';
@@ -21,12 +24,17 @@ import variables from '@styles/variables';
import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
+import type {SelectedParticipant} from '@src/types/onyx/NewGroupChatDraft';
type NewChatPageWithOnyxProps = {
/** All reports shared with the user */
reports: OnyxCollection;
+ /** New group chat draft data */
+ newGroupDraft: OnyxEntry;
+
/** All of the personal details for everyone */
personalDetails: OnyxEntry;
@@ -45,7 +53,7 @@ type NewChatPageProps = NewChatPageWithOnyxProps & {
const excludedGroupEmails = CONST.EXPENSIFY_EMAILS.filter((value) => value !== CONST.EMAIL.CONCIERGE);
-function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingForReports, dismissedReferralBanners}: NewChatPageProps) {
+function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingForReports, dismissedReferralBanners, newGroupDraft}: NewChatPageProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const [searchTerm, setSearchTerm] = useState('');
@@ -57,6 +65,8 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF
const {isSmallScreenWidth} = useWindowDimensions();
const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false);
+ const personalData = useCurrentUserPersonalDetails();
+
const maxParticipantsReached = selectedOptions.length === CONST.REPORT.MAXIMUM_PARTICIPANTS;
const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached);
@@ -146,7 +156,6 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF
[],
true,
);
-
setSelectedOptions(newSelectedOptions);
setFilteredRecentReports(recentReports);
setFilteredPersonalDetails(newChatPersonalDetails);
@@ -158,27 +167,45 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF
* or navigates to the existing chat if one with those participants already exists.
*/
const createChat = (option: OptionData) => {
- if (!option.login) {
+ let login = '';
+
+ if (option.login) {
+ login = option.login;
+ } else if (selectedOptions.length === 1) {
+ login = selectedOptions[0].login ?? '';
+ }
+
+ if (!login) {
+ Log.warn('Tried to create chat with empty login');
return;
}
- Report.navigateToAndOpenReport([option.login]);
- };
+ Report.navigateToAndOpenReport([login]);
+ };
/**
- * Creates a new group chat with all the selected options and the current user,
- * or navigates to the existing chat if one with those participants already exists.
+ * Navigates to create group confirm page
*/
- const createGroup = () => {
- const logins = selectedOptions.map((option) => option.login).filter((login): login is string => typeof login === 'string');
-
- if (logins.length < 1) {
+ const navigateToConfirmPage = () => {
+ if (!personalData || !personalData.login || !personalData.accountID) {
return;
}
-
- Report.navigateToAndOpenReport(logins);
+ const selectedParticipants: SelectedParticipant[] = selectedOptions.map((option: OptionData) => ({login: option.login ?? '', accountID: option.accountID ?? -1}));
+ const logins = [...selectedParticipants, {login: personalData.login, accountID: personalData.accountID}];
+ Report.setGroupDraft(logins);
+ Navigation.navigate(ROUTES.NEW_CHAT_CONFIRM);
};
const updateOptions = useCallback(() => {
+ let newSelectedOptions;
+ if (newGroupDraft?.participants) {
+ const selectedParticipants = newGroupDraft.participants.filter((participant) => participant.accountID !== personalData.accountID);
+ newSelectedOptions = selectedParticipants.map((participant): OptionData => {
+ const baseOption = OptionsListUtils.getParticipantsOption({accountID: participant.accountID, login: participant.login, reportID: ''}, personalDetails);
+ return {...baseOption, reportID: baseOption.reportID ?? ''};
+ });
+ setSelectedOptions(newSelectedOptions);
+ }
+
const {
recentReports,
personalDetails: newChatPersonalDetails,
@@ -188,7 +215,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF
personalDetails,
betas ?? [],
searchTerm,
- selectedOptions,
+ newSelectedOptions ?? selectedOptions,
isGroupChat ? excludedGroupEmails : [],
false,
true,
@@ -200,12 +227,13 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF
[],
true,
);
+
setFilteredRecentReports(recentReports);
setFilteredPersonalDetails(newChatPersonalDetails);
setFilteredUserToInvite(userToInvite);
// props.betas is not added as dependency since it doesn't change during the component lifecycle
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [reports, personalDetails, searchTerm]);
+ }, [reports, personalDetails, searchTerm, newGroupDraft]);
useEffect(() => {
const interactionTask = doInteractionTask(() => {
@@ -266,9 +294,9 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF
shouldShowConfirmButton
shouldShowReferralCTA={!dismissedReferralBanners?.[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]}
referralContentType={CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT}
- confirmButtonText={selectedOptions.length > 1 ? translate('newChatPage.createGroup') : translate('newChatPage.createChat')}
+ confirmButtonText={selectedOptions.length > 1 ? translate('common.next') : translate('newChatPage.createChat')}
textInputAlert={isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''}
- onConfirmSelection={createGroup}
+ onConfirmSelection={selectedOptions.length > 1 ? navigateToConfirmPage : createChat}
textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')}
safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle}
isLoadingNewOptions={isSearchingForReports}
@@ -288,6 +316,9 @@ export default withOnyx({
dismissedReferralBanners: {
key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS,
},
+ newGroupDraft: {
+ key: ONYXKEYS.NEW_GROUP_CHAT_DRAFT,
+ },
reports: {
key: ONYXKEYS.COLLECTION.REPORT,
},
diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx
index 9d620472bf3a..8aa2b855176c 100644
--- a/src/pages/home/HeaderView.tsx
+++ b/src/pages/home/HeaderView.tsx
@@ -21,7 +21,6 @@ import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-import {getGroupChatName} from '@libs/GroupChatUtils';
import * as HeaderUtils from '@libs/HeaderUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {ReportWithoutHasDraft} from '@libs/OnyxSelectors/reportWithoutHasDraftSelector';
@@ -77,6 +76,7 @@ function HeaderView({report, personalDetails, parentReport, parentReportAction,
const theme = useTheme();
const styles = useThemeStyles();
const isSelfDM = ReportUtils.isSelfDM(report);
+ const isGroupChat = ReportUtils.isGroupChat(report) || ReportUtils.isDeprecatedGroupDM(report);
// Currently, currentUser is not included in participantAccountIDs, so for selfDM, we need to add the currentUser as participants.
const participants = isSelfDM ? [session?.accountID ?? -1] : (report?.participantAccountIDs ?? []).slice(0, 5);
const participantPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails);
@@ -89,7 +89,7 @@ function HeaderView({report, personalDetails, parentReport, parentReportAction,
const isTaskReport = ReportUtils.isTaskReport(report);
const reportHeaderData = !isTaskReport && !isChatThread && report.parentReportID ? parentReport : report;
// Use sorted display names for the title for group chats on native small screen widths
- const title = ReportUtils.isGroupChat(report) ? getGroupChatName(report, true) : ReportUtils.getReportName(reportHeaderData);
+ const title = isGroupChat ? ReportUtils.getGroupChatName(report.participantAccountIDs ?? [], true) : ReportUtils.getReportName(reportHeaderData);
const subtitle = ReportUtils.getChatRoomSubtitle(reportHeaderData);
const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(reportHeaderData);
const isConcierge = ReportUtils.hasSingleParticipant(report) && participants.includes(CONST.ACCOUNT_ID.CONCIERGE);
@@ -144,7 +144,7 @@ function HeaderView({report, personalDetails, parentReport, parentReportAction,
Report.updateNotificationPreference(reportID, report.notificationPreference, CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, false, report.parentReportID, report.parentReportActionID),
);
- const canJoinOrLeave = !isSelfDM && (isChatThread || isUserCreatedPolicyRoom || canLeaveRoom);
+ const canJoinOrLeave = !isSelfDM && !isGroupChat && (isChatThread || isUserCreatedPolicyRoom || canLeaveRoom);
const canJoin = canJoinOrLeave && !isWhisperAction && report.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
const canLeave = canJoinOrLeave && ((isChatThread && !!report.notificationPreference?.length) || isUserCreatedPolicyRoom || canLeaveRoom);
if (canJoin) {
diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
index 24603de5679c..c154e39e0124 100644
--- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
+++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
@@ -21,6 +21,7 @@ import personalDetailsPropType from '@pages/personalDetailsPropType';
import * as App from '@userActions/App';
import * as IOU from '@userActions/IOU';
import * as Policy from '@userActions/Policy';
+import * as Report from '@userActions/Report';
import * as Task from '@userActions/Task';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -266,7 +267,7 @@ function FloatingActionButtonAndPopover(props) {
{
icon: Expensicons.ChatBubble,
text: translate('sidebarScreen.fabNewChat'),
- onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.NEW)),
+ onSelected: () => interceptAnonymousUser(Report.startNewChat),
},
{
icon: Expensicons.MoneyCircle,
diff --git a/src/pages/settings/Report/ReportSettingsPage.tsx b/src/pages/settings/Report/ReportSettingsPage.tsx
index 383cbbcb0833..f1c4047ae33e 100644
--- a/src/pages/settings/Report/ReportSettingsPage.tsx
+++ b/src/pages/settings/Report/ReportSettingsPage.tsx
@@ -11,7 +11,6 @@ import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import {getGroupChatName} from '@libs/GroupChatUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
import type {ReportSettingsNavigatorParamList} from '@navigation/types';
@@ -48,7 +47,8 @@ function ReportSettingsPage({report, policies}: ReportSettingsPageProps) {
const shouldShowNotificationPref = !isMoneyRequestReport && report?.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
const roomNameLabel = translate(isMoneyRequestReport ? 'workspace.editor.nameInputLabel' : 'newRoomPage.roomName');
- const reportName = ReportUtils.isGroupChat(report) ? getGroupChatName(report) : ReportUtils.getReportName(report);
+ const reportName =
+ ReportUtils.isDeprecatedGroupDM(report) || ReportUtils.isGroupChat(report) ? ReportUtils.getGroupChatName(report.participantAccountIDs ?? []) : ReportUtils.getReportName(report);
const shouldShowWriteCapability = !isMoneyRequestReport;
diff --git a/src/types/onyx/NewGroupChatDraft.ts b/src/types/onyx/NewGroupChatDraft.ts
new file mode 100644
index 000000000000..97dd63aa5f68
--- /dev/null
+++ b/src/types/onyx/NewGroupChatDraft.ts
@@ -0,0 +1,11 @@
+type SelectedParticipant = {
+ accountID: number;
+ login: string;
+};
+
+type NewGroupChatDraft = {
+ participants: SelectedParticipant[];
+ reportName: string;
+};
+export type {SelectedParticipant};
+export default NewGroupChatDraft;
diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts
index 02dfcbbbfc5f..722102e3ab6e 100644
--- a/src/types/onyx/Report.ts
+++ b/src/types/onyx/Report.ts
@@ -192,4 +192,4 @@ type ReportCollectionDataSet = CollectionDataSet {
groupChat =
Object.values(allReports ?? {}).find(
(report) =>
- report?.type === CONST.REPORT.TYPE.CHAT && isEqual(report.participantAccountIDs, [CARLOS_ACCOUNT_ID, JULES_ACCOUNT_ID, VIT_ACCOUNT_ID]),
+ report?.type === CONST.REPORT.TYPE.CHAT &&
+ isEqual(report.participantAccountIDs, [CARLOS_ACCOUNT_ID, JULES_ACCOUNT_ID, VIT_ACCOUNT_ID, RORY_ACCOUNT_ID]),
) ?? null;
expect(isEmptyObject(groupChat)).toBe(false);
expect(groupChat?.pendingFields).toStrictEqual({createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD});