From 51353d1a4affb0e92a9334aa15e4e23f49db2dd0 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Mon, 13 Oct 2025 16:05:51 +0500 Subject: [PATCH 01/14] Revert "[CP Staging] Revert "feat: allow users to change room avatar"" --- src/components/RoomHeaderAvatars.tsx | 36 +++++++++++++++++-- .../UpdatePolicyRoomAvatarParams.ts | 8 +++++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 ++ src/libs/ReportUtils.ts | 23 ++++++++++++ src/libs/actions/Report.ts | 30 +++++++++++++++- src/pages/ReportAvatar.tsx | 9 ++++- src/pages/ReportDetailsPage.tsx | 27 +++++++------- .../EnforceActionExportRestrictions.ts | 5 +++ tests/unit/ReportUtilsGetIconsTest.ts | 20 +++++++++++ 10 files changed, 141 insertions(+), 20 deletions(-) create mode 100644 src/libs/API/parameters/UpdatePolicyRoomAvatarParams.ts diff --git a/src/components/RoomHeaderAvatars.tsx b/src/components/RoomHeaderAvatars.tsx index e2c661526fb0..b5735b9172d1 100644 --- a/src/components/RoomHeaderAvatars.tsx +++ b/src/components/RoomHeaderAvatars.tsx @@ -2,24 +2,29 @@ import React, {memo} from 'react'; import {View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import {clearAvatarErrors, updatePolicyRoomAvatar} from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; +import {isUserCreatedPolicyRoom} from '@libs/ReportUtils'; +import {isDefaultAvatar} from '@libs/UserUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import type {Report} from '@src/types/onyx'; import type {Icon} from '@src/types/onyx/OnyxCommon'; import Avatar from './Avatar'; +import AvatarWithImagePicker from './AvatarWithImagePicker'; import * as Expensicons from './Icon/Expensicons'; import PressableWithoutFocus from './Pressable/PressableWithoutFocus'; import Text from './Text'; type RoomHeaderAvatarsProps = { icons: Icon[]; - reportID: string; + report: Report; }; -function RoomHeaderAvatars({icons, reportID}: RoomHeaderAvatarsProps) { +function RoomHeaderAvatars({icons, report}: RoomHeaderAvatarsProps) { const navigateToAvatarPage = (icon: Icon) => { if (icon.type === CONST.ICON_TYPE_WORKSPACE && icon.id) { - Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(reportID, icon.id.toString())); + Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(report?.reportID, icon.id.toString())); return; } @@ -30,6 +35,7 @@ function RoomHeaderAvatars({icons, reportID}: RoomHeaderAvatarsProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const isPolicyRoom = isUserCreatedPolicyRoom(report); if (!icons.length) { return null; @@ -42,6 +48,30 @@ function RoomHeaderAvatars({icons, reportID}: RoomHeaderAvatarsProps) { return; } + if (isPolicyRoom) { + return ( + Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(report.reportID))} + onImageRemoved={() => updatePolicyRoomAvatar(report.reportID)} + onImageSelected={(file) => updatePolicyRoomAvatar(report.reportID, file)} + editIcon={Expensicons.Camera} + editIconStyle={styles.smallEditIconAccount} + pendingAction={report.pendingFields?.avatar} + errors={report.errorFields?.avatar ?? null} + errorRowStyles={styles.mt6} + onErrorClose={() => clearAvatarErrors(report.reportID)} + style={[styles.w100, styles.mb3, styles.alignItemsStart, styles.sectionMenuItemTopDescription]} + type={icon.type} + editorMaskImage={Expensicons.ImageCropSquareMask} + /> + ); + } + return ( , policy: OnyxInputOrEntry): Icon[] { + if (!report) { + return []; + } + if (report?.avatarUrl) { + return [ + { + source: report.avatarUrl, + type: CONST.ICON_TYPE_WORKSPACE, + name: getReportName(report), + id: report?.policyID, + }, + ]; + } + return [getWorkspaceIcon(report, policy)]; +} + /** * Returns the appropriate icons for the given chat report using the stored personalDetails. * The Avatar sources can be URLs or Icon components according to the chat type. @@ -3503,6 +3523,9 @@ function getIcons( if (isDomainRoom(report)) { return getIconsForDomainRoom(report); } + if (isUserCreatedPolicyRoom(report)) { + return getIconsForUserCreatedPolicyRoom(report, policy); + } if (isAdminRoom(report) || isAnnounceRoom(report) || isChatRoom(report) || (isArchivedNonExpenseReport(report, isReportArchived) && !chatIncludesConcierge(report))) { return getIconsForPolicyRoom(report, personalDetails, policy, invoiceReceiverPolicy); } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index ddbae5c1d8b1..48d4ac585d2a 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -50,6 +50,7 @@ import type { UpdateCommentParams, UpdateGroupChatAvatarParams, UpdateGroupChatMemberRolesParams, + UpdatePolicyRoomAvatarParams, UpdatePolicyRoomNameParams, UpdateReportNotificationPreferenceParams, UpdateReportPrivateNoteParams, @@ -940,7 +941,15 @@ function updateChatName(reportID: string, reportName: string, type: typeof CONST API.write(command, parameters, {optimisticData, successData, failureData}); } -function updateGroupChatAvatar(reportID: string, file?: File | CustomRNImageManipulatorResult) { +/** + * Helper function to build optimistic, success, and failure Onyx updates + * when updating or removing a report's avatar. + * + * @param reportID - The report ID of the policy room. + * @param [file] - (Optional) The selected image file to update the avatar with. + * If not provided, the existing avatar will be removed. + */ +function buildUpdateReportAvatarOnyxData(reportID: string, file?: File | CustomRNImageManipulatorResult) { // If we have no file that means we are removing the avatar. const optimisticData: OnyxUpdate[] = [ { @@ -983,10 +992,28 @@ function updateGroupChatAvatar(reportID: string, file?: File | CustomRNImageMani }, }, ]; + + return {optimisticData, successData, failureData}; +} + +/** + * Updates the avatar for a group chat. + */ +function updateGroupChatAvatar(reportID: string, file?: File | CustomRNImageManipulatorResult) { + const {optimisticData, successData, failureData} = buildUpdateReportAvatarOnyxData(reportID, file); const parameters: UpdateGroupChatAvatarParams = {file, reportID}; API.write(WRITE_COMMANDS.UPDATE_GROUP_CHAT_AVATAR, parameters, {optimisticData, failureData, successData}); } +/** + * Updates the avatar for a policy room. + */ +function updatePolicyRoomAvatar(reportID: string, file?: File | CustomRNImageManipulatorResult) { + const {optimisticData, successData, failureData} = buildUpdateReportAvatarOnyxData(reportID, file); + const parameters: UpdatePolicyRoomAvatarParams = {reportID, file}; + API.write(WRITE_COMMANDS.UPDATE_POLICY_ROOM_AVATAR, parameters, {optimisticData, failureData, successData}); +} + /** * Clear error and pending fields for the report avatar */ @@ -6221,6 +6248,7 @@ export { unsubscribeFromReportChannel, updateDescription, updateGroupChatAvatar, + updatePolicyRoomAvatar, updateGroupChatMemberRoles, updateChatName, updateLastVisitTime, diff --git a/src/pages/ReportAvatar.tsx b/src/pages/ReportAvatar.tsx index f04c0d78e61d..a2637549a1c4 100644 --- a/src/pages/ReportAvatar.tsx +++ b/src/pages/ReportAvatar.tsx @@ -4,7 +4,7 @@ import useOnyx from '@hooks/useOnyx'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {AuthScreensParamList} from '@libs/Navigation/types'; -import {getDefaultGroupAvatar, getPolicyName, getReportName, getWorkspaceIcon, isGroupChat, isThread} from '@libs/ReportUtils'; +import {getDefaultGroupAvatar, getPolicyName, getReportName, getWorkspaceIcon, isGroupChat, isThread, isUserCreatedPolicyRoom} from '@libs/ReportUtils'; import {getFullSizeAvatar} from '@libs/UserUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -26,6 +26,13 @@ function ReportAvatar({route}: ReportAvatarProps) { isWorkspaceAvatar: false, }; } + if (isUserCreatedPolicyRoom(report)) { + return { + source: report?.avatarUrl ? getFullSizeAvatar(report.avatarUrl, 0) : undefined, + headerTitle: getReportName(report), + isWorkspaceAvatar: false, + }; + } return { source: getFullSizeAvatar(getWorkspaceIcon(report, policy).source, 0), diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 6da0de0b2663..e5beddf46721 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -19,6 +19,7 @@ import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeed import type {PromotedAction} from '@components/PromotedActionsBar'; import PromotedActionsBar, {PromotedActions} from '@components/PromotedActionsBar'; import ReportActionAvatars from '@components/ReportActionAvatars'; +import RoomHeaderAvatars from '@components/RoomHeaderAvatars'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import {useSearchContext} from '@components/Search/SearchContext'; @@ -581,6 +582,16 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail ) : null; const renderedAvatar = useMemo(() => { + if (isChatRoom && !isThread) { + return ( + + + + ); + } if (!isGroupChat || isThread) { return ( @@ -617,21 +628,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail style={[styles.w100, styles.mb3]} /> ); - }, [ - isGroupChat, - isThread, - icons, - report.avatarUrl, - report.pendingFields?.avatar, - report.errorFields?.avatar, - report.reportID, - styles.avatarXLarge, - styles.smallEditIconAccount, - styles.mt6, - styles.w100, - styles.mb3, - moneyRequestReport, - ]); + }, [isGroupChat, isThread, isChatRoom, icons, report, styles.avatarXLarge, styles.smallEditIconAccount, styles.mt6, styles.w100, styles.mb3, moneyRequestReport?.reportID]); const canJoin = canJoinChat(report, parentReportAction, policy, !!reportNameValuePairs?.private_isArchived); diff --git a/tests/actions/EnforceActionExportRestrictions.ts b/tests/actions/EnforceActionExportRestrictions.ts index be76cb9f5b02..5c467834715d 100644 --- a/tests/actions/EnforceActionExportRestrictions.ts +++ b/tests/actions/EnforceActionExportRestrictions.ts @@ -88,6 +88,11 @@ describe('ReportUtils', () => { expect(ReportUtils.getIconsForGroupChat).toBeUndefined(); }); + it('does not export getIconsForUserCreatedPolicyRoom', () => { + // @ts-expect-error the test is asserting that it's undefined, so the TS error is normal + expect(ReportUtils.getIconsForUserCreatedPolicyRoom).toBeUndefined(); + }); + it('does not export getAllReportActions', () => { // @ts-expect-error the test is asserting that it's undefined, so the TS error is normal expect(ReportUtils.getAllReportActions).toBeUndefined(); diff --git a/tests/unit/ReportUtilsGetIconsTest.ts b/tests/unit/ReportUtilsGetIconsTest.ts index 2b30796a0384..7441f8d04f80 100644 --- a/tests/unit/ReportUtilsGetIconsTest.ts +++ b/tests/unit/ReportUtilsGetIconsTest.ts @@ -21,6 +21,7 @@ import { isSelfDM, isTaskReport, isThread, + isUserCreatedPolicyRoom, isWorkspaceTaskReport, isWorkspaceThread, } from '@libs/ReportUtils'; @@ -215,6 +216,25 @@ describe('getIcons', () => { expect(icons.at(0)?.name).toBe('Workspace-Test-001'); }); + it('should return the correct icons for a user created policy room', () => { + const report: Report = { + ...LHNTestUtils.getFakeReport([], 0, true), + chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, + policyID: '1', + avatarUrl: 'https://example.com/avatar.png', + }; + const policy = LHNTestUtils.getFakePolicy('1'); + + // Verify report type conditions + expect(isUserCreatedPolicyRoom(report)).toBe(true); + expect(isChatReport(report)).toBe(true); + + const icons = getIcons(report, FAKE_PERSONAL_DETAILS, null, '', -1, policy); + expect(icons).toHaveLength(1); + expect(icons.at(0)?.type).toBe(CONST.ICON_TYPE_WORKSPACE); + expect(icons.at(0)?.source).toBe('https://example.com/avatar.png'); + }); + it('should return the correct icons for a policy expense chat', () => { const report: Report = { ...LHNTestUtils.getFakeReport([1], 0, true), From 7a4a421447fbd01740ca7e2a90de7cf87a18966b Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Mon, 13 Oct 2025 16:18:51 +0500 Subject: [PATCH 02/14] fix: prevent anonymous (not logged-in) users from editing room avatar --- src/components/RoomHeaderAvatars.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/RoomHeaderAvatars.tsx b/src/components/RoomHeaderAvatars.tsx index b5735b9172d1..d75ed1a08a37 100644 --- a/src/components/RoomHeaderAvatars.tsx +++ b/src/components/RoomHeaderAvatars.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {clearAvatarErrors, updatePolicyRoomAvatar} from '@libs/actions/Report'; +import {isAnonymousUser} from '@libs/actions/Session'; import Navigation from '@libs/Navigation/Navigation'; import {isUserCreatedPolicyRoom} from '@libs/ReportUtils'; import {isDefaultAvatar} from '@libs/UserUtils'; @@ -48,7 +49,7 @@ function RoomHeaderAvatars({icons, report}: RoomHeaderAvatarsProps) { return; } - if (isPolicyRoom) { + if (isPolicyRoom && !isAnonymousUser()) { return ( Date: Mon, 13 Oct 2025 20:19:41 +0500 Subject: [PATCH 03/14] fix: default WorkspaceAvatar to use first letter instead of WorkspaceBuilding --- src/components/AvatarButtonWithIcon.tsx | 5 +++++ src/components/AvatarWithImagePicker.tsx | 5 +++++ src/components/RoomHeaderAvatars.tsx | 3 ++- src/libs/ReportUtils.ts | 2 +- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/AvatarButtonWithIcon.tsx b/src/components/AvatarButtonWithIcon.tsx index d2800733d6ca..c99161469915 100644 --- a/src/components/AvatarButtonWithIcon.tsx +++ b/src/components/AvatarButtonWithIcon.tsx @@ -61,6 +61,9 @@ type AvatarButtonWithIconProps = { /** Optionally override the default "Edit" icon */ editIcon?: IconAsset; + + /** The name associated with avatar */ + name?: string; }; /** @@ -82,6 +85,7 @@ function AvatarButtonWithIcon({ disabled = false, editIcon = Expensicons.Pencil, anchorRef, + name = '', }: AvatarButtonWithIconProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -110,6 +114,7 @@ function AvatarButtonWithIcon({ fallbackIcon={fallbackIcon} size={size} type={type} + name={name} /> ) : ( diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index 4212aa602571..2b6621baa3aa 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -83,6 +83,9 @@ type AvatarWithImagePickerProps = Omit ); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4d868cc859f1..1114ecf5f8b8 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3479,7 +3479,7 @@ function getIconsForUserCreatedPolicyRoom(report: OnyxInputOrEntry, poli { source: report.avatarUrl, type: CONST.ICON_TYPE_WORKSPACE, - name: getReportName(report), + name: getPolicyName({report, policy}), id: report?.policyID, }, ]; From 90a6bc16663a6ec81df49579f0d30ebe59c952a7 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Mon, 13 Oct 2025 20:26:41 +0500 Subject: [PATCH 04/14] fix: pass policyName to display correct default Workspace avatar in WorkspaceOverviewPage --- src/pages/workspace/WorkspaceOverviewPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index 9363bf866006..713b1025d955 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -355,6 +355,7 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa source={policy?.avatarURL ?? ''} avatarID={policy?.id} size={CONST.AVATAR_SIZE.X_LARGE} + name={policyName} avatarStyle={styles.avatarXLarge} enablePreview DefaultAvatar={DefaultAvatar} From 67ee603e6a3a23d07c9226e031fb64548567fd98 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Thu, 23 Oct 2025 17:41:31 +0500 Subject: [PATCH 05/14] fix: change command name to UpdateRoomAvatar --- ...ePolicyRoomAvatarParams.ts => UpdateRoomAvatarParams.ts} | 4 ++-- src/libs/API/parameters/index.ts | 2 +- src/libs/API/types.ts | 4 ++-- src/libs/actions/Report.ts | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) rename src/libs/API/parameters/{UpdatePolicyRoomAvatarParams.ts => UpdateRoomAvatarParams.ts} (67%) diff --git a/src/libs/API/parameters/UpdatePolicyRoomAvatarParams.ts b/src/libs/API/parameters/UpdateRoomAvatarParams.ts similarity index 67% rename from src/libs/API/parameters/UpdatePolicyRoomAvatarParams.ts rename to src/libs/API/parameters/UpdateRoomAvatarParams.ts index 63c98f9819c3..fadd66b4d84c 100644 --- a/src/libs/API/parameters/UpdatePolicyRoomAvatarParams.ts +++ b/src/libs/API/parameters/UpdateRoomAvatarParams.ts @@ -1,8 +1,8 @@ import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; -type UpdatePolicyRoomAvatarParams = { +type UpdateRoomAvatarParams = { reportID: string; file: File | CustomRNImageManipulatorResult | undefined; }; -export default UpdatePolicyRoomAvatarParams; +export default UpdateRoomAvatarParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 84f1a07f4318..e1f7fb3a1c4d 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -104,7 +104,7 @@ export type {default as UpdateStatusParams} from './UpdateStatusParams'; export type {default as UpdateThemeParams} from './UpdateThemeParams'; export type {default as UpdateUserAvatarParams} from './UpdateUserAvatarParams'; export type {default as UpdateGroupChatAvatarParams} from './UpdateGroupChatAvatarParams'; -export type {default as UpdatePolicyRoomAvatarParams} from './UpdatePolicyRoomAvatarParams'; +export type {default as UpdateRoomAvatarParams} from './UpdateRoomAvatarParams'; export type {default as ValidateBankAccountWithTransactionsParams} from './ValidateBankAccountWithTransactionsParams'; export type {default as ValidateLoginParams} from './ValidateLoginParams'; export type {default as ValidateSecondaryLoginParams} from './ValidateSecondaryLoginParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index e0b3ce13af8c..e3ea06cb6cfa 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -71,7 +71,7 @@ const WRITE_COMMANDS = { UPDATE_SELECTED_TIMEZONE: 'UpdateSelectedTimezone', UPDATE_USER_AVATAR: 'UpdateUserAvatar', UPDATE_GROUP_CHAT_AVATAR: 'UpdateGroupChatAvatar', - UPDATE_POLICY_ROOM_AVATAR: 'UpdatePolicyRoomAvatar', + UPDATE_ROOM_AVATAR: 'UpdateRoomAvatar', DELETE_USER_AVATAR: 'DeleteUserAvatar', REFER_TEACHERS_UNITE_VOLUNTEER: 'ReferTeachersUniteVolunteer', ADD_SCHOOL_PRINCIPAL: 'AddSchoolPrincipal', @@ -633,7 +633,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.INVITE_TO_ROOM]: Parameters.InviteToRoomParams; [WRITE_COMMANDS.INVITE_TO_GROUP_CHAT]: Parameters.InviteToGroupChatParams; [WRITE_COMMANDS.UPDATE_GROUP_CHAT_AVATAR]: Parameters.UpdateGroupChatAvatarParams; - [WRITE_COMMANDS.UPDATE_POLICY_ROOM_AVATAR]: Parameters.UpdatePolicyRoomAvatarParams; + [WRITE_COMMANDS.UPDATE_ROOM_AVATAR]: Parameters.UpdateRoomAvatarParams; [WRITE_COMMANDS.PUSHER_PING]: Parameters.PusherPingParams; [WRITE_COMMANDS.LEAVE_GROUP_CHAT]: Parameters.LeaveGroupChatParams; [WRITE_COMMANDS.REMOVE_FROM_GROUP_CHAT]: Parameters.RemoveFromGroupChatParams; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index fabf5958c942..e1c795de0a72 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -50,11 +50,11 @@ import type { UpdateCommentParams, UpdateGroupChatAvatarParams, UpdateGroupChatMemberRolesParams, - UpdatePolicyRoomAvatarParams, UpdatePolicyRoomNameParams, UpdateReportNotificationPreferenceParams, UpdateReportPrivateNoteParams, UpdateReportWriteCapabilityParams, + UpdateRoomAvatarParams, UpdateRoomDescriptionParams, } from '@libs/API/parameters'; import type ExportReportCSVParams from '@libs/API/parameters/ExportReportCSVParams'; @@ -995,8 +995,8 @@ function updateGroupChatAvatar(reportID: string, file?: File | CustomRNImageMani */ function updatePolicyRoomAvatar(reportID: string, file?: File | CustomRNImageManipulatorResult) { const {optimisticData, successData, failureData} = buildUpdateReportAvatarOnyxData(reportID, file); - const parameters: UpdatePolicyRoomAvatarParams = {reportID, file}; - API.write(WRITE_COMMANDS.UPDATE_POLICY_ROOM_AVATAR, parameters, {optimisticData, failureData, successData}); + const parameters: UpdateRoomAvatarParams = {reportID, file}; + API.write(WRITE_COMMANDS.UPDATE_ROOM_AVATAR, parameters, {optimisticData, failureData, successData}); } /** From e5cf954d4661aa74662e7beee91b4590b7ddaf76 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Tue, 28 Oct 2025 17:48:07 +0500 Subject: [PATCH 06/14] update can edit room avatar permissions --- src/components/RoomHeaderAvatars.tsx | 22 ++++++++++++++++------ src/pages/ReportDetailsPage.tsx | 1 + 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/components/RoomHeaderAvatars.tsx b/src/components/RoomHeaderAvatars.tsx index d50b7dd4db9d..0b405417a138 100644 --- a/src/components/RoomHeaderAvatars.tsx +++ b/src/components/RoomHeaderAvatars.tsx @@ -1,15 +1,18 @@ import React, {memo} from 'react'; import {View} from 'react-native'; +import {OnyxEntry} from 'react-native-onyx'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import {clearAvatarErrors, updatePolicyRoomAvatar} from '@libs/actions/Report'; +import {clearAvatarErrors, getCurrentUserAccountID, getCurrentUserEmail, updatePolicyRoomAvatar} from '@libs/actions/Report'; import {isAnonymousUser} from '@libs/actions/Session'; import Navigation from '@libs/Navigation/Navigation'; -import {isUserCreatedPolicyRoom} from '@libs/ReportUtils'; +import {isPolicyMember} from '@libs/PolicyUtils'; +import {getParticipantsAccountIDsForDisplay, isAuditor, isReportParticipant, isUserCreatedPolicyRoom} from '@libs/ReportUtils'; import {isDefaultAvatar} from '@libs/UserUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import type {Report} from '@src/types/onyx'; +import type {Policy, Report} from '@src/types/onyx'; import type {Icon} from '@src/types/onyx/OnyxCommon'; import Avatar from './Avatar'; import AvatarWithImagePicker from './AvatarWithImagePicker'; @@ -20,9 +23,10 @@ import Text from './Text'; type RoomHeaderAvatarsProps = { icons: Icon[]; report: Report; + policy: OnyxEntry; }; -function RoomHeaderAvatars({icons, report}: RoomHeaderAvatarsProps) { +function RoomHeaderAvatars({icons, report, policy}: RoomHeaderAvatarsProps) { const navigateToAvatarPage = (icon: Icon) => { if (icon.type === CONST.ICON_TYPE_WORKSPACE && icon.id) { Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(report?.reportID, icon.id.toString())); @@ -36,7 +40,13 @@ function RoomHeaderAvatars({icons, report}: RoomHeaderAvatarsProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const isPolicyRoom = isUserCreatedPolicyRoom(report); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + + const canEditRoomAvatar = + isUserCreatedPolicyRoom(report) && + isPolicyMember(policy, currentUserPersonalDetails?.login) && + isReportParticipant(currentUserPersonalDetails?.accountID, report) && + !isAuditor(report); if (!icons.length) { return null; @@ -49,7 +59,7 @@ function RoomHeaderAvatars({icons, report}: RoomHeaderAvatarsProps) { return; } - if (isPolicyRoom && !isAnonymousUser()) { + if (canEditRoomAvatar) { return ( ); From be630fd738d8a2163610a630539e2c99c76b22d5 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Tue, 28 Oct 2025 18:04:29 +0500 Subject: [PATCH 07/14] lint --- src/components/RoomHeaderAvatars.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/RoomHeaderAvatars.tsx b/src/components/RoomHeaderAvatars.tsx index 0b405417a138..83eeb9b660dc 100644 --- a/src/components/RoomHeaderAvatars.tsx +++ b/src/components/RoomHeaderAvatars.tsx @@ -1,14 +1,13 @@ import React, {memo} from 'react'; import {View} from 'react-native'; -import {OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import {clearAvatarErrors, getCurrentUserAccountID, getCurrentUserEmail, updatePolicyRoomAvatar} from '@libs/actions/Report'; -import {isAnonymousUser} from '@libs/actions/Session'; +import {clearAvatarErrors, updatePolicyRoomAvatar} from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; import {isPolicyMember} from '@libs/PolicyUtils'; -import {getParticipantsAccountIDsForDisplay, isAuditor, isReportParticipant, isUserCreatedPolicyRoom} from '@libs/ReportUtils'; +import {isAuditor, isReportParticipant, isUserCreatedPolicyRoom} from '@libs/ReportUtils'; import {isDefaultAvatar} from '@libs/UserUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; From 4c5c737c322d66f1d54ea3577c87af3b38e3c8da Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Thu, 30 Oct 2025 01:03:16 +0500 Subject: [PATCH 08/14] fix merge conflict --- src/components/AvatarWithImagePicker.tsx | 139 ++++++++++------------- src/libs/ReportUtils.ts | 1 + 2 files changed, 62 insertions(+), 78 deletions(-) diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index 219c693aaec9..60914e47fdf1 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -70,9 +70,6 @@ type AvatarWithImagePickerProps = Omit - {({show}) => ( - - {({openPicker}) => { - const menuItems = createMenuItems(openPicker); - - // If the current avatar isn't a default avatar and we are not overriding this behavior allow the "View Photo" option - if (!shouldDisableViewPhoto && !isUsingDefaultAvatar) { - menuItems.push({ - icon: Expensicons.Eye, - text: translate('avatarWithImagePicker.viewPhoto'), - onSelected: () => { - if (typeof onViewPhotoPress !== 'function') { - show(); - return; - } - onViewPhotoPress(); - }, - shouldCallAfterModalHide: true, - }); - } - - return ( - <> - - onPressAvatar(openPicker)} - avatarStyle={avatarStyle} - pendingAction={pendingAction} - fallbackIcon={fallbackIcon} - anchorRef={anchorRef} - DefaultAvatar={DefaultAvatar} - editIcon={editIcon} - size={size} - type={type} - disabledStyle={disabledStyle} - editIconStyle={editIconStyle} - name={name} - /> - - setIsMenuVisible(false)} - onItemSelected={(item, index) => { - setIsMenuVisible(false); - // In order for the file picker to open dynamically, the click - // function must be called from within an event handler that was initiated - // by the user on Safari. - if (index === 0 && isSafari()) { - openPicker({ - onPicked: (data) => showAvatarCropModal(data.at(0) ?? {}), - }); - } - }} - menuItems={menuItems} - anchorAlignment={anchorAlignment} - anchorRef={anchorRef} - /> - - ); - }} - - )} - + {({openPicker}) => { + const menuItems = createMenuItems(openPicker); + + // If the current avatar isn't a default avatar and we are not overriding this behavior allow the "View Photo" option + if (onViewPhotoPress && !isUsingDefaultAvatar) { + menuItems.push({ + icon: Expensicons.Eye, + text: translate('avatarWithImagePicker.viewPhoto'), + onSelected: onViewPhotoPress, + shouldCallAfterModalHide: true, + }); + } + + return ( + <> + + onPressAvatar(openPicker)} + avatarStyle={avatarStyle} + pendingAction={pendingAction} + fallbackIcon={fallbackIcon} + anchorRef={anchorRef} + DefaultAvatar={DefaultAvatar} + editIcon={editIcon} + size={size} + type={type} + disabledStyle={disabledStyle} + editIconStyle={editIconStyle} + name={name} + /> + + setIsMenuVisible(false)} + onItemSelected={(item, index) => { + setIsMenuVisible(false); + // In order for the file picker to open dynamically, the click + // function must be called from within an event handler that was initiated + // by the user on Safari. + if (index === 0 && isSafari()) { + openPicker({ + onPicked: (data) => showAvatarCropModal(data.at(0) ?? {}), + }); + } + }} + menuItems={menuItems} + anchorAlignment={anchorAlignment} + anchorRef={anchorRef} + /> + + ); + }} + {!!errorData.validationError && ( { handlePreexistingReport(report); }); From 25e40b1b127c29c8d9df8bf0a9c459b9217b24ed Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Thu, 30 Oct 2025 17:55:58 +0500 Subject: [PATCH 09/14] feat: add room avatar updated log message in all locals --- src/languages/de.ts | 2 ++ src/languages/en.ts | 2 ++ src/languages/es.ts | 2 ++ src/languages/fr.ts | 2 ++ src/languages/it.ts | 2 ++ src/languages/ja.ts | 2 ++ src/languages/nl.ts | 2 ++ src/languages/pl.ts | 2 ++ src/languages/pt-BR.ts | 2 ++ src/languages/zh-hans.ts | 2 ++ 10 files changed, 20 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index 2bd3f0d26878..0203e2e9b1d4 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7145,6 +7145,8 @@ ${amount} für ${merchant} - ${date}`, roomChangeLog: { updateRoomDescription: 'setze die Raumbeschreibung auf:', clearRoomDescription: 'Raumbeschreibung gelöscht', + changedRoomAvatar: 'Hat das Raum-Avatar geändert', + removedRoomAvatar: 'Hat das Raum-Avatar entfernt', }, delegate: { switchAccount: 'Konten wechseln:', diff --git a/src/languages/en.ts b/src/languages/en.ts index 445f97f58ef7..e6adc7b7c1ac 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7094,6 +7094,8 @@ const translations = { roomChangeLog: { updateRoomDescription: 'set the room description to:', clearRoomDescription: 'cleared the room description', + changedRoomAvatar: 'changed the room avatar', + removedRoomAvatar: 'removed the room avatar', }, delegate: { switchAccount: 'Switch accounts:', diff --git a/src/languages/es.ts b/src/languages/es.ts index 05f168b0369f..0a68a98750b4 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7589,6 +7589,8 @@ ${amount} para ${merchant} - ${date}`, roomChangeLog: { updateRoomDescription: 'establece la descripción de la sala a:', clearRoomDescription: 'la descripción de la habitación ha sido borrada', + changedRoomAvatar: 'Cambió el avatar de la sala', + removedRoomAvatar: 'Eliminó el avatar de la sala', }, delegate: { switchAccount: 'Cambiar de cuenta:', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 41041203c045..b4b9cf92229b 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7148,6 +7148,8 @@ ${amount} pour ${merchant} - ${date}`, roomChangeLog: { updateRoomDescription: 'définir la description de la salle sur :', clearRoomDescription: 'effacé la description de la salle', + changedRoomAvatar: 'A changé l’avatar de la salle', + removedRoomAvatar: 'A supprimé l’avatar de la salle', }, delegate: { switchAccount: 'Changer de compte :', diff --git a/src/languages/it.ts b/src/languages/it.ts index 947068e6ac30..2b0b16753ae8 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7153,6 +7153,8 @@ ${amount} per ${merchant} - ${date}`, roomChangeLog: { updateRoomDescription: 'imposta la descrizione della stanza su:', clearRoomDescription: 'cancellato la descrizione della stanza', + changedRoomAvatar: "Ha cambiato l'avatar della stanza", + removedRoomAvatar: "Ha rimosso l'avatar della stanza", }, delegate: { switchAccount: 'Cambia account:', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index b21529a4a43d..bc5dd4aa22db 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7082,6 +7082,8 @@ ${date} - ${merchant}に${amount}`, roomChangeLog: { updateRoomDescription: '部屋の説明を次のように設定します:', clearRoomDescription: '部屋の説明をクリアしました', + changedRoomAvatar: 'ルームのアバターを変更しました', + removedRoomAvatar: 'ルームのアバターを削除しました', }, delegate: { switchAccount: 'アカウントを切り替える:', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 04dbe1ef57e8..fefb5971ee2b 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7135,6 +7135,8 @@ ${amount} voor ${merchant} - ${date}`, roomChangeLog: { updateRoomDescription: 'stel de kamerbeschrijving in op:', clearRoomDescription: 'de kamerbeschrijving gewist', + changedRoomAvatar: 'De avatar van de kamer is gewijzigd', + removedRoomAvatar: 'De avatar van de kamer is verwijderd', }, delegate: { switchAccount: 'Accounts wisselen:', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 9699057292ec..d92b7223ae1e 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7123,6 +7123,8 @@ ${amount} dla ${merchant} - ${date}`, roomChangeLog: { updateRoomDescription: 'ustaw opis pokoju na:', clearRoomDescription: 'wyczyszczono opis pokoju', + changedRoomAvatar: 'Zmieniono awatar pokoju', + removedRoomAvatar: 'Usunięto awatar pokoju', }, delegate: { switchAccount: 'Przełącz konta:', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 7050ce68dc56..602e893bd8ef 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7130,6 +7130,8 @@ ${amount} para ${merchant} - ${date}`, roomChangeLog: { updateRoomDescription: 'defina a descrição da sala para:', clearRoomDescription: 'limpou a descrição da sala', + changedRoomAvatar: 'Alterou o avatar da sala', + removedRoomAvatar: 'Removeu o avatar da sala', }, delegate: { switchAccount: 'Alternar contas:', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 2db0379d1000..8af25e6010ef 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6985,6 +6985,8 @@ ${merchant}的${amount} - ${date}`, roomChangeLog: { updateRoomDescription: '将房间描述设置为:', clearRoomDescription: '清除了房间描述', + changedRoomAvatar: '更改了房间头像', + removedRoomAvatar: '移除了房间头像', }, delegate: { switchAccount: '切换账户:', From 9167f5ed25b360808b974977e0c014f7ee15f840 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Thu, 30 Oct 2025 18:01:55 +0500 Subject: [PATCH 10/14] add getRoomAvatarUpdatedMessage util to get the message accordingly --- src/CONST/index.ts | 1 + .../API/parameters/UpdateRoomAvatarParams.ts | 1 + src/libs/ReportActionsUtils.ts | 17 +++++++++++++++++ src/types/onyx/OriginalMessage.ts | 3 +++ 4 files changed, 22 insertions(+) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index cf0c6368090a..86f634120491 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1335,6 +1335,7 @@ const CONST = { REMOVE_FROM_ROOM: 'REMOVEFROMROOM', LEAVE_ROOM: 'LEAVEROOM', UPDATE_ROOM_DESCRIPTION: 'UPDATEROOMDESCRIPTION', + UPDATE_ROOM_AVATAR: 'UPDATEROOMAVATAR', }, REJECTEDTRANSACTION_THREAD: 'REJECTEDTRANSACTION_THREAD', REJECTED_TRANSACTION_MARKASRESOLVED: 'REJECTEDTRANSACTIONMARKASRESOLVED', diff --git a/src/libs/API/parameters/UpdateRoomAvatarParams.ts b/src/libs/API/parameters/UpdateRoomAvatarParams.ts index fadd66b4d84c..889d13e3d80d 100644 --- a/src/libs/API/parameters/UpdateRoomAvatarParams.ts +++ b/src/libs/API/parameters/UpdateRoomAvatarParams.ts @@ -3,6 +3,7 @@ import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types type UpdateRoomAvatarParams = { reportID: string; file: File | CustomRNImageManipulatorResult | undefined; + reportActionID: string; }; export default UpdateRoomAvatarParams; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index e82026942aee..8fc56d03e7d1 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1905,6 +1905,11 @@ function getReportActionMessageFragments(action: ReportAction): Message[] { return [{text: message, html: `${message}`, type: 'COMMENT'}]; } + if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.UPDATE_ROOM_AVATAR)) { + const message = getRoomAvatarUpdatedMessage(action); + return [{text: message, html: `${message}`, type: 'COMMENT'}]; + } + if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DESCRIPTION)) { const message = getWorkspaceDescriptionUpdatedMessage(action); return [{text: message, html: `${message}`, type: 'COMMENT'}]; @@ -2262,6 +2267,17 @@ function getUpdateRoomDescriptionMessage(reportAction: ReportAction): string { return translateLocal('roomChangeLog.clearRoomDescription'); } +function getRoomAvatarUpdatedMessage(reportAction: ReportAction): string { + const originalMessage = getOriginalMessage(reportAction) as OriginalMessageChangeLog; + if (originalMessage?.avatarUrl) { + // eslint-disable-next-line @typescript-eslint/no-deprecated + return translateLocal('roomChangeLog.changedRoomAvatar'); + } + + // eslint-disable-next-line @typescript-eslint/no-deprecated + return translateLocal('roomChangeLog.removedRoomAvatar'); +} + function getRetractedMessage(): string { // eslint-disable-next-line @typescript-eslint/no-deprecated return translateLocal('iou.retracted'); @@ -3406,6 +3422,7 @@ export { getExportIntegrationLastMessageText, getExportIntegrationMessageHTML, getUpdateRoomDescriptionMessage, + getRoomAvatarUpdatedMessage, didMessageMentionCurrentUser, getPolicyChangeLogAddEmployeeMessage, getPolicyChangeLogUpdateEmployee, diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 8751b4c420d5..5b0a4ff326e3 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -367,6 +367,9 @@ type OriginalMessageChangeLog = { /** Name of the added category */ categoryName?: string; + + /** Avatar URL of workspace room */ + avatarUrl?: string; }; /** Model of change log */ From 4fc58be0c892de680c70665963816a0034de0ece Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Thu, 30 Oct 2025 18:07:49 +0500 Subject: [PATCH 11/14] build optimistic room avatar updated report action --- src/libs/ReportUtils.ts | 38 ++++++++++++++++++++++++++++++++++++++ src/libs/actions/Report.ts | 32 +++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 918f17295e2d..3b299c8ecf28 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -570,6 +570,11 @@ type OptimisticRoomDescriptionUpdatedReportAction = Pick< 'actorAccountID' | 'created' | 'message' | 'person' | 'reportActionID' | 'pendingAction' | 'actionName' | 'originalMessage' >; +type OptimisticRoomAvatarUpdatedReportAction = Pick< + ReportAction, + 'actorAccountID' | 'created' | 'message' | 'person' | 'reportActionID' | 'pendingAction' | 'actionName' | 'originalMessage' +>; + type OptimisticChatReport = Pick< Report, | 'type' @@ -7709,6 +7714,38 @@ function buildOptimisticRoomDescriptionUpdatedReportAction(description: string): }; } +/** + * Returns the necessary reportAction onyx data to indicate that the room avatar has been updated + */ +function buildOptimisticRoomAvatarUpdatedReportAction(avatarUrl: string): OptimisticRoomAvatarUpdatedReportAction { + const now = DateUtils.getDBTime(); + return { + reportActionID: rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.UPDATE_ROOM_AVATAR, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + actorAccountID: currentUserAccountID, + message: [ + { + type: CONST.REPORT.MESSAGE.TYPE.COMMENT, + text: avatarUrl ? `changed room avatar` : 'removed the room avatar', + html: avatarUrl ? `changed room avatar` : 'removed the room avatar', + }, + ], + person: [ + { + type: CONST.REPORT.MESSAGE.TYPE.TEXT, + style: 'strong', + text: getCurrentUserDisplayNameOrEmail(), + }, + ], + originalMessage: { + avatarUrl, + lastModified: now, + }, + created: now, + }; +} + /** * Returns the necessary reportAction onyx data to indicate that the transaction has been put on hold optimistically * @param [created] - Action created time @@ -12247,6 +12284,7 @@ export { buildOptimisticChangePolicyReportAction, buildOptimisticRenamedRoomReportAction, buildOptimisticRoomDescriptionUpdatedReportAction, + buildOptimisticRoomAvatarUpdatedReportAction, buildOptimisticReportPreview, buildOptimisticActionableTrackExpenseWhisper, buildOptimisticSubmittedReportAction, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 68c3e7a62f44..61c0135d2108 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -112,6 +112,7 @@ import { buildOptimisticMovedReportAction, buildOptimisticRenamedRoomReportAction, buildOptimisticReportPreview, + buildOptimisticRoomAvatarUpdatedReportAction, buildOptimisticRoomDescriptionUpdatedReportAction, buildOptimisticSelfDMReport, buildOptimisticUnHoldReportAction, @@ -993,8 +994,37 @@ function updateGroupChatAvatar(reportID: string, file?: File | CustomRNImageMani * Updates the avatar for a policy room. */ function updatePolicyRoomAvatar(reportID: string, file?: File | CustomRNImageManipulatorResult) { + const avatarUrl = file?.uri ?? ''; const {optimisticData, successData, failureData} = buildUpdateReportAvatarOnyxData(reportID, file); - const parameters: UpdateRoomAvatarParams = {reportID, file}; + + const optimisticAction = buildOptimisticRoomAvatarUpdatedReportAction(avatarUrl); + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: {[optimisticAction.reportActionID]: optimisticAction}, + }); + + // Update the report with last action details + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + lastActorAccountID: currentUserAccountID, + lastVisibleActionCreated: optimisticAction.created, + lastMessageText: (optimisticAction?.message as Message[])?.at(0)?.text, + }, + }); + + // Add success data to clear pending action + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [optimisticAction.reportActionID]: {pendingAction: null}, + }, + }); + + const parameters: UpdateRoomAvatarParams = {reportID, file, reportActionID: optimisticAction.reportActionID}; API.write(WRITE_COMMANDS.UPDATE_ROOM_AVATAR, parameters, {optimisticData, failureData, successData}); } From 82325e811e26f55a3ec975b4cdbd464de6acd83b Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Thu, 30 Oct 2025 18:10:48 +0500 Subject: [PATCH 12/14] use getRoomAvatarUpdatedMessage to get alternate text and last message text from report --- src/libs/OptionsListUtils/index.ts | 3 +++ src/libs/SidebarUtils.ts | 3 +++ src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 3 +++ 3 files changed, 9 insertions(+) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index c22c12fa3ffb..4a02b6c33622 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -51,6 +51,7 @@ import { getReportActionHtml, getReportActionMessageText, getRetractedMessage, + getRoomAvatarUpdatedMessage, getRoomChangeLogMessage, getSortedReportActions, getTravelUpdateMessage, @@ -764,6 +765,8 @@ function getLastMessageTextForReport({ lastMessageTextFromReport = translateLocal('violations.resolvedDuplicates'); } else if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.UPDATE_ROOM_DESCRIPTION)) { lastMessageTextFromReport = getUpdateRoomDescriptionMessage(lastReportAction); + } else if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.UPDATE_ROOM_AVATAR)) { + lastMessageTextFromReport = getRoomAvatarUpdatedMessage(lastReportAction); } else if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.RETRACTED)) { lastMessageTextFromReport = getRetractedMessage(); } else if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.REOPENED)) { diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 2ffe5bf060cd..44198e6fecc6 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -52,6 +52,7 @@ import { getReportAction, getReportActionMessageText, getRetractedMessage, + getRoomAvatarUpdatedMessage, getTagListNameUpdatedMessage, getTravelUpdateMessage, getUpdatedApprovalRuleMessage, @@ -896,6 +897,8 @@ function getOptionData({ result.alternateText = getMessageOfOldDotReportAction(lastAction); } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.UPDATE_ROOM_DESCRIPTION) { result.alternateText = getUpdateRoomDescriptionMessage(lastAction); + } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.UPDATE_ROOM_AVATAR) { + result.alternateText = getRoomAvatarUpdatedMessage(lastAction); } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EMPLOYEE) { result.alternateText = getPolicyChangeLogAddEmployeeMessage(lastAction); } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EMPLOYEE) { diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 9c82262e6aba..df4ee4f59f6c 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -50,6 +50,7 @@ import { getRenamedAction, getReopenedMessage, getReportActionMessageText, + getRoomAvatarUpdatedMessage, getTagListNameUpdatedMessage, getTravelUpdateMessage, getUpdatedApprovalRuleMessage, @@ -683,6 +684,8 @@ const ContextMenuActions: ContextMenuAction[] = [ setClipboardMessage(getExportIntegrationMessageHTML(reportAction)); } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.UPDATE_ROOM_DESCRIPTION) { setClipboardMessage(getUpdateRoomDescriptionMessage(reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.UPDATE_ROOM_AVATAR) { + setClipboardMessage(getRoomAvatarUpdatedMessage(reportAction)); } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EMPLOYEE) { setClipboardMessage(getPolicyChangeLogAddEmployeeMessage(reportAction)); } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EMPLOYEE) { From 24a56d4c2eeb9b929fa8cbfc2bf1eee06913cfc6 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Thu, 30 Oct 2025 20:22:54 +0500 Subject: [PATCH 13/14] update canEditRoomAvatar to check current user in participants --- src/components/RoomHeaderAvatars.tsx | 18 ++++++------------ src/pages/ReportDetailsPage.tsx | 1 + 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/components/RoomHeaderAvatars.tsx b/src/components/RoomHeaderAvatars.tsx index 83eeb9b660dc..b8308ddd7720 100644 --- a/src/components/RoomHeaderAvatars.tsx +++ b/src/components/RoomHeaderAvatars.tsx @@ -1,13 +1,11 @@ import React, {memo} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import {clearAvatarErrors, updatePolicyRoomAvatar} from '@libs/actions/Report'; +import {clearAvatarErrors, getCurrentUserAccountID, updatePolicyRoomAvatar} from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; -import {isPolicyMember} from '@libs/PolicyUtils'; -import {isAuditor, isReportParticipant, isUserCreatedPolicyRoom} from '@libs/ReportUtils'; +import {isUserCreatedPolicyRoom} from '@libs/ReportUtils'; import {isDefaultAvatar} from '@libs/UserUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -23,9 +21,10 @@ type RoomHeaderAvatarsProps = { icons: Icon[]; report: Report; policy: OnyxEntry; + participants: number[]; }; -function RoomHeaderAvatars({icons, report, policy}: RoomHeaderAvatarsProps) { +function RoomHeaderAvatars({icons, report, policy, participants}: RoomHeaderAvatarsProps) { const navigateToAvatarPage = (icon: Icon) => { if (icon.type === CONST.ICON_TYPE_WORKSPACE && icon.id) { Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(report?.reportID, icon.id.toString())); @@ -39,13 +38,8 @@ function RoomHeaderAvatars({icons, report, policy}: RoomHeaderAvatarsProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - - const canEditRoomAvatar = - isUserCreatedPolicyRoom(report) && - isPolicyMember(policy, currentUserPersonalDetails?.login) && - isReportParticipant(currentUserPersonalDetails?.accountID, report) && - !isAuditor(report); + const currentUserAccountID = getCurrentUserAccountID(); + const canEditRoomAvatar = isUserCreatedPolicyRoom(report) && participants.includes(currentUserAccountID) && !!policy && policy.role !== CONST.POLICY.ROLE.AUDITOR; if (!icons.length) { return null; diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 33c3bbdddcc1..f9e30c166e6d 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -599,6 +599,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail icons={icons} report={report} policy={policy} + participants={participants} /> ); From 332d571154d8ef22e92df864b3ff1eebdb919ff7 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Thu, 30 Oct 2025 22:33:44 +0500 Subject: [PATCH 14/14] clear pending action from failure data --- src/libs/ReportActionsUtils.ts | 2 +- src/libs/ReportUtils.ts | 8 ++++---- src/libs/actions/Report.ts | 16 +++++++++++++--- src/types/onyx/OriginalMessage.ts | 2 +- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 8fc56d03e7d1..5b1e940e3446 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -2269,7 +2269,7 @@ function getUpdateRoomDescriptionMessage(reportAction: ReportAction): string { function getRoomAvatarUpdatedMessage(reportAction: ReportAction): string { const originalMessage = getOriginalMessage(reportAction) as OriginalMessageChangeLog; - if (originalMessage?.avatarUrl) { + if (originalMessage?.avatarURL) { // eslint-disable-next-line @typescript-eslint/no-deprecated return translateLocal('roomChangeLog.changedRoomAvatar'); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 3b299c8ecf28..d5583e4ede62 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -7717,7 +7717,7 @@ function buildOptimisticRoomDescriptionUpdatedReportAction(description: string): /** * Returns the necessary reportAction onyx data to indicate that the room avatar has been updated */ -function buildOptimisticRoomAvatarUpdatedReportAction(avatarUrl: string): OptimisticRoomAvatarUpdatedReportAction { +function buildOptimisticRoomAvatarUpdatedReportAction(avatarURL: string): OptimisticRoomAvatarUpdatedReportAction { const now = DateUtils.getDBTime(); return { reportActionID: rand64(), @@ -7727,8 +7727,8 @@ function buildOptimisticRoomAvatarUpdatedReportAction(avatarUrl: string): Optimi message: [ { type: CONST.REPORT.MESSAGE.TYPE.COMMENT, - text: avatarUrl ? `changed room avatar` : 'removed the room avatar', - html: avatarUrl ? `changed room avatar` : 'removed the room avatar', + text: avatarURL ? `changed the room avatar` : 'removed the room avatar', + html: avatarURL ? `changed the room avatar` : 'removed the room avatar', }, ], person: [ @@ -7739,7 +7739,7 @@ function buildOptimisticRoomAvatarUpdatedReportAction(avatarUrl: string): Optimi }, ], originalMessage: { - avatarUrl, + avatarURL, lastModified: now, }, created: now, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 61c0135d2108..0414953ff417 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -994,10 +994,10 @@ function updateGroupChatAvatar(reportID: string, file?: File | CustomRNImageMani * Updates the avatar for a policy room. */ function updatePolicyRoomAvatar(reportID: string, file?: File | CustomRNImageManipulatorResult) { - const avatarUrl = file?.uri ?? ''; + const avatarURL = file?.uri ?? ''; const {optimisticData, successData, failureData} = buildUpdateReportAvatarOnyxData(reportID, file); - const optimisticAction = buildOptimisticRoomAvatarUpdatedReportAction(avatarUrl); + const optimisticAction = buildOptimisticRoomAvatarUpdatedReportAction(avatarURL); optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, @@ -1011,7 +1011,7 @@ function updatePolicyRoomAvatar(reportID: string, file?: File | CustomRNImageMan value: { lastActorAccountID: currentUserAccountID, lastVisibleActionCreated: optimisticAction.created, - lastMessageText: (optimisticAction?.message as Message[])?.at(0)?.text, + lastMessageText: (optimisticAction.message as Message[]).at(0)?.text, }, }); @@ -1024,6 +1024,16 @@ function updatePolicyRoomAvatar(reportID: string, file?: File | CustomRNImageMan }, }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [optimisticAction.reportActionID]: { + pendingAction: null, + }, + }, + }); + const parameters: UpdateRoomAvatarParams = {reportID, file, reportActionID: optimisticAction.reportActionID}; API.write(WRITE_COMMANDS.UPDATE_ROOM_AVATAR, parameters, {optimisticData, failureData, successData}); } diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 5b0a4ff326e3..e86af9b86791 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -369,7 +369,7 @@ type OriginalMessageChangeLog = { categoryName?: string; /** Avatar URL of workspace room */ - avatarUrl?: string; + avatarURL?: string; }; /** Model of change log */