diff --git a/assets/images/chart-pie.svg b/assets/images/chart-pie.svg new file mode 100644 index 000000000000..97da1ef66cef --- /dev/null +++ b/assets/images/chart-pie.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/chunks/expensify-icons.chunk.ts b/src/components/Icon/chunks/expensify-icons.chunk.ts index 4d388c980589..2b519f85fd99 100644 --- a/src/components/Icon/chunks/expensify-icons.chunk.ts +++ b/src/components/Icon/chunks/expensify-icons.chunk.ts @@ -44,6 +44,7 @@ import CarWithKey from '@assets/images/car-with-key.svg'; import Car from '@assets/images/car.svg'; import CaretUpDown from '@assets/images/caret-up-down.svg'; import Cash from '@assets/images/cash.svg'; +import ChartPie from '@assets/images/chart-pie.svg'; import ChatBubbleCounter from '@assets/images/chatbubble-counter.svg'; import ChatBubbleReply from '@assets/images/chatbubble-reply.svg'; import ChatBubbleUnread from '@assets/images/chatbubble-unread.svg'; @@ -334,6 +335,7 @@ const Expensicons = { CarPlus, Cash, CertiniaSquare, + ChartPie, ChatBubble, ChatBubbles, Checkbox, diff --git a/src/hooks/useConciergeAskState.ts b/src/hooks/useConciergeAskState.ts new file mode 100644 index 000000000000..9d41745a7caa --- /dev/null +++ b/src/hooks/useConciergeAskState.ts @@ -0,0 +1,57 @@ +import {isCreatedAction, isCurrentUserPendingAddAction} from '@libs/ReportActionsUtils'; + +import {useConciergeSessionState} from '@pages/inbox/ConciergeSessionContext'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReportActions} from '@src/types/onyx/ReportAction'; + +import type {OnyxEntry} from 'react-native-onyx'; + +import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; +import useIsInSidePanel from './useIsInSidePanel'; +import useOnyx from './useOnyx'; +import usePermissions from './usePermissions'; + +type ConciergeAskState = { + /** Whether to show the welcome empty state and the quick actions that go with it */ + shouldShowWelcome: boolean; + + /** Whether to label the composer as the start of a new question, which it is once earlier history is expanded */ + shouldLabelComposerAsNewQuestion: boolean; +}; + +/** + * Drives the Ask Concierge screen in the main Concierge DM. The empty state lives in the report actions + * list and the composer label and quick actions live in the report footer, and those are siblings, so + * both derive their state here rather than one passing it to the other. + */ +function useConciergeAskState(reportID: string | undefined): ConciergeAskState { + const {isBetaEnabled} = usePermissions(); + const isInSidePanel = useIsInSidePanel(); + const {sessionStartTime, showFullHistory} = useConciergeSessionState(); + const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); + + // Derive the boolean inside the Onyx selector so the screen re-renders only when session activity + // flips, not on every report-action change in the Concierge chat. + const hasSessionActivitySelector = (actions: OnyxEntry) => { + if (!actions || !sessionStartTime) { + return false; + } + return Object.values(actions).some((action) => isCurrentUserPendingAddAction(action, currentUserAccountID) || (!isCreatedAction(action) && action.created >= sessionStartTime)); + }; + const [hasSessionActivity] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { + selector: hasSessionActivitySelector, + }); + + // Waiting for the session boundary keeps an existing conversation from flashing the welcome screen on open. + const isAskConciergeChat = !!reportID && reportID === conciergeReportID && !isInSidePanel && !!sessionStartTime && isBetaEnabled(CONST.BETAS.CONCIERGE_RESPOND_IN_THREAD); + + return { + shouldShowWelcome: isAskConciergeChat && !showFullHistory && !hasSessionActivity, + shouldLabelComposerAsNewQuestion: isAskConciergeChat && showFullHistory && !hasSessionActivity, + }; +} + +export default useConciergeAskState; diff --git a/src/languages/de.ts b/src/languages/de.ts index 670707b049f5..13c2c426226e 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -488,7 +488,26 @@ const translations: TranslationDeepObject = { quarter: 'Quartal', vacationDelegate: 'Urlaubsvertretung', expensifyLogo: 'Expensify-Logo', - concierge: {greeting: 'Hallo, wie kann ich helfen?', showHistory: 'Verlauf anzeigen'}, + concierge: { + greeting: 'Hallo, wie kann ich helfen?', + subtitle: 'Dein persönlicher KI-Assistent', + welcomeDescription: 'Concierge kann Fragen beantworten, Ausgaben aktualisieren und mehr.', + viewChatHistory: 'Chatverlauf anzeigen', + hideChatHistory: 'Chatverlauf ausblenden', + askNewQuestion: 'Neue Frage stellen', + newChat: 'Neuer Chat', + composerPlaceholder: 'Wie viel habe ich letzten Monat ausgegeben?', + quickActions: { + createExpense: 'Ausgabe erstellen', + createExpensePrompt: 'Erstelle eine Ausgabe', + analyzeSpend: 'Ausgaben analysieren', + analyzeSpendPrompt: 'Wie viel habe ich letzten Monat ausgegeben?', + configureWorkspace: 'Arbeitsbereich einrichten', + configureWorkspacePrompt: 'Hilf mir, meinen Arbeitsbereich einzurichten', + getSupport: 'Support erhalten', + getSupportPrompt: 'Ich brauche bei etwas Hilfe', + }, + }, duplicateReport: 'Duplizierten Bericht', approver: 'Genehmiger', goToConcierge: 'Zu Concierge', diff --git a/src/languages/el.ts b/src/languages/el.ts index 5345331a15a0..e303b5053c0e 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -492,7 +492,23 @@ const translations: TranslationDeepObject = { tagGLCode: 'Ετικέτα κωδικού GL', concierge: { greeting: 'Γεια σας, πώς μπορώ να σας βοηθήσω;', - showHistory: 'Εμφάνιση ιστορικού', + subtitle: 'Ο προσωπικός σας βοηθός AI', + welcomeDescription: 'Ο Concierge μπορεί να απαντήσει σε ερωτήσεις, να ενημερώσει δαπάνες και άλλα.', + viewChatHistory: 'Προβολή ιστορικού συνομιλιών', + hideChatHistory: 'Απόκρυψη ιστορικού συνομιλιών', + askNewQuestion: 'Κάντε μια νέα ερώτηση', + newChat: 'Νέα συνομιλία', + composerPlaceholder: 'Πόσα ξόδεψα τον προηγούμενο μήνα;', + quickActions: { + createExpense: 'Δημιουργία δαπάνης', + createExpensePrompt: 'Δημιούργησε μια δαπάνη', + analyzeSpend: 'Ανάλυση δαπανών', + analyzeSpendPrompt: 'Πόσα ξόδεψα τον προηγούμενο μήνα;', + configureWorkspace: 'Ρύθμιση χώρου εργασίας', + configureWorkspacePrompt: 'Βοήθησέ με να ρυθμίσω τον χώρο εργασίας μου', + getSupport: 'Λήψη υποστήριξης', + getSupportPrompt: 'Χρειάζομαι βοήθεια με κάτι', + }, }, vacationDelegate: 'Εκπρόσωπος διακοπών', expensifyLogo: 'Λογότυπο Expensify', diff --git a/src/languages/en.ts b/src/languages/en.ts index 7ebbdcde6fb3..97e3f6a9740c 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -538,7 +538,23 @@ const translations = { tagGLCode: 'Tag GL code', concierge: { greeting: 'Hi there, how can I help?', - showHistory: 'Show history', + subtitle: 'Your personal AI assistant', + welcomeDescription: 'Concierge can answer questions, update expenses, and more.', + viewChatHistory: 'View chat history', + hideChatHistory: 'Hide chat history', + askNewQuestion: 'Ask a new question', + newChat: 'New chat', + composerPlaceholder: 'How much did I spend last month?', + quickActions: { + createExpense: 'Create expense', + createExpensePrompt: 'Create an expense', + analyzeSpend: 'Analyze spend', + analyzeSpendPrompt: 'How much did I spend last month?', + configureWorkspace: 'Configure workspace', + configureWorkspacePrompt: 'Help me configure my workspace', + getSupport: 'Get support', + getSupportPrompt: 'I need help with something', + }, }, vacationDelegate: 'Vacation delegate', expensifyLogo: 'Expensify logo', diff --git a/src/languages/es.ts b/src/languages/es.ts index 073dd24d79db..37baa13f0575 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -481,7 +481,23 @@ const translations: TranslationDeepObject = { quarter: 'Trimestre', concierge: { greeting: 'Hola, ¿en qué puedo ayudarte?', - showHistory: 'Mostrar historial', + subtitle: 'Tu asistente de IA personal', + welcomeDescription: 'Concierge puede responder preguntas, actualizar gastos y mucho más.', + viewChatHistory: 'Ver historial del chat', + hideChatHistory: 'Ocultar historial del chat', + askNewQuestion: 'Haz una nueva pregunta', + newChat: 'Nuevo chat', + composerPlaceholder: '¿Cuánto gasté el mes pasado?', + quickActions: { + createExpense: 'Crear gasto', + createExpensePrompt: 'Crea un gasto', + analyzeSpend: 'Analizar gastos', + analyzeSpendPrompt: '¿Cuánto gasté el mes pasado?', + configureWorkspace: 'Configurar espacio de trabajo', + configureWorkspacePrompt: 'Ayúdame a configurar mi espacio de trabajo', + getSupport: 'Obtener ayuda', + getSupportPrompt: 'Necesito ayuda con algo', + }, }, vacationDelegate: 'Delegado de vacaciones', expensifyLogo: 'Logo de Expensify', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index e8ca853b7569..13c936d8614b 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -488,7 +488,26 @@ const translations: TranslationDeepObject = { quarter: 'Trimestre', vacationDelegate: 'Délégué de vacances', expensifyLogo: 'Logo Expensify', - concierge: {greeting: 'Bonjour, comment puis-je vous aider ?', showHistory: 'Afficher l’historique'}, + concierge: { + greeting: 'Bonjour, comment puis-je vous aider ?', + subtitle: 'Votre assistant IA personnel', + welcomeDescription: 'Concierge peut répondre à vos questions, mettre à jour vos dépenses et plus encore.', + viewChatHistory: 'Afficher l’historique du chat', + hideChatHistory: 'Masquer l’historique du chat', + askNewQuestion: 'Poser une nouvelle question', + newChat: 'Nouveau chat', + composerPlaceholder: 'Combien ai-je dépensé le mois dernier ?', + quickActions: { + createExpense: 'Créer une dépense', + createExpensePrompt: 'Crée une dépense', + analyzeSpend: 'Analyser les dépenses', + analyzeSpendPrompt: 'Combien ai-je dépensé le mois dernier ?', + configureWorkspace: 'Configurer l’espace de travail', + configureWorkspacePrompt: 'Aide-moi à configurer mon espace de travail', + getSupport: 'Obtenir de l’aide', + getSupportPrompt: 'J’ai besoin d’aide pour quelque chose', + }, + }, duplicateReport: 'Note de frais en double', approver: 'Approbateur', goToConcierge: 'Aller à Concierge', diff --git a/src/languages/it.ts b/src/languages/it.ts index 86ea6baca0bf..98861b81ea9e 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -488,7 +488,26 @@ const translations: TranslationDeepObject = { quarter: 'Trimestre', vacationDelegate: 'Delega ferie', expensifyLogo: 'Logo Expensify', - concierge: {greeting: 'Ciao, come posso aiutarti?', showHistory: 'Mostra cronologia'}, + concierge: { + greeting: 'Ciao, come posso aiutarti?', + subtitle: 'Il tuo assistente AI personale', + welcomeDescription: 'Concierge può rispondere a domande, aggiornare spese e altro ancora.', + viewChatHistory: 'Mostra cronologia chat', + hideChatHistory: 'Nascondi cronologia chat', + askNewQuestion: 'Fai una nuova domanda', + newChat: 'Nuova chat', + composerPlaceholder: 'Quanto ho speso il mese scorso?', + quickActions: { + createExpense: 'Crea spesa', + createExpensePrompt: 'Crea una spesa', + analyzeSpend: 'Analizza le spese', + analyzeSpendPrompt: 'Quanto ho speso il mese scorso?', + configureWorkspace: 'Configura area di lavoro', + configureWorkspacePrompt: 'Aiutami a configurare la mia area di lavoro', + getSupport: 'Ottieni assistenza', + getSupportPrompt: 'Ho bisogno di aiuto con una cosa', + }, + }, duplicateReport: 'Report duplicato', approver: 'Approvante', goToConcierge: 'Vai a Concierge', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index c9bd9d9c8960..a46732111a1b 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -487,7 +487,26 @@ const translations: TranslationDeepObject = { quarter: '四半期', vacationDelegate: '休暇代理人', expensifyLogo: 'Expensifyロゴ', - concierge: {greeting: 'こんにちは、どのようにお手伝いできますか?', showHistory: '履歴を表示'}, + concierge: { + greeting: 'こんにちは、どのようにお手伝いできますか?', + subtitle: 'あなた専用のAIアシスタント', + welcomeDescription: 'Conciergeは質問への回答や経費の更新などを行えます。', + viewChatHistory: 'チャット履歴を表示', + hideChatHistory: 'チャット履歴を非表示', + askNewQuestion: '新しい質問をする', + newChat: '新しいチャット', + composerPlaceholder: '先月はいくら使いましたか?', + quickActions: { + createExpense: '経費を作成', + createExpensePrompt: '経費を作成して', + analyzeSpend: '支出を分析', + analyzeSpendPrompt: '先月はいくら使いましたか?', + configureWorkspace: 'ワークスペースを設定', + configureWorkspacePrompt: 'ワークスペースの設定を手伝って', + getSupport: 'サポートを受ける', + getSupportPrompt: '困っていることがあります', + }, + }, duplicateReport: 'レポートを複製', approver: '承認者', goToConcierge: 'Conciergeへ移動', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 4031271b6da1..6035992e57d4 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -487,7 +487,26 @@ const translations: TranslationDeepObject = { quarter: 'Kwartaal', vacationDelegate: 'Vertegenwoordiger tijdens vakantie', expensifyLogo: 'Expensify-logo', - concierge: {greeting: 'Hoi, waarmee kan ik je helpen?', showHistory: 'Geschiedenis weergeven'}, + concierge: { + greeting: 'Hoi, waarmee kan ik je helpen?', + subtitle: 'Je persoonlijke AI-assistent', + welcomeDescription: 'Concierge kan vragen beantwoorden, uitgaven bijwerken en meer.', + viewChatHistory: 'Chatgeschiedenis weergeven', + hideChatHistory: 'Chatgeschiedenis verbergen', + askNewQuestion: 'Stel een nieuwe vraag', + newChat: 'Nieuwe chat', + composerPlaceholder: 'Hoeveel heb ik vorige maand uitgegeven?', + quickActions: { + createExpense: 'Uitgave maken', + createExpensePrompt: 'Maak een uitgave', + analyzeSpend: 'Uitgaven analyseren', + analyzeSpendPrompt: 'Hoeveel heb ik vorige maand uitgegeven?', + configureWorkspace: 'Werkruimte instellen', + configureWorkspacePrompt: 'Help me mijn werkruimte in te stellen', + getSupport: 'Hulp krijgen', + getSupportPrompt: 'Ik heb ergens hulp bij nodig', + }, + }, duplicateReport: 'Dubbel rapport', approver: 'Fiatteur', goToConcierge: 'Ga naar Concierge', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 50a014fdd300..22d411f70a90 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -491,7 +491,26 @@ const translations: TranslationDeepObject = { quarter: 'Kwartał', vacationDelegate: 'Zastępca urlopowy', expensifyLogo: 'Logo Expensify', - concierge: {greeting: 'Cześć, w czym mogę pomóc?', showHistory: 'Pokaż historię'}, + concierge: { + greeting: 'Cześć, w czym mogę pomóc?', + subtitle: 'Twój osobisty asystent AI', + welcomeDescription: 'Concierge może odpowiadać na pytania, aktualizować wydatki i nie tylko.', + viewChatHistory: 'Pokaż historię czatu', + hideChatHistory: 'Ukryj historię czatu', + askNewQuestion: 'Zadaj nowe pytanie', + newChat: 'Nowy czat', + composerPlaceholder: 'Ile wydałem w zeszłym miesiącu?', + quickActions: { + createExpense: 'Utwórz wydatek', + createExpensePrompt: 'Utwórz wydatek', + analyzeSpend: 'Analizuj wydatki', + analyzeSpendPrompt: 'Ile wydałem w zeszłym miesiącu?', + configureWorkspace: 'Skonfiguruj przestrzeń roboczą', + configureWorkspacePrompt: 'Pomóż mi skonfigurować moją przestrzeń roboczą', + getSupport: 'Uzyskaj pomoc', + getSupportPrompt: 'Potrzebuję z czymś pomocy', + }, + }, duplicateReport: 'Zduplikowany raport', approver: 'Osoba zatwierdzająca', goToConcierge: 'Przejdź do Concierge', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index d3f8b357efa2..4072041bee4f 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -486,7 +486,26 @@ const translations: TranslationDeepObject = { quarter: 'Trimestre', vacationDelegate: 'Delegado de férias', expensifyLogo: 'Logo da Expensify', - concierge: {greeting: 'Oi, como posso ajudar?', showHistory: 'Mostrar histórico'}, + concierge: { + greeting: 'Oi, como posso ajudar?', + subtitle: 'Seu assistente de IA pessoal', + welcomeDescription: 'O Concierge pode responder perguntas, atualizar despesas e muito mais.', + viewChatHistory: 'Ver histórico do chat', + hideChatHistory: 'Ocultar histórico do chat', + askNewQuestion: 'Faça uma nova pergunta', + newChat: 'Novo chat', + composerPlaceholder: 'Quanto gastei no mês passado?', + quickActions: { + createExpense: 'Criar despesa', + createExpensePrompt: 'Crie uma despesa', + analyzeSpend: 'Analisar gastos', + analyzeSpendPrompt: 'Quanto gastei no mês passado?', + configureWorkspace: 'Configurar espaço de trabalho', + configureWorkspacePrompt: 'Ajude-me a configurar meu espaço de trabalho', + getSupport: 'Obter suporte', + getSupportPrompt: 'Preciso de ajuda com uma coisa', + }, + }, duplicateReport: 'Duplicar relatório', approver: 'Aprovador', goToConcierge: 'Ir para o Concierge', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 0355e53ad566..229ac87f64e0 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -483,7 +483,26 @@ const translations: TranslationDeepObject = { quarter: '季度', vacationDelegate: '休假代理', expensifyLogo: 'Expensify徽标', - concierge: {greeting: '你好,我能帮你做什么?', showHistory: '显示历史'}, + concierge: { + greeting: '你好,我能帮你做什么?', + subtitle: '你的专属 AI 助手', + welcomeDescription: 'Concierge 可以回答问题、更新支出等。', + viewChatHistory: '查看聊天记录', + hideChatHistory: '隐藏聊天记录', + askNewQuestion: '提出新问题', + newChat: '新聊天', + composerPlaceholder: '我上个月花了多少钱?', + quickActions: { + createExpense: '创建支出', + createExpensePrompt: '创建一笔支出', + analyzeSpend: '分析支出', + analyzeSpendPrompt: '我上个月花了多少钱?', + configureWorkspace: '配置工作区', + configureWorkspacePrompt: '帮我配置工作区', + getSupport: '获取支持', + getSupportPrompt: '我需要一些帮助', + }, + }, duplicateReport: '重复报销单', approver: '审批人', goToConcierge: '前往 Concierge', diff --git a/src/pages/inbox/ConciergeSessionContext.tsx b/src/pages/inbox/ConciergeSessionContext.tsx index 6122908e12ff..51e60a7be3a5 100644 --- a/src/pages/inbox/ConciergeSessionContext.tsx +++ b/src/pages/inbox/ConciergeSessionContext.tsx @@ -17,6 +17,8 @@ type ConciergeSessionStateContextType = { type ConciergeSessionActionsContextType = { startSession: (unreadBoundary?: string | null) => void; + /** Begins a brand new session, so the Concierge DM drops back to its welcome state */ + resetSession: () => void; setShowFullHistory: (show: boolean) => void; setHadMessagesAtSessionStart: (value: boolean) => void; }; @@ -29,6 +31,7 @@ const ConciergeSessionStateContext = createContext({ startSession: () => {}, + resetSession: () => {}, setShowFullHistory: () => {}, setHadMessagesAtSessionStart: () => {}, }); @@ -105,8 +108,15 @@ function ConciergeSessionProvider({children}: PropsWithChildren) { } }, []); + const resetSession = useCallback(() => { + sessionCreatedAtRef.current = Date.now(); + setSessionStartTime(getServerAnchoredDBTime()); + setShowFullHistory(false); + setHadMessagesAtSessionStart(false); + }, []); + const stateValue = useMemo(() => ({sessionStartTime, showFullHistory, hadMessagesAtSessionStart}), [sessionStartTime, showFullHistory, hadMessagesAtSessionStart]); - const actionsValue = useMemo(() => ({startSession, setShowFullHistory, setHadMessagesAtSessionStart}), [startSession]); + const actionsValue = useMemo(() => ({startSession, resetSession, setShowFullHistory, setHadMessagesAtSessionStart}), [startSession, resetSession]); return ( diff --git a/src/pages/inbox/HeaderView.tsx b/src/pages/inbox/HeaderView.tsx index 3b822f72ab62..ff734d5a963c 100644 --- a/src/pages/inbox/HeaderView.tsx +++ b/src/pages/inbox/HeaderView.tsx @@ -25,6 +25,7 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useParentReportAction from '@hooks/useParentReportAction'; +import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import {useDerivedReportNamesByReportIDs} from '@hooks/useReportAttributes'; import useReportIsArchived from '@hooks/useReportIsArchived'; @@ -34,6 +35,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; +import Navigation from '@libs/Navigation/Navigation'; import Parser from '@libs/Parser'; import {getPersonalDetailByEmail, getPersonalDetailsForAccountIDs} from '@libs/PersonalDetailsUtils'; import {getHumanAgentAccountIDFromReportAction, getHumanAgentFirstName} from '@libs/ReportActionsUtils'; @@ -86,6 +88,7 @@ import {callFunctionIfActionIsAllowed} from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -97,6 +100,8 @@ import {isPast} from 'date-fns'; import React, {useMemo} from 'react'; import {Keyboard, View} from 'react-native'; +import {useConciergeSessionActions} from './ConciergeSessionContext'; + type HeaderViewProps = { /** Toggles the navigationMenu open and closed */ onNavigationMenuButtonClicked: () => void; @@ -109,7 +114,7 @@ function HeaderView({onNavigationMenuButtonClicked, reportID}: HeaderViewProps) const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); const parentReportAction = useParentReportAction(report); - const icons = useMemoizedLazyExpensifyIcons(['BackArrow', 'Close', 'DotIndicator']); + const icons = useMemoizedLazyExpensifyIcons(['BackArrow', 'Close', 'DotIndicator', 'Plus']); // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth, shouldUseNarrowLayout, isInLandscapeMode} = useResponsiveLayout(); const isInSidePanel = useIsInSidePanel(); @@ -145,6 +150,10 @@ function HeaderView({onNavigationMenuButtonClicked, reportID}: HeaderViewProps) const isSelfDM = isSelfDMReportUtils(report); const isGroupChat = isGroupChatReportUtils(report) || isDeprecatedGroupDM(report, isReportArchived); const isConciergeChat = isConciergeChatReport(report, conciergeReportID); + const {isBetaEnabled} = usePermissions(); + const {resetSession: resetConciergeSession} = useConciergeSessionActions(); + const isAskConciergeEnabled = isBetaEnabled(CONST.BETAS.CONCIERGE_RESPOND_IN_THREAD) && !isInSidePanel; + const isConciergeThread = !!conciergeReportID && report?.parentReportID === conciergeReportID; const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [onboarding] = useOnyx(ONYXKEYS.NVP_ONBOARDING); const allParticipants = getParticipantsAccountIDsForDisplay(report, false, true, undefined, reportMetadata); @@ -174,7 +183,8 @@ function HeaderView({onNavigationMenuButtonClicked, reportID}: HeaderViewProps) const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); // Use sorted display names for the title for group chats on native small screen widths const title = getReportName(reportHeaderData, derivedReportHeaderName); - const subtitle = getChatRoomSubtitle(reportHeaderData, reportHeaderDataPolicy, conciergeReportID, translate, false, isReportHeaderDataArchived); + const chatRoomSubtitle = getChatRoomSubtitle(reportHeaderData, reportHeaderDataPolicy, conciergeReportID, translate, false, isReportHeaderDataArchived); + const subtitle = isAskConciergeEnabled && isConciergeChat ? translate('common.concierge.subtitle') : chatRoomSubtitle; // This is used to get the status badge for invoice report subtitle. const statusTextForInvoiceReport = isParentInvoiceAndIsChatThread ? getReportStatusTranslation({stateNum: reportHeaderData?.stateNum, statusNum: reportHeaderData?.statusNum, translate}) @@ -292,6 +302,21 @@ function HeaderView({onNavigationMenuButtonClicked, reportID}: HeaderViewProps) ); + const startNewConciergeChat = () => { + resetConciergeSession(); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(conciergeReportID)); + }; + + const newConciergeChatButton = ( + + ); + const renderAdditionalText = () => { if (shouldShowSubtitle() || isPersonalExpenseChat || !policyName || !isEmptyObject(parentNavigationSubtitleData) || isSelfDM) { return null; @@ -472,6 +497,7 @@ function HeaderView({onNavigationMenuButtonClicked, reportID}: HeaderViewProps) )} + {isAskConciergeEnabled && isConciergeThread && newConciergeChatButton} {shouldShowBookCall && !shouldStackBookCall && bookCallButton} {shouldShowOnBoardingHelpDropdownButton && !shouldUseNarrowLayout && onboardingHelpDropdownButton} {!shouldUseNarrowLayout && !shouldShowDiscount && isChatUsedForOnboarding && ( diff --git a/src/pages/inbox/report/ConciergeChatHistoryToggle.tsx b/src/pages/inbox/report/ConciergeChatHistoryToggle.tsx new file mode 100644 index 000000000000..ee9f6469dbd6 --- /dev/null +++ b/src/pages/inbox/report/ConciergeChatHistoryToggle.tsx @@ -0,0 +1,67 @@ +import Button from '@components/ButtonComposed'; + +import useConciergeAskState from '@hooks/useConciergeAskState'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import {useConciergeSessionActions} from '@pages/inbox/ConciergeSessionContext'; + +import CONST from '@src/CONST'; + +import React from 'react'; +import {View} from 'react-native'; + +type ConciergeChatHistoryToggleProps = { + /** The ID of the report being displayed */ + reportID: string; + + /** Whether there are messages hidden before the session start */ + hasPreviousMessages: boolean; + + /** Whether the earlier conversation is currently shown */ + shouldShowFullHistory: boolean; + + /** Callback to reveal the earlier conversation */ + onShowPreviousMessages: () => void; +}; + +/** + * Expands and collapses the earlier Concierge conversation in the main Concierge DM. It renders as part + * of the inverted list's header so it sits at the bottom of the conversation, directly above the composer. + */ +function ConciergeChatHistoryToggle({reportID, hasPreviousMessages, shouldShowFullHistory, onShowPreviousMessages}: ConciergeChatHistoryToggleProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['UpArrow', 'DownArrow']); + const {shouldShowWelcome, shouldLabelComposerAsNewQuestion} = useConciergeAskState(reportID); + const {setShowFullHistory} = useConciergeSessionActions(); + + const hideChatHistory = () => setShowFullHistory(false); + + // Both flags are false once the user asks something, which is when this control would otherwise + // land between the latest message and the composer. + if (!shouldShowWelcome && !shouldLabelComposerAsNewQuestion) { + return null; + } + + if (!hasPreviousMessages) { + return null; + } + + return ( + + + + + + ); +} + +export default ConciergeChatHistoryToggle; diff --git a/src/pages/inbox/report/ConciergeQuickActions.tsx b/src/pages/inbox/report/ConciergeQuickActions.tsx new file mode 100644 index 000000000000..0436d0704861 --- /dev/null +++ b/src/pages/inbox/report/ConciergeQuickActions.tsx @@ -0,0 +1,65 @@ +import Button from '@components/ButtonComposed'; + +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; + +import {saveReportDraftComment} from '@userActions/Report'; + +import CONST from '@src/CONST'; + +import React from 'react'; +import {View} from 'react-native'; + +const QUICK_ACTIONS = [ + {iconName: 'Plus', labelKey: 'common.concierge.quickActions.createExpense', promptKey: 'common.concierge.quickActions.createExpensePrompt'}, + {iconName: 'ChartPie', labelKey: 'common.concierge.quickActions.analyzeSpend', promptKey: 'common.concierge.quickActions.analyzeSpendPrompt'}, + {iconName: 'Wrench', labelKey: 'common.concierge.quickActions.configureWorkspace', promptKey: 'common.concierge.quickActions.configureWorkspacePrompt'}, + {iconName: 'QuestionMark', labelKey: 'common.concierge.quickActions.getSupport', promptKey: 'common.concierge.quickActions.getSupportPrompt'}, +] as const; + +const QUICK_ACTION_ICON_NAMES = QUICK_ACTIONS.map((action) => action.iconName); + +type ConciergeQuickActionsProps = { + /** The ID of the Concierge report the prompt is written into */ + reportID: string; +}; + +/** + * Conversation starters shown under the composer while the Concierge chat is still empty. Pressing one + * writes its prompt into the composer so the user can edit it before sending. + */ +function ConciergeQuickActions({reportID}: ConciergeQuickActionsProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + const {translate} = useLocalize(); + const expensifyIcons = useMemoizedLazyExpensifyIcons(QUICK_ACTION_ICON_NAMES); + + return ( + + {QUICK_ACTIONS.map(({iconName, labelKey, promptKey}) => ( + + ))} + + ); +} + +export default ConciergeQuickActions; diff --git a/src/pages/inbox/report/ConciergeWelcome.tsx b/src/pages/inbox/report/ConciergeWelcome.tsx new file mode 100644 index 000000000000..b89e85844cc4 --- /dev/null +++ b/src/pages/inbox/report/ConciergeWelcome.tsx @@ -0,0 +1,27 @@ +import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; + +import HomeSectionEmptyState from '@pages/home/HomeSectionEmptyState'; + +import React from 'react'; + +const ILLUSTRATION_NAMES = ['ConciergeBot'] as const; + +/** + * Empty state shown in the main Concierge DM before the user asks anything. + */ +function ConciergeWelcome() { + const {translate} = useLocalize(); + const illustrations = useMemoizedLazyIllustrations(ILLUSTRATION_NAMES); + + return ( + + ); +} + +export default ConciergeWelcome; diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerInput.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerInput.tsx index 777265b2b8a2..b464af77d9f1 100644 --- a/src/pages/inbox/report/ReportActionCompose/ComposerInput.tsx +++ b/src/pages/inbox/report/ReportActionCompose/ComposerInput.tsx @@ -3,6 +3,7 @@ import type {LocalizedTranslate} from '@components/LocaleContextProvider'; import useIsScrollLikelyLayoutTriggered from '@hooks/useIsScrollLikelyLayoutTriggered'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import usePermissions from '@hooks/usePermissions'; import useReportIsArchived from '@hooks/useReportIsArchived'; import {setIsComposerFullSize} from '@libs/actions/Report'; @@ -45,6 +46,7 @@ function ComposerInput() { const {setIsFullComposerAvailable, onBlur, onFocus, setComposerRef} = useComposerActions(); const {containerRef, suggestionsRef, isNextModalWillOpenRef} = useComposerMeta(); const {isEditingInComposer, didResetComposerHeightWhileEditing} = useComposerEditState(); + const {isBetaEnabled} = usePermissions(); const isSubmittingEdit = isEditingInComposer || didResetComposerHeightWhileEditing; const {submitDraftAndClearComposer, validateAndSubmitDraft} = useComposerSubmit(reportID); @@ -69,6 +71,8 @@ function ComposerInput() { const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); const isReportArchived = useReportIsArchived(report?.reportID); + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); + const isAskConciergeChat = !!reportID && reportID === conciergeReportID && isBetaEnabled(CONST.BETAS.CONCIERGE_RESPOND_IN_THREAD); const includesConcierge = chatIncludesConcierge({participants: report?.participants}); const isGroupPolicyReport = !!report?.policyID && report.policyID !== CONST.POLICY.ID_FAKE; @@ -79,6 +83,8 @@ function ComposerInput() { let inputPlaceholder = translate('reportActionCompose.writeSomething'); if (includesConcierge && userBlockedFromConcierge) { inputPlaceholder = translate('reportActionCompose.blockedFromConcierge'); + } else if (isAskConciergeChat) { + inputPlaceholder = translate('common.concierge.composerPlaceholder'); } else if (isExpenseRelatedReport && canUserPerformWriteAction && isEnglishLocale) { inputPlaceholder = getRandomPlaceholder(translate); } diff --git a/src/pages/inbox/report/ReportActionItemCreated.tsx b/src/pages/inbox/report/ReportActionItemCreated.tsx index 42e6a02e8e43..98a1c2181eed 100644 --- a/src/pages/inbox/report/ReportActionItemCreated.tsx +++ b/src/pages/inbox/report/ReportActionItemCreated.tsx @@ -3,6 +3,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import ReportWelcomeText from '@components/ReportWelcomeText'; +import useConciergeAskState from '@hooks/useConciergeAskState'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useIsInSidePanel from '@hooks/useIsInSidePanel'; import useLocalize from '@hooks/useLocalize'; @@ -24,6 +25,7 @@ import React, {memo} from 'react'; import {View} from 'react-native'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; +import ConciergeWelcome from './ConciergeWelcome'; type ReportActionItemCreatedProps = { /** The id of the report */ @@ -55,11 +57,16 @@ function ReportActionItemCreated({reportID, policyID}: ReportActionItemCreatedPr .map(Number) .find((id) => id !== currentUserAccountID) ?? CONST.DEFAULT_NUMBER_ID; const [isParticipantOptimistic = true] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: isOptimisticPersonalDetailSelector(otherParticipantAccountID)}); + const {shouldShowWelcome} = useConciergeAskState(reportID); if (!isChatReport(report)) { return null; } + if (shouldShowWelcome) { + return ; + } + const shouldDisableDetailPage = shouldDisableDetailPageReportUtils(report, isParticipantOptimistic); return ( diff --git a/src/pages/inbox/report/ReportActionsList.tsx b/src/pages/inbox/report/ReportActionsList.tsx index a733aa6ed3d3..8c47616fd636 100644 --- a/src/pages/inbox/report/ReportActionsList.tsx +++ b/src/pages/inbox/report/ReportActionsList.tsx @@ -2,6 +2,7 @@ import {renderScrollComponent as renderActionSheetAwareScrollView} from '@compon import InvertedFlashList from '@components/FlashList/InvertedFlashList'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; +import useConciergeAskState from '@hooks/useConciergeAskState'; import useEnvironment from '@hooks/useEnvironment'; import useLinkedMessageOfflineLoading from '@hooks/useLinkedMessageOfflineLoading'; import useLocalize from '@hooks/useLocalize'; @@ -63,6 +64,7 @@ import {useRoute} from '@react-navigation/native'; import {isTrackIntentUserSelector} from '@selectors/Onboarding'; import React, {useEffect, useRef, useState} from 'react'; +import ConciergeChatHistoryToggle from './ConciergeChatHistoryToggle'; import FloatingMessageCounter from './FloatingMessageCounter'; import ReportActionIndexContext from './ReportActionIndexContext'; import {useReportActionsListActions, useReportActionsListState} from './ReportActionsListContext'; @@ -135,6 +137,7 @@ function ReportActionsListContent({reportID, conciergeChat, onLayout}: ReportAct const route = useRoute>(); const reportActionIDFromRoute = route?.params?.reportActionID; const {sessionStartTime} = useConciergeSessionState(); + const {shouldShowWelcome: shouldShowConciergeWelcome} = useConciergeAskState(reportID); const didLayout = useRef(false); @@ -407,10 +410,18 @@ function ReportActionsListContent({reportID, conciergeChat, onLayout}: ReportAct ]; const listHeaderComponent = ( - + <> + + + ); const shouldShowOfflineSkeleton = isOffline && !sortedVisibleReportActions.some((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED); @@ -469,7 +480,7 @@ function ReportActionsListContent({reportID, conciergeChat, onLayout}: ReportAct keyExtractor={keyExtractor} drawDistance={1500} renderScrollComponent={renderActionSheetAwareScrollView} - contentContainerStyle={styles.chatContentScrollView} + contentContainerStyle={[styles.chatContentScrollView, shouldShowConciergeWelcome && styles.chatContentScrollViewCentered]} onEndReached={loadOlderChatsOnEndReached} onEndReachedThreshold={0.75} onStartReached={loadNewerChatsAfterTransitions} diff --git a/src/pages/inbox/report/ReportFooter.tsx b/src/pages/inbox/report/ReportFooter.tsx index 7d230e1f9128..1731dc74cd8b 100644 --- a/src/pages/inbox/report/ReportFooter.tsx +++ b/src/pages/inbox/report/ReportFooter.tsx @@ -4,7 +4,9 @@ import Banner from '@components/Banner'; import BlockedReportFooter from '@components/BlockedReportFooter'; import OfflineIndicator from '@components/OfflineIndicator'; import SwipeableView from '@components/SwipeableView'; +import Text from '@components/Text'; +import useConciergeAskState from '@hooks/useConciergeAskState'; import {useIsReportLoadPending} from '@hooks/useInFlightRequests'; import useIsAnonymousUser from '@hooks/useIsAnonymousUser'; import useIsReportReadyToDisplay from '@hooks/useIsReportReadyToDisplay'; @@ -37,6 +39,7 @@ import {isBlockedFromChatSelector} from '@selectors/BlockedFromChat'; import React from 'react'; import {Keyboard, View} from 'react-native'; +import ConciergeQuickActions from './ConciergeQuickActions'; import EnableNotificationsBanner, {BANNER_COMPOSER_OVERLAP_PX} from './EnableNotificationsBanner'; import ReportActionCompose from './ReportActionCompose/ReportActionCompose'; import SystemChatReportFooterMessage from './SystemChatReportFooterMessage'; @@ -92,6 +95,7 @@ function ReportFooter() { const isSystemChat = isSystemChatUtil(report); const isAdminsOnlyPostingRoom = isAdminsOnlyPostingRoomUtil(report); const shouldShowComposerForActiveEditDraft = useShouldShowComposerForActiveEditDraft(); + const {shouldShowWelcome: shouldShowConciergeWelcome, shouldLabelComposerAsNewQuestion} = useConciergeAskState(reportIDFromRoute); if (!isCurrentReportLoadedFromOnyx || !report || !reportIDFromRoute) { return null; @@ -108,6 +112,7 @@ function ReportFooter() { ); return ( + {shouldLabelComposerAsNewQuestion && {translate('common.concierge.askNewQuestion')}} {shouldShowEnableNotificationsBanner ? ( <> @@ -116,6 +121,7 @@ function ReportFooter() { ) : ( composer )} + {shouldShowConciergeWelcome && } ); } diff --git a/src/pages/inbox/report/ShowPreviousMessagesButton.tsx b/src/pages/inbox/report/ShowPreviousMessagesButton.tsx index a79be7182b07..ab587d701a8e 100644 --- a/src/pages/inbox/report/ShowPreviousMessagesButton.tsx +++ b/src/pages/inbox/report/ShowPreviousMessagesButton.tsx @@ -3,6 +3,7 @@ import Button from '@components/ButtonComposed'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; @@ -33,12 +34,17 @@ function ShowPreviousMessagesButton({reportID, actionType, hasPreviousMessages, const styles = useThemeStyles(); const {translate} = useLocalize(); const expensifyIcons = useMemoizedLazyExpensifyIcons(['UpArrow']); + const {isBetaEnabled} = usePermissions(); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const isConciergeChat = reportID === conciergeReportID; if (!isConciergeChat) { return null; } + // ConciergeChatHistoryToggle takes over in the Ask Concierge design, where the control also collapses the history again. + if (isBetaEnabled(CONST.BETAS.CONCIERGE_RESPOND_IN_THREAD)) { + return null; + } if (!onPress) { return null; } @@ -60,7 +66,7 @@ function ShowPreviousMessagesButton({reportID, actionType, hasPreviousMessages, size={CONST.BUTTON_SIZE.SMALL} onPress={onPress} > - {translate('common.concierge.showHistory')} + {translate('common.concierge.viewChatHistory')} diff --git a/src/styles/index.ts b/src/styles/index.ts index 4af9fe456c0c..2b2ffa55bfe7 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -2242,6 +2242,11 @@ const staticStyles = (theme: ThemeColors) => ...chatContentScrollViewPlatformStyles, }, + // The list is inverted, so centering here lifts a short welcome screen off the composer and into the middle of the chat. + chatContentScrollViewCentered: { + justifyContent: 'center', + }, + // Chat Item chatItem: { display: 'flex', diff --git a/tests/ui/ReportActionsListTest.tsx b/tests/ui/ReportActionsListTest.tsx index 0b1aac4cf61e..c68e081a6307 100644 --- a/tests/ui/ReportActionsListTest.tsx +++ b/tests/ui/ReportActionsListTest.tsx @@ -300,7 +300,7 @@ describe('ReportActionsList (body)', () => { revealDraftFromReportAction: jest.fn(), }); mockUseConciergeSessionState.mockReturnValue({sessionStartTime: null, showFullHistory: false, hadMessagesAtSessionStart: false}); - mockUseConciergeSessionActions.mockReturnValue({startSession: jest.fn(), setShowFullHistory: jest.fn(), setHadMessagesAtSessionStart: jest.fn()}); + mockUseConciergeSessionActions.mockReturnValue({startSession: jest.fn(), resetSession: jest.fn(), setShowFullHistory: jest.fn(), setHadMessagesAtSessionStart: jest.fn()}); mockUseOnyx.mockImplementation((key: string, options) => { // useReportActionsListModel derives app-load state from the request queue via useIsAppLoadPending, @@ -791,7 +791,7 @@ describe('ReportActionsList (body)', () => { mockUseIsInSidePanel.mockReturnValue(false); mockUseSidePanelState.mockReturnValue(defaultSidePanelState); mockUseConciergeSessionState.mockReturnValue({sessionStartTime, showFullHistory, hadMessagesAtSessionStart: false}); - mockUseConciergeSessionActions.mockReturnValue({startSession: jest.fn(), setShowFullHistory: jest.fn(), setHadMessagesAtSessionStart: jest.fn()}); + mockUseConciergeSessionActions.mockReturnValue({startSession: jest.fn(), resetSession: jest.fn(), setShowFullHistory: jest.fn(), setHadMessagesAtSessionStart: jest.fn()}); mockUseOnyx.mockImplementation((key: string, options) => { if (key === ONYXKEYS.CONCIERGE_REPORT_ID) { @@ -953,9 +953,9 @@ describe('ReportActionsList (body)', () => { it('should call startSession on mount for main DM concierge', () => { const mockStartSession = jest.fn(); - mockUseConciergeSessionActions.mockReturnValue({startSession: mockStartSession, setShowFullHistory: jest.fn(), setHadMessagesAtSessionStart: jest.fn()}); + mockUseConciergeSessionActions.mockReturnValue({startSession: mockStartSession, resetSession: jest.fn(), setShowFullHistory: jest.fn(), setHadMessagesAtSessionStart: jest.fn()}); setupMainDMConciergeMocks(); - mockUseConciergeSessionActions.mockReturnValue({startSession: mockStartSession, setShowFullHistory: jest.fn(), setHadMessagesAtSessionStart: jest.fn()}); + mockUseConciergeSessionActions.mockReturnValue({startSession: mockStartSession, resetSession: jest.fn(), setShowFullHistory: jest.fn(), setHadMessagesAtSessionStart: jest.fn()}); mockUsePaginatedReportActions.mockReturnValue({ ...defaultPaginatedReportActionsResult, diff --git a/tests/unit/ShowPreviousMessagesButtonTest.tsx b/tests/unit/ShowPreviousMessagesButtonTest.tsx index 301da70b4d17..8203eee4caec 100644 --- a/tests/unit/ShowPreviousMessagesButtonTest.tsx +++ b/tests/unit/ShowPreviousMessagesButtonTest.tsx @@ -96,4 +96,15 @@ describe('ShowPreviousMessagesButton', () => { renderButton({showFullHistory: true}); expect(screen.queryByRole('button')).toBeNull(); }); + + it('renders nothing when the Ask Concierge beta is enabled', async () => { + await Onyx.set(ONYXKEYS.BETAS, [CONST.BETAS.CONCIERGE_RESPOND_IN_THREAD]); + await waitForBatchedUpdates(); + + renderButton(); + expect(screen.queryByRole('button')).toBeNull(); + + await Onyx.set(ONYXKEYS.BETAS, []); + await waitForBatchedUpdates(); + }); });