Messaging in New Expensify is fast, flexible, and works seamlessly even when offline. Every message is optimistically rendered while syncing in the background, ensuring a smooth experience across all devices.
Composing Messages
Basic Text Messages
The simplest way to communicate is through text messages:
Navigate to a Chat
Open any 1-on-1 chat, group conversation, or report thread.
Type Your Message
Click the message input field at the bottom and start typing.
Send
Press Enter (or Cmd/Ctrl + Enter ) to send. Your message appears immediately.
Messages are sent optimistically—they appear in your chat immediately and sync to the server in the background. If you’re offline, they’ll send automatically when reconnected.
Message Architecture
Here’s how messages are created under the hood:
// From src/libs/actions/Report/index.ts
function addComment (
reportID : string ,
text : string = '' ,
file ?: FileObject ,
) {
const reportComment = Parser . htmlToMarkdown ( text . trim ());
// Build optimistic report action
const optimisticReportAction = buildOptimisticAddCommentReportAction (
reportComment ,
file ,
);
const optimisticData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . REPORT_ACTIONS }${ reportID } ` ,
value: {
[optimisticReportAction.reportActionID]: optimisticReportAction ,
},
},
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . REPORT }${ reportID } ` ,
value: {
lastMessageText: reportComment ,
lastVisibleActionCreated: optimisticReportAction . created ,
},
},
];
API . write ( WRITE_COMMANDS . ADD_COMMENT , parameters , {
optimisticData ,
successData ,
failureData ,
});
}
Text Formatting
New Expensify supports rich text formatting using markdown:
Supported Markdown
Parsing Implementation
Markdown is parsed using the ExpensiMark parser:
// From src/libs/Parser.ts
import ExpensiMark from 'expensify-common/lib/ExpensiMark' ;
const parser = new ExpensiMark ();
// Convert HTML to Markdown for storage
function htmlToMarkdown ( html : string ) : string {
return parser . htmlToMarkdown ( html );
}
// Convert Markdown to HTML for display
function markdownToHTML ( markdown : string ) : string {
return parser . replace ( markdown );
}
Attachments
Uploading Files
Attach files to your messages in several ways:
Drag and Drop
File Picker
Mobile Camera
Drag files from your computer directly into the message composer. Supported file types:
Images (PNG, JPG, GIF, WebP)
PDFs
Documents (limited support)
Click the attachment icon in the message composer to select files from your device.
On mobile, tap the camera icon to take a photo directly from the app.
File Upload Code
// Handling file attachments from src/libs/actions/Report/index.ts
function addComment (
reportID : string ,
text : string = '' ,
file ?: FileObject ,
) {
let attachmentParams : Partial < AddCommentOrAttachmentParams > = {};
if ( file ) {
attachmentParams = {
file ,
source: file . uri ?? '' ,
filename: file . name ,
};
}
const parameters : AddCommentOrAttachmentParams = {
reportID ,
reportComment: text ,
... attachmentParams ,
};
}
Editing Messages
Edit your own messages to fix typos or update information:
Access Edit Mode
Hover over your message and click the edit icon (or long-press on mobile).
Make Changes
Update the text. The original message is preserved in history.
Save
Click Save or press Enter . An “edited” indicator appears on the message.
// Editing messages from src/libs/actions/Report/index.ts
function editReportComment (
reportID : string ,
originalReportAction : ReportAction ,
textForNewComment : string ,
) {
const htmlForNewComment = Parser . replace ( textForNewComment );
const optimisticReportActions = {
[originalReportAction.reportActionID]: {
message: [
{
type: 'COMMENT' ,
html: htmlForNewComment ,
text: textForNewComment ,
},
],
pendingAction: CONST . RED_BRICK_ROAD_PENDING_ACTION . UPDATE ,
},
};
API . write ( WRITE_COMMANDS . UPDATE_COMMENT , parameters , {
optimisticData ,
successData ,
failureData ,
});
}
You can only edit your own messages. Messages older than 30 days cannot be edited.
Deleting Messages
Remove messages from the conversation:
Select Message
Hover over the message you want to delete.
Delete
Click the delete icon (trash can) and confirm.
Result
The message is replaced with “[Deleted message]” placeholder.
// Deleting messages from src/libs/actions/Report/index.ts
function deleteReportComment (
reportID : string ,
reportAction : ReportAction ,
) {
const deletedMessage = [
{
translationKey: '' ,
type: 'COMMENT' ,
html: '' ,
text: '' ,
isEdited: true ,
isDeletedParentAction: true ,
},
];
const optimisticReportActions = {
[reportAction.reportActionID]: {
pendingAction: CONST . RED_BRICK_ROAD_PENDING_ACTION . DELETE ,
previousMessage: reportAction . message ,
message: deletedMessage ,
},
};
}
Thread Management
Conversation Threads
Every report and expense has its own conversation thread:
Expense Threads Discuss individual expenses with approvers and team members.
Report Threads Coordinate on entire expense reports during the approval process.
Transaction Threads Comment on specific transactions within a report.
Task Threads Discuss task details and progress updates.
Thread Structure
// Thread creation from src/libs/ReportUtils.ts
function buildTransactionThread (
reportAction : ReportAction ,
reportID : string ,
policyID : string ,
) : OptimisticChatReport {
const reportName = `Transaction Thread` ;
const currentTime = DateUtils . getDBTime ();
return {
reportID: generateReportID (),
chatType: CONST . REPORT . CHAT_TYPE . TRANSACTION_THREAD ,
parentReportID: reportID ,
parentReportActionID: reportAction . reportActionID ,
reportName ,
policyID ,
lastActorAccountID: reportAction . actorAccountID ,
lastReadTime: currentTime ,
};
}
Typing Indicators
See when other participants are typing:
// Real-time typing from src/libs/actions/Report/index.ts
function broadcastUserIsTyping ( reportID : string ) {
const privateUserChannelName = `private-user-accountID- ${ currentUserAccountID } ` ;
const typingStatus : UserIsTypingEvent = {
[reportID]: true ,
};
Pusher . sendEvent (
privateUserChannelName ,
Pusher . TYPE . USER_IS_TYPING ,
typingStatus
);
}
function broadcastUserIsLeavingRoom ( reportID : string ) {
const privateUserChannelName = `private-user-accountID- ${ currentUserAccountID } ` ;
const leavingStatus : UserIsLeavingRoomEvent = {
[reportID]: true ,
};
Pusher . sendEvent (
privateUserChannelName ,
Pusher . TYPE . USER_IS_LEAVING_ROOM ,
leavingStatus
);
}
Message Status
Understand message delivery states:
Sending
Gray checkmark—message is being sent to server
Sent
Green checkmark—message successfully delivered
Failed
Red indicator—message failed to send (tap to retry)
Best Practices
Format important information with bold or italic text, but don’t overdo it. Clear, concise writing is more effective than heavy formatting.
Reply in the correct thread context. Don’t start new conversations for follow-up questions on existing expenses or reports.
If you need to correct a message, edit it rather than deleting and reposting. This preserves conversation context.
Attach Files Thoughtfully
Upload receipts and documents directly to expense entries rather than sharing them in chat when possible.
Next Steps
Task Management Learn how to create and manage tasks from chat conversations
Mentions & Notifications Master @mentions and notification settings
Chat Overview Back to chat features overview