diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 9cd55b41455b..a268c008cee8 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -453,12 +453,13 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: string; [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES]: number; [ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE]: boolean; - [ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING]: boolean; + [ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING]: OnyxTypes.ReportUserIsTyping; [ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM]: boolean; [ONYXKEYS.COLLECTION.SECURITY_GROUP]: OnyxTypes.SecurityGroup; [ONYXKEYS.COLLECTION.TRANSACTION]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; [ONYXKEYS.COLLECTION.SELECTED_TAB]: string; + [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; // Forms [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: OnyxTypes.AddDebitCardForm; diff --git a/src/components/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx index 3187bf3604e8..712ef6be769e 100644 --- a/src/components/ArchivedReportFooter.tsx +++ b/src/components/ArchivedReportFooter.tsx @@ -8,7 +8,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, Report, ReportAction} from '@src/types/onyx'; +import type {PersonalDetailsList, Report, ReportAction} from '@src/types/onyx'; import Banner from './Banner'; type ArchivedReportFooterOnyxProps = { @@ -16,7 +16,7 @@ type ArchivedReportFooterOnyxProps = { reportClosedAction: OnyxEntry; /** Personal details of all users */ - personalDetails: OnyxEntry>; + personalDetails: OnyxEntry; }; type ArchivedReportFooterProps = ArchivedReportFooterOnyxProps & { diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 22bfc83218ac..655f6a40609f 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -8,29 +8,13 @@ import {Emoji, HeaderEmoji, PickerEmojis} from '@assets/emojis/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {FrequentlyUsedEmoji} from '@src/types/onyx'; +import {ReportActionReaction, UsersReactions} from '@src/types/onyx/ReportActionReactions'; import {SupportedLanguage} from './EmojiTrie'; type HeaderIndice = {code: string; index: number; icon: React.FC}; type EmojiSpacer = {code: string; spacer: boolean}; type EmojiPickerList = Array; type ReplacedEmoji = {text: string; emojis: Emoji[]; cursorPosition?: number}; -type UserReactions = { - id: string; - skinTones: Record; -}; -type UserReactionsWithTimestamps = UserReactions & { - oldestTimestamp: string; -}; -type UsersReactionsList = { - createdAt: string; - users: Record; -}; -type TimestampedUsersReactions = Record; -type EnrichedUserReactions = { - createdAt: string; - oldestTimestamp: string; - users: TimestampedUsersReactions; -}; let frequentlyUsedEmojis: FrequentlyUsedEmoji[] = []; Onyx.connect({ @@ -442,9 +426,9 @@ function suggestEmojis(text: string, lang: keyof SupportedLanguage, limit = CONS /** * Retrieve preferredSkinTone as Number to prevent legacy 'default' String value */ -const getPreferredSkinToneIndex = (val: string | number): number | string => { - if (val !== null && Number.isInteger(Number(val))) { - return val; +const getPreferredSkinToneIndex = (value: string | number | null): number => { + if (value !== null && Number.isInteger(Number(value))) { + return Number(value); } return CONST.EMOJI_DEFAULT_SKIN_TONE; @@ -473,7 +457,7 @@ const getPreferredEmojiCode = (emoji: Emoji, preferredSkinTone: number): string * array of emoji codes, that represents all used variations of the * emoji, sorted by the reaction timestamp. */ -const getUniqueEmojiCodes = (emojiAsset: Emoji, users: TimestampedUsersReactions): string[] => { +const getUniqueEmojiCodes = (emojiAsset: Emoji, users: UsersReactions): string[] => { const emojiCodes: Record = Object.values(users ?? {}).reduce((result: Record, userSkinTones) => { Object.keys(userSkinTones?.skinTones ?? {}).forEach((skinTone) => { const createdAt = userSkinTones.skinTones[Number(skinTone)]; @@ -492,12 +476,11 @@ const getUniqueEmojiCodes = (emojiAsset: Emoji, users: TimestampedUsersReactions /** * Given an emoji reaction object and its name, it populates it with the oldest reaction timestamps. */ -const enrichEmojiReactionWithTimestamps = (emoji: UsersReactionsList, emojiName: string): EnrichedUserReactions => { +const enrichEmojiReactionWithTimestamps = (emoji: ReportActionReaction, emojiName: string): ReportActionReaction => { let oldestEmojiTimestamp: string | null = null; - const usersWithTimestamps: Record = {}; - Object.keys(emoji.users ?? {}).forEach((id) => { - const user = emoji?.users?.[id]; + const usersWithTimestamps: UsersReactions = {}; + Object.entries(emoji.users ?? {}).forEach(([id, user]) => { const userTimestamps = Object.values(user?.skinTones ?? {}); const oldestUserTimestamp = userTimestamps.reduce((min, curr) => (curr < min ? curr : min), userTimestamps[0]); @@ -527,7 +510,7 @@ const enrichEmojiReactionWithTimestamps = (emoji: UsersReactionsList, emojiName: * Uses the NEW FORMAT for "emojiReactions" * @param usersReactions - all the users reactions */ -function hasAccountIDEmojiReacted(accountID: string, usersReactions: TimestampedUsersReactions, skinTone?: number) { +function hasAccountIDEmojiReacted(accountID: number, usersReactions: UsersReactions, skinTone?: number) { if (skinTone === undefined) { return Boolean(usersReactions[accountID]); } @@ -541,7 +524,7 @@ function hasAccountIDEmojiReacted(accountID: string, usersReactions: Timestamped /** * Given an emoji reaction and current user's account ID, it returns the reusable details of the emoji reaction. */ -const getEmojiReactionDetails = (emojiName: string, reaction: UsersReactionsList, currentUserAccountID: string) => { +const getEmojiReactionDetails = (emojiName: string, reaction: ReportActionReaction, currentUserAccountID: number) => { const {users, oldestTimestamp} = enrichEmojiReactionWithTimestamps(reaction, emojiName); const emoji = findEmojiByName(emojiName); diff --git a/src/libs/GroupChatUtils.ts b/src/libs/GroupChatUtils.ts index db64f6574824..862c50700c0c 100644 --- a/src/libs/GroupChatUtils.ts +++ b/src/libs/GroupChatUtils.ts @@ -1,10 +1,10 @@ import Onyx, {OnyxEntry} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; -import {PersonalDetails, Report} from '@src/types/onyx'; +import {PersonalDetailsList, Report} from '@src/types/onyx'; import * as OptionsListUtils from './OptionsListUtils'; import * as ReportUtils from './ReportUtils'; -let allPersonalDetails: OnyxEntry> = {}; +let allPersonalDetails: OnyxEntry = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (val) => (allPersonalDetails = val), diff --git a/src/libs/Notification/LocalNotification/types.ts b/src/libs/Notification/LocalNotification/types.ts index 25db8b080e2b..fb18c6931560 100644 --- a/src/libs/Notification/LocalNotification/types.ts +++ b/src/libs/Notification/LocalNotification/types.ts @@ -1,4 +1,5 @@ import {ImageSourcePropType} from 'react-native'; +import {OnyxEntry} from 'react-native-onyx'; import {Report, ReportAction} from '@src/types/onyx'; type PushParams = { @@ -11,7 +12,7 @@ type PushParams = { }; type ReportCommentParams = { - report: Report; + report: OnyxEntry; reportAction: ReportAction; onClick: () => void; }; diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js index 560480dcec9d..88b476a03100 100644 --- a/src/libs/PersonalDetailsUtils.js +++ b/src/libs/PersonalDetailsUtils.js @@ -80,7 +80,7 @@ function getAccountIDsByLogins(logins) { * Given a list of accountIDs, find the associated personal detail and return related logins. * * @param {Array} accountIDs Array of user accountIDs - * @returns {Array} - Array of logins according to passed accountIDs + * @returns {Array} - Array of logins according to passed accountIDs */ function getLoginsByAccountIDs(accountIDs) { return _.reduce( diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 19129959d016..347e5b68e960 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -2,11 +2,10 @@ import Str from 'expensify-common/lib/str'; import {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {PersonalDetails, Policy, PolicyMembers, PolicyTag, PolicyTags} from '@src/types/onyx'; +import {PersonalDetailsList, Policy, PolicyMembers, PolicyTag, PolicyTags} from '@src/types/onyx'; import {EmptyObject, isEmptyObject} from '@src/types/utils/EmptyObject'; type MemberEmailsToAccountIDs = Record; -type PersonalDetailsList = Record; type UnitRate = {rate: number}; /** diff --git a/src/libs/Pusher/pusher.ts b/src/libs/Pusher/pusher.ts index dd8af08db229..33219b1dfe73 100644 --- a/src/libs/Pusher/pusher.ts +++ b/src/libs/Pusher/pusher.ts @@ -5,7 +5,7 @@ import {LiteralUnion, ValueOf} from 'type-fest'; import Log from '@libs/Log'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {OnyxUpdateEvent, OnyxUpdatesFromServer} from '@src/types/onyx'; +import {OnyxUpdateEvent, OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx'; import DeepValueOf from '@src/types/utils/DeepValueOf'; import TYPE from './EventType'; import Pusher from './library'; @@ -24,12 +24,25 @@ type Args = { type PushJSON = OnyxUpdateEvent[] | OnyxUpdatesFromServer; +type UserIsTypingEvent = ReportUserIsTyping & { + userLogin?: string; +}; + +type UserIsLeavingRoomEvent = Record & { + userLogin?: string; +}; + +type PusherEventMap = { + [TYPE.USER_IS_TYPING]: UserIsTypingEvent; + [TYPE.USER_IS_LEAVING_ROOM]: UserIsLeavingRoomEvent; +}; + +type EventData = EventName extends keyof PusherEventMap ? PusherEventMap[EventName] : PushJSON; + type EventCallbackError = {type: ValueOf; data: {code: number}}; type ChunkedDataEvents = {chunks: unknown[]; receivedFinal: boolean}; -type EventData = {id?: string; chunk?: unknown; final?: boolean; index: number}; - type SocketEventCallback = (eventName: SocketEventName, data?: States | EventCallbackError) => void; type PusherWithAuthParams = InstanceType & { @@ -139,13 +152,13 @@ function getChannel(channelName: string): Channel | undefined { /** * Binds an event callback to a channel + eventName */ -function bindEventToChannel(channel: Channel | undefined, eventName: PusherEventName, eventCallback: (data: PushJSON) => void = () => {}) { +function bindEventToChannel(channel: Channel | undefined, eventName: EventName, eventCallback: (data: EventData) => void = () => {}) { if (!eventName) { return; } const chunkedDataEvents: Record = {}; - const callback = (eventData: string | Record | EventData) => { + const callback = (eventData: EventData) => { if (shouldForceOffline) { Log.info('[Pusher] Ignoring a Push event because shouldForceOffline = true'); return; @@ -207,7 +220,12 @@ function bindEventToChannel(channel: Channel | undefined, eventName: PusherEvent * Subscribe to a channel and an event * @param [onResubscribe] Callback to be called when reconnection happen */ -function subscribe(channelName: string, eventName: PusherEventName, eventCallback: (data: PushJSON) => void = () => {}, onResubscribe = () => {}): Promise { +function subscribe( + channelName: string, + eventName: EventName, + eventCallback: (data: EventData) => void = () => {}, + onResubscribe = () => {}, +): Promise { return new Promise((resolve, reject) => { // We cannot call subscribe() before init(). Prevent any attempt to do this on dev. if (!socket) { @@ -307,7 +325,7 @@ function isSubscribed(channelName: string): boolean { /** * Sends an event over a specific event/channel in pusher. */ -function sendEvent(channelName: string, eventName: PusherEventName, payload: Record) { +function sendEvent(channelName: string, eventName: EventName, payload: EventData) { // Check to see if we are subscribed to this channel before sending the event. Sending client events over channels // we are not subscribed too will throw errors and cause reconnection attempts. Subscriptions are not instant and // can happen later than we expect. @@ -394,4 +412,4 @@ export { getPusherSocketID, }; -export type {EventCallbackError, States, PushJSON}; +export type {EventCallbackError, States, PushJSON, UserIsTypingEvent, UserIsLeavingRoomEvent}; diff --git a/src/libs/PusherUtils.ts b/src/libs/PusherUtils.ts index e5782d12acd3..9f7e21ef9005 100644 --- a/src/libs/PusherUtils.ts +++ b/src/libs/PusherUtils.ts @@ -26,7 +26,7 @@ function triggerMultiEventHandler(eventType: string, data: OnyxUpdate[]): Promis * Abstraction around subscribing to private user channel events. Handles all logs and errors automatically. */ function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string, onEvent: (pushJSON: PushJSON) => void) { - const pusherChannelName = `${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}${accountID}${CONFIG.PUSHER.SUFFIX}`; + const pusherChannelName = `${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}${accountID}${CONFIG.PUSHER.SUFFIX}` as const; function logPusherEvent(pushJSON: PushJSON) { Log.info(`[Report] Handled ${eventName} event sent by Pusher`, false, pushJSON); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9e51d35fb253..49dd69392b29 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -16,10 +16,11 @@ import CONST from '@src/CONST'; import {ParentNavigationSummaryParams, TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {Beta, Login, PersonalDetails, Policy, PolicyTags, Report, ReportAction, Session, Transaction} from '@src/types/onyx'; +import {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, PolicyTags, Report, ReportAction, Session, Transaction} from '@src/types/onyx'; import {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; -import {ChangeLog, IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMessage'; -import {Message, ReportActions} from '@src/types/onyx/ReportAction'; +import {ChangeLog, IOUMessage, OriginalMessageActionName, OriginalMessageCreated} from '@src/types/onyx/OriginalMessage'; +import {NotificationPreference} from '@src/types/onyx/Report'; +import {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; import {Receipt, WaypointCollection} from '@src/types/onyx/Transaction'; import DeepValueOf from '@src/types/utils/DeepValueOf'; import {EmptyObject, isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; @@ -188,10 +189,8 @@ type OptimisticClosedReportAction = Pick< 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'message' | 'originalMessage' | 'pendingAction' | 'person' | 'reportActionID' | 'shouldShow' >; -type OptimisticCreatedReportAction = Pick< - ReportAction, - 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'pendingAction' ->; +type OptimisticCreatedReportAction = OriginalMessageCreated & + Pick; type OptimisticChatReport = Pick< Report, @@ -318,7 +317,6 @@ type DisplayNameWithTooltips = Array, + personalDetailsList: PersonalDetails[] | PersonalDetailsList, isMultipleParticipantReport: boolean, shouldFallbackToHidden = true, ): DisplayNameWithTooltips { @@ -1437,7 +1435,7 @@ function getDisplayNamesWithTooltips( const avatar = UserUtils.getDefaultAvatar(accountID); let pronouns = user.pronouns; - if (pronouns && pronouns.startsWith(CONST.PRONOUNS.PREFIX)) { + if (pronouns?.startsWith(CONST.PRONOUNS.PREFIX)) { const pronounTranslationKey = pronouns.replace(CONST.PRONOUNS.PREFIX, ''); pronouns = Localize.translateLocal(`pronouns.${pronounTranslationKey}` as TranslationPaths); } @@ -2394,7 +2392,7 @@ function getParsedComment(text: string): string { return text.length <= CONST.MAX_MARKUP_LENGTH ? parser.replace(text) : lodashEscape(text); } -function buildOptimisticAddCommentReportAction(text?: string, file?: File & {source: string; uri: string}): OptimisticReportAction { +function buildOptimisticAddCommentReportAction(text?: string, file?: File): OptimisticReportAction { const parser = new ExpensiMark(); const commentText = getParsedComment(text ?? ''); const isAttachment = !text && file !== undefined; @@ -3077,7 +3075,7 @@ function buildOptimisticChatReport( oldPolicyName = '', visibility: ValueOf | undefined = undefined, writeCapability: ValueOf | undefined = undefined, - notificationPreference: string | number = CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + notificationPreference: NotificationPreference = CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, parentReportActionID = '', parentReportID = '', welcomeMessage = '', @@ -4306,12 +4304,12 @@ function shouldAutoFocusOnKeyPress(event: KeyboardEvent): boolean { * Navigates to the appropriate screen based on the presence of a private note for the current user. */ function navigateToPrivateNotes(report: Report, session: Session) { - if (isEmpty(report) || isEmpty(session)) { + if (isEmpty(report) || isEmpty(session) || !session.accountID) { return; } - const currentUserPrivateNote = report.privateNotes?.[String(session.accountID)]?.note ?? ''; + const currentUserPrivateNote = report.privateNotes?.[session.accountID]?.note ?? ''; if (isEmpty(currentUserPrivateNote)) { - Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, String(session.accountID))); + Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session.accountID)); return; } Navigation.navigate(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); @@ -4488,4 +4486,4 @@ export { shouldAutoFocusOnKeyPress, }; -export type {OptionData}; +export type {OptionData, OptimisticChatReport}; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 1da1469a2687..222355c6a7df 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -245,7 +245,6 @@ function getOptionData( const result: ReportUtils.OptionData = { alternateText: null, - pendingAction: null, allReportErrors: null, brickRoadIndicator: null, tooltipText: null, @@ -285,7 +284,7 @@ function getOptionData( result.isExpenseRequest = ReportUtils.isExpenseRequest(report); result.isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); result.shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report); - result.pendingAction = report.pendingFields ? report.pendingFields.addWorkspaceRoom || report.pendingFields.createChat : null; + result.pendingAction = report.pendingFields ? report.pendingFields.addWorkspaceRoom || report.pendingFields.createChat : undefined; result.allReportErrors = OptionsListUtils.getAllReportErrors(report, reportActions) as OnyxCommon.Errors; result.brickRoadIndicator = Object.keys(result.allReportErrors ?? {}).length !== 0 ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; result.ownerAccountID = report.ownerAccountID; @@ -305,7 +304,7 @@ function getOptionData( result.hasOutstandingChildRequest = report.hasOutstandingChildRequest; result.parentReportID = report.parentReportID ?? ''; result.isWaitingOnBankAccount = report.isWaitingOnBankAccount; - result.notificationPreference = report.notificationPreference ?? ''; + result.notificationPreference = report.notificationPreference; result.isAllowedToComment = ReportUtils.canUserPerformWriteAction(report); result.chatType = report.chatType; diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 29d18d543a11..02b5f70db285 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -9,7 +9,7 @@ import * as UserUtils from '@libs/UserUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {DateOfBirthForm, PersonalDetails, PrivatePersonalDetails} from '@src/types/onyx'; +import {DateOfBirthForm, PersonalDetails, PersonalDetailsList, PrivatePersonalDetails} from '@src/types/onyx'; import {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails'; type FirstAndLastName = { @@ -27,7 +27,7 @@ Onyx.connect({ }, }); -let allPersonalDetails: OnyxEntry> = null; +let allPersonalDetails: OnyxEntry = null; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (val) => (allPersonalDetails = val), diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.ts similarity index 61% rename from src/libs/actions/Report.js rename to src/libs/actions/Report.ts index 5bc6bfe8c58d..134b78627c61 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.ts @@ -2,10 +2,12 @@ import {format as timezoneFormat, utcToZonedTime} from 'date-fns-tz'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; import lodashDebounce from 'lodash/debounce'; -import lodashGet from 'lodash/get'; +import isEmpty from 'lodash/isEmpty'; import {DeviceEventEmitter, InteractionManager} from 'react-native'; -import Onyx from 'react-native-onyx'; -import _ from 'underscore'; +import Onyx, {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import {NullishDeep} from 'react-native-onyx/lib/types'; +import {PartialDeep, ValueOf} from 'type-fest'; +import {Emoji} from '@assets/emojis/types'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import * as API from '@libs/API'; import * as CollectionUtils from '@libs/CollectionUtils'; @@ -16,6 +18,7 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import LocalNotification from '@libs/Notification/LocalNotification'; +import {ReportCommentParams} from '@libs/Notification/LocalNotification/types'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as Pusher from '@libs/Pusher/pusher'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; @@ -25,70 +28,84 @@ import Visibility from '@libs/Visibility'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; +import ROUTES, {Route} from '@src/ROUTES'; +import {PersonalDetails, PersonalDetailsList, ReportActionReactions, ReportUserIsTyping} from '@src/types/onyx'; +import {Decision, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; +import Report, {NotificationPreference, WriteCapability} from '@src/types/onyx/Report'; +import ReportAction, {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; +import {EmptyObject, isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import * as Session from './Session'; import * as Welcome from './Welcome'; -let currentUserAccountID; +type SubscriberCallback = (isFromCurrentUser: boolean, reportActionID: string | undefined) => void; + +type ActionSubscriber = { + reportID: string; + callback: SubscriberCallback; +}; + +let currentUserAccountID = -1; Onyx.connect({ key: ONYXKEYS.SESSION, - callback: (val) => { + callback: (value) => { // When signed out, val is undefined - if (!val) { + if (!value?.accountID) { return; } - currentUserAccountID = val.accountID; + currentUserAccountID = value.accountID; }, }); -let preferredSkinTone; +let preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE; Onyx.connect({ key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, - callback: (val) => { - preferredSkinTone = EmojiUtils.getPreferredSkinToneIndex(val); + callback: (value) => { + preferredSkinTone = EmojiUtils.getPreferredSkinToneIndex(value); }, }); -const allReportActions = {}; +const allReportActions: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - callback: (actions, key) => { - if (!key || !actions) { + callback: (action, key) => { + if (!key || !action) { return; } const reportID = CollectionUtils.extractCollectionItemID(key); - allReportActions[reportID] = actions; + allReportActions[reportID] = action; }, }); -const currentReportData = {}; +const currentReportData: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, - callback: (data, key) => { - if (!key || !data) { + callback: (report, key) => { + if (!key || !report) { return; } const reportID = CollectionUtils.extractCollectionItemID(key); - currentReportData[reportID] = data; + currentReportData[reportID] = report; }, }); let isNetworkOffline = false; Onyx.connect({ key: ONYXKEYS.NETWORK, - callback: (val) => (isNetworkOffline = lodashGet(val, 'isOffline', false)), + callback: (value) => { + isNetworkOffline = value?.isOffline ?? false; + }, }); -let allPersonalDetails; +let allPersonalDetails: OnyxEntry = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (val) => { - allPersonalDetails = val || {}; + callback: (value) => { + allPersonalDetails = value ?? {}; }, }); -const draftNoteMap = {}; +const draftNoteMap: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT, callback: (value, key) => { @@ -101,17 +118,12 @@ Onyx.connect({ }, }); -const allReports = {}; -let conciergeChatReportID; -const typingWatchTimers = {}; +const allReports: OnyxCollection = {}; +let conciergeChatReportID: string | undefined; +const typingWatchTimers: Record = {}; -/** - * Get the private pusher channel name for a Report. - * - * @param {String} reportID - * @returns {String} - */ -function getReportChannelName(reportID) { +/** 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}`; } @@ -122,26 +134,21 @@ function getReportChannelName(reportID) { * 2. The "old" way from e.com which is passed as {userLogin: login} (e.g. {userLogin: bstites@expensify.com}) * * This method makes sure that no matter which we get, we return the "new" format - * - * @param {Object} status - * @returns {Object} */ -function getNormalizedStatus(status) { - let normalizedStatus = status; +function getNormalizedStatus(typingStatus: Pusher.UserIsTypingEvent | Pusher.UserIsLeavingRoomEvent): ReportUserIsTyping { + let normalizedStatus: ReportUserIsTyping; - if (_.first(_.keys(status)) === 'userLogin') { - normalizedStatus = {[status.userLogin]: true}; + if (typingStatus.userLogin) { + normalizedStatus = {[typingStatus.userLogin]: true}; + } else { + normalizedStatus = typingStatus; } return normalizedStatus; } -/** - * Initialize our pusher subscriptions to listen for someone typing in a report. - * - * @param {String} reportID - */ -function subscribeToReportTypingEvents(reportID) { +/** Initialize our pusher subscriptions to listen for someone typing in a report. */ +function subscribeToReportTypingEvents(reportID: string) { if (!reportID) { return; } @@ -155,7 +162,7 @@ function subscribeToReportTypingEvents(reportID) { // login OR by 'Concierge'. If the pusher message comes from NewDot, it is keyed by accountID // since personal details are keyed by accountID. const normalizedTypingStatus = getNormalizedStatus(typingStatus); - const accountIDOrLogin = _.first(_.keys(normalizedTypingStatus)); + const accountIDOrLogin = Object.keys(normalizedTypingStatus)[0]; if (!accountIDOrLogin) { return; @@ -173,22 +180,18 @@ function subscribeToReportTypingEvents(reportID) { // Wait for 1.5s of no additional typing events before setting the status back to false. typingWatchTimers[reportUserIdentifier] = setTimeout(() => { - const typingStoppedStatus = {}; + const typingStoppedStatus: ReportUserIsTyping = {}; typingStoppedStatus[accountIDOrLogin] = false; Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`, typingStoppedStatus); delete typingWatchTimers[reportUserIdentifier]; }, 1500); }).catch((error) => { - Log.hmmm('[Report] Failed to initially subscribe to Pusher channel', false, {errorType: error.type, pusherChannelName}); + Log.hmmm('[Report] Failed to initially subscribe to Pusher channel', {errorType: error.type, pusherChannelName}); }); } -/** - * Initialize our pusher subscriptions to listen for someone leaving a room. - * - * @param {String} reportID - */ -function subscribeToReportLeavingEvents(reportID) { +/** Initialize our pusher subscriptions to listen for someone leaving a room. */ +function subscribeToReportLeavingEvents(reportID: string) { if (!reportID) { return; } @@ -197,12 +200,12 @@ function subscribeToReportLeavingEvents(reportID) { Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${reportID}`, false); const pusherChannelName = getReportChannelName(reportID); - Pusher.subscribe(pusherChannelName, Pusher.TYPE.USER_IS_LEAVING_ROOM, (leavingStatus) => { + Pusher.subscribe(pusherChannelName, Pusher.TYPE.USER_IS_LEAVING_ROOM, (leavingStatus: Pusher.UserIsLeavingRoomEvent) => { // If the pusher message comes from OldDot, we expect the leaving status to be keyed by user // login OR by 'Concierge'. If the pusher message comes from NewDot, it is keyed by accountID // since personal details are keyed by accountID. const normalizedLeavingStatus = getNormalizedStatus(leavingStatus); - const accountIDOrLogin = _.first(_.keys(normalizedLeavingStatus)); + const accountIDOrLogin = Object.keys(normalizedLeavingStatus)[0]; if (!accountIDOrLogin) { return; @@ -214,16 +217,14 @@ function subscribeToReportLeavingEvents(reportID) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${reportID}`, true); }).catch((error) => { - Log.hmmm('[Report] Failed to initially subscribe to Pusher channel', false, {errorType: error.type, pusherChannelName}); + Log.hmmm('[Report] Failed to initially subscribe to Pusher channel', {errorType: error.type, pusherChannelName}); }); } /** * Remove our pusher subscriptions to listen for someone typing in a report. - * - * @param {String} reportID */ -function unsubscribeFromReportChannel(reportID) { +function unsubscribeFromReportChannel(reportID: string) { if (!reportID) { return; } @@ -235,10 +236,8 @@ function unsubscribeFromReportChannel(reportID) { /** * Remove our pusher subscriptions to listen for someone leaving a report. - * - * @param {String} reportID */ -function unsubscribeFromLeavingRoomReportChannel(reportID) { +function unsubscribeFromLeavingRoomReportChannel(reportID: string) { if (!reportID) { return; } @@ -249,31 +248,23 @@ function unsubscribeFromLeavingRoomReportChannel(reportID) { } // New action subscriber array for report pages -let newActionSubscribers = []; +let newActionSubscribers: ActionSubscriber[] = []; /** * Enables the Report actions file to let the ReportActionsView know that a new comment has arrived in realtime for the current report * Add subscriber for report id - * @param {String} reportID - * @param {Function} callback - * @returns {Function} Remove subscriber for report id + * @returns Remove subscriber for report id */ -function subscribeToNewActionEvent(reportID, callback) { +function subscribeToNewActionEvent(reportID: string, callback: SubscriberCallback): () => void { newActionSubscribers.push({callback, reportID}); return () => { - newActionSubscribers = _.filter(newActionSubscribers, (subscriber) => subscriber.reportID !== reportID); + newActionSubscribers = newActionSubscribers.filter((subscriber) => subscriber.reportID !== reportID); }; } -/** - * Notify the ReportActionsView that a new comment has arrived - * - * @param {String} reportID - * @param {Number} accountID - * @param {String} reportActionID - */ -function notifyNewAction(reportID, accountID, reportActionID) { - const actionSubscriber = _.find(newActionSubscribers, (subscriber) => subscriber.reportID === reportID); +/** Notify the ReportActionsView that a new comment has arrived */ +function notifyNewAction(reportID: string, accountID?: number, reportActionID?: string) { + const actionSubscriber = newActionSubscribers.find((subscriber) => subscriber.reportID === reportID); if (!actionSubscriber) { return; } @@ -287,15 +278,11 @@ function notifyNewAction(reportID, accountID, reportActionID) { * - Adding one comment * - Adding one attachment * - Add both a comment and attachment simultaneously - * - * @param {String} reportID - * @param {String} [text] - * @param {Object} [file] */ -function addActions(reportID, text = '', file) { +function addActions(reportID: string, text = '', file?: File) { let reportCommentText = ''; - let reportCommentAction; - let attachmentAction; + let reportCommentAction: Partial | undefined; + let attachmentAction: Partial | undefined; let commandName = 'AddComment'; if (text) { @@ -313,43 +300,53 @@ function addActions(reportID, text = '', file) { } // Always prefer the file as the last action over text - const lastAction = attachmentAction || reportCommentAction; - + const lastAction = attachmentAction ?? reportCommentAction; const currentTime = DateUtils.getDBTime(); + const lastComment = lastAction?.message?.[0]; + const lastCommentText = ReportUtils.formatReportLastMessageText(lastComment?.text ?? ''); - const lastCommentText = ReportUtils.formatReportLastMessageText(lastAction.message[0].text); - - const optimisticReport = { + const optimisticReport: Partial = { lastVisibleActionCreated: currentTime, - lastMessageTranslationKey: lodashGet(lastAction, 'message[0].translationKey', ''), + lastMessageTranslationKey: lastComment?.translationKey ?? '', lastMessageText: lastCommentText, lastMessageHtml: lastCommentText, lastActorAccountID: currentUserAccountID, lastReadTime: currentTime, }; - if (ReportUtils.getReportNotificationPreference(ReportUtils.getReport(reportID)) === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) { + const report = ReportUtils.getReport(reportID); + + if (isNotEmptyObject(report) && ReportUtils.getReportNotificationPreference(report) === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) { optimisticReport.notificationPreference = CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS; } // Optimistically add the new actions to the store before waiting to save them to the server - const optimisticReportActions = {}; - if (text) { + const optimisticReportActions: OnyxCollection> = {}; + if (text && reportCommentAction?.reportActionID) { optimisticReportActions[reportCommentAction.reportActionID] = reportCommentAction; } - if (file) { + if (file && attachmentAction?.reportActionID) { optimisticReportActions[attachmentAction.reportActionID] = attachmentAction; } - const parameters = { + type AddCommentOrAttachementParameters = { + reportID: string; + reportActionID?: string; + commentReportActionID?: string | null; + reportComment?: string; + file?: File; + timezone?: string; + }; + + const parameters: AddCommentOrAttachementParameters = { reportID, - reportActionID: file ? attachmentAction.reportActionID : reportCommentAction.reportActionID, + reportActionID: file ? attachmentAction?.reportActionID : reportCommentAction?.reportActionID, commentReportActionID: file && reportCommentAction ? reportCommentAction.reportActionID : null, reportComment: reportCommentText, file, }; - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, @@ -362,15 +359,21 @@ function addActions(reportID, text = '', file) { }, ]; - const successData = [ + const successReportActions: OnyxCollection> = {}; + + Object.entries(optimisticReportActions).forEach(([actionKey]) => { + successReportActions[actionKey] = {pendingAction: null}; + }); + + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: _.mapObject(optimisticReportActions, () => ({pendingAction: null})), + value: successReportActions, }, ]; - let failureReport = { + let failureReport: Partial = { lastMessageTranslationKey: '', lastMessageText: '', lastVisibleActionCreated: '', @@ -378,8 +381,8 @@ function addActions(reportID, text = '', file) { const {lastMessageText = '', lastMessageTranslationKey = ''} = ReportActionsUtils.getLastVisibleMessage(reportID); if (lastMessageText || lastMessageTranslationKey) { const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID); - const lastVisibleActionCreated = lodashGet(lastVisibleAction, 'created'); - const lastActorAccountID = lodashGet(lastVisibleAction, 'actorAccountID'); + const lastVisibleActionCreated = lastVisibleAction?.created; + const lastActorAccountID = lastVisibleAction?.actorAccountID; failureReport = { lastMessageTranslationKey, lastMessageText, @@ -387,7 +390,17 @@ function addActions(reportID, text = '', file) { lastActorAccountID, }; } - const failureData = [ + + const failureReportActions: OnyxCollection> = {}; + + Object.entries(optimisticReportActions).forEach(([actionKey, action]) => { + failureReportActions[actionKey] = { + ...action, + errors: ErrorUtils.getMicroSecondOnyxError('report.genericAddCommentFailureMessage'), + }; + }); + + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, @@ -396,21 +409,18 @@ function addActions(reportID, text = '', file) { { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: _.mapObject(optimisticReportActions, (action) => ({ - ...action, - errors: ErrorUtils.getMicroSecondOnyxError('report.genericAddCommentFailureMessage'), - })), + value: failureReportActions, }, ]; // Update optimistic data for parent report action if the report is a child report const optimisticParentReportData = ReportUtils.getOptimisticDataForParentReportAction(reportID, currentTime, CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - if (!_.isEmpty(optimisticParentReportData)) { + if (isNotEmptyObject(optimisticParentReportData)) { optimisticData.push(optimisticParentReportData); } // Update the timezone if it's been 5 minutes from the last time the user added a comment - if (DateUtils.canUpdateTimezone()) { + if (DateUtils.canUpdateTimezone() && currentUserAccountID) { const timezone = DateUtils.getCurrentTimezone(); parameters.timezone = JSON.stringify(timezone); optimisticData.push({ @@ -426,62 +436,58 @@ function addActions(reportID, text = '', file) { successData, failureData, }); - notifyNewAction(reportID, lastAction.actorAccountID, lastAction.reportActionID); + notifyNewAction(reportID, lastAction?.actorAccountID, lastAction?.reportActionID); } -/** - * - * Add an attachment and optional comment. - * - * @param {String} reportID - * @param {File} file - * @param {String} [text] - */ -function addAttachment(reportID, file, text = '') { +/** Add an attachment and optional comment. */ +function addAttachment(reportID: string, file: File, text = '') { addActions(reportID, text, file); } -/** - * Add a single comment to a report - * - * @param {String} reportID - * @param {String} text - */ -function addComment(reportID, text) { +/** Add a single comment to a report */ +function addComment(reportID: string, text: string) { addActions(reportID, text); } -function reportActionsExist(reportID) { - return allReportActions[reportID] !== undefined; +function reportActionsExist(reportID: string): boolean { + return allReportActions?.[reportID] !== undefined; } /** * Gets the latest page of report actions and updates the last read message * If a chat with the passed reportID is not found, we will create a chat based on the passed participantList * - * @param {String} reportID - * @param {Array} participantLoginList The list of users that are included in a new chat, not including the user creating it - * @param {Object} newReportObject The optimistic report object created when making a new chat, saved as optimistic data - * @param {String} parentReportActionID The parent report action that a thread was created from (only passed for new threads) - * @param {Boolean} isFromDeepLink Whether or not this report is being opened from a deep link - * @param {Array} participantAccountIDList The list of accountIDs that are included in a new chat, not including the user creating it - */ -function openReport(reportID, participantLoginList = [], newReportObject = {}, parentReportActionID = '0', isFromDeepLink = false, participantAccountIDList = []) { + * @param participantLoginList The list of users that are included in a new chat, not including the user creating it + * @param newReportObject The optimistic report object created when making a new chat, saved as optimistic data + * @param parentReportActionID The parent report action that a thread was created from (only passed for new threads) + * @param isFromDeepLink Whether or not this report is being opened from a deep link + * @param participantAccountIDList The list of accountIDs that are included in a new chat, not including the user creating it + */ +function openReport( + reportID: string, + participantLoginList: string[] = [], + newReportObject: Partial = {}, + parentReportActionID = '0', + isFromDeepLink = false, + participantAccountIDList: number[] = [], +) { if (!reportID) { return; } + const optimisticReport = reportActionsExist(reportID) + ? {} + : { + reportName: allReports?.[reportID]?.reportName ?? CONST.REPORT.DEFAULT_REPORT_NAME, + }; + const commandName = 'OpenReport'; - const optimisticReportData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: reportActionsExist(reportID) - ? {} - : { - reportName: lodashGet(allReports, [reportID, 'reportName'], CONST.REPORT.DEFAULT_REPORT_NAME), - }, + value: optimisticReport, }, { onyxMethod: Onyx.METHOD.MERGE, @@ -494,7 +500,7 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p }, ]; - const reportSuccessData = [ + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, @@ -517,7 +523,7 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p }, ]; - const reportFailureData = [ + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, @@ -527,13 +533,18 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p }, ]; - const onyxData = { - optimisticData: optimisticReportData, - successData: reportSuccessData, - failureData: reportFailureData, + type OpenReportParameters = { + reportID: string; + emailList?: string; + accountIDList?: string; + parentReportActionID?: string; + shouldRetry?: boolean; + createdReportActionID?: string; + clientLastReadTime?: string; + idempotencyKey?: string; }; - const params = { + const parameters: OpenReportParameters = { reportID, emailList: participantLoginList ? participantLoginList.join(',') : '', accountIDList: participantAccountIDList ? participantAccountIDList.join(',') : '', @@ -542,23 +553,24 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p }; if (isFromDeepLink) { - params.shouldRetry = false; + parameters.shouldRetry = false; } + const report = ReportUtils.getReport(reportID); // If we open an exist report, but it is not present in Onyx yet, we should change the method to set for this report // and we need data to be available when we navigate to the chat page - if (_.isEmpty(ReportUtils.getReport(reportID))) { - onyxData.optimisticData[0].onyxMethod = Onyx.METHOD.SET; + if (isEmptyObject(report)) { + optimisticData[0].onyxMethod = Onyx.METHOD.SET; } // If we are creating a new report, we need to add the optimistic report data and a report action - if (!_.isEmpty(newReportObject)) { + if (isNotEmptyObject(newReportObject)) { // Change the method to set for new reports because it doesn't exist yet, is faster, // and we need the data to be available when we navigate to the chat page - onyxData.optimisticData[0].onyxMethod = Onyx.METHOD.SET; - onyxData.optimisticData[0].value = { + optimisticData[0].onyxMethod = Onyx.METHOD.SET; + optimisticData[0].value = { + ...optimisticReport, reportName: CONST.REPORT.DEFAULT_REPORT_NAME, - ...onyxData.optimisticData[0].value, ...newReportObject, pendingFields: { createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, @@ -566,28 +578,33 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p isOptimisticReport: true, }; - let emailCreatingAction = CONST.REPORT.OWNER_EMAIL_FAKE; + let emailCreatingAction: string = CONST.REPORT.OWNER_EMAIL_FAKE; if (newReportObject.ownerAccountID && newReportObject.ownerAccountID !== CONST.REPORT.OWNER_ACCOUNT_ID_FAKE) { - emailCreatingAction = lodashGet(allPersonalDetails, [newReportObject.ownerAccountID, 'login'], ''); + emailCreatingAction = allPersonalDetails?.[newReportObject.ownerAccountID]?.login ?? ''; } const optimisticCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(emailCreatingAction); - onyxData.optimisticData.push({ + optimisticData.push({ onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, value: {[optimisticCreatedAction.reportActionID]: optimisticCreatedAction}, }); - onyxData.successData.push({ + successData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, value: {[optimisticCreatedAction.reportActionID]: {pendingAction: null}}, }); // Add optimistic personal details for new participants - const optimisticPersonalDetails = {}; - const settledPersonalDetails = {}; - _.map(participantLoginList, (login, index) => { - const accountID = newReportObject.participantAccountIDs[index]; - optimisticPersonalDetails[accountID] = allPersonalDetails[accountID] || { + const optimisticPersonalDetails: OnyxCollection = {}; + const settledPersonalDetails: OnyxCollection = {}; + participantLoginList.forEach((login, index) => { + const accountID = newReportObject?.participantAccountIDs?.[index]; + + if (!accountID) { + return; + } + + optimisticPersonalDetails[accountID] = allPersonalDetails?.[accountID] ?? { login, accountID, avatar: UserUtils.getDefaultAvatarURL(accountID), @@ -595,37 +612,37 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p isOptimisticPersonalDetail: true, }; - settledPersonalDetails[accountID] = allPersonalDetails[accountID] || null; + settledPersonalDetails[accountID] = allPersonalDetails?.[accountID] ?? null; }); - onyxData.optimisticData.push({ + + optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PERSONAL_DETAILS_LIST, value: optimisticPersonalDetails, }); - - onyxData.successData.push({ + successData.push({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PERSONAL_DETAILS_LIST, value: settledPersonalDetails, }); - onyxData.failureData.push({ + failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PERSONAL_DETAILS_LIST, value: settledPersonalDetails, }); // Add the createdReportActionID parameter to the API call - params.createdReportActionID = optimisticCreatedAction.reportActionID; - params.idempotencyKey = `${params.idempotencyKey}_NewReport_${optimisticCreatedAction.reportActionID}`; + parameters.createdReportActionID = optimisticCreatedAction.reportActionID; + parameters.idempotencyKey = `${parameters.idempotencyKey}_NewReport_${optimisticCreatedAction.reportActionID}`; // If we are creating a thread, ensure the report action has childReportID property added if (newReportObject.parentReportID && parentReportActionID) { - onyxData.optimisticData.push({ + optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${newReportObject.parentReportID}`, value: {[parentReportActionID]: {childReportID: reportID, childType: CONST.REPORT.TYPE.CHAT}}, }); - onyxData.failureData.push({ + failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${newReportObject.parentReportID}`, value: {[parentReportActionID]: {childReportID: '0', childType: ''}}, @@ -633,27 +650,27 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p } } - params.clientLastReadTime = lodashGet(currentReportData, [reportID, 'lastReadTime'], ''); + parameters.clientLastReadTime = currentReportData?.[reportID]?.lastReadTime ?? ''; if (isFromDeepLink) { // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects(commandName, params, onyxData).finally(() => { + API.makeRequestWithSideEffects(commandName, parameters, {optimisticData, successData, failureData}).finally(() => { Onyx.set(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, false); }); } else { // eslint-disable-next-line rulesdir/no-multiple-api-calls - API.write(commandName, params, onyxData); + API.write(commandName, parameters, {optimisticData, successData, failureData}); } } /** * This will find an existing chat, or create a new one if none exists, for the given user or set of users. It will then navigate to this chat. * - * @param {Array} userLogins list of user logins to start a chat report with. - * @param {Boolean} shouldDismissModal a flag to determine if we should dismiss modal before navigate to report or navigate to report directly. + * @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, shouldDismissModal = true) { - let newChat = {}; +function navigateToAndOpenReport(userLogins: string[], shouldDismissModal = true) { + let newChat: ReportUtils.OptimisticChatReport | EmptyObject = {}; const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins(userLogins); const chat = ReportUtils.getChatByParticipants(participantAccountIDs); @@ -675,10 +692,10 @@ function navigateToAndOpenReport(userLogins, shouldDismissModal = true) { /** * This will find an existing chat, or create a new one if none exists, for the given accountID or set of accountIDs. It will then navigate to this chat. * - * @param {Array} participantAccountIDs of user logins to start a chat report with. + * @param participantAccountIDs of user logins to start a chat report with. */ -function navigateToAndOpenReportWithAccountIDs(participantAccountIDs) { - let newChat = {}; +function navigateToAndOpenReportWithAccountIDs(participantAccountIDs: number[]) { + let newChat: ReportUtils.OptimisticChatReport | EmptyObject = {}; const chat = ReportUtils.getChatByParticipants(participantAccountIDs); if (!chat) { newChat = ReportUtils.buildOptimisticChatReport(participantAccountIDs); @@ -693,23 +710,22 @@ function navigateToAndOpenReportWithAccountIDs(participantAccountIDs) { /** * This will navigate to an existing thread, or create a new one if necessary * - * @param {String} childReportID The reportID we are trying to open - * @param {Object} parentReportAction the parent comment of a thread - * @param {String} parentReportID The reportID of the parent - * + * @param childReportID The reportID we are trying to open + * @param parentReportAction the parent comment of a thread + * @param parentReportID The reportID of the parent */ -function navigateToAndOpenChildReport(childReportID = '0', parentReportAction = {}, parentReportID = '0') { +function navigateToAndOpenChildReport(childReportID = '0', parentReportAction: Partial = {}, parentReportID = '0') { if (childReportID !== '0') { openReport(childReportID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); } else { - const participantAccountIDs = _.uniq([currentUserAccountID, Number(parentReportAction.actorAccountID)]); - const parentReport = allReports[parentReportID]; + const participantAccountIDs = [...new Set([currentUserAccountID, Number(parentReportAction.actorAccountID)])]; + const parentReport = allReports?.[parentReportID]; const newChat = ReportUtils.buildOptimisticChatReport( participantAccountIDs, - lodashGet(parentReportAction, ['message', 0, 'text']), - lodashGet(parentReport, 'chatType', ''), - lodashGet(parentReport, 'policyID', CONST.POLICY.OWNER_EMAIL_FAKE), + parentReportAction?.message?.[0]?.text, + parentReport?.chatType, + parentReport?.policyID ?? CONST.POLICY.OWNER_EMAIL_FAKE, CONST.POLICY.OWNER_ACCOUNT_ID_FAKE, false, '', @@ -720,242 +736,246 @@ function navigateToAndOpenChildReport(childReportID = '0', parentReportAction = parentReportID, ); - const participantLogins = PersonalDetailsUtils.getLoginsByAccountIDs(newChat.participantAccountIDs); + const participantLogins = PersonalDetailsUtils.getLoginsByAccountIDs(newChat?.participantAccountIDs ?? []); openReport(newChat.reportID, participantLogins, newChat, parentReportAction.reportActionID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(newChat.reportID)); } } -/** - * Get the latest report history without marking the report as read. - * - * @param {String} reportID - */ -function reconnect(reportID) { - API.write( - 'ReconnectToReport', +/** Get the latest report history without marking the report as read. */ +function reconnect(reportID: string) { + const optimisticData: OnyxUpdate[] = [ { - reportID, + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + reportName: allReports?.[reportID]?.reportName ?? CONST.REPORT.DEFAULT_REPORT_NAME, + }, }, { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - reportName: lodashGet(allReports, [reportID, 'reportName'], CONST.REPORT.DEFAULT_REPORT_NAME), - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { - isLoadingInitialReportActions: true, - isLoadingNewerReportActions: false, - isLoadingOlderReportActions: false, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { - isLoadingInitialReportActions: false, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { - isLoadingInitialReportActions: false, - }, - }, - ], + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingInitialReportActions: true, + isLoadingNewerReportActions: false, + isLoadingOlderReportActions: false, + }, }, - ); + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingInitialReportActions: false, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingInitialReportActions: false, + }, + }, + ]; + + type ReconnectToReportParameters = { + reportID: string; + }; + + const parameters: ReconnectToReportParameters = { + reportID, + }; + + API.write('ReconnectToReport', parameters, {optimisticData, successData, failureData}); } /** * Gets the older actions that have not been read yet. * Normally happens when you scroll up on a chat, and the actions have not been read yet. - * - * @param {String} reportID - * @param {String} reportActionID */ -function getOlderActions(reportID, reportActionID) { - API.read( - 'GetOlderActions', +function getOlderActions(reportID: string, reportActionID: string) { + const optimisticData: OnyxUpdate[] = [ { - reportID, - reportActionID, + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingOlderReportActions: true, + }, }, + ]; + + const successData: OnyxUpdate[] = [ { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { - isLoadingOlderReportActions: true, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { - isLoadingOlderReportActions: false, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { - isLoadingOlderReportActions: false, - }, - }, - ], + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingOlderReportActions: false, + }, }, - ); + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingOlderReportActions: false, + }, + }, + ]; + + type GetOlderActionsParameters = { + reportID: string; + reportActionID: string; + }; + + const parameters: GetOlderActionsParameters = { + reportID, + reportActionID, + }; + + API.read('GetOlderActions', parameters, {optimisticData, successData, failureData}); } /** * Gets the newer actions that have not been read yet. * Normally happens when you are not located at the bottom of the list and scroll down on a chat. - * - * @param {String} reportID - * @param {String} reportActionID */ -function getNewerActions(reportID, reportActionID) { - API.read( - 'GetNewerActions', +function getNewerActions(reportID: string, reportActionID: string) { + const optimisticData: OnyxUpdate[] = [ { - reportID, - reportActionID, + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingNewerReportActions: true, + }, }, + ]; + + const successData: OnyxUpdate[] = [ { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { - isLoadingNewerReportActions: true, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { - isLoadingNewerReportActions: false, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { - isLoadingNewerReportActions: false, - }, - }, - ], + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingNewerReportActions: false, + }, }, - ); + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingNewerReportActions: false, + }, + }, + ]; + + type GetNewerActionsParameters = { + reportID: string; + reportActionID: string; + }; + + const parameters: GetNewerActionsParameters = { + reportID, + reportActionID, + }; + + API.read('GetNewerActions', parameters, {optimisticData, successData, failureData}); } /** * Gets metadata info about links in the provided report action - * - * @param {String} reportID - * @param {String} reportActionID */ -function expandURLPreview(reportID, reportActionID) { - API.read('ExpandURLPreview', { +function expandURLPreview(reportID: string, reportActionID: string) { + type ExpandURLPreviewParameters = { + reportID: string; + reportActionID: string; + }; + + const parameters: ExpandURLPreviewParameters = { reportID, reportActionID, - }); + }; + + API.read('ExpandURLPreview', parameters); } -/** - * Marks the new report actions as read - * - * @param {String} reportID - */ -function readNewestAction(reportID) { +/** Marks the new report actions as read */ +function readNewestAction(reportID: string) { const lastReadTime = DateUtils.getDBTime(); - API.write( - 'ReadNewestAction', - { - reportID, - lastReadTime, - }, + + const optimisticData: OnyxUpdate[] = [ { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - lastReadTime, - }, - }, - ], + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + lastReadTime, + }, }, - ); + ]; + + type ReadNewestActionParameters = { + reportID: string; + lastReadTime: string; + }; + + const parameters: ReadNewestActionParameters = { + reportID, + lastReadTime, + }; + + API.write('ReadNewestAction', parameters, {optimisticData}); } /** * Sets the last read time on a report - * - * @param {String} reportID - * @param {String} reportActionCreated */ -function markCommentAsUnread(reportID, reportActionCreated) { +function markCommentAsUnread(reportID: string, reportActionCreated: string) { // If no action created date is provided, use the last action's - const actionCreationTime = reportActionCreated || lodashGet(allReports, [reportID, 'lastVisibleActionCreated'], DateUtils.getDBTime(new Date(0))); + const actionCreationTime = reportActionCreated || (allReports?.[reportID]?.lastVisibleActionCreated ?? DateUtils.getDBTime(0)); // We subtract 1 millisecond so that the lastReadTime is updated to just before a given reportAction's created date // For example, if we want to mark a report action with ID 100 and created date '2014-04-01 16:07:02.999' unread, we set the lastReadTime to '2014-04-01 16:07:02.998' // Since the report action with ID 100 will be the first with a timestamp above '2014-04-01 16:07:02.998', it's the first one that will be shown as unread const lastReadTime = DateUtils.subtractMillisecondsFromDateTime(actionCreationTime, 1); - API.write( - 'MarkAsUnread', - { - reportID, - lastReadTime, - }, + + const optimisticData: OnyxUpdate[] = [ { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - lastReadTime, - }, - }, - ], + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + lastReadTime, + }, }, - ); + ]; + + type MarkAsUnreadParameters = { + reportID: string; + lastReadTime: string; + }; + + const parameters: MarkAsUnreadParameters = { + reportID, + lastReadTime, + }; + + API.write('MarkAsUnread', parameters, {optimisticData}); DeviceEventEmitter.emit(`unreadAction_${reportID}`, lastReadTime); } -/** - * Toggles the pinned state of the report. - * - * @param {Object} reportID - * @param {Boolean} isPinnedChat - */ -function togglePinnedState(reportID, isPinnedChat) { +/** Toggles the pinned state of the report. */ +function togglePinnedState(reportID: string, isPinnedChat: boolean) { const pinnedValue = !isPinnedChat; // Optimistically pin/unpin the report before we send out the command - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, @@ -963,76 +983,57 @@ function togglePinnedState(reportID, isPinnedChat) { }, ]; - API.write( - 'TogglePinnedChat', - { - reportID, - pinnedValue, - }, - {optimisticData}, - ); + type TogglePinnedChatParameters = { + reportID: string; + pinnedValue: boolean; + }; + + const parameters: TogglePinnedChatParameters = { + reportID, + pinnedValue, + }; + + API.write('TogglePinnedChat', parameters, {optimisticData}); } /** * Saves the comment left by the user as they are typing. By saving this data the user can switch between chats, close * tab, refresh etc without worrying about loosing what they typed out. - * - * @param {String} reportID - * @param {String} comment */ -function saveReportComment(reportID, comment) { +function saveReportComment(reportID: string, comment: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, comment); } -/** - * Saves the number of lines for the comment - * @param {String} reportID - * @param {Number} numberOfLines - */ -function saveReportCommentNumberOfLines(reportID, numberOfLines) { +/** Saves the number of lines for the comment */ +function saveReportCommentNumberOfLines(reportID: string, numberOfLines: number) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES}${reportID}`, numberOfLines); } -/** - * Immediate indication whether the report has a draft comment. - * - * @param {String} reportID - * @param {Boolean} hasDraft - * @returns {Promise} - */ -function setReportWithDraft(reportID, hasDraft) { +/** Immediate indication whether the report has a draft comment. */ +function setReportWithDraft(reportID: string, hasDraft: boolean): Promise { return Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {hasDraft}); } -/** - * Broadcasts whether or not a user is typing on a report over the report's private pusher channel. - * - * @param {String} reportID - */ -function broadcastUserIsTyping(reportID) { +/** Broadcasts whether or not a user is typing on a report over the report's private pusher channel. */ +function broadcastUserIsTyping(reportID: string) { const privateReportChannelName = getReportChannelName(reportID); - const typingStatus = {}; - typingStatus[currentUserAccountID] = true; + const typingStatus: Pusher.UserIsTypingEvent = { + [currentUserAccountID]: true, + }; Pusher.sendEvent(privateReportChannelName, Pusher.TYPE.USER_IS_TYPING, typingStatus); } -/** - * Broadcasts to the report's private pusher channel whether a user is leaving a report - * - * @param {String} reportID - */ -function broadcastUserIsLeavingRoom(reportID) { + +/** Broadcasts to the report's private pusher channel whether a user is leaving a report */ +function broadcastUserIsLeavingRoom(reportID: string) { const privateReportChannelName = getReportChannelName(reportID); - const leavingStatus = {}; - leavingStatus[currentUserAccountID] = true; + const leavingStatus: Pusher.UserIsLeavingRoomEvent = { + [currentUserAccountID]: true, + }; Pusher.sendEvent(privateReportChannelName, Pusher.TYPE.USER_IS_LEAVING_ROOM, leavingStatus); } -/** - * When a report changes in Onyx, this fetches the report from the API if the report doesn't have a name - * - * @param {Object} report - */ -function handleReportChanged(report) { +/** When a report changes in Onyx, this fetches the report from the API if the report doesn't have a name */ +function handleReportChanged(report: OnyxEntry) { if (!report) { return; } @@ -1040,7 +1041,7 @@ function handleReportChanged(report) { // It is possible that we optimistically created a DM/group-DM for a set of users for which a report already exists. // In this case, the API will let us know by returning a preexistingReportID. // We should clear out the optimistically created report and re-route the user to the preexisting report. - if (report && report.reportID && report.preexistingReportID) { + if (report?.reportID && report.preexistingReportID) { Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, null); // Only re-route them if they are still looking at the optimistically created report @@ -1051,7 +1052,7 @@ function handleReportChanged(report) { return; } - if (report && report.reportID) { + if (allReports && report?.reportID) { allReports[report.reportID] = report; if (ReportUtils.isConciergeChatReport(report)) { @@ -1071,16 +1072,16 @@ Onyx.connect({ callback: handleReportChanged, }); -/** - * Deletes a comment from the report, basically sets it as empty string - * - * @param {String} reportID - * @param {Object} reportAction - */ -function deleteReportComment(reportID, reportAction) { +/** Deletes a comment from the report, basically sets it as empty string */ +function deleteReportComment(reportID: string, reportAction: ReportAction) { const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); const reportActionID = reportAction.reportActionID; - const deletedMessage = [ + + if (!reportActionID || !originalReportID) { + return; + } + + const deletedMessage: Message[] = [ { translationKey: '', type: 'COMMENT', @@ -1090,7 +1091,7 @@ function deleteReportComment(reportID, reportAction) { isDeletedParentAction: ReportActionsUtils.isThreadParentMessage(reportAction, reportID), }, ]; - const optimisticReportActions = { + const optimisticReportActions: NullishDeep = { [reportActionID]: { pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, previousMessage: reportAction.message, @@ -1102,16 +1103,16 @@ function deleteReportComment(reportID, reportAction) { // If we are deleting the last visible message, let's find the previous visible one (or set an empty one if there are none) and update the lastMessageText in the LHN. // Similarly, if we are deleting the last read comment we will want to update the lastVisibleActionCreated to use the previous visible message. - let optimisticReport = { + let optimisticReport: Partial = { lastMessageTranslationKey: '', lastMessageText: '', lastVisibleActionCreated: '', }; - const {lastMessageText = '', lastMessageTranslationKey = ''} = ReportUtils.getLastVisibleMessage(originalReportID, optimisticReportActions); + const {lastMessageText = '', lastMessageTranslationKey = ''} = ReportUtils.getLastVisibleMessage(originalReportID, optimisticReportActions as ReportActions); if (lastMessageText || lastMessageTranslationKey) { - const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(originalReportID, optimisticReportActions); - const lastVisibleActionCreated = lodashGet(lastVisibleAction, 'created'); - const lastActorAccountID = lodashGet(lastVisibleAction, 'actorAccountID'); + const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(originalReportID, optimisticReportActions as ReportActions); + const lastVisibleActionCreated = lastVisibleAction?.created; + const lastActorAccountID = lastVisibleAction?.actorAccountID; optimisticReport = { lastMessageTranslationKey, lastMessageText, @@ -1122,7 +1123,7 @@ function deleteReportComment(reportID, reportAction) { // If the API call fails we must show the original message again, so we revert the message content back to how it was // and and remove the pendingAction so the strike-through clears - const failureData = [ + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, @@ -1136,7 +1137,7 @@ function deleteReportComment(reportID, reportAction) { }, ]; - const successData = [ + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, @@ -1149,7 +1150,7 @@ function deleteReportComment(reportID, reportAction) { }, ]; - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, @@ -1163,32 +1164,28 @@ function deleteReportComment(reportID, reportAction) { ]; // Update optimistic data for parent report action if the report is a child report and the reportAction has no visible child - const childVisibleActionCount = reportAction.childVisibleActionCount || 0; + const childVisibleActionCount = reportAction.childVisibleActionCount ?? 0; if (childVisibleActionCount === 0) { const optimisticParentReportData = ReportUtils.getOptimisticDataForParentReportAction( originalReportID, - optimisticReport.lastVisibleActionCreated, + optimisticReport?.lastVisibleActionCreated ?? '', CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); - if (!_.isEmpty(optimisticParentReportData)) { + if (isNotEmptyObject(optimisticParentReportData)) { optimisticData.push(optimisticParentReportData); } } - // Check to see if the report action we are deleting is the first comment on a thread report. In this case, we need to trigger - // an update to let the LHN know that the parentReportAction is now deleted. - if (ReportUtils.isThreadFirstChat(reportAction, reportID)) { - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: {updateReportInLHN: true}, - }); - } + type DeleteCommentParameters = { + reportID: string; + reportActionID: string; + }; - const parameters = { + const parameters: DeleteCommentParameters = { reportID: originalReportID, reportActionID, }; + API.write('DeleteComment', parameters, {optimisticData, successData, failureData}); } @@ -1198,29 +1195,24 @@ function deleteReportComment(reportID, reportAction) { * html="test https://www.google.com test" * links=["https://www.google.com"] * returns: "test https://www.google.com test" - * - * @param {String} html - * @param {Array} links - * @returns {String} */ -const removeLinksFromHtml = (html, links) => { +function removeLinksFromHtml(html: string, links: string[]): string { let htmlCopy = html.slice(); - _.forEach(links, (link) => { + links.forEach((link) => { // We want to match the anchor tag of the link and replace the whole anchor tag with the text of the anchor tag const regex = new RegExp(`<(a)[^><]*href\\s*=\\s*(['"])(${Str.escapeForRegExp(link)})\\2(?:".*?"|'.*?'|[^'"><])*>([\\s\\S]*?)<\\/\\1>(?![^<]*(<\\/pre>|<\\/code>))`, 'g'); htmlCopy = htmlCopy.replace(regex, '$4'); }); return htmlCopy; -}; +} /** * This function will handle removing only links that were purposely removed by the user while editing. * - * @param {String} newCommentText text of the comment after editing. - * @param {String} originalCommentMarkdown original markdown of the comment before editing. - * @returns {String} + * @param newCommentText text of the comment after editing. + * @param originalCommentMarkdown original markdown of the comment before editing. */ -const handleUserDeletedLinksInHtml = (newCommentText, originalCommentMarkdown) => { +function handleUserDeletedLinksInHtml(newCommentText: string, originalCommentMarkdown: string): string { const parser = new ExpensiMark(); if (newCommentText.length > CONST.MAX_MARKUP_LENGTH) { return newCommentText; @@ -1228,24 +1220,22 @@ const handleUserDeletedLinksInHtml = (newCommentText, originalCommentMarkdown) = const htmlForNewComment = parser.replace(newCommentText); const removedLinks = parser.getRemovedMarkdownLinks(originalCommentMarkdown, newCommentText); return removeLinksFromHtml(htmlForNewComment, removedLinks); -}; +} -/** - * Saves a new message for a comment. Marks the comment as edited, which will be reflected in the UI. - * - * @param {String} reportID - * @param {Object} originalReportAction - * @param {String} textForNewComment - */ -function editReportComment(reportID, originalReportAction, textForNewComment) { +/** Saves a new message for a comment. Marks the comment as edited, which will be reflected in the UI. */ +function editReportComment(reportID: string, originalReportAction: OnyxEntry, textForNewComment: string) { const parser = new ExpensiMark(); const originalReportID = ReportUtils.getOriginalReportID(reportID, originalReportAction); + if (!originalReportID || !originalReportAction) { + return; + } + // Do not autolink if someone explicitly tries to remove a link from message. // https://github.com/Expensify/App/issues/9090 // https://github.com/Expensify/App/issues/13221 - const originalCommentHTML = lodashGet(originalReportAction, 'message[0].html'); - const originalCommentMarkdown = parser.htmlToMarkdown(originalCommentHTML).trim(); + const originalCommentHTML = originalReportAction.message?.[0]?.html; + const originalCommentMarkdown = parser.htmlToMarkdown(originalCommentHTML ?? '').trim(); // Skip the Edit if draft is not changed if (originalCommentMarkdown === textForNewComment) { @@ -1259,12 +1249,12 @@ function editReportComment(reportID, originalReportAction, textForNewComment) { // For longer comments, skip parsing and display plaintext for performance reasons. It takes over 40s to parse a 100k long string!! let parsedOriginalCommentHTML = originalCommentHTML; if (textForNewComment.length <= CONST.MAX_MARKUP_LENGTH) { - const autolinkFilter = {filterRules: _.filter(_.pluck(parser.rules, 'name'), (name) => name !== 'autolink')}; + const autolinkFilter = {filterRules: parser.rules.map((rule) => rule.name).filter((name) => name !== 'autolink')}; parsedOriginalCommentHTML = parser.replace(originalCommentMarkdown, autolinkFilter); } // Delete the comment if it's empty - if (_.isEmpty(htmlForNewComment)) { + if (!htmlForNewComment) { deleteReportComment(originalReportID, originalReportAction); return; } @@ -1276,13 +1266,14 @@ function editReportComment(reportID, originalReportAction, textForNewComment) { // Optimistically update the reportAction with the new message const reportActionID = originalReportAction.reportActionID; - const originalMessage = lodashGet(originalReportAction, ['message', 0]); - const optimisticReportActions = { + const originalMessage = originalReportAction?.message?.[0]; + const optimisticReportActions: PartialDeep = { [reportActionID]: { pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, message: [ { ...originalMessage, + type: CONST.REPORT.MESSAGE.TYPE.COMMENT, isEdited: true, html: htmlForNewComment, text: reportComment, @@ -1291,7 +1282,7 @@ function editReportComment(reportID, originalReportAction, textForNewComment) { }, }; - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, @@ -1299,8 +1290,8 @@ function editReportComment(reportID, originalReportAction, textForNewComment) { }, ]; - const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(originalReportID, optimisticReportActions); - if (reportActionID === lodashGet(lastVisibleAction, 'reportActionID')) { + const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(originalReportID, optimisticReportActions as ReportActions); + if (reportActionID === lastVisibleAction?.reportActionID) { const lastMessageText = ReportUtils.formatReportLastMessageText(reportComment); const optimisticReport = { lastMessageTranslationKey: '', @@ -1313,7 +1304,7 @@ function editReportComment(reportID, originalReportAction, textForNewComment) { }); } - const failureData = [ + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, @@ -1326,7 +1317,7 @@ function editReportComment(reportID, originalReportAction, textForNewComment) { }, ]; - const successData = [ + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, @@ -1338,66 +1329,64 @@ function editReportComment(reportID, originalReportAction, textForNewComment) { }, ]; - const parameters = { + type UpdateCommentParameters = { + reportID: string; + reportComment: string; + reportActionID: string; + }; + + const parameters: UpdateCommentParameters = { reportID: originalReportID, reportComment: htmlForNewComment, reportActionID, }; + API.write('UpdateComment', parameters, {optimisticData, successData, failureData}); } -/** - * Saves the draft for a comment report action. This will put the comment into "edit mode" - * - * @param {String} reportID - * @param {Object} reportAction - * @param {String} draftMessage - */ -function saveReportActionDraft(reportID, reportAction, draftMessage) { +/** Saves the draft for a comment report action. This will put the comment into "edit mode" */ +function saveReportActionDraft(reportID: string, reportAction: ReportAction, draftMessage: string) { const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`, {[reportAction.reportActionID]: draftMessage}); } -/** - * Saves the number of lines for the report action draft - * @param {String} reportID - * @param {Number} reportActionID - * @param {Number} numberOfLines - */ -function saveReportActionDraftNumberOfLines(reportID, reportActionID, numberOfLines) { +/** Saves the number of lines for the report action draft */ +function saveReportActionDraftNumberOfLines(reportID: string, reportActionID: string, numberOfLines: number) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES}${reportID}_${reportActionID}`, numberOfLines); } -/** - * @param {String} reportID - * @param {String} previousValue - * @param {String} newValue - * @param {boolean} navigate - * @param {String} parentReportID - * @param {String} parentReportActionID - * @param {Object} report - */ -function updateNotificationPreference(reportID, previousValue, newValue, navigate, parentReportID = 0, parentReportActionID = 0, report = {}) { +function updateNotificationPreference( + reportID: string, + previousValue: NotificationPreference | undefined, + newValue: NotificationPreference, + navigate: boolean, + parentReportID?: string, + parentReportActionID?: string, + report: OnyxEntry | EmptyObject = {}, +) { if (previousValue === newValue) { - if (navigate && report.reportID) { + if (navigate && isNotEmptyObject(report) && report.reportID) { ReportUtils.goBackToDetailsPage(report); } return; } - const optimisticData = [ + + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: {notificationPreference: newValue}, }, ]; - const failureData = [ + + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: {notificationPreference: previousValue}, }, ]; + if (parentReportID && parentReportActionID) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -1410,8 +1399,16 @@ function updateNotificationPreference(reportID, previousValue, newValue, navigat value: {[parentReportActionID]: {childReportNotificationPreference: previousValue}}, }); } - API.write('UpdateReportNotificationPreference', {reportID, notificationPreference: newValue}, {optimisticData, failureData}); - if (navigate) { + + type UpdateReportNotificationPreferenceParameters = { + reportID: string; + notificationPreference: NotificationPreference; + }; + + const parameters: UpdateReportNotificationPreferenceParameters = {reportID, notificationPreference: newValue}; + + API.write('UpdateReportNotificationPreference', parameters, {optimisticData, failureData}); + if (navigate && isNotEmptyObject(report)) { ReportUtils.goBackToDetailsPage(report); } } @@ -1419,29 +1416,28 @@ function updateNotificationPreference(reportID, previousValue, newValue, navigat /** * This will subscribe to an existing thread, or create a new one and then subsribe to it if necessary * - * @param {String} childReportID The reportID we are trying to open - * @param {Object} parentReportAction the parent comment of a thread - * @param {String} parentReportID The reportID of the parent - * @param {String} prevNotificationPreference The previous notification preference for the child report - * + * @param childReportID The reportID we are trying to open + * @param parentReportAction the parent comment of a thread + * @param parentReportID The reportID of the parent + * @param prevNotificationPreference The previous notification preference for the child report */ -function toggleSubscribeToChildReport(childReportID = '0', parentReportAction = {}, parentReportID = '0', prevNotificationPreference) { +function toggleSubscribeToChildReport(childReportID = '0', parentReportAction: Partial = {}, parentReportID = '0', prevNotificationPreference?: NotificationPreference) { if (childReportID !== '0') { openReport(childReportID); - const parentReportActionID = lodashGet(parentReportAction, 'reportActionID', '0'); + const parentReportActionID = parentReportAction?.reportActionID ?? '0'; if (!prevNotificationPreference || prevNotificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) { updateNotificationPreference(childReportID, prevNotificationPreference, CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, false, parentReportID, parentReportActionID); } else { updateNotificationPreference(childReportID, prevNotificationPreference, CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, false, parentReportID, parentReportActionID); } } else { - const participantAccountIDs = _.uniq([currentUserAccountID, Number(parentReportAction.actorAccountID)]); - const parentReport = allReports[parentReportID]; + const participantAccountIDs = [...new Set([currentUserAccountID, Number(parentReportAction?.actorAccountID)])]; + const parentReport = allReports?.[parentReportID]; const newChat = ReportUtils.buildOptimisticChatReport( participantAccountIDs, - lodashGet(parentReportAction, ['message', 0, 'text']), - lodashGet(parentReport, 'chatType', ''), - lodashGet(parentReport, 'policyID', CONST.POLICY.OWNER_EMAIL_FAKE), + parentReportAction?.message?.[0]?.text, + parentReport?.chatType, + parentReport?.policyID ?? CONST.POLICY.OWNER_EMAIL_FAKE, CONST.POLICY.OWNER_ACCOUNT_ID_FAKE, false, '', @@ -1452,20 +1448,15 @@ function toggleSubscribeToChildReport(childReportID = '0', parentReportAction = parentReportID, ); - const participantLogins = PersonalDetailsUtils.getLoginsByAccountIDs(newChat.participantAccountIDs); + const participantLogins = PersonalDetailsUtils.getLoginsByAccountIDs(participantAccountIDs); openReport(newChat.reportID, participantLogins, newChat, parentReportAction.reportActionID); const notificationPreference = prevNotificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; - updateNotificationPreference(newChat.reportID, prevNotificationPreference, notificationPreference, false, parentReportID, parentReportAction.reportActionID); + updateNotificationPreference(newChat.reportID, prevNotificationPreference, notificationPreference, false, parentReportID, parentReportAction?.reportActionID); } } -/** - * @param {String} reportID - * @param {String} previousValue - * @param {String} newValue - */ -function updateWelcomeMessage(reportID, previousValue, newValue) { +function updateWelcomeMessage(reportID: string, previousValue: string, newValue: string) { // No change needed, navigate back if (previousValue === newValue) { Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(reportID)); @@ -1473,49 +1464,62 @@ function updateWelcomeMessage(reportID, previousValue, newValue) { } const parsedWelcomeMessage = ReportUtils.getParsedComment(newValue); - const optimisticData = [ + + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: {welcomeMessage: parsedWelcomeMessage}, }, ]; - const failureData = [ + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: {welcomeMessage: previousValue}, }, ]; - API.write('UpdateWelcomeMessage', {reportID, welcomeMessage: parsedWelcomeMessage}, {optimisticData, failureData}); + + type UpdateWelcomeMessageParameters = { + reportID: string; + welcomeMessage: string; + }; + + const parameters: UpdateWelcomeMessageParameters = {reportID, welcomeMessage: parsedWelcomeMessage}; + + API.write('UpdateWelcomeMessage', parameters, {optimisticData, failureData}); Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(reportID)); } -/** - * @param {Object} report - * @param {String} newValue - */ -function updateWriteCapabilityAndNavigate(report, newValue) { +function updateWriteCapabilityAndNavigate(report: Report, newValue: WriteCapability) { if (report.writeCapability === newValue) { Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report.reportID)); return; } - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, value: {writeCapability: newValue}, }, ]; - const failureData = [ + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, value: {writeCapability: report.writeCapability}, }, ]; - API.write('UpdateReportWriteCapability', {reportID: report.reportID, writeCapability: newValue}, {optimisticData, failureData}); + + type UpdateReportWriteCapabilityParameters = { + reportID: string; + writeCapability: WriteCapability; + }; + + const parameters: UpdateReportWriteCapabilityParameters = {reportID: report.reportID, writeCapability: newValue}; + + API.write('UpdateReportWriteCapability', parameters, {optimisticData, failureData}); // Return to the report settings page since this field utilizes push-to-page Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report.reportID)); } @@ -1523,7 +1527,7 @@ function updateWriteCapabilityAndNavigate(report, newValue) { /** * Navigates to the 1:1 report with Concierge * - * @param {Boolean} ignoreConciergeReportID - Flag to ignore conciergeChatReportID during navigation. The default behavior is to not ignore. + * @param ignoreConciergeReportID - Flag to ignore conciergeChatReportID during navigation. The default behavior is to not ignore. */ function navigateToConciergeChat(ignoreConciergeReportID = false) { // If conciergeChatReportID contains a concierge report ID, we navigate to the concierge chat using the stored report ID. @@ -1542,18 +1546,14 @@ function navigateToConciergeChat(ignoreConciergeReportID = false) { } } -/** - * Add a policy report (workspace room) optimistically and navigate to it. - * - * @param {Object} policyReport - */ -function addPolicyReport(policyReport) { +/** Add a policy report (workspace room) optimistically and navigate to it. */ +function addPolicyReport(policyReport: ReportUtils.OptimisticChatReport) { const createdReportAction = ReportUtils.buildOptimisticCreatedReportAction(CONST.POLICY.OWNER_EMAIL_FAKE); // Onyx.set is used on the optimistic data so that it is present before navigating to the workspace room. With Onyx.merge the workspace room reportID is not present when // fetchReportIfNeeded is called on the ReportScreen, so openReport is called which is unnecessary since the optimistic data will be stored in Onyx. // Therefore, Onyx.set is used instead of Onyx.merge. - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${policyReport.reportID}`, @@ -1575,7 +1575,7 @@ function addPolicyReport(policyReport) { value: {isLoading: true}, }, ]; - const successData = [ + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${policyReport.reportID}`, @@ -1600,7 +1600,7 @@ function addPolicyReport(policyReport) { value: {isLoading: false}, }, ]; - const failureData = [ + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${policyReport.reportID}`, @@ -1617,53 +1617,61 @@ function addPolicyReport(policyReport) { }, ]; - API.write( - 'AddWorkspaceRoom', - { - policyID: policyReport.policyID, - reportName: policyReport.reportName, - visibility: policyReport.visibility, - reportID: policyReport.reportID, - createdReportActionID: createdReportAction.reportActionID, - writeCapability: policyReport.writeCapability, - welcomeMessage: policyReport.welcomeMessage, - }, - {optimisticData, successData, failureData}, - ); + type AddWorkspaceRoomParameters = { + reportID: string; + createdReportActionID: string; + policyID?: string; + reportName?: string; + visibility?: ValueOf; + writeCapability?: WriteCapability; + welcomeMessage?: string; + }; + + const parameters: AddWorkspaceRoomParameters = { + policyID: policyReport.policyID, + reportName: policyReport.reportName, + visibility: policyReport.visibility, + reportID: policyReport.reportID, + createdReportActionID: createdReportAction.reportActionID, + writeCapability: policyReport.writeCapability, + welcomeMessage: policyReport.welcomeMessage, + }; + + API.write('AddWorkspaceRoom', parameters, {optimisticData, successData, failureData}); + Navigation.dismissModal(policyReport.reportID); } -/** - * Deletes a report, along with its reportActions, any linked reports, and any linked IOU report. - * - * @param {String} reportID - */ -function deleteReport(reportID) { - const report = allReports[reportID]; - const onyxData = { +/** Deletes a report, along with its reportActions, any linked reports, and any linked IOU report. */ +function deleteReport(reportID: string) { + const report = allReports?.[reportID]; + const onyxData: Record = { [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: null, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]: null, }; // Delete linked transactions - const reportActionsForReport = allReportActions[reportID]; - _.chain(reportActionsForReport) - .filter((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) - .map((reportAction) => reportAction.originalMessage.IOUTransactionID) - .uniq() - .each((transactionID) => (onyxData[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] = null)); + const reportActionsForReport = allReportActions?.[reportID]; + + const transactionIDs = Object.values(reportActionsForReport ?? {}) + .filter((reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) + .map((reportAction) => reportAction.originalMessage.IOUTransactionID); + + [...new Set(transactionIDs)].forEach((transactionID) => { + onyxData[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] = null; + }); Onyx.multiSet(onyxData); // Delete linked IOU report - if (report && report.iouReportID) { + if (report?.iouReportID) { deleteReport(report.iouReportID); } } /** - * @param {String} reportID The reportID of the policy report (workspace room) + * @param reportID The reportID of the policy report (workspace room) */ -function navigateToConciergeChatAndDeleteReport(reportID) { +function navigateToConciergeChatAndDeleteReport(reportID: string) { // Dismiss the current report screen and replace it with Concierge Chat Navigation.goBack(ROUTES.HOME); navigateToConciergeChat(); @@ -1671,12 +1679,9 @@ function navigateToConciergeChatAndDeleteReport(reportID) { } /** - * @param {Object} policyRoomReport - * @param {Number} policyRoomReport.reportID - * @param {String} policyRoomReport.reportName - * @param {String} policyRoomName The updated name for the policy room + * @param policyRoomName The updated name for the policy room */ -function updatePolicyRoomNameAndNavigate(policyRoomReport, policyRoomName) { +function updatePolicyRoomNameAndNavigate(policyRoomReport: Report, policyRoomName: string) { const reportID = policyRoomReport.reportID; const previousName = policyRoomReport.reportName; @@ -1685,7 +1690,8 @@ function updatePolicyRoomNameAndNavigate(policyRoomReport, policyRoomName) { Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(reportID)); return; } - const optimisticData = [ + + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, @@ -1700,7 +1706,7 @@ function updatePolicyRoomNameAndNavigate(policyRoomReport, policyRoomName) { }, }, ]; - const successData = [ + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, @@ -1711,7 +1717,7 @@ function updatePolicyRoomNameAndNavigate(policyRoomReport, policyRoomName) { }, }, ]; - const failureData = [ + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, @@ -1720,14 +1726,22 @@ function updatePolicyRoomNameAndNavigate(policyRoomReport, policyRoomName) { }, }, ]; - API.write('UpdatePolicyRoomName', {reportID, policyRoomName}, {optimisticData, successData, failureData}); + + type UpdatePolicyRoomNameParameters = { + reportID: string; + policyRoomName: string; + }; + + const parameters: UpdatePolicyRoomNameParameters = {reportID, policyRoomName}; + + API.write('UpdatePolicyRoomName', parameters, {optimisticData, successData, failureData}); Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(reportID)); } /** - * @param {String} reportID The reportID of the policy room. + * @param reportID The reportID of the policy room. */ -function clearPolicyRoomNameErrors(reportID) { +function clearPolicyRoomNameErrors(reportID: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { errorFields: { reportName: null, @@ -1738,21 +1752,15 @@ function clearPolicyRoomNameErrors(reportID) { }); } -/** - * @param {String} reportID - * @param {Boolean} isComposerFullSize - */ -function setIsComposerFullSize(reportID, isComposerFullSize) { +function setIsComposerFullSize(reportID: string, isComposerFullSize: boolean) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}`, isComposerFullSize); } /** - * @param {String} reportID - * @param {Object|null} action the associated report action (optional) - * @param {Boolean} isRemote whether or not this notification is a remote push notification - * @returns {Boolean} + * @param action the associated report action (optional) + * @param isRemote whether or not this notification is a remote push notification */ -function shouldShowReportActionNotification(reportID, action = null, isRemote = false) { +function shouldShowReportActionNotification(reportID: string, action: ReportAction | null = null, isRemote = false): boolean { const tag = isRemote ? '[PushNotification]' : '[LocalNotification]'; // Due to payload size constraints, some push notifications may have their report action stripped @@ -1768,7 +1776,7 @@ function shouldShowReportActionNotification(reportID, action = null, isRemote = } // We don't want to send a local notification if the user preference is daily, mute or hidden. - const notificationPreference = lodashGet(allReports, [reportID, 'notificationPreference'], CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS); + const notificationPreference = allReports?.[reportID]?.notificationPreference ?? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS; if (notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS) { Log.info(`${tag} No notification because user preference is to be notified: ${notificationPreference}`); return false; @@ -1786,40 +1794,36 @@ function shouldShowReportActionNotification(reportID, action = null, isRemote = return false; } - const report = allReports[reportID]; + const report = allReports?.[reportID]; if (!report || (report && report.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)) { Log.info(`${tag} No notification because the report does not exist or is pending deleted`, false); return false; } // If this notification was delayed and the user saw the message already, don't show it - if (action && report && report.lastReadTime >= action.created) { + if (action && report?.lastReadTime && report.lastReadTime >= action.created) { Log.info(`${tag} No notification because the comment was already read`, false, {created: action.created, lastReadTime: report.lastReadTime}); return false; } // Only show notifications for supported types of report actions if (!ReportActionsUtils.isNotifiableReportAction(action)) { - Log.info(`${tag} No notification because this action type is not supported`, false, {actionName: lodashGet(action, 'actionName')}); + Log.info(`${tag} No notification because this action type is not supported`, false, {actionName: action?.actionName}); return false; } return true; } -/** - * @param {String} reportID - * @param {Object} reportAction - */ -function showReportActionNotification(reportID, reportAction) { +function showReportActionNotification(reportID: string, reportAction: ReportAction) { if (!shouldShowReportActionNotification(reportID, reportAction)) { return; } Log.info('[LocalNotification] Creating notification'); - const report = allReports[reportID]; + const report = allReports?.[reportID] ?? null; - const notificationParams = { + const notificationParams: ReportCommentParams = { report, reportAction, onClick: () => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)), @@ -1833,29 +1837,18 @@ function showReportActionNotification(reportID, reportAction) { notifyNewAction(reportID, reportAction.actorAccountID, reportAction.reportActionID); } -/** - * Clear the errors associated with the IOUs of a given report. - * - * @param {String} reportID - */ -function clearIOUError(reportID) { +/** Clear the errors associated with the IOUs of a given report. */ +function clearIOUError(reportID: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {errorFields: {iou: null}}); } /** * Adds a reaction to the report action. * Uses the NEW FORMAT for "emojiReactions" - * @param {String} reportID - * @param {String} reportActionID - * @param {Object} emoji - * @param {String} emoji.name - * @param {String} emoji.code - * @param {String[]} [emoji.types] - * @param {Number} [skinTone] */ -function addEmojiReaction(reportID, reportActionID, emoji, skinTone = preferredSkinTone) { +function addEmojiReaction(reportID: string, reportActionID: string, emoji: Emoji, skinTone: string | number = preferredSkinTone) { const createdAt = timezoneFormat(utcToZonedTime(new Date(), 'UTC'), CONST.DATE.FNS_DB_FORMAT_STRING); - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`, @@ -1866,7 +1859,7 @@ function addEmojiReaction(reportID, reportActionID, emoji, skinTone = preferredS users: { [currentUserAccountID]: { skinTones: { - [!_.isUndefined(skinTone) ? skinTone : -1]: createdAt, + [skinTone ?? CONST.EMOJI_DEFAULT_SKIN_TONE]: createdAt, }, }, }, @@ -1875,7 +1868,7 @@ function addEmojiReaction(reportID, reportActionID, emoji, skinTone = preferredS }, ]; - const failureData = [ + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`, @@ -1887,7 +1880,7 @@ function addEmojiReaction(reportID, reportActionID, emoji, skinTone = preferredS }, ]; - const successData = [ + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`, @@ -1899,7 +1892,16 @@ function addEmojiReaction(reportID, reportActionID, emoji, skinTone = preferredS }, ]; - const parameters = { + type AddEmojiReactionParameters = { + reportID: string; + skinTone: string | number; + emojiCode: string; + reportActionID: string; + createdAt: string; + useEmojiReactions: boolean; + }; + + const parameters: AddEmojiReactionParameters = { reportID, skinTone, emojiCode: emoji.name, @@ -1908,21 +1910,16 @@ function addEmojiReaction(reportID, reportActionID, emoji, skinTone = preferredS // This will be removed as part of https://github.com/Expensify/App/issues/19535 useEmojiReactions: true, }; + API.write('AddEmojiReaction', parameters, {optimisticData, successData, failureData}); } /** * Removes a reaction to the report action. * Uses the NEW FORMAT for "emojiReactions" - * @param {String} reportID - * @param {String} reportActionID - * @param {Object} emoji - * @param {String} emoji.name - * @param {String} emoji.code - * @param {String[]} [emoji.types] */ -function removeEmojiReaction(reportID, reportActionID, emoji) { - const optimisticData = [ +function removeEmojiReaction(reportID: string, reportActionID: string, emoji: Emoji) { + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`, @@ -1936,37 +1933,51 @@ function removeEmojiReaction(reportID, reportActionID, emoji) { }, ]; - const parameters = { + type RemoveEmojiReactionParameters = { + reportID: string; + reportActionID: string; + emojiCode: string; + useEmojiReactions: boolean; + }; + + const parameters: RemoveEmojiReactionParameters = { reportID, reportActionID, emojiCode: emoji.name, // This will be removed as part of https://github.com/Expensify/App/issues/19535 useEmojiReactions: true, }; + API.write('RemoveEmojiReaction', parameters, {optimisticData}); } /** * Calls either addEmojiReaction or removeEmojiReaction depending on if the current user has reacted to the report action. * Uses the NEW FORMAT for "emojiReactions" - * @param {String} reportID - * @param {Object} reportAction - * @param {Object} reactionObject - * @param {Object} existingReactions - * @param {Number} [paramSkinTone] */ -function toggleEmojiReaction(reportID, reportAction, reactionObject, existingReactions, paramSkinTone = preferredSkinTone) { +function toggleEmojiReaction( + reportID: string, + reportAction: ReportAction, + reactionObject: Emoji, + existingReactions: ReportActionReactions | undefined, + paramSkinTone: number = preferredSkinTone, +) { const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); + + if (!originalReportID) { + return; + } + const originalReportAction = ReportActionsUtils.getReportAction(originalReportID, reportAction.reportActionID); - if (_.isEmpty(originalReportAction)) { + if (isEmptyObject(originalReportAction)) { return; } // This will get cleaned up as part of https://github.com/Expensify/App/issues/16506 once the old emoji // format is no longer being used const emoji = EmojiUtils.findEmojiByCode(reactionObject.code); - const existingReactionObject = lodashGet(existingReactions, [emoji.name]); + const existingReactionObject = existingReactions?.[emoji.name]; // Only use skin tone if emoji supports it const skinTone = emoji.types === undefined ? -1 : paramSkinTone; @@ -1979,11 +1990,7 @@ function toggleEmojiReaction(reportID, reportAction, reactionObject, existingRea addEmojiReaction(originalReportID, reportAction.reportActionID, emoji, skinTone); } -/** - * @param {String|null} url - * @param {Boolean} isAuthenticated - */ -function openReportFromDeepLink(url, isAuthenticated) { +function openReportFromDeepLink(url: string, isAuthenticated: boolean) { const reportID = ReportUtils.getReportIDFromLink(url); if (reportID && !isAuthenticated) { @@ -2020,25 +2027,23 @@ function openReportFromDeepLink(url, isAuthenticated) { return; } - Navigation.navigate(route, CONST.NAVIGATION.ACTION_TYPE.PUSH); + Navigation.navigate(route as Route, CONST.NAVIGATION.ACTION_TYPE.PUSH); }); }); }); } -function getCurrentUserAccountID() { +function getCurrentUserAccountID(): number { return currentUserAccountID; } -/** - * Leave a report by setting the state to submitted and closed - * - * @param {String} reportID - * @param {Boolean} isWorkspaceMemberLeavingWorkspaceRoom - */ -function leaveRoom(reportID, isWorkspaceMemberLeavingWorkspaceRoom = false) { - const report = lodashGet(allReports, [reportID], {}); - const reportKeys = _.keys(report); +/** Leave a report by setting the state to submitted and closed */ +function leaveRoom(reportID: string, isWorkspaceMemberLeavingWorkspaceRoom = false) { + const report = allReports?.[reportID]; + + if (!report) { + return; + } // Pusher's leavingStatus should be sent earlier. // Place the broadcast before calling the LeaveRoom API to prevent a race condition @@ -2047,7 +2052,7 @@ function leaveRoom(reportID, isWorkspaceMemberLeavingWorkspaceRoom = false) { // If a workspace member is leaving a workspace room, they don't actually lose the room from Onyx. // Instead, their notification preference just gets set to "hidden". - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ isWorkspaceMemberLeavingWorkspaceRoom ? { onyxMethod: Onyx.METHOD.MERGE, @@ -2060,6 +2065,7 @@ function leaveRoom(reportID, isWorkspaceMemberLeavingWorkspaceRoom = false) { onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { + reportID, stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS.CLOSED, chatType: report.chatType, @@ -2070,169 +2076,169 @@ function leaveRoom(reportID, isWorkspaceMemberLeavingWorkspaceRoom = false) { }, }, ]; - - const successData = [ + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: isWorkspaceMemberLeavingWorkspaceRoom ? {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN} : _.object(reportKeys, Array(reportKeys.length).fill(null)), + value: isWorkspaceMemberLeavingWorkspaceRoom + ? {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN} + : Object.keys(report).reduce>((acc, key) => { + acc[key] = null; + return acc; + }, {}), }, ]; - API.write( - 'LeaveRoom', - { - reportID, - }, + const failureData: OnyxUpdate[] = [ { - optimisticData, - successData, - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: report, - }, - ], + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: report, }, - ); + ]; + + type LeaveRoomParameters = { + reportID: string; + }; + + const parameters: LeaveRoomParameters = { + reportID, + }; + + API.write('LeaveRoom', parameters, {optimisticData, successData, failureData}); if (isWorkspaceMemberLeavingWorkspaceRoom) { const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins([CONST.EMAIL.CONCIERGE]); const chat = ReportUtils.getChatByParticipants(participantAccountIDs); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(chat.reportID)); + if (chat?.reportID) { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(chat.reportID)); + } } } -/** - * Invites people to a room - * - * @param {String} reportID - * @param {Object} inviteeEmailsToAccountIDs - */ -function inviteToRoom(reportID, inviteeEmailsToAccountIDs) { - const report = lodashGet(allReports, [reportID], {}); +/** Invites people to a room */ +function inviteToRoom(reportID: string, inviteeEmailsToAccountIDs: Record) { + const report = allReports?.[reportID]; + + if (!report) { + return; + } - const inviteeEmails = _.keys(inviteeEmailsToAccountIDs); - const inviteeAccountIDs = _.values(inviteeEmailsToAccountIDs); + const inviteeEmails = Object.keys(inviteeEmailsToAccountIDs); + const inviteeAccountIDs = Object.values(inviteeEmailsToAccountIDs); - const {participantAccountIDs} = report; - const participantAccountIDsAfterInvitation = _.uniq([...participantAccountIDs, ...inviteeAccountIDs]); + const participantAccountIDsAfterInvitation = [...new Set([...(report?.participantAccountIDs ?? []), ...inviteeAccountIDs])].filter( + (accountID): accountID is number => typeof accountID === 'number', + ); - API.write( - 'InviteToRoom', + const optimisticData: OnyxUpdate[] = [ { - reportID, - inviteeEmails, + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + participantAccountIDs: participantAccountIDsAfterInvitation, + }, }, + ]; + + const failureData: OnyxUpdate[] = [ { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - participantAccountIDs: participantAccountIDsAfterInvitation, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - participantAccountIDs, - }, - }, - ], + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + participantAccountIDs: report.participantAccountIDs, + }, }, - ); + ]; + + type InviteToRoomParameters = { + reportID: string; + inviteeEmails: string[]; + }; + + const parameters: InviteToRoomParameters = { + reportID, + inviteeEmails, + }; + + API.write('InviteToRoom', parameters, {optimisticData, failureData}); } -/** - * Removes people from a room - * - * @param {String} reportID - * @param {Array} targetAccountIDs - */ -function removeFromRoom(reportID, targetAccountIDs) { - const report = lodashGet(allReports, [reportID], {}); +/** Removes people from a room */ +function removeFromRoom(reportID: string, targetAccountIDs: number[]) { + const report = allReports?.[reportID]; - const {participantAccountIDs} = report; - const participantAccountIDsAfterRemoval = _.difference(participantAccountIDs, targetAccountIDs); + const participantAccountIDsAfterRemoval = report?.participantAccountIDs?.filter((id: number) => !targetAccountIDs.includes(id)); - API.write( - 'RemoveFromRoom', + const optimisticData: OnyxUpdate[] = [ { - reportID, - targetAccountIDs, + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + participantAccountIDs: participantAccountIDsAfterRemoval, + }, }, + ]; + + const failureData: OnyxUpdate[] = [ { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - participantAccountIDs: participantAccountIDsAfterRemoval, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - participantAccountIDs, - }, - }, - ], + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + participantAccountIDs: report?.participantAccountIDs, + }, + }, + ]; - // We need to add success data here since in high latency situations, - // the OpenRoomMembersPage call has the chance of overwriting the optimistic data we set above. - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - participantAccountIDs: participantAccountIDsAfterRemoval, - }, - }, - ], + // We need to add success data here since in high latency situations, + // the OpenRoomMembersPage call has the chance of overwriting the optimistic data we set above. + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + participantAccountIDs: participantAccountIDsAfterRemoval, + }, }, - ); + ]; + + type RemoveFromRoomParameters = { + reportID: string; + targetAccountIDs: number[]; + }; + + const parameters: RemoveFromRoomParameters = { + reportID, + targetAccountIDs, + }; + + API.write('RemoveFromRoom', parameters, {optimisticData, failureData, successData}); } -/** - * @param {String} reportID - */ -function setLastOpenedPublicRoom(reportID) { +function setLastOpenedPublicRoom(reportID: string) { Onyx.set(ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID, reportID); } -/** - * Navigates to the last opened public room - * - * @param {String} lastOpenedPublicRoomID - */ -function openLastOpenedPublicRoom(lastOpenedPublicRoomID) { +/** Navigates to the last opened public room */ +function openLastOpenedPublicRoom(lastOpenedPublicRoomID: string) { Navigation.isNavigationReady().then(() => { setLastOpenedPublicRoom(''); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(lastOpenedPublicRoomID)); }); } -/** - * Flag a comment as offensive - * - * @param {String} reportID - * @param {Object} reportAction - * @param {String} severity - */ -function flagComment(reportID, reportAction, severity) { +/** Flag a comment as offensive */ +function flagComment(reportID: string, reportAction: OnyxEntry, severity: string) { const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); - const message = reportAction.message[0]; - let updatedDecision; + const message = reportAction?.message?.[0]; + + if (!message) { + return; + } + + let updatedDecision: Decision; if (severity === CONST.MODERATION.FLAG_SEVERITY_SPAM || severity === CONST.MODERATION.FLAG_SEVERITY_INCONSIDERATE) { - if (!message.moderationDecision) { + if (!message?.moderationDecision) { updatedDecision = { decision: CONST.MODERATION.MODERATOR_DECISION_PENDING, }; @@ -2251,12 +2257,12 @@ function flagComment(reportID, reportAction, severity) { const reportActionID = reportAction.reportActionID; - const updatedMessage = { + const updatedMessage: Message = { ...message, moderationDecision: updatedDecision, }; - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, @@ -2269,7 +2275,7 @@ function flagComment(reportID, reportAction, severity) { }, ]; - const failureData = [ + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, @@ -2282,7 +2288,7 @@ function flagComment(reportID, reportAction, severity) { }, ]; - const successData = [ + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, @@ -2294,7 +2300,13 @@ function flagComment(reportID, reportAction, severity) { }, ]; - const parameters = { + type FlagCommentParameters = { + severity: string; + reportActionID: string; + isDevRequest: boolean; + }; + + const parameters: FlagCommentParameters = { severity, reportActionID, // This check is to prevent flooding Concierge with test flags @@ -2305,15 +2317,9 @@ function flagComment(reportID, reportAction, severity) { API.write('FlagComment', parameters, {optimisticData, successData, failureData}); } -/** - * Updates a given user's private notes on a report - * - * @param {String} reportID - * @param {Number} accountID - * @param {String} note - */ -const updatePrivateNotes = (reportID, accountID, note) => { - const optimisticData = [ +/** Updates a given user's private notes on a report */ +const updatePrivateNotes = (reportID: string, accountID: number, note: string) => { + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, @@ -2329,7 +2335,7 @@ const updatePrivateNotes = (reportID, accountID, note) => { }, ]; - const successData = [ + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, @@ -2344,7 +2350,7 @@ const updatePrivateNotes = (reportID, accountID, note) => { }, ]; - const failureData = [ + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, @@ -2358,152 +2364,134 @@ const updatePrivateNotes = (reportID, accountID, note) => { }, ]; - API.write( - 'UpdateReportPrivateNote', - { - reportID, - privateNotes: note, - }, - {optimisticData, successData, failureData}, - ); + type UpdateReportPrivateNoteParameters = { + reportID: string; + privateNotes: string; + }; + + const parameters: UpdateReportPrivateNoteParameters = {reportID, privateNotes: note}; + + API.write('UpdateReportPrivateNote', parameters, {optimisticData, successData, failureData}); }; -/** - * Fetches all the private notes for a given report - * - * @param {String} reportID - */ -function getReportPrivateNote(reportID) { - if (_.isEmpty(reportID)) { +/** Fetches all the private notes for a given report */ +function getReportPrivateNote(reportID: string) { + if (!reportID) { return; } - API.read( - 'GetReportPrivateNote', + + const optimisticData: OnyxUpdate[] = [ { - reportID, + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + isLoadingPrivateNotes: true, + }, }, + ]; + + const successData: OnyxUpdate[] = [ { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - isLoadingPrivateNotes: true, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - isLoadingPrivateNotes: false, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - isLoadingPrivateNotes: false, - }, - }, - ], + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + isLoadingPrivateNotes: false, + }, }, - ); + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + isLoadingPrivateNotes: false, + }, + }, + ]; + + type GetReportPrivateNoteParameters = { + reportID: string; + }; + + const parameters: GetReportPrivateNoteParameters = {reportID}; + + API.read('GetReportPrivateNote', parameters, {optimisticData, successData, failureData}); } -/** - * Loads necessary data for rendering the RoomMembersPage - * - * @param {String|Number} reportID - */ -function openRoomMembersPage(reportID) { - API.read('OpenRoomMembersPage', { - reportID, - }); +/** Loads necessary data for rendering the RoomMembersPage */ +function openRoomMembersPage(reportID: string) { + type OpenRoomMembersPageParameters = { + reportID: string; + }; + + const parameters: OpenRoomMembersPageParameters = {reportID}; + + API.read('OpenRoomMembersPage', parameters); } /** * Checks if there are any errors in the private notes for a given report * - * @param {Object} report - * @returns {Boolean} Returns true if there are errors in any of the private notes on the report + * @returns Returns true if there are errors in any of the private notes on the report */ -function hasErrorInPrivateNotes(report) { - const privateNotes = lodashGet(report, 'privateNotes', {}); - return _.some(privateNotes, (privateNote) => !_.isEmpty(privateNote.errors)); +function hasErrorInPrivateNotes(report: OnyxEntry): boolean { + const privateNotes = report?.privateNotes ?? {}; + return Object.values(privateNotes).some((privateNote) => !isEmpty(privateNote.errors)); } -/** - * Clears all errors associated with a given private note - * - * @param {String} reportID - * @param {Number} accountID - */ -function clearPrivateNotesError(reportID, accountID) { +/** Clears all errors associated with a given private note */ +function clearPrivateNotesError(reportID: string, accountID: number) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {privateNotes: {[accountID]: {errors: null}}}); } -function getDraftPrivateNote(reportID) { - return draftNoteMap[reportID] || ''; +function getDraftPrivateNote(reportID: string): string { + return draftNoteMap?.[reportID] ?? ''; } /** * Saves the private notes left by the user as they are typing. By saving this data the user can switch between chats, close * tab, refresh etc without worrying about loosing what they typed out. - * - * @param {String} reportID - * @param {String} note */ -function savePrivateNotesDraft(reportID, note) { +function savePrivateNotesDraft(reportID: string, note: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT}${reportID}`, note); } -/** - * @private - * @param {string} searchInput - */ -function searchForReports(searchInput) { +function searchForReports(searchInput: string) { // We do not try to make this request while offline because it sets a loading indicator optimistically if (isNetworkOffline) { Onyx.set(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, false); return; } - API.read( - 'SearchForReports', - {searchInput}, + const successData: OnyxUpdate[] = [ { - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, - value: false, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, - value: false, - }, - ], + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, + value: false, }, - ); + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, + value: false, + }, + ]; + + type SearchForReportsParameters = { + searchInput: string; + }; + + const parameters: SearchForReportsParameters = {searchInput}; + + API.read('SearchForReports', parameters, {successData, failureData}); } -/** - * @private - * @param {string} searchInput - */ const debouncedSearchInServer = lodashDebounce(searchForReports, CONST.TIMING.SEARCH_FOR_REPORTS_DEBOUNCE_TIME, {leading: false}); -/** - * @param {string} searchInput - */ -function searchInServer(searchInput) { +function searchInServer(searchInput: string) { if (isNetworkOffline || !searchInput.trim().length) { Onyx.set(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, false); return; diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js index e5037d250d2e..5ed44fb5d983 100644 --- a/src/libs/actions/Task.js +++ b/src/libs/actions/Task.js @@ -747,7 +747,6 @@ function cancelTask(taskReportID, taskTitle, originalStateNum, originalStatusNum lastVisibleActionCreated: optimisticCancelReportAction.created, lastMessageText: message, lastActorAccountID: optimisticCancelReportAction.actorAccountID, - updateReportInLHN: true, isDeletedParentAction: true, }, }, diff --git a/src/types/modules/pusher.d.ts b/src/types/modules/pusher.d.ts index fb7bbaa97f79..9705ec4935c8 100644 --- a/src/types/modules/pusher.d.ts +++ b/src/types/modules/pusher.d.ts @@ -8,8 +8,7 @@ declare global { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions interface File { - source?: string; - - uri?: string; + source: string; + uri: string; } } diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index f76fbd5ffd7d..fc7f8eb8ba31 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -45,6 +45,7 @@ type IOUMessage = { /** Only exists when we are sending money */ IOUDetails?: IOUDetails; }; + type OriginalMessageIOU = { actionName: typeof CONST.REPORT.ACTIONS.TYPE.IOU; originalMessage: IOUMessage; @@ -69,7 +70,7 @@ type DecisionName = ValueOf< >; type Decision = { decision: DecisionName; - timestamp: string; + timestamp?: string; }; type User = { @@ -105,6 +106,7 @@ type OriginalMessageAddComment = { reactions?: Reaction[]; }; }; + type OriginalMessageSubmitted = { actionName: typeof CONST.REPORT.ACTIONS.TYPE.SUBMITTED; originalMessage: unknown; @@ -117,7 +119,7 @@ type OriginalMessageClosed = { type OriginalMessageCreated = { actionName: typeof CONST.REPORT.ACTIONS.TYPE.CREATED; - originalMessage: unknown; + originalMessage?: unknown; }; type OriginalMessageRenamed = { @@ -183,7 +185,8 @@ type OriginalMessagePolicyTask = { | typeof CONST.REPORT.ACTIONS.TYPE.TASKEDITED | typeof CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED | typeof CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED - | typeof CONST.REPORT.ACTIONS.TYPE.TASKREOPENED; + | typeof CONST.REPORT.ACTIONS.TYPE.TASKREOPENED + | typeof CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE; originalMessage: unknown; }; @@ -231,4 +234,4 @@ type OriginalMessage = | OriginalMessageMoved; export default OriginalMessage; -export type {ChronosOOOEvent, Decision, Reaction, ActionName, IOUMessage, Closed, OriginalMessageActionName, ChangeLog}; +export type {ChronosOOOEvent, Decision, Reaction, ActionName, IOUMessage, Closed, OriginalMessageActionName, ChangeLog, OriginalMessageIOU, OriginalMessageCreated}; diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index af559eafd0a1..8f824272230e 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -76,6 +76,8 @@ type PersonalDetails = { payPalMeAddress?: string; }; +type PersonalDetailsList = Record; + export default PersonalDetails; -export type {Timezone, SelectedTimezone}; +export type {Timezone, SelectedTimezone, PersonalDetailsList}; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index a077bf1a3281..362df191ba6e 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -3,6 +3,16 @@ import CONST from '@src/CONST'; import * as OnyxCommon from './OnyxCommon'; import PersonalDetails from './PersonalDetails'; +type NotificationPreference = ValueOf; + +type WriteCapability = ValueOf; + +type Note = { + note: string; + errors?: OnyxCommon.Errors; + pendingAction?: OnyxCommon.PendingAction; +}; + type Report = { /** The specific type of chat */ chatType?: ValueOf; @@ -47,7 +57,7 @@ type Report = { lastMentionedTime?: string | null; /** The current user's notification preference for this report */ - notificationPreference?: string | number; + notificationPreference?: NotificationPreference; /** The policy name to use */ policyName?: string | null; @@ -89,7 +99,7 @@ type Report = { statusNum?: ValueOf; /** Which user role is capable of posting messages on the report */ - writeCapability?: ValueOf; + writeCapability?: WriteCapability; /** The report type */ type?: string; @@ -136,6 +146,9 @@ type Report = { /** Total amount of money owed for IOU report */ iouReportAmount?: number; + /** Is this action pending? */ + pendingAction?: OnyxCommon.PendingAction; + /** Pending fields for the report */ pendingFields?: Record; @@ -148,7 +161,10 @@ type Report = { isChatRoom?: boolean; participantsList?: Array>; text?: string; - privateNotes?: Record; + privateNotes?: Record; + isLoadingPrivateNotes?: boolean; }; export default Report; + +export type {NotificationPreference, WriteCapability}; diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index a0e90f4e9c34..8e56aaa67345 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -1,8 +1,10 @@ import {ValueOf} from 'type-fest'; import {AvatarSource} from '@libs/UserUtils'; import CONST from '@src/CONST'; +import {EmptyObject} from '@src/types/utils/EmptyObject'; import * as OnyxCommon from './OnyxCommon'; import OriginalMessage, {Decision, Reaction} from './OriginalMessage'; +import {NotificationPreference} from './Report'; import {Receipt} from './Transaction'; type Message = { @@ -52,6 +54,37 @@ type Message = { taskReportID?: string; }; +type ImageMetadata = { + /** The height of the image. */ + height?: number; + + /** The width of the image. */ + width?: number; + + /** The URL of the image. */ + url?: string; +}; + +type LinkMetadata = { + /** The URL of the link. */ + url?: string; + + /** A description of the link. */ + description?: string; + + /** The title of the link. */ + title?: string; + + /** The publisher of the link. */ + publisher?: string; + + /** The image associated with the link. */ + image?: ImageMetadata; + + /** The provider logo associated with the link. */ + logo?: ImageMetadata; +}; + type Person = { type?: string; style?: string; @@ -79,6 +112,9 @@ type ReportActionBase = { /** report action message */ message?: Message[]; + /** report action message */ + previousMessage?: Message[]; + /** Whether we have received a response back from the server */ isLoading?: boolean; @@ -121,8 +157,8 @@ type ReportActionBase = { childMoneyRequestCount?: number; isFirstItem?: boolean; - /** Information about attachments of report action */ - attachmentInfo?: (File & {source: string; uri: string}) | Record; + /** Informations about attachments of report action */ + attachmentInfo?: File | EmptyObject; /** Receipt tied to report action */ receipt?: Receipt; @@ -130,16 +166,28 @@ type ReportActionBase = { /** ISO-formatted datetime */ lastModified?: string; + /** Is this action pending? */ pendingAction?: OnyxCommon.PendingAction; delegateAccountID?: string; /** Server side errors keyed by microtime */ errors?: OnyxCommon.Errors; + /** Whether the report action is attachment */ isAttachment?: boolean; + + /** Recent receipt transaction IDs keyed by reportID */ childRecentReceiptTransactionIDs?: Record; + + /** ReportID of the report action */ reportID?: string; + /** Metadata of the link */ + linkMetadata?: LinkMetadata[]; + + /** The current user's notification preference for this report's child */ + childReportNotificationPreference?: NotificationPreference; + /** We manually add this field while sorting to detect the end of the list */ isNewestReportAction?: boolean; }; @@ -149,4 +197,4 @@ type ReportAction = ReportActionBase & OriginalMessage; type ReportActions = Record; export default ReportAction; -export type {Message, ReportActions}; +export type {ReportActions, ReportActionBase, Message}; diff --git a/src/types/onyx/ReportActionReactions.ts b/src/types/onyx/ReportActionReactions.ts index 196e2707bbd2..348a4b1baf62 100644 --- a/src/types/onyx/ReportActionReactions.ts +++ b/src/types/onyx/ReportActionReactions.ts @@ -1,15 +1,34 @@ -type User = { +import * as OnyxCommon from './OnyxCommon'; + +type UserReaction = { + /** ID of user reaction */ + id: string; + /** The skin tone which was used and also the timestamp of when it was added */ - skinTones: Record; + skinTones: Record; + + /** Oldest timestamp of when the emoji was added */ + oldestTimestamp: string; }; +type UsersReactions = Record; + type ReportActionReaction = { /** The time the emoji was added */ createdAt: string; + + /** Oldest timestamp of when the emoji was added */ + oldestTimestamp: string; + /** All the users who have added this emoji */ - users: Record; + users: UsersReactions; + + /** Is this action pending? */ + pendingAction?: OnyxCommon.PendingAction; }; type ReportActionReactions = Record; export default ReportActionReactions; + +export type {UsersReactions, ReportActionReaction}; diff --git a/src/types/onyx/ReportUserIsTyping.ts b/src/types/onyx/ReportUserIsTyping.ts new file mode 100644 index 000000000000..1e6f482ffa7a --- /dev/null +++ b/src/types/onyx/ReportUserIsTyping.ts @@ -0,0 +1,3 @@ +type ReportUserIsTyping = Record; + +export default ReportUserIsTyping; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index dcaa4ee3d623..7d18f4d70c8d 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -20,7 +20,7 @@ import Modal from './Modal'; import Network from './Network'; import {OnyxUpdateEvent, OnyxUpdatesFromServer} from './OnyxUpdatesFromServer'; import PersonalBankAccount from './PersonalBankAccount'; -import PersonalDetails from './PersonalDetails'; +import PersonalDetails, {PersonalDetailsList} from './PersonalDetails'; import PlaidData from './PlaidData'; import Policy from './Policy'; import PolicyCategory, {PolicyCategories} from './PolicyCategory'; @@ -37,6 +37,7 @@ import ReportAction, {ReportActions} from './ReportAction'; import ReportActionReactions from './ReportActionReactions'; import ReportActionsDrafts from './ReportActionsDrafts'; import ReportMetadata from './ReportMetadata'; +import ReportUserIsTyping from './ReportUserIsTyping'; import Request from './Request'; import Response from './Response'; import ScreenShareRequest from './ScreenShareRequest'; @@ -82,6 +83,7 @@ export type { OnyxUpdatesFromServer, PersonalBankAccount, PersonalDetails, + PersonalDetailsList, PlaidData, Policy, PolicyCategory, @@ -118,4 +120,5 @@ export type { WalletStatement, WalletTerms, WalletTransfer, + ReportUserIsTyping, };