Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move Leave button into a row of the Report Details page #41823

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f245c2f
Move Leave button into a row of the Report Details page
ZhenjaHorbach May 8, 2024
43334c4
Remove condition for ChatDetailsQuickActionsBar
ZhenjaHorbach May 13, 2024
746ad28
Update branch and fix conflicts
ZhenjaHorbach May 15, 2024
ca4e3ad
Update condition for leave modal
ZhenjaHorbach May 15, 2024
d1814c1
Update dependency for menuItems
ZhenjaHorbach May 15, 2024
7c630c8
Update conditions for leave button
ZhenjaHorbach May 15, 2024
0aa0fb9
Merge branch 'main' into move-leave-button-into-row-of-the-report-det…
ZhenjaHorbach May 16, 2024
564b82f
Fix comments
ZhenjaHorbach May 16, 2024
6c53db0
Disable ActionsBar for isGroupDMChat
ZhenjaHorbach May 16, 2024
8a9c211
Merge branch 'main' into move-leave-button-into-row-of-the-report-det…
ZhenjaHorbach May 16, 2024
f6202cc
Fix comments
ZhenjaHorbach May 28, 2024
6bdf55c
Merge branch 'main' into move-leave-button-into-row-of-the-report-det…
ZhenjaHorbach May 28, 2024
37e90c7
Merge branch 'main' into move-leave-button-into-row-of-the-report-det…
ZhenjaHorbach May 28, 2024
7d6fe50
Fix ts issue
ZhenjaHorbach May 28, 2024
1c8f9d3
Fix eslint issue
ZhenjaHorbach May 28, 2024
2436d71
Move isPolicyExpenseChat
ZhenjaHorbach May 28, 2024
ec49893
Add condition for share button
ZhenjaHorbach May 28, 2024
bf1dac5
Fix conflicts and update branch
ZhenjaHorbach May 29, 2024
0b0126e
Add comment for isHidden
ZhenjaHorbach May 29, 2024
ab5a203
Merge branch 'main' into move-leave-button-into-row-of-the-report-det…
ZhenjaHorbach May 29, 2024
25a6964
Refactor types for ActionBar
ZhenjaHorbach May 29, 2024
d09d4d7
Merge branch 'main' into move-leave-button-into-row-of-the-report-det…
ZhenjaHorbach May 30, 2024
19c22bf
Fix comments
ZhenjaHorbach May 30, 2024
9f957f5
Merge branch 'main' into move-leave-button-into-row-of-the-report-det…
ZhenjaHorbach May 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2094,7 +2094,6 @@ const CONST = {
INFO: 'info',
},
REPORT_DETAILS_MENU_ITEM: {
SHARE_CODE: 'shareCode',
MEMBERS: 'member',
INVITE: 'invite',
SETTINGS: 'settings',
Expand Down
63 changes: 7 additions & 56 deletions src/components/PromotedActionsBar.tsx
Original file line number Diff line number Diff line change
@@ -1,97 +1,48 @@
import React, {useState} from 'react';
import React from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as HeaderUtils from '@libs/HeaderUtils';
import * as ReportActions from '@userActions/Report';
import type OnyxReport from '@src/types/onyx/Report';
import Button from './Button';
import ConfirmModal from './ConfirmModal';
import type {ThreeDotsMenuItem} from './HeaderWithBackButton/types';
import * as Expensicons from './Icon/Expensicons';

type PromotedAction = {
key: string;
} & ThreeDotsMenuItem;

type ReportPromotedAction = (report: OnyxReport) => PromotedAction;

type PromotedActionsType = {
pin: ReportPromotedAction;
};
type PromotedActionsType = Record<'pin' | 'share', (report: OnyxReport) => PromotedAction>;

const PromotedActions = {
pin: (report) => ({
key: 'pin',
...HeaderUtils.getPinMenuItem(report),
}),
share: (report) => ({
key: 'share',
...HeaderUtils.getShareMenuItem(report),
}),
} satisfies PromotedActionsType;

type PromotedActionsBarProps = {
/** The report of actions */
report?: OnyxReport;

/** The list of actions to show */
promotedActions: PromotedAction[];

/** The style of the container */
containerStyle?: StyleProp<ViewStyle>;

/**
* Whether to show the `Leave` button.
* @deprecated Remove this prop when @src/pages/ReportDetailsPage.tsx is updated
*/
shouldShowLeaveButton?: boolean;
};

function PromotedActionsBar({report, promotedActions, containerStyle, shouldShowLeaveButton}: PromotedActionsBarProps) {
function PromotedActionsBar({promotedActions, containerStyle}: PromotedActionsBarProps) {
const theme = useTheme();
const styles = useThemeStyles();
const [isLastMemberLeavingGroupModalVisible, setIsLastMemberLeavingGroupModalVisible] = useState(false);
const {translate} = useLocalize();

if (promotedActions.length === 0) {
return null;
}

return (
<View style={[styles.flexRow, styles.ph5, styles.mb5, styles.gap2, styles.mw100, styles.w100, styles.justifyContentCenter, containerStyle]}>
{/* TODO: Remove the `Leave` button when @src/pages/ReportDetailsPage.tsx is updated */}
{shouldShowLeaveButton && report && (
// The `Leave` button is left to make the component backward compatible with the existing code.
// After the `Leave` button is moved to the `MenuItem` list, this block can be removed.
<View style={[styles.flex1]}>
<ConfirmModal
danger
title={translate('groupChat.lastMemberTitle')}
isVisible={isLastMemberLeavingGroupModalVisible}
onConfirm={() => {
setIsLastMemberLeavingGroupModalVisible(false);
ReportActions.leaveGroupChat(report.reportID);
}}
onCancel={() => setIsLastMemberLeavingGroupModalVisible(false)}
prompt={translate('groupChat.lastMemberWarning')}
confirmText={translate('common.leave')}
cancelText={translate('common.cancel')}
/>
<Button
onPress={() => {
if (Object.keys(report?.participants ?? {}).length === 1) {
setIsLastMemberLeavingGroupModalVisible(true);
return;
}

ReportActions.leaveGroupChat(report.reportID);
}}
icon={Expensicons.Exit}
style={styles.flex1}
medium
text={translate('common.leave')}
/>
</View>
)}
{promotedActions.map(({key, onSelected, ...props}) => (
<View
style={[styles.flex1, styles.mw50]}
Expand Down
11 changes: 11 additions & 0 deletions src/libs/HeaderUtils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type {ThreeDotsMenuItem} from '@components/HeaderWithBackButton/types';
import * as Expensicons from '@components/Icon/Expensicons';
import ROUTES from '@src/ROUTES';
import type OnyxReport from '@src/types/onyx/Report';
import * as Report from './actions/Report';
import * as Session from './actions/Session';
import * as Localize from './Localize';
import Navigation from './Navigation/Navigation';

function getPinMenuItem(report: OnyxReport): ThreeDotsMenuItem {
const isPinned = !!report.isPinned;
Expand All @@ -15,7 +17,16 @@ function getPinMenuItem(report: OnyxReport): ThreeDotsMenuItem {
};
}

function getShareMenuItem(report: OnyxReport): ThreeDotsMenuItem {
return {
icon: Expensicons.QrCode,
text: Localize.translateLocal('common.share'),
onSelected: () => Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE.getRoute(report?.reportID ?? '')),
};
}

export {
// eslint-disable-next-line import/prefer-default-export
getPinMenuItem,
getShareMenuItem,
};
2 changes: 1 addition & 1 deletion src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ const isPolicyEmployee = (policyID: string, policies: OnyxCollection<Policy>): b
/**
* Checks if the current user is an owner (creator) of the policy.
*/
const isPolicyOwner = (policy: OnyxEntry<Policy>, currentUserAccountID: number): boolean => policy?.ownerAccountID === currentUserAccountID;
const isPolicyOwner = (policy: OnyxEntry<Policy> | EmptyObject, currentUserAccountID: number): boolean => policy?.ownerAccountID === currentUserAccountID;

/**
* Create an object mapping member emails to their accountIDs. Filter for members without errors if includeMemberWithErrors is false, and get the login email from the personalDetail object using the accountID.
Expand Down
85 changes: 59 additions & 26 deletions src/pages/ReportDetailsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useRoute} from '@react-navigation/native';
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useEffect, useMemo} from 'react';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {View} from 'react-native';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
Expand Down Expand Up @@ -29,6 +29,7 @@ import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportUtils from '@libs/ReportUtils';
import * as Report from '@userActions/Report';
import ConfirmModal from '@src/components/ConfirmModal';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
Expand All @@ -49,6 +50,7 @@ type ReportDetailsPageMenuItem = {
action: () => void;
brickRoadIndicator?: ValueOf<typeof CONST.BRICK_ROAD_INDICATOR_STATUS>;
subtitle?: number;
shouldShowRightIcon?: boolean;
};

type ReportDetailsPageOnyxProps = {
Expand All @@ -65,10 +67,11 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
const {isOffline} = useNetwork();
const styles = useThemeStyles();
const route = useRoute();
const [isLastMemberLeavingGroupModalVisible, setIsLastMemberLeavingGroupModalVisible] = useState(false);
const policy = useMemo(() => policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? ''}`], [policies, report?.policyID]);
const isPolicyAdmin = useMemo(() => PolicyUtils.isPolicyAdmin(policy ?? null), [policy]);
const isPolicyEmployee = useMemo(() => PolicyUtils.isPolicyEmployee(report?.policyID ?? '', policies), [report?.policyID, policies]);
const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report);
const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(report), [report]);
const shouldUseFullTitle = useMemo(() => ReportUtils.shouldUseFullTitleToDisplay(report), [report]);
const isChatRoom = useMemo(() => ReportUtils.isChatRoom(report), [report]);
const isUserCreatedPolicyRoom = useMemo(() => ReportUtils.isUserCreatedPolicyRoom(report), [report]);
Expand Down Expand Up @@ -101,6 +104,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
});

const isGroupDMChat = useMemo(() => ReportUtils.isDM(report) && participants.length > 1, [report, participants.length]);

const isPrivateNotesFetchTriggered = report?.isLoadingPrivateNotes !== undefined;

const isSelfDM = useMemo(() => ReportUtils.isSelfDM(report), [report]);
Expand All @@ -114,23 +118,22 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
Report.getReportPrivateNote(report?.reportID ?? '');
}, [report?.reportID, isOffline, isPrivateNotesFetchTriggered, isSelfDM]);

const leaveChat = useCallback(() => {
if (isChatRoom) {
const isWorkspaceMemberLeavingWorkspaceRoom = (report.visibility === CONST.REPORT.VISIBILITY.RESTRICTED || isPolicyExpenseChat) && isPolicyEmployee;
Report.leaveRoom(report.reportID, isWorkspaceMemberLeavingWorkspaceRoom);
return;
}
Report.leaveGroupChat(report.reportID);
}, [isChatRoom, isPolicyEmployee, isPolicyExpenseChat, report.reportID, report.visibility]);

const menuItems: ReportDetailsPageMenuItem[] = useMemo(() => {
const items: ReportDetailsPageMenuItem[] = [];

if (isSelfDM) {
return [];
}

if (!isGroupDMChat) {
items.push({
key: CONST.REPORT_DETAILS_MENU_ITEM.SHARE_CODE,
translationKey: 'common.shareCode',
icon: Expensicons.QrCode,
isAnonymousAction: true,
action: () => Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE.getRoute(report?.reportID ?? '')),
});
}

if (isArchivedRoom) {
return items;
}
Expand All @@ -152,6 +155,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
icon: Expensicons.Users,
subtitle: activeChatMembers.length,
isAnonymousAction: false,
shouldShowRightIcon: true,
action: () => {
if (isUserCreatedPolicyRoom || isChatThread || isPolicyExpenseChat) {
Navigation.navigate(ROUTES.ROOM_MEMBERS.getRoute(report?.reportID ?? ''));
Expand All @@ -166,6 +170,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
translationKey: 'common.invite',
icon: Expensicons.Users,
isAnonymousAction: false,
shouldShowRightIcon: true,
action: () => {
Navigation.navigate(ROUTES.ROOM_INVITE.getRoute(report?.reportID ?? ''));
},
Expand All @@ -177,6 +182,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
translationKey: 'common.settings',
icon: Expensicons.Gear,
isAnonymousAction: false,
shouldShowRightIcon: true,
action: () => {
Navigation.navigate(ROUTES.REPORT_SETTINGS.getRoute(report?.reportID ?? ''));
},
Expand All @@ -189,15 +195,32 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
translationKey: 'privateNotes.title',
icon: Expensicons.Pencil,
isAnonymousAction: false,
shouldShowRightIcon: true,
action: () => ReportUtils.navigateToPrivateNotes(report, session),
brickRoadIndicator: Report.hasErrorInPrivateNotes(report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
});
}

if (isGroupChat || (isChatRoom && ReportUtils.canLeaveChat(report, policy ?? null))) {
items.push({
key: CONST.REPORT_DETAILS_MENU_ITEM.LEAVE_ROOM,
translationKey: 'common.leave',
icon: Expensicons.Exit,
isAnonymousAction: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming from #43404:
@ZhenjaHorbach was there any reason for setting this value to true?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm
Actually good question
But I don't think it should be true

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for confirmation

action: () => {
if (Object.keys(report?.participants ?? {}).length === 1 && isGroupChat) {
setIsLastMemberLeavingGroupModalVisible(true);
return;
}

leaveChat();
},
});
}

return items;
}, [
isSelfDM,
isGroupDMChat,
isArchivedRoom,
isGroupChat,
isDefaultRoom,
Expand All @@ -209,8 +232,11 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
report,
isMoneyRequestReport,
isInvoiceReport,
isChatRoom,
policy,
activeChatMembers.length,
session,
leaveChat,
]);

const displayNamesWithTooltips = useMemo(() => {
Expand Down Expand Up @@ -276,10 +302,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
);
}, [report, icons, isMoneyRequestReport, isInvoiceReport, isGroupChat, isThread, styles]);

const reportName =
ReportUtils.isDeprecatedGroupDM(report) || ReportUtils.isGroupChat(report)
? ReportUtils.getGroupChatName(undefined, false, report.reportID ?? '')
: ReportUtils.getReportName(report);
const reportName = ReportUtils.isDeprecatedGroupDM(report) || isGroupChat ? ReportUtils.getGroupChatName(undefined, false, report.reportID ?? '') : ReportUtils.getReportName(report);
return (
<ScreenWrapper testID={ReportDetailsPage.displayName}>
<FullPageNotFoundView shouldShow={isEmptyObject(report)}>
Expand Down Expand Up @@ -329,7 +352,10 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
</View>
</View>
{shouldShowReportDescription && (
<OfflineWithFeedback pendingAction={report.pendingFields?.description}>
<OfflineWithFeedback
pendingAction={report.pendingFields?.description}
style={styles.mb5}
>
<MenuItemWithTopDescription
shouldShowRightIcon={canEditReportDescription}
interactive={canEditReportDescription}
Expand All @@ -341,13 +367,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
/>
</OfflineWithFeedback>
)}
{isGroupChat && (
<PromotedActionsBar
report={report}
promotedActions={[PromotedActions.pin(report)]}
shouldShowLeaveButton
/>
)}
<PromotedActionsBar promotedActions={[PromotedActions.pin(report), ...(isGroupDMChat ? [] : [PromotedActions.share(report)])]} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB but I think it is slightly over engineered to have "pin" and "share" functions that return an object. No need for all these different methods IMO. Just use the objects.

{menuItems.map((item) => {
const brickRoadIndicator =
ReportUtils.hasReportNameError(report) && item.key === CONST.REPORT_DETAILS_MENU_ITEM.SETTINGS ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined;
Expand All @@ -359,12 +379,25 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
icon={item.icon}
onPress={item.action}
isAnonymousAction={item.isAnonymousAction}
shouldShowRightIcon
shouldShowRightIcon={item.shouldShowRightIcon}
brickRoadIndicator={brickRoadIndicator ?? item.brickRoadIndicator}
/>
);
})}
</ScrollView>
<ConfirmModal
danger
title={translate('groupChat.lastMemberTitle')}
isVisible={isLastMemberLeavingGroupModalVisible}
onConfirm={() => {
setIsLastMemberLeavingGroupModalVisible(false);
Report.leaveGroupChat(report.reportID);
}}
onCancel={() => setIsLastMemberLeavingGroupModalVisible(false)}
prompt={translate('groupChat.lastMemberWarning')}
confirmText={translate('common.leave')}
cancelText={translate('common.cancel')}
/>
</FullPageNotFoundView>
</ScreenWrapper>
);
Expand Down
Loading